request->get('patient_id'); if (!$patientId) { return $this->fail('患者ID不能为空'); } // 调用逻辑层 $result = DiagnosisLogic::getPatientSignature($patientId); if ($result === false) { return $this->fail(DiagnosisLogic::getError()); } return $this->data($result); } /** * @notes 患者挂断视频:后端 DeleteCloudRecording + 结束通话记录(与诊单 patient_id 校验) * @return \think\response\Json */ public function patientHangupVideo() { $params = [ 'diagnosis_id' => (int)$this->request->post('diagnosis_id', 0), 'patient_id' => (int)$this->request->post('patient_id', 0), 'room_id' => trim((string)$this->request->post('room_id', '')), ]; if ($params['patient_id'] <= 0) { return $this->fail('患者ID不能为空'); } if ($params['diagnosis_id'] <= 0 && $params['room_id'] === '') { return $this->fail('诊单ID或房间号须至少传一项'); } $result = DiagnosisLogic::patientHangupVideoCall($params); if ($result === false) { return $this->fail(DiagnosisLogic::getError()); } return $this->success('已结束通话'); } /** * @notes 获取诊单详情(供小程序调用) * @return \think\response\Json */ public function diagnosisDetail() { $params = [ 'id' => $this->request->get('id'), 'user_id' => $this->userId, 'share_user_id' => $this->request->get('share_user_id', 0) ]; if (!$params['id']) { return $this->fail('诊单ID不能为空'); } try { $result = DiagnosisLogic::diagnosisDetailWithRecord($params); if ($result === false) { return $this->fail(DiagnosisLogic::getError()); } return $this->data($result); } catch (\Exception $e) { return $this->fail($e->getMessage()); } } /** * @notes 确认诊单(供小程序调用) * @return \think\response\Json */ public function confirmDiagnosis() { $params = [ 'id' => $this->request->post('id'), 'user_id' => $this->userId, 'share_user_id' => $this->request->post('share_user', 0) ]; if (!$params['id']) { return $this->fail('诊单ID不能为空'); } try { $result = DiagnosisLogic::confirmDiagnosisRecord($params); if ($result === false) { return $this->fail(DiagnosisLogic::getError()); } return $this->success('确认成功'); } catch (\Exception $e) { return $this->fail($e->getMessage()); } } /** * @notes 获取字典数据(供小程序调用) * @return \think\response\Json */ public function getDict() { $type = $this->request->get('type', ''); if (!$type) { return $this->fail('字典类型不能为空'); } try { $data = ConfigLogic::getDictByType($type); return $this->data($data); } catch (\Exception $e) { return $this->fail($e->getMessage()); } } /** * @notes 获取就诊卡列表(供小程序调用) * @return \think\response\Json */ public function getCardList() { $user_id =$this->userId; if (!$user_id) { return $this->fail('患者ID不能为空'); } try { $result = DiagnosisLogic::getCardList($user_id); if ($result === false) { return $this->fail(DiagnosisLogic::getError()); } return $this->data($result); } catch (\Exception $e) { return $this->fail($e->getMessage()); } } /** * @notes 新建就诊卡(供小程序调用) * @return \think\response\Json */ public function addCard() { $params = [ 'user_id' => $this->userId, 'patient_id' => $this->request->post('patient_id'), 'patient_name' => $this->request->post('patient_name'), 'gender' => $this->request->post('gender'), 'age' => $this->request->post('age'), 'id_card' => $this->request->post('id_card'), 'height' => $this->request->post('height'), 'weight' => $this->request->post('weight'), 'systolic_pressure' => $this->request->post('systolic_pressure'), 'diastolic_pressure' => $this->request->post('diastolic_pressure'), 'fasting_blood_sugar' => $this->request->post('fasting_blood_sugar'), 'diabetes_discovery_year' => $this->request->post('diabetes_discovery_year'), 'local_hospital_diagnosis' => $this->request->post('local_hospital_diagnosis'), 'local_hospital_name' => $this->request->post('local_hospital_name'), 'local_hospital_visit_date' => $this->request->post('local_hospital_visit_date'), 'diagnosis_date' => $this->request->post('diagnosis_date'), 'diagnosis_type' => $this->request->post('diagnosis_type'), 'syndrome_type' => $this->request->post('syndrome_type'), 'diabetes_type' => $this->request->post('diabetes_type'), 'appetite' => $this->request->post('appetite'), 'water_intake' => $this->request->post('water_intake'), 'diet_condition' => $this->request->post('diet_condition'), 'weight_change' => $this->request->post('weight_change'), 'body_feeling' => $this->request->post('body_feeling'), 'sleep_condition' => $this->request->post('sleep_condition'), 'eye_condition' => $this->request->post('eye_condition'), 'head_feeling' => $this->request->post('head_feeling'), 'sweat_condition' => $this->request->post('sweat_condition'), 'skin_condition' => $this->request->post('skin_condition'), 'urine_condition' => $this->request->post('urine_condition'), 'stool_condition' => $this->request->post('stool_condition'), 'kidney_condition' => $this->request->post('kidney_condition'), 'fatty_liver_degree' => $this->request->post('fatty_liver_degree'), 'past_history' => $this->request->post('past_history'), 'trauma_history' => $this->request->post('trauma_history'), 'surgery_history' => $this->request->post('surgery_history'), 'allergy_history' => $this->request->post('allergy_history'), 'family_history' => $this->request->post('family_history'), 'pregnancy_history' => $this->request->post('pregnancy_history'), 'symptoms' => $this->request->post('symptoms'), 'tongue_coating' => $this->request->post('tongue_coating'), 'tongue_images' => $this->request->post('tongue_images'), 'report_files' => $this->request->post('report_files'), 'pulse' => $this->request->post('pulse'), 'treatment_principle' => $this->request->post('treatment_principle'), 'prescription' => $this->request->post('prescription'), 'doctor_advice' => $this->request->post('doctor_advice'), 'remark' => $this->request->post('remark'), 'create_source' => DailyPhoneLogic::CREATE_SOURCE_MNP, ]; // patient_id 可选:不传则创建时自动生成(新患者首张就诊卡) try { $result = DiagnosisLogic::addCard($params); if ($result === false) { return $this->fail(DiagnosisLogic::getError()); } return $this->success('创建成功', $result); } catch (\Exception $e) { return $this->fail($e->getMessage()); } } /** * @notes 更新就诊卡(供小程序调用) * @return \think\response\Json */ public function updateCard() { $params = [ 'id' => $this->request->post('id'), 'patient_id' => $this->request->post('patient_id'), 'patient_name' => $this->request->post('patient_name'), 'gender' => $this->request->post('gender'), 'age' => $this->request->post('age'), 'id_card' => $this->request->post('id_card'), // 生命体征 'height' => $this->request->post('height'), 'weight' => $this->request->post('weight'), 'systolic_pressure' => $this->request->post('systolic_pressure'), 'diastolic_pressure' => $this->request->post('diastolic_pressure'), 'fasting_blood_sugar' => $this->request->post('fasting_blood_sugar'), // 主诉 'diabetes_discovery_year' => $this->request->post('diabetes_discovery_year'), 'local_hospital_diagnosis' => $this->request->post('local_hospital_diagnosis'), 'local_hospital_name' => $this->request->post('local_hospital_name'), 'local_hospital_visit_date' => $this->request->post('local_hospital_visit_date'), // 诊断信息 'diagnosis_date' => $this->request->post('diagnosis_date'), 'diagnosis_type' => $this->request->post('diagnosis_type'), 'syndrome_type' => $this->request->post('syndrome_type'), 'diabetes_type' => $this->request->post('diabetes_type'), // 现病史字段 'appetite' => $this->request->post('appetite'), 'water_intake' => $this->request->post('water_intake'), 'diet_condition' => $this->request->post('diet_condition'), 'weight_change' => $this->request->post('weight_change'), 'body_feeling' => $this->request->post('body_feeling'), 'sleep_condition' => $this->request->post('sleep_condition'), 'eye_condition' => $this->request->post('eye_condition'), 'head_feeling' => $this->request->post('head_feeling'), 'sweat_condition' => $this->request->post('sweat_condition'), 'skin_condition' => $this->request->post('skin_condition'), 'urine_condition' => $this->request->post('urine_condition'), 'stool_condition' => $this->request->post('stool_condition'), 'kidney_condition' => $this->request->post('kidney_condition'), 'fatty_liver_degree' => $this->request->post('fatty_liver_degree'), // 既往史字段 'past_history' => $this->request->post('past_history'), 'trauma_history' => $this->request->post('trauma_history'), 'surgery_history' => $this->request->post('surgery_history'), 'allergy_history' => $this->request->post('allergy_history'), 'family_history' => $this->request->post('family_history'), 'pregnancy_history' => $this->request->post('pregnancy_history'), // 临床信息 'symptoms' => $this->request->post('symptoms'), 'tongue_coating' => $this->request->post('tongue_coating'), 'tongue_images' => $this->request->post('tongue_images'), 'report_files' => $this->request->post('report_files'), 'pulse' => $this->request->post('pulse'), 'treatment_principle' => $this->request->post('treatment_principle'), 'prescription' => $this->request->post('prescription'), 'doctor_advice' => $this->request->post('doctor_advice'), 'remark' => $this->request->post('remark') ]; if (!$params['id']) { return $this->fail('诊单ID不能为空'); } if (!$params['patient_id']) { return $this->fail('患者ID不能为空'); } try { $result = DiagnosisLogic::updateCard($params); if ($result === false) { return $this->fail(DiagnosisLogic::getError()); } return $this->success('更新成功'); } catch (\Exception $e) { return $this->fail($e->getMessage()); } } /** * @notes 日常记录窗口数据(血糖血压 / 饮食 / 运动)—— 患者端 * * 路由:GET /api/tcm/dailyTrackingWindow?diagnosis_id=:id&patient_id=:pid&start_date=&end_date= * 说明:供小程序「日常记录」页面拉取一段时间内的跟踪数据,复用后台 fetchTrackingWindow。 * * @return \think\response\Json */ public function dailyTrackingWindow() { $diagnosisId = (int) $this->request->get('diagnosis_id', 0); $patientId = (int) $this->request->get('patient_id', 0); $startDate = (string) $this->request->get('start_date', ''); $endDate = (string) $this->request->get('end_date', ''); $check = $this->ensurePatientOwnsDiagnosis($diagnosisId); if (!$check['ok']) { return $this->fail($check['error']); } $diagnosis = $check['diagnosis']; try { if ($patientId > 0 && (int) $diagnosis['patient_id'] !== $patientId) { return $this->fail('无权查看该诊单的日常记录'); } $data = DiagnosisLogic::fetchTrackingWindow($diagnosisId, $startDate, $endDate); // 把诊断的基础信息一并返回,便于小程序展示头部摘要 $data['diagnosis'] = [ 'id' => (int) $diagnosis['id'], 'patient_id' => (int) $diagnosis['patient_id'], 'patient_name' => (string) ($diagnosis['patient_name'] ?? ''), 'age' => (int) ($diagnosis['age'] ?? 0), 'gender' => (int) ($diagnosis['gender'] ?? 0), ]; return $this->data($data); } catch (\Exception $e) { return $this->fail($e->getMessage()); } } /** * @notes 日常记录-跟踪备注列表(患者端) * * 路由:GET /api/tcm/dailyTrackingNotes?diagnosis_id=:id&patient_id=:pid * * @return \think\response\Json */ public function dailyTrackingNotes() { $diagnosisId = (int) $this->request->get('diagnosis_id', 0); $patientId = (int) $this->request->get('patient_id', 0); $check = $this->ensurePatientOwnsDiagnosis($diagnosisId); if (!$check['ok']) { return $this->fail($check['error']); } $diagnosis = $check['diagnosis']; try { if ($patientId > 0 && (int) $diagnosis['patient_id'] !== $patientId) { return $this->fail('无权查看该诊单的跟踪备注'); } $notes = TrackingNoteLogic::getByDiagnosis($diagnosisId); return $this->data($notes); } catch (\Exception $e) { return $this->fail($e->getMessage()); } } /** * 解析小程序 form-urlencoded 提交的 JSON 字段 * * @param mixed $raw * @param string $arrayKey ThinkPHP 数组字段名,如 badges/a * @return array */ private function decodeJsonPostField($raw, string $arrayKey): array { if (is_string($raw) && $raw !== '') { $decoded = json_decode($raw, true); if (is_array($decoded)) { return $decoded; } } if (is_array($raw)) { return $raw; } $arr = $this->request->post($arrayKey, []); return is_array($arr) ? $arr : []; } /** * @notes 校验当前登录患者是否拥有指定诊单(通过 diagnosis_view_records 关联) * @param int $diagnosisId * @return array{ok:bool,diagnosis?:\app\common\model\tcm\Diagnosis,error?:string} */ private function ensurePatientOwnsDiagnosis(int $diagnosisId): array { if (!$this->userId) { return ['ok' => false, 'error' => '请先登录']; } if ($diagnosisId <= 0) { return ['ok' => false, 'error' => '诊单ID不能为空']; } $diagnosis = Diagnosis::where('id', $diagnosisId)->find(); if (!$diagnosis) { return ['ok' => false, 'error' => '诊单不存在']; } if ((int) ($diagnosis['show_card'] ?? 1) !== 1) { return ['ok' => false, 'error' => '该就诊卡已在统计端隐藏']; } // 通过 diagnosis_view_records 校验当前小程序用户与该诊单的归属关系 // 该表是「就诊卡列表」的来源(参考 DiagnosisLogic::getCardList) $owned = \think\facade\Db::name('diagnosis_view_records') ->where('user_id', $this->userId) ->where('diagnosis_id', $diagnosisId) ->where('delete_time', null) ->find(); if (!$owned) { return ['ok' => false, 'error' => '无权操作该诊单']; } return ['ok' => true, 'diagnosis' => $diagnosis]; } /** * @notes 日常记录-手机号快捷建档(无就诊卡时,已绑定手机号即可录入血糖) * * 路由:POST /api/tcm/dailyEnsurePhoneContext * * @return \think\response\Json */ public function dailyEnsurePhoneContext() { $sex = $this->request->post('sex', null); $options = []; if ($sex !== null && $sex !== '') { $options['sex'] = (int) $sex; } $result = DailyPhoneLogic::ensureDailyContext((int) $this->userId, $options); if (empty($result['ok'])) { $data = []; if (!empty($result['need_mobile'])) { $data['need_mobile'] = 1; } return $this->fail($result['error'] ?? '无法开始录入', $data); } unset($result['ok'], $result['error'], $result['need_mobile']); return $this->data($result); } /** * @notes 日常记录-获取今日已录入的血糖血压记录(患者端) * * 路由:GET /api/tcm/dailyTodayBloodRecord?diagnosis_id=:id * * 用于「日常记录」页打开「录入今日」表单时预填数据;只返回 source=1 的当天记录。 * * @return \think\response\Json */ public function dailyTodayBloodRecord() { $diagnosisId = (int) $this->request->get('diagnosis_id', 0); $check = $this->ensurePatientOwnsDiagnosis($diagnosisId); if (!$check['ok']) { return $this->fail($check['error']); } try { $todayStart = strtotime(date('Y-m-d 00:00:00')); $todayEnd = strtotime(date('Y-m-d 23:59:59')); $record = BloodRecord::where('diagnosis_id', $diagnosisId) ->where('source', 1) ->where('record_date', '>=', $todayStart) ->where('record_date', '<=', $todayEnd) ->order('id', 'desc') ->find(); if (!$record) { return $this->data(['record' => null, 'today' => date('Y-m-d')]); } $data = $record->toArray(); $data['record_date'] = date('Y-m-d', (int) $data['record_date']); return $this->data(['record' => $data, 'today' => date('Y-m-d')]); } catch (\Exception $e) { return $this->fail($e->getMessage()); } } /** * @notes 日常记录-保存今日的血糖血压记录(患者端) * * 路由:POST /api/tcm/dailySaveBloodRecord * 必填:diagnosis_id * 可选数值:fasting_blood_sugar, postprandial_blood_sugar, other_blood_sugar, * systolic_pressure, diastolic_pressure * 可选字符串:remark * * 语义:同一天同一诊单只有一条 source=1 的记录;存在则 update,不存在则 create。 * 日期强制为「今天」;任何 record_date 入参被忽略,避免补录历史。 * * @return \think\response\Json */ public function dailySaveBloodRecord() { $diagnosisId = (int) $this->request->post('diagnosis_id', 0); $check = $this->ensurePatientOwnsDiagnosis($diagnosisId); if (!$check['ok']) { return $this->fail($check['error']); } $diagnosis = $check['diagnosis']; // 解析并校验数值字段;空字符串当 null 处理 $parseDecimal = function ($v) { if ($v === null || $v === '' || $v === '0' || $v === 0) return null; if (!is_numeric($v)) return null; $f = (float) $v; return $f > 0 ? $f : null; }; $parseInt = function ($v) { if ($v === null || $v === '') return null; if (!is_numeric($v)) return null; $i = (int) $v; return $i > 0 ? $i : null; }; $fasting = $parseDecimal($this->request->post('fasting_blood_sugar')); $postprandial = $parseDecimal($this->request->post('postprandial_blood_sugar')); $other = $parseDecimal($this->request->post('other_blood_sugar')); $systolic = $parseInt($this->request->post('systolic_pressure')); $diastolic = $parseInt($this->request->post('diastolic_pressure')); $remark = trim((string) $this->request->post('remark', '')); // 至少要填一个有效字段 if ($fasting === null && $postprandial === null && $other === null && $systolic === null && $diastolic === null && $remark === '') { return $this->fail('请至少填写一项血糖或血压数据'); } // 边界校验,避免误录天文数字 if ($fasting !== null && ($fasting < 0.1 || $fasting > 50)) return $this->fail('空腹血糖数值异常'); if ($postprandial !== null && ($postprandial < 0.1 || $postprandial > 50)) return $this->fail('餐后血糖数值异常'); if ($other !== null && ($other < 0.1 || $other > 50)) return $this->fail('其他血糖数值异常'); if ($systolic !== null && ($systolic < 40 || $systolic > 300)) return $this->fail('收缩压数值异常'); if ($diastolic !== null && ($diastolic < 20 || $diastolic > 200)) return $this->fail('舒张压数值异常'); try { $todayStart = strtotime(date('Y-m-d 00:00:00')); $todayEnd = strtotime(date('Y-m-d 23:59:59')); $existing = BloodRecord::where('diagnosis_id', $diagnosisId) ->where('source', 1) ->where('record_date', '>=', $todayStart) ->where('record_date', '<=', $todayEnd) ->order('id', 'desc') ->find(); $payload = [ 'diagnosis_id' => $diagnosisId, 'patient_id' => (int) $diagnosis['patient_id'], 'record_date' => $todayStart, 'record_time' => date('H:i'), 'fasting_blood_sugar' => $fasting, 'postprandial_blood_sugar' => $postprandial, 'other_blood_sugar' => $other, 'systolic_pressure' => $systolic, 'diastolic_pressure' => $diastolic, 'remark' => $remark, 'source' => 1, ]; if ($existing) { // 分表单项更新时保留未提交字段,避免血糖/血压互相覆盖 $keepDecimal = function ($new, string $oldKey) use ($parseDecimal, $existing) { if ($new !== null) { return $new; } return $parseDecimal($existing[$oldKey] ?? null); }; $keepInt = function ($new, string $oldKey) use ($parseInt, $existing) { if ($new !== null) { return $new; } return $parseInt($existing[$oldKey] ?? null); }; $fasting = $keepDecimal($fasting, 'fasting_blood_sugar'); $postprandial = $keepDecimal($postprandial, 'postprandial_blood_sugar'); $other = $keepDecimal($other, 'other_blood_sugar'); $systolic = $keepInt($systolic, 'systolic_pressure'); $diastolic = $keepInt($diastolic, 'diastolic_pressure'); if ($remark === '') { $remark = trim((string) ($existing['remark'] ?? '')); } $payload = [ 'diagnosis_id' => $diagnosisId, 'patient_id' => (int) $diagnosis['patient_id'], 'record_date' => $todayStart, 'record_time' => date('H:i'), 'fasting_blood_sugar' => $fasting, 'postprandial_blood_sugar' => $postprandial, 'other_blood_sugar' => $other, 'systolic_pressure' => $systolic, 'diastolic_pressure' => $diastolic, 'remark' => $remark, 'source' => 1, 'id' => $existing['id'], ]; BloodRecord::update($payload); return $this->success('已更新今日记录', ['id' => $existing['id']]); } $created = BloodRecord::create($payload); return $this->success('录入成功', ['id' => $created->id]); } catch (\Exception $e) { return $this->fail($e->getMessage()); } } /** * @notes 日常记录-删除今日的血糖血压记录(患者端) * * 路由:POST /api/tcm/dailyDeleteBloodRecord * 必填:diagnosis_id, id * * 只允许删除当前用户拥有的诊单 + 当天 + source=1 的记录;其他情况一律拒绝。 * * @return \think\response\Json */ public function dailyDeleteBloodRecord() { $diagnosisId = (int) $this->request->post('diagnosis_id', 0); $recordId = (int) $this->request->post('id', 0); $check = $this->ensurePatientOwnsDiagnosis($diagnosisId); if (!$check['ok']) { return $this->fail($check['error']); } if ($recordId <= 0) { return $this->fail('记录ID不能为空'); } try { $todayStart = strtotime(date('Y-m-d 00:00:00')); $todayEnd = strtotime(date('Y-m-d 23:59:59')); $record = BloodRecord::where('id', $recordId) ->where('diagnosis_id', $diagnosisId) ->where('source', 1) ->find(); if (!$record) { return $this->fail('记录不存在或无权删除'); } $ts = (int) $record['record_date']; if ($ts < $todayStart || $ts > $todayEnd) { return $this->fail('只能删除当天的记录'); } BloodRecord::destroy($recordId); return $this->success('已删除'); } catch (\Exception $e) { return $this->fail($e->getMessage()); } } /** * @notes 获取稳糖乐园状态(积分、今日任务、可领取积分) * * 路由:GET /api/tcm/dailyGetGamify?diagnosis_id=:id * * @return \think\response\Json */ public function dailyGetGamify() { $diagnosisId = (int) $this->request->get('diagnosis_id', 0); $result = DailyGamifyLogic::getState((int) $this->userId, $diagnosisId); if ($result === false) { return $this->fail(DailyGamifyLogic::getError()); } return $this->data($result); } /** * @notes 给小树浇水(领取今日已完成任务的积分,服务端校验) * * 路由:POST /api/tcm/dailyWaterTree * 必填:diagnosis_id * * @return \think\response\Json */ public function dailyWaterTree() { $diagnosisId = (int) $this->request->post('diagnosis_id', 0); $result = DailyGamifyLogic::waterTree((int) $this->userId, $diagnosisId); if ($result === false) { return $this->fail(DailyGamifyLogic::getError()); } // 统一走 success,保证 msg 与 data 同时返回,前端可提示且刷新状态 return $this->success($result['message'] ?? '操作成功', $result, 1, 1); } /** * @notes 日常记录-获取今日饮食记录 * * 路由:GET /api/tcm/dailyTodayDietRecord?diagnosis_id=:id */ public function dailyTodayDietRecord() { $diagnosisId = (int) $this->request->get('diagnosis_id', 0); $check = $this->ensurePatientOwnsDiagnosis($diagnosisId); if (!$check['ok']) { return $this->fail($check['error']); } try { $todayStart = strtotime(date('Y-m-d 00:00:00')); $todayEnd = strtotime(date('Y-m-d 23:59:59')); $record = DietRecord::where('diagnosis_id', $diagnosisId) ->where('record_date', '>=', $todayStart) ->where('record_date', '<=', $todayEnd) ->order('id', 'desc') ->find(); if (!$record) { return $this->data(['record' => null, 'today' => date('Y-m-d')]); } $data = $record->toArray(); $data['record_date'] = date('Y-m-d', (int) $data['record_date']); return $this->data(['record' => $data, 'today' => date('Y-m-d')]); } catch (\Exception $e) { return $this->fail($e->getMessage()); } } /** * @notes 日常记录-保存今日饮食 * * 路由:POST /api/tcm/dailySaveDietRecord */ public function dailySaveDietRecord() { $diagnosisId = (int) $this->request->post('diagnosis_id', 0); $check = $this->ensurePatientOwnsDiagnosis($diagnosisId); if (!$check['ok']) { return $this->fail($check['error']); } $diagnosis = $check['diagnosis']; $breakfast = trim((string) $this->request->post('breakfast_foods', '')); $lunch = trim((string) $this->request->post('lunch_foods', '')); $dinner = trim((string) $this->request->post('dinner_foods', '')); $note = trim((string) $this->request->post('note', '')); if ($breakfast === '' && $lunch === '' && $dinner === '') { return $this->fail('请至少填写一餐饮食'); } try { $todayStart = strtotime(date('Y-m-d 00:00:00')); $todayEnd = strtotime(date('Y-m-d 23:59:59')); $existing = DietRecord::where('diagnosis_id', $diagnosisId) ->where('record_date', '>=', $todayStart) ->where('record_date', '<=', $todayEnd) ->order('id', 'desc') ->find(); $payload = [ 'diagnosis_id' => $diagnosisId, 'patient_id' => (int) $diagnosis['patient_id'], 'record_date' => $todayStart, 'breakfast_foods' => $breakfast, 'lunch_foods' => $lunch, 'dinner_foods' => $dinner, 'note' => $note, ]; if ($existing) { $payload['id'] = $existing['id']; DietRecord::update($payload); return $this->success('已更新今日饮食', ['id' => $existing['id']]); } $created = DietRecord::create($payload); return $this->success('饮食记录已保存', ['id' => $created->id]); } catch (\Exception $e) { return $this->fail($e->getMessage()); } } /** * @notes 日常记录-AI 今日饮食推荐(霍大夫升糖指数) * * 路由:GET /api/tcm/dailyDietAiRecommend?diagnosis_id=:id&refresh=0|1 */ public function dailyDietAiRecommend() { $diagnosisId = (int) $this->request->get('diagnosis_id', 0); $check = $this->ensurePatientOwnsDiagnosis($diagnosisId); if (!$check['ok']) { return $this->fail($check['error']); } $refresh = (int) $this->request->get('refresh', 0) === 1; $result = DailyDietAiLogic::getDailyRecommend($diagnosisId, $refresh); if (empty($result['ok'])) { return $this->fail($result['error'] ?? '获取推荐失败'); } return $this->data($result['data']); } /** * @notes 日常记录-AI 饮食问答(能不能吃某食物) * * 路由:POST /api/tcm/dailyDietAiAsk body: diagnosis_id, question */ public function dailyDietAiAsk() { $diagnosisId = (int) $this->request->post('diagnosis_id', 0); $check = $this->ensurePatientOwnsDiagnosis($diagnosisId); if (!$check['ok']) { return $this->fail($check['error']); } $question = trim((string) $this->request->post('question', '')); $result = DailyDietAiLogic::askFood($diagnosisId, $question); if (empty($result['ok'])) { return $this->fail($result['error'] ?? '咨询失败'); } return $this->data($result['data']); } /** * @notes 日常记录-AI 今日饮食推荐(SSE 流式) * * 路由:GET /api/tcm/dailyDietAiRecommendStream?diagnosis_id=:id&refresh=0|1 */ public function dailyDietAiRecommendStream() { $diagnosisId = (int) $this->request->get('diagnosis_id', 0); $check = $this->ensurePatientOwnsDiagnosis($diagnosisId); if (!$check['ok']) { return $this->fail($check['error']); } $refresh = (int) $this->request->get('refresh', 0) === 1; $this->runDailyDietSse(static function (callable $emit) use ($diagnosisId, $refresh): void { DailyDietAiLogic::streamDailyRecommend($diagnosisId, $refresh, $emit); }); } /** * @notes 日常记录-AI 饮食问答(SSE 流式) * * 路由:POST /api/tcm/dailyDietAiAskStream body: diagnosis_id, question */ public function dailyDietAiAskStream() { $diagnosisId = (int) $this->request->post('diagnosis_id', 0); $check = $this->ensurePatientOwnsDiagnosis($diagnosisId); if (!$check['ok']) { return $this->fail($check['error']); } $question = trim((string) $this->request->post('question', '')); $this->runDailyDietSse(static function (callable $emit) use ($diagnosisId, $question): void { DailyDietAiLogic::streamAskFood($diagnosisId, $question, $emit); }); } /** * @param callable(callable(string, array):void):void $runner */ private function runDailyDietSse(callable $runner): void { while (ob_get_level() > 0) { ob_end_clean(); } @ini_set('output_buffering', 'off'); @ini_set('zlib.output_compression', '0'); if (function_exists('apache_setenv')) { @apache_setenv('no-gzip', '1'); } header('Content-Type: text/event-stream; charset=utf-8'); header('Cache-Control: no-cache, no-transform'); header('Connection: keep-alive'); header('X-Accel-Buffering: no'); header('Content-Encoding: none'); // 撑开 Nginx/FastCGI 缓冲,让后续 chunk 能立即下发 echo ':' . str_repeat(' ', 2048) . "\n\n"; if (function_exists('ob_flush')) { @ob_flush(); } flush(); $emit = static function (string $event, array $payload): void { echo 'event: ' . $event . "\n"; echo 'data: ' . json_encode($payload, JSON_UNESCAPED_UNICODE) . "\n\n"; if (function_exists('ob_flush')) { @ob_flush(); } flush(); }; $emit('start', ['message' => '已连接,正在生成…']); try { $runner($emit); } catch (\Throwable $e) { $emit('error', ['message' => '服务异常,请稍后再试']); } exit; } /** * @notes 日常记录-获取今日运动记录 * * 路由:GET /api/tcm/dailyTodayExerciseRecord?diagnosis_id=:id */ public function dailyTodayExerciseRecord() { $diagnosisId = (int) $this->request->get('diagnosis_id', 0); $check = $this->ensurePatientOwnsDiagnosis($diagnosisId); if (!$check['ok']) { return $this->fail($check['error']); } try { $todayStart = strtotime(date('Y-m-d 00:00:00')); $todayEnd = strtotime(date('Y-m-d 23:59:59')); $record = ExerciseRecord::where('diagnosis_id', $diagnosisId) ->where('record_date', '>=', $todayStart) ->where('record_date', '<=', $todayEnd) ->order('id', 'desc') ->find(); if (!$record) { return $this->data(['record' => null, 'today' => date('Y-m-d')]); } $data = $record->toArray(); $data['record_date'] = date('Y-m-d', (int) $data['record_date']); return $this->data(['record' => $data, 'today' => date('Y-m-d')]); } catch (\Exception $e) { return $this->fail($e->getMessage()); } } /** * @notes 日常记录-保存今日运动 * * 路由:POST /api/tcm/dailySaveExerciseRecord */ public function dailySaveExerciseRecord() { $diagnosisId = (int) $this->request->post('diagnosis_id', 0); $check = $this->ensurePatientOwnsDiagnosis($diagnosisId); if (!$check['ok']) { return $this->fail($check['error']); } $diagnosis = $check['diagnosis']; $exerciseType = trim((string) $this->request->post('exercise_type', '')); $durationRaw = $this->request->post('duration', ''); $intensity = (int) $this->request->post('intensity', 2); $note = trim((string) $this->request->post('note', '')); $duration = null; if ($durationRaw !== '' && $durationRaw !== null && is_numeric($durationRaw)) { $d = (int) $durationRaw; if ($d > 0) { $duration = $d; } } if ($exerciseType === '' && $duration === null) { return $this->fail('请填写运动类型或时长'); } if ($intensity < 1 || $intensity > 3) { $intensity = 2; } try { $todayStart = strtotime(date('Y-m-d 00:00:00')); $todayEnd = strtotime(date('Y-m-d 23:59:59')); $existing = ExerciseRecord::where('diagnosis_id', $diagnosisId) ->where('record_date', '>=', $todayStart) ->where('record_date', '<=', $todayEnd) ->order('id', 'desc') ->find(); $payload = [ 'diagnosis_id' => $diagnosisId, 'patient_id' => (int) $diagnosis['patient_id'], 'record_date' => $todayStart, 'exercise_type' => $exerciseType, 'duration' => $duration, 'intensity' => $intensity, 'note' => $note, ]; if ($existing) { $payload['id'] = $existing['id']; ExerciseRecord::update($payload); return $this->success('已更新今日运动', ['id' => $existing['id']]); } $created = ExerciseRecord::create($payload); return $this->success('运动记录已保存', ['id' => $created->id]); } catch (\Exception $e) { return $this->fail($e->getMessage()); } } /** * @notes 保存稳糖分 / 勋章 / 任务领奖状态 * * 路由:POST /api/tcm/dailySaveGamify * 参数:diagnosis_id, points, badges(数组), task_awards(对象) * * @return \think\response\Json */ public function dailySaveGamify() { $diagnosisId = (int) $this->request->post('diagnosis_id', 0); $points = (int) $this->request->post('points', 0); $badges = $this->decodeJsonPostField($this->request->post('badges', ''), 'badges/a'); $taskAwards = $this->decodeJsonPostField($this->request->post('task_awards', ''), 'task_awards/a'); $result = DailyGamifyLogic::saveState((int) $this->userId, $diagnosisId, $points, $badges, $taskAwards); if ($result === false) { return $this->fail(DailyGamifyLogic::getError()); } return $this->data($result); } /** * @notes 生成日常记录分享邀请码(仅当天有效) * * 路由:POST /api/tcm/dailyCreateShareInvite * 必填:diagnosis_id * * @return \think\response\Json */ public function dailyCreateShareInvite() { $diagnosisId = (int) $this->request->post('diagnosis_id', 0); $result = DailyShareLogic::createInvite((int) $this->userId, $diagnosisId); if ($result === false) { return $this->fail(DailyShareLogic::getError()); } return $this->data($result); } /** * @notes 凭邀请码查看分享战报(无需登录,含近7日血糖/血压记录) * * 路由:GET /api/tcm/dailySharePreview?invite_code=XXXXXXXX * * @return \think\response\Json */ public function dailySharePreview() { $inviteCode = (string) $this->request->get('invite_code', ''); $viewerKey = (string) $this->request->get('viewer_key', ''); $result = DailyShareLogic::previewByInviteCode($inviteCode, $viewerKey); if ($result === false) { return $this->fail(DailyShareLogic::getError()); } return $this->data($result); } /** * @notes 家人点赞(邀请观看页,无需登录) * * 路由:POST /api/tcm/dailyFamilyLike * 必填:invite_code, viewer_key * 可选:nickname(家人/亲友/老伴 等) * * @return \think\response\Json */ public function dailyFamilyLike() { $inviteCode = (string) $this->request->post('invite_code', ''); $viewerKey = (string) $this->request->post('viewer_key', ''); $nickname = (string) $this->request->post('nickname', ''); $result = DailyFamilyLikeLogic::addLike($inviteCode, $viewerKey, $nickname); if ($result === false) { return $this->fail(DailyFamilyLikeLogic::getError()); } return $this->data($result); } /** * @notes 患者查看今日家人点赞汇总 * * 路由:GET /api/tcm/dailyFamilyLikeSummary?diagnosis_id= * * @return \think\response\Json */ public function dailyFamilyLikeSummary() { $diagnosisId = (int) $this->request->get('diagnosis_id', 0); $result = DailyFamilyLikeLogic::summaryForPatient((int) $this->userId, $diagnosisId); if ($result === false) { return $this->fail(DailyFamilyLikeLogic::getError()); } return $this->data($result); } /** * @notes 根据订单号获取订单详情(供小程序支付页调用) * @return \think\response\Json */ public function getOrderByNo() { $orderNo = $this->request->get('order_no', ''); if (empty($orderNo)) { return $this->fail('订单号不能为空'); } try { $order = \app\common\model\Order::where('order_no', $orderNo)->find(); if (!$order) { return $this->fail('订单不存在'); } $typeMap = [1 => '挂号费', 2 => '问诊费', 3 => '药品费用']; $statusMap = [1 => '待支付', 2 => '已支付', 3 => '已取消', 4 => '已退款']; return $this->data([ 'id' => $order['id'], 'order_no' => $order['order_no'], 'order_type' => $order['order_type'], 'order_type_desc' => $typeMap[$order['order_type']] ?? '未知', 'amount' => $order['amount'], 'status' => $order['status'], 'status_desc' => $statusMap[$order['status']] ?? '未知', 'remark' => $order['remark'] ?? '', 'create_time' => $order['create_time'], ]); } catch (\Exception $e) { return $this->fail($e->getMessage()); } } }