'合成运动测试', 'transcript' => $text, 'uncertainties' => [], 'items' => [[ 'kind' => 'exercise', 'values' => $values, 'record_date' => $date, 'record_time' => null, 'date_text' => $date, 'time_text' => '', 'time_period' => null, 'time_estimated' => true, 'evidence' => [['text' => $text]], 'needs_review' => true, ]], ]), 'real encrypted extraction/review complete'); $detail = Store::detail($task['id']); $expect(count($detail['items']) === 1, 'one exercise item normalized'); $items = $detail['items']; $items[0]['target_id'] = $target; $items[0]['selected'] = false; $items[0]['needs_review'] = true; $draft = Store::saveDraft($task['id'], $detail['version'], $items, 1); $items = $draft['items']; $items[0]['selected'] = true; $items[0]['needs_review'] = false; $draft = Store::saveDraft($task['id'], $draft['version'], $items, 1); $result = Apply::apply($task['id'], $draft['version'], $draft['items'], 1, $f['actor']); $expect($result['status'] === 'applied', 'real Apply transaction completed'); $id = $result['applied_items'][0]['record_id']; return ExerciseRecordLogic::detail(['id' => $id]); }; try { // Mirror verified deployed nullable numeric columns AND their fact-fabricating defaults. $f['pdo']->exec('ALTER TABLE zyt_patient_exercise_record MODIFY duration INT NULL DEFAULT 0, MODIFY intensity TINYINT NULL DEFAULT 1, ADD images TEXT NULL'); $row = $make(['exercise_type' => '散步'], '2026-09-29'); echo 'EXERCISE_INSERT_RELOAD ' . json_encode(['duration' => $row['duration'], 'intensity' => $row['intensity']], JSON_UNESCAPED_UNICODE) . PHP_EOL; $expect($row['duration'] === null && $row['intensity'] === null, 'missing duration/intensity must reload NULL, never database defaults 0/1'); $expect(ExerciseRecord::find($row['id'])->append(['intensity_text'])->toArray()['intensity_text'] === '未知', 'real read label is unknown, not low intensity'); $expect(ExerciseRecordLogic::edit(['id' => $row['id'], 'record_date' => '2026-09-29', 'note' => '仅更改备注', 'duration' => null, 'intensity' => null]), 'DailyMatrix null edit accepted'); $reload = ExerciseRecordLogic::detail(['id' => $row['id']]); $expect($reload['duration'] === null && $reload['intensity'] === null && $reload['note'] === '仅更改备注', 'manual edit/reload retains nulls'); // Exercise editor uses the public controller and app Request with the global trim filter. $request = (new app\Request())->withServer(['REQUEST_METHOD' => 'POST'])->withPost([ 'id' => $row['id'], 'record_date' => '2026-09-29', 'duration' => null, 'intensity' => null, 'note' => ' 只改备注 ', ]); $controller = (new ReflectionClass(app\adminapi\controller\tcm\ExerciseRecordController::class))->newInstanceWithoutConstructor(); $property = new ReflectionProperty($controller, 'request'); $property->setAccessible(true); $property->setValue($controller, $request); $response = $controller->edit()->getData(); echo 'EXERCISE_CONTROLLER_NULL_EDIT code=' . $response['code'] . PHP_EOL; $expect($response['code'] === 1, 'real editor controller must accept JSON null through global trim filter'); $reload = ExerciseRecordLogic::detail(['id' => $row['id']]); $expect($reload['duration'] === null && $reload['intensity'] === null, 'controller edit/reload preserves null scalars'); $expect($reload['note'] === '只改备注', 'existing text trimming remains unchanged'); $known = $make(['exercise_type' => '散步', 'duration' => 30, 'intensity' => 2], '2026-09-28'); $expect((int) $known['duration'] === 30 && (int) $known['intensity'] === 2, 'explicit values retained'); $updated = $make(['note' => '只补充备注'], '2026-09-28', (int) $known['id']); $expect((int) $updated['duration'] === 30 && (int) $updated['intensity'] === 2, 'audio update omitted fields retain known values'); $durationOnly = $make(['exercise_type' => '散步', 'duration' => 20], '2026-09-27'); $expect((int) $durationOnly['duration'] === 20 && $durationOnly['intensity'] === null, 'duration only leaves intensity unknown'); $intensityOnly = $make(['exercise_type' => '散步', 'intensity' => 1], '2026-09-26'); $expect($intensityOnly['duration'] === null && (int) $intensityOnly['intensity'] === 1, 'intensity only leaves duration unknown'); $expect(ExerciseRecordLogic::edit(['id' => $row['id'], 'duration' => 0]), 'manual historical zero accepted'); $zero = ExerciseRecordLogic::detail(['id' => $row['id']]); $expect((int) $zero['duration'] === 0 && $zero['duration'] !== null, 'manual zero differs from missing; audio validator unchanged'); echo 'FOLLOWUP_AUDIO_MISSING_EXERCISE assertions=' . $checks . ' PASS real_mysql=1 actual_apply_reload=1 manual_edit_reload=1 update_omission_preserved=1' . PHP_EOL; } finally { followupAudioTestDatabaseCleanup($f); }