diff --git a/admin/src/api/patient.ts b/admin/src/api/patient.ts index d26e8929..dec564f3 100644 --- a/admin/src/api/patient.ts +++ b/admin/src/api/patient.ts @@ -16,8 +16,13 @@ export function notifyAssistant(params: { id: number }) { return request.post({ url: '/doctor.appointment/notifyAssistant', params }) } -// 医生备注 - 添加/追加(同一天追加 content + tongue_images) -export function addDoctorNote(params: { diagnosis_id: number; content?: string; tongue_images?: string[] }) { +// 医生备注 - 添加/追加(同一天追加 content + tongue_images + report_files) +export function addDoctorNote(params: { + diagnosis_id: number + content?: string + tongue_images?: string[] + report_files?: string[] +}) { return request.post({ url: '/doctor.appointment/addDoctorNote', params }) } @@ -25,3 +30,12 @@ export function addDoctorNote(params: { diagnosis_id: number; content?: string; export function getDoctorNotes(params: { diagnosis_id: number }) { return request.get({ url: '/doctor.appointment/doctorNotes', params }) } + +// 医生备注 - 删除单张图片 +export function deleteDoctorNoteImage(params: { + note_id: number + image_type: 'tongue_images' | 'report_files' + image_path: string +}) { + return request.post({ url: '/doctor.appointment/deleteDoctorNoteImage', params }) +} diff --git a/admin/src/components/chat-dialog/index.vue b/admin/src/components/chat-dialog/index.vue index 32bb5380..ed1a2f2c 100644 --- a/admin/src/components/chat-dialog/index.vue +++ b/admin/src/components/chat-dialog/index.vue @@ -135,16 +135,10 @@ import { bindCallRoom, endCall, getCallSignature, - startCall, - tcmDiagnosisDetail, - tcmDiagnosisEdit + startCall } from '@/api/tcm' import { CallLocalRecorder } from '@/utils/call-local-recorder' import { captureVideoFrameFromElement } from '@/utils/call-video-screenshot' -import { - DIAGNOSIS_TONGUE_IMAGES_UPDATED, - type DiagnosisTongueImagesUpdatedDetail -} from '@/utils/diagnosis-sync-events' import feedback from '@/utils/feedback' import { formatTUICallUserError, @@ -1274,9 +1268,8 @@ const onCallKitDragEnd = () => { document.removeEventListener('mouseup', onCallKitDragEnd) } -/** 截取当前视频画面并上传,同步到对应患者诊单的「舌苔照片」 */ +/** 截取当前视频画面并上传,保存到医生备注 */ const handleCallKitScreenshot = async () => { - // 同步防重入:避免连点触发两次上传、诊单里出现两张图 if (captureUploading.value) return captureUploading.value = true try { @@ -1305,32 +1298,8 @@ const handleCallKitScreenshot = async () => { feedback.msgWarning('请先保存诊单后再从视频同步舌苔照片') return } - const detail = await tcmDiagnosisDetail({ id: diagnosisId.value }) - if (Number(detail.patient_id) !== Number(patientId.value)) { - feedback.msgError('患者与当前诊单不匹配,无法同步') - return - } - const previous: string[] = [...((detail.tongue_images || []) as string[])] - // if (previous.length >= 9) { - // feedback.msgWarning('舌苔照片已达上限 9 张') - // return - // } - const next = [...previous, path] - const payload = JSON.parse(JSON.stringify(detail)) as Record - payload.tongue_images = next - await tcmDiagnosisEdit(payload) - feedback.msgSuccess('舌苔照片已同步到诊单') - window.dispatchEvent( - new CustomEvent(DIAGNOSIS_TONGUE_IMAGES_UPDATED, { - detail: { - diagnosisId: diagnosisId.value, - tongue_images: next - } - }) - ) - try { - await addDoctorNote({ diagnosis_id: diagnosisId.value, tongue_images: [path] }) - } catch { /* 写入备注表失败不阻塞主流程 */ } + await addDoctorNote({ diagnosis_id: diagnosisId.value, tongue_images: [path] }) + feedback.msgSuccess('舌苔照片已保存') } catch (e: unknown) { const err = e as Error console.error(err) diff --git a/admin/src/utils/diagnosis-sync-events.ts b/admin/src/utils/diagnosis-sync-events.ts deleted file mode 100644 index 9aeba387..00000000 --- a/admin/src/utils/diagnosis-sync-events.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * 跨组件同步诊单数据(如视频截屏写入舌苔照后刷新编辑抽屉) - */ -export const DIAGNOSIS_TONGUE_IMAGES_UPDATED = 'zyt:diagnosis-tongue-images-updated' - -export type DiagnosisTongueImagesUpdatedDetail = { - diagnosisId: number - tongue_images: string[] -} diff --git a/admin/src/views/patient/reception/components/NoteTimeline.vue b/admin/src/views/patient/reception/components/NoteTimeline.vue index dbe1f350..0c30c477 100644 --- a/admin/src/views/patient/reception/components/NoteTimeline.vue +++ b/admin/src/views/patient/reception/components/NoteTimeline.vue @@ -1,47 +1,108 @@ diff --git a/admin/src/views/patient/reception/index.vue b/admin/src/views/patient/reception/index.vue index 0d4c7f9b..3f70c49d 100644 --- a/admin/src/views/patient/reception/index.vue +++ b/admin/src/views/patient/reception/index.vue @@ -69,7 +69,7 @@ 已过号
- {{ row.appointment_time || '—' }} · 医助 {{ row.assistant_name || '—' }} + {{ formatTimeHM(row.appointment_time) }} · 医助 {{ row.assistant_name || '—' }}
@@ -246,9 +246,12 @@
-
医生备注 & 舌苔照片
- - +
医生备注 & 舌苔照片 & 检查报告
+
@@ -478,6 +481,11 @@ const formatGenderAge = (row: QueueRow) => { return parts.filter((p) => p && p !== '—').join(' ') } +const formatTimeHM = (time?: string) => { + if (!time) return '—' + return time.replace(/^(\d{2}:\d{2})(:\d{2})?$/, '$1') +} + const formatPeriod = (period?: string) => { if (period === 'morning') return '上午' if (period === 'afternoon') return '下午' @@ -938,6 +946,7 @@ onUnmounted(() => { gap: 14px; flex: 1; min-height: 0; + align-items: flex-start; } .queue-col { width: 288px; @@ -952,7 +961,6 @@ onUnmounted(() => { flex-direction: column; gap: 10px; height: calc(100vh - 260px); - min-height: 480px; } .queue-head { display: flex; @@ -980,7 +988,7 @@ onUnmounted(() => { .queue-row { border: 1px solid var(--rx-line); border-radius: 8px; - padding: 10px; + padding: 12px 10px; display: flex; justify-content: space-between; align-items: center; @@ -1011,6 +1019,7 @@ onUnmounted(() => { display: flex; align-items: baseline; gap: 6px; + flex-wrap: wrap; } .name-meta { font-size: 12px; @@ -1018,7 +1027,7 @@ onUnmounted(() => { font-weight: 400; } .time-line { - margin-top: 2px; + margin-top: 4px; font-size: 12px; color: var(--rx-soft); font-family: var(--rx-font-data); diff --git a/admin/src/views/tcm/diagnosis/add.vue b/admin/src/views/tcm/diagnosis/add.vue index 2a871ef3..a0521f07 100644 --- a/admin/src/views/tcm/diagnosis/add.vue +++ b/admin/src/views/tcm/diagnosis/add.vue @@ -417,23 +417,6 @@
诊断详情
- - -
支持上传多张舌苔照片,最多9张
-
- - - -
支持上传检查报告、病历、彩超等影像科检查图片,最多10个文件
-
@@ -549,8 +532,6 @@ const formData = ref({ allergy_history: 0, family_history: 0, pregnancy_history: 0, - tongue_images: [], - report_files: [], symptoms: '', tongue_coating: '', pulse: '', diff --git a/admin/src/views/tcm/diagnosis/edit.vue b/admin/src/views/tcm/diagnosis/edit.vue index df1addb3..56f3ca3b 100644 --- a/admin/src/views/tcm/diagnosis/edit.vue +++ b/admin/src/views/tcm/diagnosis/edit.vue @@ -493,25 +493,6 @@ 诊断信息 - - -
支持上传多张舌苔照片,最多9张
-
- - - -
支持上传检查报告、病历、彩超等影像科检查图片,最多10个文件
-
@@ -529,8 +510,11 @@
- - +
@@ -567,40 +551,6 @@
- -
- 诊断信息 - - -
舌苔照片
-
- -
- 暂无 -
- -
检查报告
-
- -
- 暂无 -
-
-
{ const diagNotes = ref([]) +const fetchDiagNotes = async () => { + if (!formData.value.id) return + try { + diagNotes.value = await getDoctorNotes({ diagnosis_id: Number(formData.value.id) }) || [] + } catch { diagNotes.value = [] } +} + watch(() => activeTab.value, async (tab) => { if (tab === 'doctorNote' && formData.value.id) { - try { - diagNotes.value = await getDoctorNotes({ diagnosis_id: Number(formData.value.id) }) || [] - } catch { diagNotes.value = [] } + await fetchDiagNotes() } }) @@ -772,21 +723,6 @@ watch([visible, activeTab], () => { } }) -function onTongueImagesSyncedFromCall(e: Event) { - if (!visible.value) return - const d = (e as CustomEvent).detail - if (!d?.diagnosisId || !Array.isArray(d.tongue_images)) return - if (Number(formData.value.id) !== Number(d.diagnosisId)) return - formData.value.tongue_images = [...d.tongue_images] -} - -onMounted(() => { - window.addEventListener(DIAGNOSIS_TONGUE_IMAGES_UPDATED, onTongueImagesSyncedFromCall as EventListener) -}) - -onUnmounted(() => { - window.removeEventListener(DIAGNOSIS_TONGUE_IMAGES_UPDATED, onTongueImagesSyncedFromCall as EventListener) -}) const formData = ref({ id: '', @@ -831,8 +767,6 @@ const formData = ref({ family_history: 0, pregnancy_history: 0, assistant_id: undefined as number | undefined, - tongue_images: [] as string[], - report_files: [], symptoms: '', tongue_coating: '', pulse: '', @@ -1263,8 +1197,6 @@ const handleClose = () => { family_history: 0, pregnancy_history: 0, assistant_id: undefined as number | undefined, - tongue_images: [] as string[], - report_files: [], symptoms: '', tongue_coating: '', pulse: '', diff --git a/server/app/adminapi/controller/doctor/AppointmentController.php b/server/app/adminapi/controller/doctor/AppointmentController.php index d3730db6..5b067590 100644 --- a/server/app/adminapi/controller/doctor/AppointmentController.php +++ b/server/app/adminapi/controller/doctor/AppointmentController.php @@ -147,4 +147,18 @@ class AppointmentController extends BaseAdminController $params = (new AppointmentValidate())->goCheck('doctorNotes'); return $this->data(DoctorNoteLogic::getByDiagnosis((int) $params['diagnosis_id'])); } + + public function deleteDoctorNoteImage() + { + $params = (new AppointmentValidate())->post()->goCheck('deleteDoctorNoteImage'); + $result = DoctorNoteLogic::deleteImage( + (int) $params['note_id'], + $params['image_type'], + $params['image_path'] + ); + if ($result === false) { + return $this->fail(DoctorNoteLogic::getError()); + } + return $this->success('删除成功'); + } } diff --git a/server/app/adminapi/logic/doctor/DoctorNoteLogic.php b/server/app/adminapi/logic/doctor/DoctorNoteLogic.php index a7fe9f36..6f52694e 100644 --- a/server/app/adminapi/logic/doctor/DoctorNoteLogic.php +++ b/server/app/adminapi/logic/doctor/DoctorNoteLogic.php @@ -25,8 +25,8 @@ class DoctorNoteLogic extends BaseLogic ->find(); $newContent = trim($params['content'] ?? ''); - $newImages = self::parseJsonArray($params['tongue_images'] ?? []); - $newReports = self::parseJsonArray($params['report_files'] ?? []); + $newImages = array_map([self::class, 'toRelativePath'], self::parseJsonArray($params['tongue_images'] ?? [])); + $newReports = array_map([self::class, 'toRelativePath'], self::parseJsonArray($params['report_files'] ?? [])); if ($existing) { $data = []; @@ -108,6 +108,91 @@ class DoctorNoteLogic extends BaseLogic } } + /** + * 删除备注中的单张图片 + */ + public static function deleteImage(int $noteId, string $imageType, string $imagePath): bool + { + try { + $note = DoctorNote::where('id', $noteId)->whereNull('delete_time')->find(); + if (!$note) { + self::setError('记录不存在'); + return false; + } + if (!in_array($imageType, ['tongue_images', 'report_files'])) { + self::setError('类型无效'); + return false; + } + $images = self::parseJsonArray($note->$imageType); + // 统一转为相对路径再匹配 + $targetPath = self::toRelativePath($imagePath); + $images = array_values(array_filter($images, fn($url) => self::toRelativePath($url) !== $targetPath)); + $note->$imageType = empty($images) ? null : json_encode($images, JSON_UNESCAPED_UNICODE); + $note->save(); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + /** + * 聚合某诊单所有备注中的图片(供 DiagnosisLogic::detail 使用) + */ + public static function getAggregatedImages(int $diagnosisId): array + { + $records = DoctorNote::where('diagnosis_id', $diagnosisId) + ->whereNull('delete_time') + ->select(); + + $tongueImages = []; + $reportFiles = []; + foreach ($records as $r) { + $tongueImages = array_merge($tongueImages, self::parseJsonArray($r->tongue_images)); + $reportFiles = array_merge($reportFiles, self::parseJsonArray($r->report_files)); + } + + return [ + 'tongue_images' => array_map( + fn($u) => empty($u) ? $u : FileService::getFileUrl($u), + array_values(array_unique($tongueImages)) + ), + 'report_files' => array_map( + fn($u) => empty($u) ? $u : FileService::getFileUrl($u), + array_values(array_unique($reportFiles)) + ), + ]; + } + + /** + * 如果 URL 的域名是当前配置的存储域名则去掉,否则原样保留 + */ + private static function toRelativePath(string $url): string + { + if (empty($url)) return $url; + if (stripos($url, 'http://') !== 0 && stripos($url, 'https://') !== 0) { + return $url; + } + // 获取当前存储域名 + $domain = self::getStorageDomain(); + if ($domain && stripos($url, rtrim($domain, '/')) === 0) { + $relative = substr($url, strlen(rtrim($domain, '/'))); + return ltrim($relative, '/'); + } + // 非当前存储域名,保留完整 URL + return $url; + } + + private static function getStorageDomain(): string + { + $default = \app\common\service\ConfigService::get('storage', 'default', 'local'); + if ($default === 'local') { + return request()->domain() . '/'; + } + $storage = \app\common\service\ConfigService::get('storage', $default); + return $storage ? ($storage['domain'] ?? '') : ''; + } + private static function parseJsonArray($value): array { if (is_array($value)) return $value; diff --git a/server/app/adminapi/logic/tcm/DiagnosisLogic.php b/server/app/adminapi/logic/tcm/DiagnosisLogic.php index ba2a0591..7aa7e25d 100644 --- a/server/app/adminapi/logic/tcm/DiagnosisLogic.php +++ b/server/app/adminapi/logic/tcm/DiagnosisLogic.php @@ -80,23 +80,19 @@ class DiagnosisLogic extends BaseLogic } } - // 处理舌苔照片(JSON数组) - if (isset($params['tongue_images']) && is_array($params['tongue_images'])) { - $params['tongue_images'] = json_encode($params['tongue_images'], JSON_UNESCAPED_UNICODE); - } - - // 处理检查报告(JSON数组) - if (isset($params['report_files']) && is_array($params['report_files'])) { - $params['report_files'] = json_encode($params['report_files'], JSON_UNESCAPED_UNICODE); - } - + // 图片字段迁移至 doctor_note,从 params 中提取后再写入备注表 + $newTongueImages = isset($params['tongue_images']) && is_array($params['tongue_images']) + ? $params['tongue_images'] : []; + $newReportFiles = isset($params['report_files']) && is_array($params['report_files']) + ? $params['report_files'] : []; + unset($params['tongue_images'], $params['report_files']); + // 处理诊断日期 if (isset($params['diagnosis_date'])) { $params['diagnosis_date'] = strtotime($params['diagnosis_date']); } // 表未增加 admin_id 列时勿写入,避免 SQL 报错(见 sql/1.9.20260421/add_tcm_diagnosis_admin_id.sql) - // 注意:Model::getConnection() 在 think-orm 中返回连接名(string),须用 Db 查询对象取字段 if (array_key_exists('admin_id', $params)) { $fieldNames = Db::name('tcm_diagnosis')->getTableFields(); if (! is_array($fieldNames) || ! in_array('admin_id', $fieldNames, true)) { @@ -105,10 +101,19 @@ class DiagnosisLogic extends BaseLogic } $model = Diagnosis::create($params); - + + // 图片写入 doctor_note 表 + if (!empty($newTongueImages) || !empty($newReportFiles)) { + DoctorNoteLogic::addOrAppend([ + 'diagnosis_id' => (int) $model->id, + 'tongue_images' => $newTongueImages, + 'report_files' => $newReportFiles, + ]); + } + // 自动为患者创建 TRTC 账号 self::createPatientTrtcAccount($model->id); - + return $model->id; } catch (\Exception $e) { self::setError($e->getMessage()); @@ -179,46 +184,16 @@ class DiagnosisLogic extends BaseLogic } } - // 处理舌苔照片(JSON数组) - if (isset($params['tongue_images']) && is_array($params['tongue_images'])) { - $params['tongue_images'] = json_encode($params['tongue_images'], JSON_UNESCAPED_UNICODE); - } - - // 处理检查报告(JSON数组) - if (isset($params['report_files']) && is_array($params['report_files'])) { - $params['report_files'] = json_encode($params['report_files'], JSON_UNESCAPED_UNICODE); - } + // 图片字段已迁移至 doctor_note 表管理,不再写入 tcm_diagnosis + unset($params['tongue_images'], $params['report_files']); // 处理诊断日期 if (isset($params['diagnosis_date'])) { $params['diagnosis_date'] = strtotime($params['diagnosis_date']); } - // 保存前记录旧的检查报告,用于对比新增 - $oldReportFiles = []; - if (isset($params['report_files'])) { - $old = Diagnosis::where('id', $params['id'])->value('report_files'); - if ($old) { - $oldReportFiles = is_string($old) ? (json_decode($old, true) ?: []) : (array) $old; - } - } - Diagnosis::update($params); - // 检查报告有新增时同步到医生备注 - if (isset($params['report_files'])) { - $newReportFiles = is_string($params['report_files']) - ? (json_decode($params['report_files'], true) ?: []) - : (array) $params['report_files']; - $added = array_values(array_diff($newReportFiles, $oldReportFiles)); - if (!empty($added)) { - DoctorNoteLogic::addOrAppend([ - 'diagnosis_id' => (int) $params['id'], - 'report_files' => $added, - ]); - } - } - // 如果是编辑,也确保患者有 TRTC 账号 $diagnosis = Diagnosis::find($params['id']); if ($diagnosis) { @@ -280,33 +255,36 @@ class DiagnosisLogic extends BaseLogic } - if (!empty($diagnosis['tongue_images'])) { - $files = json_decode($diagnosis['tongue_images'], true); - if (is_array($files)) { - $diagnosis['tongue_images'] = $files; - } else { - $diagnosis['tongue_images'] = array_filter(explode(',', $diagnosis['tongue_images'])) ?: []; - } - $diagnosis['tongue_images'] = array_map(function($url) { - return empty($url) ? $url : FileService::getFileUrl($url); - }, $diagnosis['tongue_images']); + // 从 doctor_note 聚合图片(已迁移至备注表统一管理) + $noteImages = DoctorNoteLogic::getAggregatedImages((int) $diagnosis['id']); + if (!empty($noteImages['tongue_images']) || !empty($noteImages['report_files'])) { + $diagnosis['tongue_images'] = $noteImages['tongue_images']; + $diagnosis['report_files'] = $noteImages['report_files']; } else { - $diagnosis['tongue_images'] = []; + // Fallback: 未迁移的旧数据仍从 tcm_diagnosis 读取 + if (!empty($diagnosis['tongue_images'])) { + $files = json_decode($diagnosis['tongue_images'], true); + if (!is_array($files)) { + $files = array_filter(explode(',', $diagnosis['tongue_images'])) ?: []; + } + $diagnosis['tongue_images'] = array_map(function($url) { + return empty($url) ? $url : FileService::getFileUrl($url); + }, $files); + } else { + $diagnosis['tongue_images'] = []; + } + if (!empty($diagnosis['report_files'])) { + $files = json_decode($diagnosis['report_files'], true); + if (!is_array($files)) { + $files = array_filter(explode(',', $diagnosis['report_files'])) ?: []; + } + $diagnosis['report_files'] = array_map(function($url) { + return empty($url) ? $url : FileService::getFileUrl($url); + }, $files); + } else { + $diagnosis['report_files'] = []; + } } - - if (!empty($diagnosis['report_files'])) { - $files = json_decode($diagnosis['report_files'], true); - if (is_array($files)) { - $diagnosis['report_files'] = $files; - } else { - $diagnosis['report_files'] = array_filter(explode(',', $diagnosis['report_files'])) ?: []; - } - $diagnosis['report_files'] = array_map(function($url) { - return empty($url) ? $url : FileService::getFileUrl($url); - }, $diagnosis['report_files']); - } else { - $diagnosis['report_files'] = []; -} // 处理诊断日期格式 if (!empty($diagnosis['diagnosis_date'])) { $diagnosis['diagnosis_date'] = date('Y-m-d', $diagnosis['diagnosis_date']); @@ -2788,12 +2766,13 @@ class DiagnosisLogic extends BaseLogic $params[$field] = implode(',', $params[$field]); } } - if (isset($params['tongue_images']) && is_array($params['tongue_images'])) { - $params['tongue_images'] = json_encode($params['tongue_images'], JSON_UNESCAPED_UNICODE); - } - if (isset($params['report_files']) && is_array($params['report_files'])) { - $params['report_files'] = json_encode($params['report_files'], JSON_UNESCAPED_UNICODE); - } + // 图片字段迁移至 doctor_note 表 + $cardTongueImages = isset($params['tongue_images']) && is_array($params['tongue_images']) + ? $params['tongue_images'] : []; + $cardReportFiles = isset($params['report_files']) && is_array($params['report_files']) + ? $params['report_files'] : []; + unset($params['tongue_images'], $params['report_files']); + if (isset($params['diagnosis_date'])) { $params['diagnosis_date'] = is_numeric($params['diagnosis_date']) ? $params['diagnosis_date'] : strtotime($params['diagnosis_date']); } @@ -2801,6 +2780,15 @@ class DiagnosisLogic extends BaseLogic $model = Diagnosis::create($params); self::createPatientTrtcAccount($model->patient_id); + // 图片写入 doctor_note + if (!empty($cardTongueImages) || !empty($cardReportFiles)) { + DoctorNoteLogic::addOrAppend([ + 'diagnosis_id' => (int) $model->id, + 'tongue_images' => $cardTongueImages, + 'report_files' => $cardReportFiles, + ]); + } + // 关联 diagnosis_view_records,便于用户通过 User::with('diagnosis') 获取就诊卡 if ($userId) { $now = time(); @@ -2906,8 +2894,6 @@ class DiagnosisLogic extends BaseLogic // 临床信息 'symptoms' => $params['symptoms'] ?? '', 'tongue_coating' => $params['tongue_coating'] ?? '', - 'tongue_images' => $params['tongue_images'] ?? '', - 'report_files' => $params['report_files'] ?? '', 'pulse' => $params['pulse'] ?? '', 'treatment_principle' => $params['treatment_principle'] ?? '', 'prescription' => $params['prescription'] ?? '', @@ -2920,14 +2906,25 @@ class DiagnosisLogic extends BaseLogic if (!empty($params['diagnosis_date'])) { $updateData['diagnosis_date'] = strtotime($params['diagnosis_date']); } elseif (!empty($params['local_hospital_visit_date'])) { - // 如果没有diagnosis_date但有local_hospital_visit_date,则使用local_hospital_visit_date $updateData['diagnosis_date'] = strtotime($params['local_hospital_visit_date']); } - - + // 执行更新 $diagnosis->save($updateData); + // 图片写入 doctor_note 表 + $cardTongueImages = isset($params['tongue_images']) && is_array($params['tongue_images']) + ? $params['tongue_images'] : []; + $cardReportFiles = isset($params['report_files']) && is_array($params['report_files']) + ? $params['report_files'] : []; + if (!empty($cardTongueImages) || !empty($cardReportFiles)) { + DoctorNoteLogic::addOrAppend([ + 'diagnosis_id' => (int) $diagnosisId, + 'tongue_images' => $cardTongueImages, + 'report_files' => $cardReportFiles, + ]); + } + \think\facade\Log::info('诊单更新成功 - diagnosis_id: ' . $diagnosisId . ', patient_id: ' . $patientId); return true; diff --git a/server/app/adminapi/validate/doctor/AppointmentValidate.php b/server/app/adminapi/validate/doctor/AppointmentValidate.php index 7956a79a..bce93c5e 100644 --- a/server/app/adminapi/validate/doctor/AppointmentValidate.php +++ b/server/app/adminapi/validate/doctor/AppointmentValidate.php @@ -25,6 +25,9 @@ class AppointmentValidate extends BaseValidate 'appointment_time' => 'require', 'appointment_type' => 'in:video,text,phone', 'channel_source' => 'require', + 'note_id' => 'require|integer', + 'image_type' => 'require|in:tongue_images,report_files', + 'image_path' => 'require', ]; /** @@ -115,4 +118,9 @@ class AppointmentValidate extends BaseValidate { return $this->only(['diagnosis_id']); } + + public function sceneDeleteDoctorNoteImage() + { + return $this->only(['note_id', 'image_type', 'image_path']); + } } diff --git a/server/app/adminapi/validate/tcm/DiagnosisValidate.php b/server/app/adminapi/validate/tcm/DiagnosisValidate.php index 422d22ba..0c535a6a 100644 --- a/server/app/adminapi/validate/tcm/DiagnosisValidate.php +++ b/server/app/adminapi/validate/tcm/DiagnosisValidate.php @@ -64,7 +64,7 @@ class DiagnosisValidate extends BaseValidate public function sceneEdit() { - return $this->only(['id', 'patient_name', 'id_card', 'phone', 'gender', 'age', 'diagnosis_date', 'diagnosis_type', 'syndrome_type', 'marital_status', 'height', 'weight', 'region', 'systolic_pressure', 'diastolic_pressure', 'fasting_blood_sugar', 'diabetes_discovery_year', 'local_hospital_diagnosis', 'local_hospital_name', 'past_history', 'symptoms', 'tongue_coating', 'pulse', 'treatment_principle', 'prescription', 'doctor_advice', 'remark', 'current_medications', 'status', 'tongue_images', 'report_files']); + return $this->only(['id', 'patient_name', 'id_card', 'phone', 'gender', 'age', 'diagnosis_date', 'diagnosis_type', 'syndrome_type', 'marital_status', 'height', 'weight', 'region', 'systolic_pressure', 'diastolic_pressure', 'fasting_blood_sugar', 'diabetes_discovery_year', 'local_hospital_diagnosis', 'local_hospital_name', 'past_history', 'symptoms', 'tongue_coating', 'pulse', 'treatment_principle', 'prescription', 'doctor_advice', 'remark', 'current_medications', 'status']); } public function sceneId() diff --git a/server/app/command/MigrateImagesToDoctorNote.php b/server/app/command/MigrateImagesToDoctorNote.php new file mode 100644 index 00000000..29588916 --- /dev/null +++ b/server/app/command/MigrateImagesToDoctorNote.php @@ -0,0 +1,164 @@ +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; + } +} diff --git a/server/config/console.php b/server/config/console.php index f2e94278..18f5121e 100644 --- a/server/config/console.php +++ b/server/config/console.php @@ -30,5 +30,7 @@ return [ 'qywx:sync-msg-archive' => 'app\\command\\QywxSyncMsgArchive', // 甘草订单物流路由同步(GET_TASK_ROUTE_LIST) 'gancao:sync-logistics' => 'app\\command\\GancaoSyncLogisticsRoute', + // 迁移诊单图片到医生备注表 + 'migrate:images-to-doctor-note' => 'app\\command\\MigrateImagesToDoctorNote', ], ]; diff --git a/server/sql/1.9.20260430/add_delete_note_image_menu.sql b/server/sql/1.9.20260430/add_delete_note_image_menu.sql new file mode 100644 index 00000000..68889202 --- /dev/null +++ b/server/sql/1.9.20260430/add_delete_note_image_menu.sql @@ -0,0 +1,6 @@ +-- 新增「删除备注图片」按钮权限 + +SET @reception_menu_id = (SELECT id FROM system_menu WHERE perms = 'doctor.appointment/reception' LIMIT 1); + +INSERT INTO system_menu (pid, type, name, icon, sort, perms, paths, component, selected, params, is_cache, is_show, is_disable, create_time, update_time, delete_time) +VALUES (@reception_menu_id, 'A', '删除备注图片', '', 5, 'doctor.appointment/deleteDoctorNoteImage', '', '', '', '', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), NULL);