setName('migrate:images-to-doctor-note') ->addOption('dry-run', null, \think\console\input\Option::VALUE_NONE, '仅统计,不实际写入') ->setDescription('迁移诊单舌苔照片/检查报告到医生备注表'); } protected function execute(Input $input, Output $output) { $dryRun = $input->getOption('dry-run'); $output->writeln('开始扫描 tcm_diagnosis 中有图片数据的记录...'); $query = Db::name('tcm_diagnosis') ->whereNull('delete_time') ->where(function ($q) { $q->whereRaw("tongue_images IS NOT NULL AND tongue_images != '' AND tongue_images != '[]'") ->whereOr(function ($q2) { $q2->whereRaw("report_files IS NOT NULL AND report_files != '' AND report_files != '[]'"); }); }) ->field('id, patient_id, create_time, tongue_images, report_files'); $total = $query->count(); $output->writeln("找到 {$total} 条需要迁移的诊单记录"); if ($total === 0) { $output->writeln('无需迁移'); return; } if ($dryRun) { $output->writeln('[dry-run] 不执行实际写入'); return; } $migrated = 0; $skipped = 0; $errors = 0; $query->chunk(100, function ($rows) use ($output, &$migrated, &$skipped, &$errors) { foreach ($rows as $row) { try { $diagnosisId = (int) $row['id']; $noteDate = $row['create_time'] > 0 ? date('Y-m-d', (int) $row['create_time']) : date('Y-m-d'); $tongueImages = array_map([$this, 'toRelativePath'], $this->parseJson($row['tongue_images'])); $reportFiles = array_map([$this, 'toRelativePath'], $this->parseJson($row['report_files'])); $tongueImages = array_values(array_filter($tongueImages)); $reportFiles = array_values(array_filter($reportFiles)); if (empty($tongueImages) && empty($reportFiles)) { $skipped++; continue; } $existing = Db::name('doctor_note') ->where('diagnosis_id', $diagnosisId) ->where('note_date', $noteDate) ->whereNull('delete_time') ->find(); if ($existing) { $data = []; if (!empty($tongueImages)) { $prev = $this->parseJson($existing['tongue_images']); $merged = array_values(array_unique(array_merge($prev, $tongueImages))); $data['tongue_images'] = json_encode($merged, JSON_UNESCAPED_UNICODE); } if (!empty($reportFiles)) { $prev = $this->parseJson($existing['report_files']); $merged = array_values(array_unique(array_merge($prev, $reportFiles))); $data['report_files'] = json_encode($merged, JSON_UNESCAPED_UNICODE); } if (!empty($data)) { $data['update_time'] = time(); Db::name('doctor_note')->where('id', $existing['id'])->update($data); } } else { Db::name('doctor_note')->insert([ 'diagnosis_id' => $diagnosisId, 'doctor_id' => 0, 'note_date' => $noteDate, 'content' => null, 'tongue_images' => !empty($tongueImages) ? json_encode($tongueImages, JSON_UNESCAPED_UNICODE) : null, 'report_files' => !empty($reportFiles) ? json_encode($reportFiles, JSON_UNESCAPED_UNICODE) : null, 'create_time' => (int) $row['create_time'], 'update_time' => time(), ]); } $migrated++; } catch (\Exception $e) { $errors++; $output->writeln("诊单 ID={$row['id']} 迁移失败: {$e->getMessage()}"); } } }); $output->writeln(''); $output->writeln('迁移完成'); $output->writeln(" 成功: {$migrated}"); $output->writeln(" 跳过(空数据): {$skipped}"); $output->writeln(" 失败: {$errors}"); } private function parseJson($value): array { if (empty($value)) return []; if (is_array($value)) return $value; if (is_string($value)) { $decoded = json_decode($value, true); return is_array($decoded) ? $decoded : []; } return []; } private function toRelativePath(string $url): string { if (empty($url)) return $url; if (stripos($url, 'http://') !== 0 && stripos($url, 'https://') !== 0) { return $url; } // 只去掉当前配置的存储域名,其他域名保留完整 URL $default = \app\common\service\ConfigService::get('storage', 'default', 'local'); if ($default === 'local') { $domain = request()->domain() . '/'; } else { $storage = \app\common\service\ConfigService::get('storage', $default); $domain = $storage ? ($storage['domain'] ?? '') : ''; } if ($domain && stripos($url, rtrim($domain, '/')) === 0) { $relative = substr($url, strlen(rtrim($domain, '/'))); return ltrim($relative, '/'); } return $url; } }