问卷 新闻

master
lion 2 days ago
parent 6377581456
commit 03a06faa0a

@ -223,9 +223,11 @@ class CourseContentEvaluationFormController extends BaseController
foreach ($survey->courseContentEvaluationForms()->orderByDesc('id')->cursor() as $response) {
$raw = SurveySchema::answerMap($response->data)[$questionId] ?? [];
$value = $other ? ($raw['other_text'] ?? $raw['allow_input_text'] ?? null) : ($raw['value'] ?? null);
$files = !$other && ($raw['edit_input'] ?? '') === 'file' && is_array($value) ? $value : [];
if ($files) $value = SurveySchema::display($raw, $files, true);
if (!is_scalar($value) || SurveySchema::blank($value)) continue;
if ($total >= ($page - 1) * $size && count($items) < $size) {
$items[] = ['id' => $response->id, 'text' => (string) $value, 'created_at' => (string) $response->created_at];
$items[] = ['id' => $response->id, 'text' => (string) $value, 'files' => $files, 'created_at' => (string) $response->created_at];
}
$total++;
}

@ -26,13 +26,22 @@ class UploadController extends CommonController
public function uploadFile(Request $request)
{
$file = $request->file('file');
if (!$file || !$file->isValid()) return $this->fail([ResponseCode::ERROR_BUSINESS, '请选择有效文件']);
//获取文件大小,单位B
$fileSize = floor($file->getSize());
if ($fileSize >= 50 * 1024 *1024) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '文件必须小于50M']);
}
$originalName = $request->input('original_name');
if (!is_string($originalName) || trim($originalName) === '') $originalName = $file->getClientOriginalName();
//过滤文件后缀
$ext = $file->getClientOriginalExtension();
$ext = strtolower($file->getClientOriginalExtension());
if ($request->filled('survey_question_id')) {
$question = \App\Models\CourseContentEvaluationAsk::find($request->input('survey_question_id'));
if (!$question || $question->edit_input !== 'file') return $this->fail([ResponseCode::ERROR_BUSINESS, '上传题目不存在']);
$allowed = \App\Services\Survey\SurveySchema::fileExtensions($question->config ?? []);
if (!in_array($ext, $allowed, true)) return $this->fail([ResponseCode::ERROR_BUSINESS, '只允许上传 '.implode('、', $allowed).' 格式']);
}
if (in_array($ext, ['zip','rar','ppt','pptx','xls','xlsx','doc','docx','png','gif','jpg','jpeg','pdf','mp4'])) {
// 保存目录
$dir = 'files';
@ -41,7 +50,7 @@ class UploadController extends CommonController
$file->storeAs($dir,$fileName,['disk'=>'public']);
// 写入上传文件记录表
$list = [
'original_name'=>$file->getClientOriginalName(),
'original_name'=>mb_substr(basename(str_replace('\\', '/', $originalName)), 0, 255),
'folder'=>'storage/'.$dir,
'name'=>$fileName,
'extension'=>$ext,

@ -27,7 +27,7 @@ class SurveySchema
public static function question(array $q): array
{
$types = ['section', 'radio', 'checkbox', 'text', 'textarea', 'number', 'date', 'datetime', 'multi_dimension'];
$types = ['section', 'radio', 'checkbox', 'text', 'textarea', 'number', 'date', 'datetime', 'multi_dimension', 'file'];
if (!in_array($q['edit_input'] ?? '', $types, true)) {
throw new InvalidArgumentException('不支持的题型');
}
@ -84,9 +84,27 @@ class SurveySchema
return self::field($q);
}
public const FILE_EXTENSIONS = ['zip', 'rar', 'ppt', 'pptx', 'xls', 'xlsx', 'doc', 'docx', 'png', 'gif', 'jpg', 'jpeg', 'pdf', 'mp4'];
public static function fileExtensions(array $config): array
{
$extensions = $config['allowed_extensions'] ?? self::FILE_EXTENSIONS;
if (!is_array($extensions) || !$extensions) throw new InvalidArgumentException('请至少选择一种允许上传的文件格式');
foreach ($extensions as $extension) {
if (!is_string($extension) || !in_array(strtolower($extension), self::FILE_EXTENSIONS, true)) throw new InvalidArgumentException('不支持的上传文件格式');
}
return array_values(array_unique(array_map('strtolower', $extensions)));
}
private static function field(array $field): array
{
$config = $field['config'] ?? [];
if ($field['edit_input'] === 'file') {
$limit = $config['max_files'] ?? 1;
if (filter_var($limit, FILTER_VALIDATE_INT) === false || $limit < 1 || $limit > 10) throw new InvalidArgumentException('上传数量须为1至10');
$field['config']['max_files'] = (int) $limit;
$field['config']['allowed_extensions'] = self::fileExtensions($config);
}
if (in_array($field['edit_input'], ['radio', 'checkbox'], true)) {
$options = self::arrayValue($field['select_item'] ?? []);
if (!$options) {
@ -118,6 +136,19 @@ class SurveySchema
if (!self::blank($config['other_key'] ?? null) && !in_array((string) $config['other_key'], $keys, true)) {
throw new InvalidArgumentException('其他选项必须属于本题选项');
}
if ($field['edit_input'] === 'checkbox' && isset($config['max_choices'])) {
$max = $config['max_choices'];
if (filter_var($max, FILTER_VALIDATE_INT) === false || $max < 0 || $max > count($options)) throw new InvalidArgumentException('最多选择项数须为0至选项数量之间的整数');
}
$notes = $config['option_notes'] ?? [];
if (!is_array($notes)) throw new InvalidArgumentException('选项附加输入框配置不正确');
foreach ($notes as $key => $note) {
if (!in_array((string) $key, $keys, true) || !is_array($note)) throw new InvalidArgumentException('附加输入框必须属于本题选项');
if ((string) $key === (string) ($config['other_key'] ?? '')) throw new InvalidArgumentException('同一选项不能重复设置补充输入框');
if (!is_string($note['placeholder'] ?? '') || mb_strlen($note['placeholder'] ?? '') > 255) throw new InvalidArgumentException('输入框提示文字不能超过255字');
$field['config']['option_notes'][$key]['required'] = !empty($note['required']);
}
if (!is_string($config['other_placeholder'] ?? '') || mb_strlen($config['other_placeholder'] ?? '') > 255) throw new InvalidArgumentException('补充输入框提示文字不能超过255字');
$field['select_item'] = $options;
}
if ($field['edit_input'] === 'number') {
@ -277,6 +308,19 @@ class SurveySchema
if ($type === 'checkbox' && !empty($config['max_choices']) && count($keys) > (int) $config['max_choices']) throw new InvalidArgumentException($label.':最多选择'.$config['max_choices'].'项');
return $type === 'checkbox' ? $keys : $keys[0];
}
if ($type === 'file') {
if (!is_array($value) || count($value) > ($config['max_files'] ?? 1)) throw new InvalidArgumentException($label.':上传文件数量不正确');
$files = [];
foreach ($value as $item) {
$id = is_array($item) ? ($item['id'] ?? null) : $item;
if (!is_scalar($id) || !ctype_digit((string) $id)) throw new InvalidArgumentException($label.':文件无效,请重新上传');
$file = \App\Models\Upload::whereKey($id)->where('creator_id', auth('mobile')->id())->first();
if (!$file || $file->size >= 50 * 1024 * 1024 || !in_array(strtolower($file->extension), ['zip','rar','ppt','pptx','xls','xlsx','doc','docx','png','gif','jpg','jpeg','pdf','mp4'], true)) throw new InvalidArgumentException($label.':文件无效或不属于当前用户,请重新上传');
if (!in_array(strtolower($file->extension), self::fileExtensions($config), true)) throw new InvalidArgumentException($label.':只允许上传 '.implode('、', self::fileExtensions($config)).' 格式');
$files[(string) $file->id] = ['id' => $file->id, 'original_name' => $file->original_name, 'url' => $file->url, 'extension' => $file->extension, 'size' => $file->size];
}
return array_values($files);
}
if ($type === 'number') {
if (!is_scalar($value) || !is_numeric($value) || !is_finite((float) $value)) {
throw new InvalidArgumentException($label . ':请输入有效数字');
@ -321,6 +365,7 @@ class SurveySchema
return '未填写';
}
$type = $field['edit_input'] ?? 'radio';
if ($type === 'file') return implode('、', array_column(is_array($value) ? $value : [], 'original_name'));
if (in_array($type, ['radio', 'checkbox'], true)) {
$values = is_array($value) ? $value : ($type === 'checkbox' ? explode(',', (string) $value) : [$value]);
$labels = [];

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -62,6 +62,40 @@ class SurveyWorkflowTest extends TestCase
return $response->json('id');
}
public function testFileAnswersValidateOwnershipAndReturnNames(): void
{
Schema::create('uploads', function (Blueprint $table) { $table->id(); $table->string('original_name'); $table->string('folder'); $table->string('name'); $table->string('extension'); $table->integer('size'); $table->integer('creator_id'); $table->timestamp('created_at')->nullable(); });
\Illuminate\Support\Facades\Storage::fake('public');
$upload = $this->post('/api/mobile/upload-file', ['file' => \Illuminate\Http\UploadedFile::fake()->create('tmp.pdf', 10, 'application/pdf'), 'original_name' => '原始材料.pdf'])->assertOk()->assertJsonPath('original_name', '原始材料.pdf')->assertJsonPath('creator_id', 200);
\Illuminate\Support\Facades\Storage::disk('public')->assertExists('files/'.$upload->json('name'));
$this->post('/api/mobile/upload-file', [])->assertJsonPath('errmsg', '请选择有效文件');
DB::table('uploads')->insert([
['id' => 81, 'original_name' => '调研附件.pdf', 'folder' => 'storage/files', 'name' => '81.pdf', 'extension' => 'pdf', 'size' => 100, 'creator_id' => 200],
['id' => 82, 'original_name' => '他人附件.pdf', 'folder' => 'storage/files', 'name' => '82.pdf', 'extension' => 'pdf', 'size' => 100, 'creator_id' => 201],
]);
$question = ['course_content_evaluation_id' => 10, 'name' => '上传附件', 'field' => 'attachment', 'edit_input' => 'file', 'need_fill' => true, 'config' => ['max_files' => 1, 'allowed_extensions' => ['pdf']]];
$id = $this->postJson('/api/admin/course-content-evaluation-ask/save', $question)->assertJsonPath('edit_input', 'file')->json('id');
$this->post('/api/mobile/upload-file', ['file' => \Illuminate\Http\UploadedFile::fake()->create('blocked.docx', 10), 'survey_question_id' => $id])->assertJsonPath('errmsg', '只允许上传 pdf 格式');
$this->assertSame(3, DB::table('uploads')->count());
$allowedUpload = $this->post('/api/mobile/upload-file', ['file' => \Illuminate\Http\UploadedFile::fake()->create('ALLOWED.PDF', 10), 'survey_question_id' => $id])->assertJsonPath('extension', 'pdf');
DB::table('uploads')->insert(['id' => 90, 'original_name' => '文件.docx', 'folder' => 'storage/files', 'name' => '90.docx', 'extension' => 'docx', 'size' => 100, 'creator_id' => 200]);
$payload = ['course_content_evaluation_id' => 10, 'answer_version' => 2, 'data' => [['id' => $id, 'value' => []]]];
$url = '/api/mobile/course/course-content-form';
$this->postJson($url, $payload)->assertJsonPath('errmsg', '上传附件:请填写此项');
foreach ([[['id' => 90]], [['id' => 82]], [['id' => 999]], [['id' => 81], ['id' => 82]]] as $invalid) {
$payload['data'][0]['value'] = $invalid;
$this->assertNotEmpty($this->postJson($url, $payload)->json('errmsg'));
$this->assertSame(0, DB::table('course_content_evaluation_forms')->count());
}
$payload['data'][0]['value'] = [['id' => 81, 'original_name' => 'fake', 'url' => 'https://evil.invalid']];
$replyId = $this->postJson($url, $payload)->assertJsonPath('data.0.value.0.original_name', '调研附件.pdf')->json('id');
$this->getJson('/api/admin/course-content-evaluation-form/show?id='.$replyId)->assertJsonPath('questions.0.display_value', '调研附件.pdf')->assertJsonPath('questions.0.value.0.id', 81);
$this->getJson('/api/admin/course-content-evaluation-form/text-answers?course_content_evaluation_id=10&question_id='.$id)->assertJsonPath('data.0.files.0.original_name', '调研附件.pdf');
$this->getJson('/api/mobile/course/evaluation-detail?course_content_evaluation_id=10')->assertJsonPath('my_response.questions.0.value.0.id', 81);
$this->postJson($url, $payload)->assertJsonPath('id', $replyId);
$this->assertSame(1, DB::table('course_content_evaluation_forms')->count());
}
public function testAudienceAndDeadlineProtectEntryAndSubmission(): void
{
Schema::create('courses', function (Blueprint $table) { $table->id(); $table->softDeletes(); });

@ -27,6 +27,28 @@ class SurveySchemaTest extends TestCase
}
}
public function testFileFormatSettings(): void
{
$this->assertSame(['pdf', 'docx'], SurveySchema::fileExtensions(['allowed_extensions' => ['PDF', 'docx', 'pdf']]));
$this->expectException(InvalidArgumentException::class);
SurveySchema::fileExtensions(['allowed_extensions' => []]);
}
public function testUnknownFileFormatIsRejected(): void
{
$this->expectException(InvalidArgumentException::class);
SurveySchema::fileExtensions(['allowed_extensions' => ['exe']]);
}
public function testSelectionLimitAndNoteConfigurationValidation(): void
{
$q = ['name' => '会场', 'field' => 'venues', 'edit_input' => 'checkbox', 'select_item' => [['key' => 'tax', 'value' => '财税']], 'config' => ['max_choices' => 1, 'option_notes' => ['tax' => ['placeholder' => '咨询问题', 'required' => true]]]];
$this->assertTrue(SurveySchema::question($q)['config']['option_notes']['tax']['required']);
$q['config']['max_choices'] = 2;
$this->expectException(InvalidArgumentException::class);
SurveySchema::question($q);
}
private function questions(): array
{
return [

Loading…
Cancel
Save