问卷 新闻

master
lion 2 days ago
parent ca5980c4e3
commit 6377581456

@ -13,6 +13,19 @@ use RuntimeException;
class CompanyNewsController extends CommonController
{
public function dashboard(Request $r)
{
if ($error = $this->validateInput($r, ['page' => 'nullable|integer|min:1'])) return $error;
$start = now()->startOfYear();
$news = CompanyNews::query()->select('id', 'title', 'source_url', 'source_published_at')
->where('review_status', 'retained')
->where('source_published_at', '>=', $start)
->where('source_published_at', '<', $start->copy()->addYear())
->orderByDesc('source_published_at')->orderByDesc('id')
->paginate(6, ['*'], 'page', (int) $r->input('page', 1));
return $this->success($news);
}
public function index(Request $r)
{
if ($error = $this->validateInput($r, [

@ -142,6 +142,7 @@ class CourseContentEvaluationAskController extends BaseController
$model->department_id = $this->getUser()->department_id;
}
$model->save();
SurveySchema::validateConditions($survey->courseContentEvaluationAsks()->orderBy('sort')->orderBy('id')->get()->toArray());
$this->saveLogs($original, $model);
return $model;
});
@ -176,6 +177,7 @@ class CourseContentEvaluationAskController extends BaseController
throw new \InvalidArgumentException('已有答卷,不能删除题目');
}
$model->delete();
SurveySchema::validateConditions($survey->courseContentEvaluationAsks()->orderBy('sort')->orderBy('id')->get()->toArray());
$this->saveLogs('delete', $model);
return $this->success('删除成功');
});
@ -199,6 +201,7 @@ class CourseContentEvaluationAskController extends BaseController
foreach ($ids as $sort => $id) {
$survey->courseContentEvaluationAsks()->whereKey($id)->update(['sort' => $sort + 1]);
}
SurveySchema::validateConditions($survey->courseContentEvaluationAsks()->orderBy('sort')->orderBy('id')->get()->toArray());
return $this->success('排序已保存');
});
} catch (\InvalidArgumentException $e) {

@ -187,8 +187,15 @@ class CourseContentEvaluationsController extends BaseController
}
}
}
if (array_key_exists('audience_course_ids', $all)) {
$ids = $all['audience_course_ids'];
if (!is_array($ids) || count(array_filter($ids, fn ($id) => !is_scalar($id) || !ctype_digit((string) $id) || (int) $id < 1))) throw new \InvalidArgumentException('限定人群请选择有效课程');
$ids = array_values(array_unique(array_map('intval', $ids)));
if (\App\Models\Course::whereIn('id', $ids)->count() !== count($ids)) throw new \InvalidArgumentException('限定人群中存在无效课程');
$all['audience_course_ids'] = $ids;
}
$original = $model->getOriginal();
$fields = ['title', 'desc', 'course_id', 'course_content_id', 'type_id', 'start_time', 'end_time', 'status'];
$fields = ['title', 'desc', 'course_id', 'course_content_id', 'type_id', 'start_time', 'end_time', 'status', 'audience_course_ids'];
$model->fill(array_intersect_key($all, array_flip($fields)));
if (!$model->title) throw new \InvalidArgumentException('请输入问卷标题');
if ($model->start_time && $model->end_time && strtotime($model->start_time) >= strtotime($model->end_time)) {

@ -187,8 +187,11 @@ class CourseController extends CommonController
}
])->find($all['course_content_evaluation_id']);
if (!$detail) return $this->fail([ResponseCode::ERROR_BUSINESS, '问卷不存在']);
if ($message = $detail->participationError($this->getUserId())) return $this->success(['id' => $detail->id, 'access_error' => $message]);
$detail->schema_hash = SurveySchema::hash($detail->courseContentEvaluationAsks->toArray());
$reply = $detail->courseContentEvaluationForms()->where('user_id', $this->getUserId())->first();
$detail->requires_profile = empty(auth('mobile')->user()->mobile);
if ($reply && !empty($reply->snapshot['questions'])) $detail->schema_hash = SurveySchema::hash($reply->snapshot['questions']);
$detail->my_response = $reply ? [
'title' => $reply->snapshot['title'] ?? $detail->title,
'desc' => $reply->snapshot['desc'] ?? $detail->desc,
@ -482,26 +485,31 @@ class CourseController extends CommonController
$model = DB::transaction(function () use ($all) {
$survey = CourseContentEvaluation::whereKey($all['course_content_evaluation_id'])->lockForUpdate()->first();
if (!$survey) throw new \InvalidArgumentException('问卷不存在');
if ($message = $survey->participationError($this->getUserId())) return ['access_error' => $message];
if ((int) $survey->status !== 1) throw new \InvalidArgumentException('问卷未发布');
if ($survey->start_time && strtotime($survey->start_time) > time()) throw new \InvalidArgumentException('评价时间未开始');
if ($survey->end_time && strtotime($survey->end_time) < time()) throw new \InvalidArgumentException('评价时间已结束');
if ($survey->courseContentEvaluationForms()->where('user_id', $this->getUserId())->exists()) {
throw new \InvalidArgumentException('您已提交过评价');
}
if (empty(auth('mobile')->user()->mobile)) throw new \InvalidArgumentException('请先绑定或注册');
$reply = $survey->courseContentEvaluationForms()->where('user_id', $this->getUserId())->first();
$questions = $survey->courseContentEvaluationAsks()->with('courseContent.teacher')->orderBy('sort')->orderBy('id')->get()->toArray();
if ($reply && !empty($reply->snapshot['questions'])) $questions = $reply->snapshot['questions'];
if (!count(array_filter($questions, fn ($q) => $q['edit_input'] !== 'section'))) throw new \InvalidArgumentException('问卷没有可填写题目');
if (!empty($all['schema_hash']) && $all['schema_hash'] !== SurveySchema::hash($questions)) {
throw new \InvalidArgumentException('问卷已更新,请刷新后重新填写');
}
$answers = SurveySchema::normalize($questions, $all['data'], ($all['answer_version'] ?? 0) == 2);
return CourseContentEvaluationForm::create([
$attributes = [
'course_id' => $survey->course_id,
'course_content_evaluation_id' => $survey->id,
'user_id' => $this->getUserId(),
'time_total' => max(0, (int) ($all['time_total'] ?? 0)),
'data' => $answers,
'snapshot' => ['version' => 2, 'title' => $survey->title, 'desc' => $survey->desc, 'questions' => $questions],
]);
'snapshot' => ['version' => 2, 'title' => $reply->snapshot['title'] ?? $survey->title, 'desc' => $reply->snapshot['desc'] ?? $survey->desc, 'questions' => $questions],
];
if ($reply) {
$reply->fill($attributes)->save();
return $reply->fresh();
}
return CourseContentEvaluationForm::create($attributes);
});
return $this->success($model);
} catch (\InvalidArgumentException $e) {

@ -4,6 +4,15 @@ namespace App\Models;
class CourseContentEvaluation extends SoftDeletesModel
{
protected $casts = ['audience_course_ids' => 'array'];
public function participationError($userId): ?string
{
if ($this->end_time && strtotime($this->end_time) <= time()) return '当前问卷已截止';
if ($this->audience_course_ids && !CourseSign::where('user_id', $userId)->whereIn('course_id', $this->audience_course_ids)->where('status', 1)->exists()) return '您还未报名相关课程';
return null;
}
protected $appends = ['course_content'];
public function getCourseContentAttribute()

@ -38,6 +38,13 @@ class SurveySchema
throw new InvalidArgumentException('字段标识须以字母开头,仅包含字母、数字和下划线');
}
$q['config'] = self::arrayValue($q['config'] ?? []);
$userField = $q['config']['user_field'] ?? '';
if ($userField !== '' && (!in_array($userField, ['name', 'mobile', 'no', 'sex', 'birthday', 'email', 'company_name', 'company_position', 'idcard', 'plate'], true)
|| !in_array($q['edit_input'], ['text', 'textarea', 'date'], true)
|| ($q['edit_input'] === 'date' && $userField !== 'birthday'))) {
throw new InvalidArgumentException('学员字段仅支持个人信息字段,日期题只能关联出生日期');
}
if ($q['edit_input'] === 'section') {
$q['rule'] = '';
$q['need_fill'] = false;
@ -148,6 +155,40 @@ class SurveySchema
return $map;
}
// Input must be in the same sort/id order used for questionnaire rendering.
public static function validateConditions(array $questions): void
{
$preceding = [];
foreach ($questions as $q) {
$condition = $q['config']['visible_when'] ?? null;
if ($condition !== null) {
if (!is_array($condition) || !is_string($condition['field'] ?? null) || !is_scalar($condition['value'] ?? null)) throw new InvalidArgumentException('显示条件格式不正确');
$parent = $preceding[$condition['field']] ?? null;
if ($q['edit_input'] === 'section' || !$parent || !in_array($parent['edit_input'], ['radio', 'checkbox'], true)) throw new InvalidArgumentException($q['name'].':显示条件只能引用本题之前的单选或多选题,请先调整条件');
$keys = array_map(fn ($o) => (string) $o['key'], self::arrayValue($parent['select_item'] ?? []));
if (!in_array((string) $condition['value'], $keys, true)) throw new InvalidArgumentException($q['name'].':显示条件引用的选项不存在,请先调整条件');
}
$preceding[$q['field']] = $q;
}
}
public static function visible(array $q, array $questions, array $map, bool $keyed = true, array $visited = []): bool
{
$condition = $q['config']['visible_when'] ?? null;
if (!$condition) return true;
$field = $q['field'] ?? (string) $q['id'];
if (in_array($field, $visited, true)) return false;
$visited[] = $field;
foreach ($questions as $parent) {
if (($parent['field'] ?? '') !== ($condition['field'] ?? '')) continue;
if (!self::visible($parent, $questions, $map, $keyed, $visited)) return false;
$value = $map[(string) $parent['id']]['value'] ?? null;
$keys = self::selectedKeys($parent, $value, $keyed);
return in_array((string) ($condition['value'] ?? ''), $keys, true);
}
return false;
}
public static function normalize(array $questions, $data, bool $keyed = false): array
{
$map = self::answerMap($data);
@ -155,6 +196,14 @@ class SurveySchema
foreach ($questions as $q) {
$raw = $map[(string) $q['id']] ?? [];
$q['other_text'] = '';
$q['option_texts'] = [];
if (!self::visible($q, $questions, $map, $keyed)) {
$q['value'] = null;
$q['hidden'] = true;
$result[] = $q;
continue;
}
$q['hidden'] = false;
if ($q['edit_input'] === 'section') {
$q['value'] = null;
} elseif ($q['edit_input'] === 'multi_dimension') {
@ -169,6 +218,14 @@ class SurveySchema
} else {
$q['value'] = self::answer($q, $raw['value'] ?? null, $keyed, $q['name']);
$config = self::arrayValue($q['config'] ?? []);
foreach ($config['option_notes'] ?? [] as $key => $settings) {
if (!in_array((string) $key, array_map('strval', (array) $q['value']), true)) continue;
$text = $raw['option_texts'][$key] ?? '';
if (!is_scalar($text) || mb_strlen((string) $text) > 10000) throw new InvalidArgumentException($q['name'].':补充文字格式不正确或过长');
$text = trim((string) $text);
if (!empty($settings['required']) && $text === '') throw new InvalidArgumentException($q['name'].':请填写补充说明');
$q['option_texts'][$key] = $text;
}
$otherKey = $config['other_key'] ?? null;
$selectedOther = !self::blank($otherKey) && in_array((string) $otherKey, array_map('strval', (array) $q['value']), true);
// Existing questionnaires allowed a separate free-text response without an "other" option.
@ -217,6 +274,7 @@ class SurveySchema
$keys[] = $match;
}
$keys = array_values(array_unique($keys));
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 === 'number') {
@ -311,6 +369,8 @@ class SurveySchema
$q = array_replace($q, $raw); // legacy submissions already carried their definition
}
$q['value'] = $raw['value'] ?? null;
$q['hidden'] = !self::visible($q, $questions, $map, $keyed);
$q['option_texts'] = self::arrayValue($raw['option_texts'] ?? []);
$q['other_text'] = $raw['other_text'] ?? $raw['allow_input_text'] ?? '';
$q['display_value'] = self::display($q, $q['value'], $keyed);
$q['selected_keys'] = self::selectedKeys($q, $q['value'], $keyed);

@ -0,0 +1,8 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up() { Schema::table('course_content_evaluations', function (Blueprint $table) { $table->json('audience_course_ids')->nullable()->comment('限定人群课程ID,空为不限定'); }); }
public function down() { Schema::table('course_content_evaluations', function (Blueprint $table) { $table->dropColumn('audience_course_ids'); }); }
};

@ -1,6 +1,6 @@
# 企业新闻舆情部署与维护
后台页面 `/companyNews/index` 支持新闻采集、公司和发布日期筛选、人工正文编辑、留存审核及批次记录。当前没有对外展示接口。所有新闻默认 `is_visible=false`,保存与审核也不会开启展示。
后台页面 `/companyNews/index` 支持新闻采集、公司和发布日期筛选、人工正文编辑、留存审核及批次记录。后台驾驶舱 `/home` 的校友动态通过需管理员登录的 `/api/admin/other/alumni-news` 读取本年度已留存新闻,每页 6 条,按原文发布日期倒序;该内部展示不依赖 `is_visible`。当前没有匿名公开展示接口。所有新闻默认 `is_visible=false`,保存与审核也不会开启展示。
## 业务规则

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

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

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

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

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

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

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

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

@ -50,7 +50,9 @@ Route::group(["namespace" => "Admin", "prefix" => "admin"], function () {
// 清除缓存
Route::post('other/clear-cache', [\App\Http\Controllers\Admin\OtherController::class, "clearCache"]);
// 企业新闻:仅后台授权访问;没有对外展示接口。
Route::get('other/alumni-news', [\App\Http\Controllers\Admin\CompanyNewsController::class, 'dashboard']);
// 企业新闻管理:仅后台授权访问。
Route::prefix('company-news')->middleware('rbac:admin')->group(function () {
Route::get('categories', [\App\Http\Controllers\Admin\CompanyNewsController::class, 'categories']);
Route::post('settings', [\App\Http\Controllers\Admin\CompanyNewsController::class, 'saveSettings']);

@ -177,6 +177,31 @@ class CompanyNewsWorkflowTest extends TestCase
$result = $controller->review(Request::create('/', 'POST', ['id' => 1, 'version' => 1, 'review_status' => 'pending']), new NewsEditor())->getData(true);
$this->assertEquals(422, $result['errcode']);
}
public function test_dashboard_returns_only_retained_current_year_news_with_pagination(): void
{
Carbon::setTestNow('2026-10-12 12:00:00');
$this->company(1);
$items = [];
for ($i = 1; $i <= 8; $i++) $items[] = $this->news('current-'.$i, '2026-01-01 00:00:00');
$items[] = $this->news('previous', '2025-12-31 23:59:59');
$items[] = $this->news('next', '2027-01-01 00:00:00');
$items[] = $this->news('discarded', '2026-10-12 00:00:00');
$this->provider([$this->response($items)]);
$collector = new NewsCollection(); $collector->start(1); $collector->runOne();
CompanyNews::where('external_news_id', '!=', 'discarded')->update(['review_status' => 'retained']);
$controller = new CompanyNewsController();
$first = $controller->dashboard(Request::create('/', 'GET', ['page' => 1]))->getData(true);
$second = $controller->dashboard(Request::create('/', 'GET', ['page' => 2]))->getData(true);
$this->assertEquals(8, $first['total']); $this->assertCount(6, $first['data']); $this->assertCount(2, $second['data']);
$this->assertGreaterThan($first['data'][1]['id'], $first['data'][0]['id']);
$this->assertEmpty(array_intersect(array_column($first['data'], 'id'), array_column($second['data'], 'id')));
$this->assertEquals(['id', 'title', 'source_url', 'source_published_at'], array_keys($first['data'][0]));
$route = app('router')->getRoutes()->match(Request::create('/api/admin/other/alumni-news', 'GET'));
$this->assertContains('sanctum.jwt:admin', $route->gatherMiddleware());
Carbon::setTestNow('2027-01-01 00:00:00');
$this->assertEquals(1, $controller->dashboard(Request::create('/'))->getData(true)['total']);
}
public function test_empty_batch_finishes_immediately(): void
{
$service = new NewsCollection(); $batch = $service->start(1);

@ -20,14 +20,14 @@ class SurveyWorkflowTest extends TestCase
config(['database.default' => 'sqlite', 'database.connections.sqlite.database' => ':memory:', 'audit.enabled' => false]);
DB::purge('sqlite');
$this->withoutMiddleware();
foreach (['2025_06_30_105931_create_course_content_evaluation_asks_table.php', '2025_06_30_105932_create_course_content_evaluation_forms_table.php', '2025_07_03_093224_create_course_content_evaluations_table.php', '2026_01_13_094952_add_dimensions_to_course_content_evaluation_asks_table.php', '2026_10_09_000001_extend_survey_questions_and_snapshots.php'] as $migration) {
foreach (['2025_06_30_105931_create_course_content_evaluation_asks_table.php', '2025_06_30_105932_create_course_content_evaluation_forms_table.php', '2025_07_03_093224_create_course_content_evaluations_table.php', '2026_01_13_094952_add_dimensions_to_course_content_evaluation_asks_table.php', '2026_10_09_000001_extend_survey_questions_and_snapshots.php', '2026_10_12_000001_add_survey_audience.php'] as $migration) {
(require database_path('migrations/' . $migration))->up();
}
foreach (['users', 'course_contents', 'teachers'] as $name) {
Schema::create($name, function (Blueprint $table) { $table->id(); $table->softDeletes(); });
}
$user = new User();
$user->forceFill(['id' => 200]);
$user->forceFill(['id' => 200, 'mobile' => '13800000000']);
Auth::guard('mobile')->setUser($user);
$admin = new Admin();
$admin->forceFill(['id' => 1, 'department_id' => 1]);
@ -62,6 +62,60 @@ class SurveyWorkflowTest extends TestCase
return $response->json('id');
}
public function testAudienceAndDeadlineProtectEntryAndSubmission(): void
{
Schema::create('courses', function (Blueprint $table) { $table->id(); $table->softDeletes(); });
Schema::create('course_signs', function (Blueprint $table) { $table->id(); $table->integer('user_id'); $table->integer('course_id'); $table->integer('status'); $table->softDeletes(); });
DB::table('courses')->insert([['id' => 11], ['id' => 12]]);
$id = $this->createQuestion();
$detail = '/api/mobile/course/evaluation-detail?course_content_evaluation_id=10';
$submit = '/api/mobile/course/course-content-form';
$payload = ['course_content_evaluation_id' => 10, 'answer_version' => 2, 'data' => [['id' => $id, 'value' => ['count' => 0, 'follow' => 'no']]]];
$this->getJson($detail)->assertJsonMissing(['access_error' => '您还未报名相关课程']);
$this->postJson('/api/admin/course-content-evaluations/save', ['id' => 10, 'audience_course_ids' => [11, 12]])->assertJsonPath('audience_course_ids', [11, 12]);
foreach ([null, 0, 2, 4] as $status) {
DB::table('course_signs')->delete();
if ($status !== null) DB::table('course_signs')->insert(['user_id' => 200, 'course_id' => 12, 'status' => $status]);
$this->getJson($detail)->assertJsonPath('access_error', '您还未报名相关课程')->assertJsonMissing(['schema_hash']);
$this->postJson($submit, $payload)->assertJsonPath('access_error', '您还未报名相关课程');
$this->assertSame(0, DB::table('course_content_evaluation_forms')->count());
}
DB::table('course_signs')->update(['status' => 1]);
$this->assertNotEmpty($this->getJson($detail)->json('schema_hash'));
$replyId = $this->postJson($submit, $payload)->assertOk()->json('id');
$this->assertNotNull($replyId);
DB::table('course_content_evaluations')->where('id', 10)->update(['end_time' => '2000-01-01 00:00:00']);
$this->getJson($detail)->assertJsonPath('access_error', '当前问卷已截止');
$this->postJson($submit, $payload)->assertJsonPath('access_error', '当前问卷已截止');
$this->assertSame(1, DB::table('course_content_evaluation_forms')->count());
DB::table('course_content_evaluations')->where('id', 10)->update(['end_time' => null]);
$this->postJson('/api/admin/course-content-evaluations/save', ['id' => 10, 'audience_course_ids' => []])->assertJsonPath('audience_course_ids', []);
DB::table('course_signs')->delete();
$this->assertNotEmpty($this->getJson($detail)->json('schema_hash'));
$this->postJson('/api/admin/course-content-evaluations/save', ['id' => 10, 'audience_course_ids' => [999]])->assertJsonPath('errmsg', '限定人群中存在无效课程');
}
public function testConditionsRejectBreakingEditsAndRollBack(): void
{
$base = '/api/admin/course-content-evaluation-ask/';
$parent = ['course_content_evaluation_id' => 10, 'name' => '保险', 'field' => 'insurance', 'edit_input' => 'radio', 'sort' => 1, 'select_item' => [['key' => 'yes', 'value' => '需要'], ['key' => 'no', 'value' => '不需要']]];
$parent['id'] = $this->postJson($base.'save', $parent)->assertOk()->json('id');
$child = ['course_content_evaluation_id' => 10, 'name' => '身份证', 'field' => 'idcard', 'edit_input' => 'text', 'sort' => 2, 'config' => ['visible_when' => ['field' => 'insurance', 'value' => 'yes']]];
$id = $this->postJson($base.'save', $child)->assertOk()->assertJsonPath('config.visible_when.value', 'yes')->json('id');
$parent['select_item'] = [['key' => 'no', 'value' => '不需要']];
$this->postJson($base.'save', $parent)->assertJsonPath('errmsg', '身份证:显示条件引用的选项不存在,请先调整条件');
$this->assertStringContainsString('yes', DB::table('course_content_evaluation_asks')->where('id', $parent['id'])->value('select_item'));
$this->getJson($base.'destroy?id='.$parent['id'])->assertJsonPath('errmsg', '身份证:显示条件只能引用本题之前的单选或多选题,请先调整条件');
$this->assertNull(DB::table('course_content_evaluation_asks')->where('id', $parent['id'])->value('deleted_at'));
$this->postJson($base.'reorder', ['course_content_evaluation_id' => 10, 'ids' => [$id, $parent['id']]])->assertJsonPath('errmsg', '身份证:显示条件只能引用本题之前的单选或多选题,请先调整条件');
$this->assertSame(1, (int) DB::table('course_content_evaluation_asks')->where('id', $parent['id'])->value('sort'));
$child['id'] = $id;
$child['config'] = [];
$this->postJson($base.'save', $child)->assertOk()->assertJsonPath('id', $id);
$this->getJson($base.'destroy?id='.$parent['id'])->assertOk();
$this->assertNotNull(DB::table('course_content_evaluation_asks')->where('id', $parent['id'])->value('deleted_at'));
}
public function testFullSubmissionStatisticsSnapshotAndStructureLock(): void
{
$id = $this->createQuestion();
@ -74,7 +128,8 @@ class SurveyWorkflowTest extends TestCase
->assertJsonPath('snapshot.questions.0.name', '融资对接');
$replyId = $reply->json('id');
$this->assertSame(1, DB::table('course_content_evaluation_forms')->count());
$this->postJson('/api/mobile/course/course-content-form', $payload)->assertJsonPath('errmsg', '您已提交过评价');
$this->postJson('/api/mobile/course/course-content-form', $payload)->assertJsonPath('id', $replyId);
$this->assertSame(1, DB::table('course_content_evaluation_forms')->count());
$this->getJson('/api/admin/course-content-evaluation-form/statistics?course_content_evaluation_id=10')
->assertJsonPath('responses', 1)->assertJsonPath('questions.0.dimensions.0.stats.valid_count', 1)
->assertJsonPath('questions.0.dimensions.0.stats.sum', 0)->assertJsonPath('questions.0.dimensions.1.stats.options.1.count', 1);
@ -112,6 +167,38 @@ class SurveyWorkflowTest extends TestCase
$this->getJson($url)->assertJsonPath('my_response', null);
}
public function testResubmissionUpdatesSameReplyAndRequiresBoundProfile(): void
{
$id = $this->createQuestion();
$payload = ['course_content_evaluation_id' => 10, 'answer_version' => 2, 'data' => [['id' => $id, 'value' => ['count' => 0, 'follow' => 'no']]]];
$replyId = $this->postJson('/api/mobile/course/course-content-form', $payload)->assertOk()->json('id');
$payload['data'][0]['value'] = ['count' => 8, 'follow' => 'yes'];
$this->postJson('/api/mobile/course/course-content-form', $payload)->assertJsonPath('id', $replyId)->assertJsonPath('data.0.value.count', 8);
$this->assertSame(1, DB::table('course_content_evaluation_forms')->count());
$this->getJson('/api/admin/course-content-evaluation-form/statistics?course_content_evaluation_id=10')->assertJsonPath('responses', 1)->assertJsonPath('questions.0.dimensions.0.stats.sum', 8);
$payload['data'][0]['value']['count'] = -1;
$this->postJson('/api/mobile/course/course-content-form', $payload)->assertJsonPath('errcode', 10001);
$this->getJson('/api/mobile/course/evaluation-detail?course_content_evaluation_id=10')->assertJsonPath('my_response.questions.0.value.count', 8);
DB::table('course_content_evaluations')->where('id', 10)->update(['end_time' => '2020-01-01 00:00:00']);
$this->postJson('/api/mobile/course/course-content-form', $payload)->assertJsonPath('access_error', '当前问卷已截止');
DB::table('course_content_evaluations')->where('id', 10)->update(['end_time' => null]);
$guest = new User();
$guest->forceFill(['id' => 201]);
Auth::guard('mobile')->setUser($guest);
$this->getJson('/api/mobile/course/evaluation-detail?course_content_evaluation_id=10')->assertJsonPath('requires_profile', true)->assertJsonPath('my_response', null);
$this->postJson('/api/mobile/course/course-content-form', $payload)->assertJsonPath('errmsg', '请先绑定或注册');
$this->assertSame(1, DB::table('course_content_evaluation_forms')->count());
}
public function testProfileFieldConfigurationIsValidatedAndStored(): void
{
$payload = ['course_content_evaluation_id' => 10, 'name' => '姓名', 'field' => 'student_name', 'edit_input' => 'text', 'config' => ['user_field' => 'name']];
$this->postJson('/api/admin/course-content-evaluation-ask/save', $payload)->assertJsonPath('config.user_field', 'name');
$payload['field'] = 'invalid_field';
$payload['config']['user_field'] = 'password';
$this->postJson('/api/admin/course-content-evaluation-ask/save', $payload)->assertJsonPath('errcode', 10001);
}
public function testInvalidQuantityAndStaleSchemaDoNotCreateReplies(): void
{
$id = $this->createQuestion();

@ -9,6 +9,24 @@ use PHPUnit\Framework\TestCase;
class SurveySchemaTest extends TestCase
{
public function testConditionReferencesAndHiddenChains(): void
{
$parent = ['id' => 1, 'field' => 'first', 'name' => '前置', 'edit_input' => 'radio', 'select_item' => [['key' => 'yes', 'value' => '是']]];
$child = ['id' => 2, 'field' => 'second', 'name' => '后续', 'edit_input' => 'radio', 'select_item' => [['key' => 'yes', 'value' => '是']], 'config' => ['visible_when' => ['field' => 'first', 'value' => 'yes']]];
$last = ['id' => 3, 'field' => 'third', 'name' => '末题', 'edit_input' => 'text', 'config' => ['visible_when' => ['field' => 'second', 'value' => 'yes']]];
SurveySchema::validateConditions([$parent, $child, $last]);
$this->assertFalse(SurveySchema::visible($last, [$parent, $child, $last], ['1' => ['value' => 'no'], '2' => ['value' => 'yes']]));
$this->assertTrue(SurveySchema::visible($last, [$parent, $child, $last], ['1' => ['value' => 'yes'], '2' => ['value' => 'yes']]));
foreach ([[$child, $parent], [$child], [array_merge($parent, ['select_item' => []]), $child]] as $invalid) {
try {
SurveySchema::validateConditions($invalid);
$this->fail('Invalid reference accepted');
} catch (InvalidArgumentException $e) {
$this->assertStringContainsString('显示条件', $e->getMessage());
}
}
}
private function questions(): array
{
return [
@ -63,6 +81,30 @@ class SurveySchemaTest extends TestCase
$this->assertSame([], $empty[2]['selected_keys']);
}
public function testConditionalIdentityAndPerOptionNotes(): void
{
$questions = [
['id' => 1, 'field' => 'insurance', 'name' => '保险', 'edit_input' => 'radio', 'select_item' => [['key' => 'yes', 'value' => '需要'], ['key' => 'no', 'value' => '不需要']]],
['id' => 2, 'field' => 'identity', 'name' => '身份证', 'edit_input' => 'text', 'need_fill' => true, 'config' => ['visible_when' => ['field' => 'insurance', 'value' => 'yes']]],
['id' => 3, 'field' => 'venue', 'name' => '会场', 'edit_input' => 'checkbox', 'config' => ['max_choices' => 2, 'option_notes' => ['tax' => []]], 'select_item' => [['key' => 'tax', 'value' => '财税'], ['key' => 'legal', 'value' => '法律'], ['key' => 'ip', 'value' => '知识产权']]],
];
$raw = [['id' => 1, 'value' => 'no'], ['id' => 2, 'value' => 'must discard'], ['id' => 3, 'value' => ['tax'], 'option_texts' => ['tax' => '咨询问题', 'legal' => '不应存储']]];
$answers = SurveySchema::normalize($questions, $raw, true);
$this->assertNull($answers[1]['value']);
$this->assertTrue($answers[1]['hidden']);
$this->assertSame(['tax' => '咨询问题'], $answers[2]['option_texts']);
$reply = SurveySchema::replyQuestions([], $answers, ['version' => 2, 'questions' => $questions]);
$this->assertTrue($reply[1]['hidden']);
$this->assertSame(['tax' => '咨询问题'], $reply[2]['option_texts']);
$raw[0]['value'] = 'yes';
$raw[1]['value'] = '';
try { SurveySchema::normalize($questions, $raw, true); $this->fail('Missing conditional answer accepted'); } catch (InvalidArgumentException $e) { $this->assertStringContainsString('身份证', $e->getMessage()); }
$raw[1]['value'] = 'valid';
$raw[2]['value'] = ['tax', 'legal', 'ip'];
$this->expectException(InvalidArgumentException::class);
SurveySchema::normalize($questions, $raw, true);
}
public function testCustomScoresAreNotLimitedToFivePointScale(): void
{
$q = $this->questions()[2];

Loading…
Cancel
Save