where('delete_time', null) ->find(); if ($exists) { self::setError('该手机号已存在'); return false; } } // 检查身份证号是否重复 if (!empty($params['id_card'])) { $exists = Diagnosis::where('id_card', $params['id_card']) ->where('delete_time', null) ->find(); if ($exists) { self::setError('该身份证号已存在'); return false; } } // 生成患者ID $params['patient_id'] = self::generatePatientId(); // 处理既往史数组 if (isset($params['past_history']) && is_array($params['past_history'])) { $params['past_history'] = implode(',', $params['past_history']); } // 处理现病史多选字段 $multiSelectFields = [ 'diet_condition', 'body_feeling', 'sleep_condition', 'eye_condition', 'head_feeling', 'sweat_condition', 'skin_condition', 'urine_condition', 'stool_condition', 'kidney_condition', 'local_hospital_diagnosis' ]; foreach ($multiSelectFields as $field) { if (isset($params[$field]) && is_array($params[$field])) { $params[$field] = implode(',', $params[$field]); } } // 处理舌苔照片(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); } // 处理诊断日期 if (isset($params['diagnosis_date'])) { $params['diagnosis_date'] = strtotime($params['diagnosis_date']); } $model = Diagnosis::create($params); // 自动为患者创建 TRTC 账号 self::createPatientTrtcAccount($model->patient_id); return $model->id; } catch (\Exception $e) { self::setError($e->getMessage()); return false; } } /** * @notes 生成患者ID * @return int */ private static function generatePatientId(): int { // 获取当前最大的患者ID $maxPatientId = Diagnosis::max('patient_id') ?? 100000; // 返回下一个患者ID return $maxPatientId + 1; } /** * @notes 编辑诊单 * @param array $params * @return bool */ public static function edit(array $params): bool { try { // 检查手机号是否重复(排除当前记录) if (!empty($params['phone'])) { $exists = Diagnosis::where('phone', $params['phone']) ->where('id', '<>', $params['id']) ->where('delete_time', null) ->find(); if ($exists) { self::setError('该手机号已存在'); return false; } } // 检查身份证号是否重复(排除当前记录) if (!empty($params['id_card'])) { $exists = Diagnosis::where('id_card', $params['id_card']) ->where('id', '<>', $params['id']) ->where('delete_time', null) ->find(); if ($exists) { self::setError('该身份证号已存在'); return false; } } // 处理既往史数组 if (isset($params['past_history']) && is_array($params['past_history'])) { $params['past_history'] = implode(',', $params['past_history']); } // 处理现病史多选字段 $multiSelectFields = [ 'diet_condition', 'body_feeling', 'sleep_condition', 'eye_condition', 'head_feeling', 'sweat_condition', 'skin_condition', 'urine_condition', 'stool_condition', 'kidney_condition', 'local_hospital_diagnosis' ]; foreach ($multiSelectFields as $field) { if (isset($params[$field]) && is_array($params[$field])) { $params[$field] = implode(',', $params[$field]); } } // 处理舌苔照片(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); } // 处理诊断日期 if (isset($params['diagnosis_date'])) { $params['diagnosis_date'] = strtotime($params['diagnosis_date']); } Diagnosis::update($params); // 如果是编辑,也确保患者有 TRTC 账号 $diagnosis = Diagnosis::find($params['id']); if ($diagnosis) { self::createPatientTrtcAccount($diagnosis->patient_id); } return true; } catch (\Exception $e) { self::setError($e->getMessage()); return false; } } /** * @notes 删除诊单 * @param array $params * @return bool */ public static function delete(array $params): bool { try { Diagnosis::destroy($params['id']); return true; } catch (\Exception $e) { self::setError($e->getMessage()); return false; } } /** * @notes 诊单详情 * @param $params * @return array */ public static function detail($params): array { $diagnosis = Diagnosis::findOrEmpty($params['id'])->toArray(); // 处理既往史为数组 if (!empty($diagnosis['past_history'])) { $diagnosis['past_history'] = explode(',', $diagnosis['past_history']); } else { $diagnosis['past_history'] = []; } // 处理现病史多选字段为数组 $multiSelectFields = [ 'diet_condition', 'body_feeling', 'sleep_condition', 'eye_condition', 'head_feeling', 'sweat_condition', 'skin_condition', 'urine_condition', 'stool_condition', 'kidney_condition', 'local_hospital_diagnosis' ]; foreach ($multiSelectFields as $field) { if (!empty($diagnosis[$field])) { $diagnosis[$field] = explode(',', $diagnosis[$field]); } else { $diagnosis[$field] = []; } } if (!empty($diagnosis['tongue_images'])) { // 先尝试 JSON 解析(兼容旧数据) $files = json_decode($diagnosis['tongue_images'], true); if (is_array($files)) { $diagnosis['tongue_images'] = $files; } else { // JSON 解析失败则按逗号分割 $diagnosis['tongue_images'] = array_filter(explode(',', $diagnosis['tongue_images'])) ?: []; } } else { $diagnosis['tongue_images'] = []; } // 处理检查报告(JSON转数组) if (!empty($diagnosis['report_files'])) { // 先尝试 JSON 解析(兼容旧数据) $files = json_decode($diagnosis['report_files'], true); if (is_array($files)) { $diagnosis['report_files'] = $files; } else { // JSON 解析失败则按逗号分割 $diagnosis['report_files'] = array_filter(explode(',', $diagnosis['report_files'])) ?: []; } } else { $diagnosis['report_files'] = []; } // 处理诊断日期格式 if (!empty($diagnosis['diagnosis_date'])) { $diagnosis['diagnosis_date'] = date('Y-m-d', $diagnosis['diagnosis_date']); } // 如果local_hospital_visit_date为空但diagnosis_date有值,则使用diagnosis_date if (empty($diagnosis['local_hospital_visit_date']) && !empty($diagnosis['diagnosis_date'])) { $diagnosis['local_hospital_visit_date'] = $diagnosis['diagnosis_date']; } $diagnosis['is_view'] =DiagnosisViewRecord::where(['diagnosis_id'=> $diagnosis['id'], 'user_id' =>$params['user_id']??0])->find() ? 1 : 0; return $diagnosis; } /** * @notes 诊单详情(患者端) * @param array $params * @return array */ public static function diagnosisDetail(array $params): array { $diagnosisId = $params['id'] ?? 0; $userId = $params['user_id'] ?? 0; if (!$diagnosisId || !$userId) { self::setError('参数不完整'); return []; } $diagnosis = Diagnosis::where('id', $diagnosisId) ->where('patient_id', $userId) ->findOrEmpty() ->toArray(); if (empty($diagnosis)) { self::setError('诊单不存在或无权限访问'); return []; } // 处理既往史为数组 if (!empty($diagnosis['past_history'])) { $diagnosis['past_history'] = explode(',', $diagnosis['past_history']); } else { $diagnosis['past_history'] = []; } // 处理现病史多选字段为数组 $multiSelectFields = [ 'diet_condition', 'body_feeling', 'sleep_condition', 'eye_condition', 'head_feeling', 'sweat_condition', 'skin_condition', 'urine_condition', 'stool_condition', 'kidney_condition', 'local_hospital_diagnosis' ]; foreach ($multiSelectFields as $field) { if (!empty($diagnosis[$field])) { $diagnosis[$field] = explode(',', $diagnosis[$field]); } else { $diagnosis[$field] = []; } } // 处理检查报告为数组 if (!empty($diagnosis['examination_report'])) { $diagnosis['examination_report'] = explode(',', $diagnosis['examination_report']); } else { $diagnosis['examination_report'] = []; } // 处理舌苔照片为数组 if (!empty($diagnosis['tongue_photo'])) { $diagnosis['tongue_photo'] = explode(',', $diagnosis['tongue_photo']); } else { $diagnosis['tongue_photo'] = []; } // 处理诊断日期格式 if (!empty($diagnosis['diagnosis_date'])) { $diagnosis['diagnosis_date'] = date('Y-m-d', $diagnosis['diagnosis_date']); } // 如果local_hospital_visit_date为空但diagnosis_date有值,则使用diagnosis_date if (empty($diagnosis['local_hospital_visit_date']) && !empty($diagnosis['diagnosis_date'])) { $diagnosis['local_hospital_visit_date'] = $diagnosis['diagnosis_date']; } return $diagnosis; } /** * @notes 检查手机号是否重复 * @param array $params * @return array */ public static function checkPhone(array $params): array { $query = Diagnosis::where('phone', $params['phone']) ->where('delete_time', null); // 编辑时排除当前记录 if (isset($params['id']) && $params['id']) { $query->where('id', '<>', $params['id']); } $exists = $query->find(); return [ 'exists' => !empty($exists), 'message' => $exists ? '该手机号已存在' : '' ]; } /** * @notes 检查身份证号是否重复 * @param array $params * @return array */ public static function checkIdCard(array $params): array { $query = Diagnosis::where('id_card', $params['id_card']) ->where('delete_time', null); // 编辑时排除当前记录 if (isset($params['id']) && $params['id']) { $query->where('id', '<>', $params['id']); } $exists = $query->find(); return [ 'exists' => !empty($exists), 'message' => $exists ? '该身份证号已存在' : '' ]; } /** * @notes 补全身份证号(自动计算年龄并更新) * @param array $params id, id_card * @return bool */ public static function fillIdCard(array $params): bool { try { $diagnosis = Diagnosis::find($params['id']); if (!$diagnosis) { self::setError('诊单不存在'); return false; } $idCard = trim($params['id_card']); $len = strlen($idCard); if ($len === 15) { $birthYear = 1900 + (int)substr($idCard, 6, 2); $birthMonth = (int)substr($idCard, 8, 2); $birthDay = (int)substr($idCard, 10, 2); } elseif ($len === 18 && preg_match('/^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/', $idCard)) { $birthYear = (int)substr($idCard, 6, 4); $birthMonth = (int)substr($idCard, 10, 2); $birthDay = (int)substr($idCard, 12, 2); } else { self::setError('身份证号格式不正确'); return false; } // 从身份证提取出生日期计算年龄 $today = getdate(); $age = $today['year'] - $birthYear; if ($today['mon'] < $birthMonth || ($today['mon'] == $birthMonth && $today['mday'] < $birthDay)) { $age--; } $age = max(0, min(150, $age)); $diagnosis->id_card = $idCard; $diagnosis->age = $age; $diagnosis->save(); return true; } catch (\Exception $e) { self::setError($e->getMessage()); return false; } } /** * @notes 指派医助 * @param array $params * @return bool */ public static function assign(array $params): bool { try { Diagnosis::where('id', $params['id'])->update([ 'assistant_id' => $params['assistant_id'] ]); return true; } catch (\Exception $e) { self::setError($e->getMessage()); return false; } } /** * @notes 获取通话签名 * @param array $params * @return array|bool */ public static function getCallSignature(array $params) { try { // 获取配置 $config = self::getTrtcConfig(); if (!$config) { self::setError('请先配置腾讯云TRTC参数'); return false; } // 获取当前管理员ID(从参数中获取) $adminId = $params['admin_id'] ?? 0; $patientId = $params['patient_id'] ?? 0; if (!$adminId) { self::setError('获取管理员信息失败'); return false; } if (!$patientId) { self::setError('获取患者信息失败'); return false; } // 医生userId $doctorUserId = 'doctor_' . $adminId; $query = Diagnosis::where('id', $patientId) ->where('delete_time', null)->find(); // 患者userId(必须与小程序端一致) $patientUserId = 'patient_' . $patientId; // 生成医生的 UserSig $userSig = self::generateUserSig($config['sdkAppId'], $config['secretKey'], $doctorUserId); if (!$userSig) { self::setError('生成签名失败'); return false; } // 导入医生账号到IM self::importDoctorAccountToIm($adminId, $doctorUserId); // 确保患者账号也已导入IM(用于跨平台通话) self::ensurePatientImAccount($patientId, $patientUserId); return [ 'sdkAppId' => (int)$config['sdkAppId'], // 确保返回整数 'userId' => $doctorUserId, // 医生的userId 'userSig' => $userSig, 'assistant_id'=>$query->assistant_id?'doctor_'.$query->assistant_id:'', 'patientUserId' => $patientUserId, // 患者的userId(用于发起通话) 'expireTime' => 86400 // 24小时 ]; } catch (\Exception $e) { self::setError($e->getMessage()); return false; } } /** * @notes 确保患者IM账号存在 * @param int $patientId * @param string $userId * @return bool */ private static function ensurePatientImAccount(int $patientId, string $userId): bool { try { // 获取患者信息 $diagnosis = Diagnosis::where('patient_id', $patientId)->find(); $nick = $diagnosis ? $diagnosis->patient_name : '患者' . $patientId; // 调用IM服务导入账号 $imService = new \app\common\service\TencentImService(); $result = $imService->importAccount($userId, $nick); if ($result['success']) { \think\facade\Log::info('确保患者IM账号存在 - patient_id: ' . $patientId . ', user_id: ' . $userId . ', nick: ' . $nick); return true; } else { \think\facade\Log::warning('导入患者IM账号失败 - patient_id: ' . $patientId . ', error: ' . ($result['message'] ?? '未知错误')); return false; } } catch (\Exception $e) { \think\facade\Log::error('确保患者IM账号失败: ' . $e->getMessage()); return false; } } /** * @notes 导入医生账号到腾讯云IM * @param int $adminId * @param string $userId * @return bool */ private static function importDoctorAccountToIm(int $adminId, string $userId): bool { try { // 获取医生信息 $admin = \app\common\model\auth\Admin::find($adminId); $nick = $admin ? $admin->name : '医生' . $adminId; // 调用IM服务导入账号 $imService = new \app\common\service\TencentImService(); $result = $imService->importAccount($userId, $nick); if ($result['success']) { \think\facade\Log::info('导入医生IM账号成功 - admin_id: ' . $adminId . ', user_id: ' . $userId . ', nick: ' . $nick); return true; } else { \think\facade\Log::warning('导入医生IM账号失败 - admin_id: ' . $adminId . ', user_id: ' . $userId . ', error: ' . $result['message']); return false; } } catch (\Exception $e) { \think\facade\Log::error('导入医生IM账号异常 - admin_id: ' . $adminId . ', user_id: ' . $userId . ', error: ' . $e->getMessage()); return false; } } /** * 所有可能以 doctor_{id} 登录 IM 的后台账号(医生 role_id=1、医助 role_id=2),用于合并会话漫游记录 * * @return array 如 ['doctor_1','doctor_2'] */ private static function collectAllDoctorImPeerAccounts(): array { try { $ids = \app\common\model\auth\Admin::alias('a') ->join('admin_role ar', 'a.id = ar.admin_id') ->whereIn('ar.role_id', [1, 2]) ->where('a.disable', 0) ->group('a.id') ->column('a.id'); $accounts = []; foreach ($ids as $id) { $accounts[] = 'doctor_' . (int)$id; } return array_values(array_unique($accounts)); } catch (\Exception $e) { \think\facade\Log::error('collectAllDoctorImPeerAccounts: ' . $e->getMessage()); return []; } } /** * @notes 拉取与本诊单相关的腾讯云 IM 单聊记录(C2C:patient_{患者ID} 与 全部医生/医助账号 doctor_* 分别会话后合并) */ public static function getImChatMessagesForDiagnosis(int $diagnosisId) { try { $config = self::getTrtcConfig(); if (!$config) { self::setError('请先配置腾讯云 TRTC / IM 参数'); return false; } $diag = Diagnosis::where('id', $diagnosisId)->where('delete_time', null)->find(); if (!$diag) { self::setError('诊单不存在'); return false; } $patientId = (int)$diag['patient_id']; if ($patientId <= 0) { self::setError('诊单缺少患者信息'); return false; } $patientImId = 'patient_' . $patientId; $doctorAccounts = self::collectAllDoctorImPeerAccounts(); $assistantId = isset($diag['assistant_id']) ? (int)$diag['assistant_id'] : 0; if ($assistantId > 0) { $doctorAccounts[] = 'doctor_' . $assistantId; } $doctorAccounts = array_values(array_unique($doctorAccounts)); if (empty($doctorAccounts)) { self::setError('未找到医生/医助角色账号,无法拉取 IM 记录'); return false; } $imService = new \app\common\service\TencentImService(); $merged = []; foreach ($doctorAccounts as $docAccount) { $batch = self::pullAllRoamMessages($imService, $docAccount, $patientImId); foreach ($batch as $row) { $merged[] = $row; } } usort($merged, function ($a, $b) { return ($a['time'] ?? 0) <=> ($b['time'] ?? 0); }); $seen = []; $unique = []; foreach ($merged as $row) { $k = $row['msg_id'] ?? ''; if ($k !== '' && isset($seen[$k])) { continue; } if ($k !== '') { $seen[$k] = true; } $unique[] = $row; } $unique = self::enrichImMessagesWithStaffNames($unique); return [ 'lists' => $unique, 'patient_im_id' => $patientImId, 'patient_name' => $diag['patient_name'] ?? '', 'doctor_accounts_queried' => $doctorAccounts, ]; } catch (\Exception $e) { self::setError($e->getMessage()); return false; } } /** * 为 from_account = doctor_{id} 的消息补充后台姓名,便于前端区分不同医生 * * @param array> $rows * @return array> */ private static function enrichImMessagesWithStaffNames(array $rows): array { $ids = []; foreach ($rows as $r) { $f = (string)($r['from_account'] ?? ''); if (preg_match('/^doctor_(\d+)$/', $f, $m)) { $ids[] = (int)$m[1]; } } $ids = array_unique($ids); if (empty($ids)) { return $rows; } $map = \app\common\model\auth\Admin::whereIn('id', $ids)->column('name', 'id'); foreach ($rows as $k => $r) { $f = (string)($r['from_account'] ?? ''); if (preg_match('/^doctor_(\d+)$/', $f, $m)) { $aid = (int)$m[1]; $rows[$k]['from_staff_name'] = $map[$aid] ?? ''; } } return $rows; } /** * @return array> */ private static function pullAllRoamMessages(\app\common\service\TencentImService $svc, string $operator, string $peer): array { $out = []; $lastKey = null; $lastTime = null; $guard = 0; do { $res = $svc->adminGetRoamMsg($operator, $peer, 100, 0, 4294967295, $lastKey, $lastTime); if (!$res['success']) { if (!empty($res['error'])) { \think\facade\Log::warning('IM漫游消息拉取失败', [ 'operator' => $operator, 'peer' => $peer, 'error' => $res['error'], 'code' => $res['rawErrorCode'] ?? 0, ]); } break; } foreach ($res['msgList'] as $raw) { if (!is_array($raw)) { continue; } $out[] = self::normalizeTimMessage($raw); } $complete = (int)$res['complete']; if ($complete === 1) { break; } $lastKey = $res['lastMsgKey']; $lastTime = $res['lastMsgTime']; if ($lastKey === null || $lastKey === '') { break; } $guard++; if ($guard > 80) { break; } } while (true); return $out; } /** * @param array $raw * @return array */ private static function normalizeTimMessage(array $raw): array { $from = (string)($raw['From_Account'] ?? ''); $to = (string)($raw['To_Account'] ?? ''); $time = (int)($raw['MsgTimeStamp'] ?? $raw['MsgTime'] ?? 0); $seq = $raw['MsgSeq'] ?? ''; $rand = $raw['MsgRandom'] ?? ''; $msgId = $seq . '_' . $rand . '_' . $from; $isDoctor = strpos($from, 'doctor_') === 0; $parsed = self::parseTimMsgBody($raw['MsgBody'] ?? []); return array_merge( [ 'msg_id' => $msgId, 'from_account' => $from, 'to_account' => $to, 'time' => $time, 'is_from_doctor' => $isDoctor, ], $parsed ); } /** * @param mixed $body * @return array */ private static function parseTimMsgBody($body): array { $out = [ 'msg_type' => 'other', 'text' => '', 'image_url' => '', 'file_url' => '', 'file_name' => '', 'raw_elem_type' => '', ]; if (!is_array($body) || !$body) { return $out; } foreach ($body as $elem) { if (!is_array($elem)) { continue; } $type = (string)($elem['MsgType'] ?? ''); $out['raw_elem_type'] = $type; $content = $elem['MsgContent'] ?? []; if (!is_array($content)) { $content = []; } switch ($type) { case 'TIMTextElem': $out['msg_type'] = 'text'; $out['text'] = (string)($content['Text'] ?? ''); return $out; case 'TIMImageElem': $out['msg_type'] = 'image'; $arr = $content['ImageInfoArray'] ?? []; if (is_array($arr)) { foreach ($arr as $info) { if (is_array($info) && !empty($info['URL'])) { $out['image_url'] = (string)$info['URL']; break; } } } return $out; case 'TIMFileElem': $out['msg_type'] = 'file'; $out['file_url'] = (string)($content['Url'] ?? ''); $out['file_name'] = (string)($content['FileName'] ?? ''); return $out; case 'TIMSoundElem': $out['msg_type'] = 'sound'; $out['file_url'] = (string)($content['Url'] ?? ''); return $out; case 'TIMVideoFileElem': $out['msg_type'] = 'video'; $out['file_url'] = (string)($content['VideoUrl'] ?? ''); return $out; case 'TIMVideoElem': $out['msg_type'] = 'video'; $out['file_url'] = (string)($content['VideoUrl'] ?? ''); return $out; case 'TIMLocationElem': $out['msg_type'] = 'location'; $out['text'] = (string)($content['Desc'] ?? ''); return $out; case 'TIMCustomElem': $out['msg_type'] = 'custom'; $out['text'] = (string)($content['Data'] ?? ''); return $out; case 'TIMFaceElem': $out['msg_type'] = 'face'; $out['text'] = (string)($content['Index'] ?? ''); return $out; default: break; } } return $out; } /** * @notes 发起通话 * @param array $params * @return bool */ public static function startCall(array $params): bool { try { // 获取当前管理员ID(从参数中获取) $adminId = $params['admin_id'] ?? 0; if (!$adminId) { self::setError('获取管理员信息失败'); return false; } // 创建通话记录 \app\common\model\tcm\CallRecord::create([ 'diagnosis_id' => $params['diagnosis_id'], 'caller_id' => $adminId, 'caller_type' => 'doctor', 'callee_id' => $params['patient_id'] ?? 0, 'callee_type' => 'patient', 'call_type' => $params['call_type'] ?? 2, // 1-语音 2-视频 'status' => 1, // 1-进行中 'start_time' => time() ]); return true; } catch (\Exception $e) { self::setError($e->getMessage()); return false; } } /** * @notes 结束通话 * @param array $params * @return bool */ public static function endCall(array $params): bool { try { // 获取当前管理员ID(从参数中获取) $adminId = $params['admin_id'] ?? 0; if (!$adminId) { self::setError('获取管理员信息失败'); return false; } // 查找最近的通话记录 $record = \app\common\model\tcm\CallRecord::where('diagnosis_id', $params['diagnosis_id']) ->where('caller_id', $adminId) ->where('status', 1) ->order('id', 'desc') ->find(); if ($record) { $endTime = time(); $duration = $endTime - $record->start_time; $record->save([ 'status' => 2, // 2-已结束 'end_time' => $endTime, 'duration' => $duration ]); } return true; } catch (\Exception $e) { self::setError($e->getMessage()); return false; } } /** * @notes 获取通话记录 * @param array $params * @return array */ public static function getCallRecords(array $params): array { try { $records = \app\common\model\tcm\CallRecord::where('diagnosis_id', $params['diagnosis_id']) ->order('id', 'desc') ->select() ->toArray(); foreach ($records as &$record) { $record['start_time_text'] = date('Y-m-d H:i:s', $record['start_time']); $record['end_time_text'] = $record['end_time'] ? date('Y-m-d H:i:s', $record['end_time']) : ''; $record['duration_text'] = self::formatDuration($record['duration']); $urls = []; if (!empty($record['recording_urls'])) { $decoded = json_decode((string)$record['recording_urls'], true); if (is_array($decoded)) { $urls = $decoded; } } $record['recording_urls_list'] = $urls; $record['recording_status_text'] = self::recordingStatusText((int)($record['recording_status'] ?? 0)); } return $records; } catch (\Exception $e) { self::setError($e->getMessage()); return []; } } /** * @notes 将 TRTC 房间号写入当前诊单最近一次通话记录,便于云端录制回调按 room_id 关联 */ public static function bindCallRoom(array $params): bool { try { $diagnosisId = (int)($params['diagnosis_id'] ?? 0); $roomId = trim((string)($params['room_id'] ?? '')); if ($diagnosisId <= 0 || $roomId === '') { self::setError('诊单ID或房间号不能为空'); return false; } $record = \app\common\model\tcm\CallRecord::where('diagnosis_id', $diagnosisId) ->where('status', 1) ->order('id', 'desc') ->find(); if (!$record) { $record = \app\common\model\tcm\CallRecord::where('diagnosis_id', $diagnosisId) ->where('room_id', '') ->order('id', 'desc') ->find(); } if (!$record) { self::setError('未找到可绑定的通话记录'); return false; } $record->save([ 'room_id' => $roomId, 'update_time' => time(), ]); return true; } catch (\Exception $e) { self::setError($e->getMessage()); return false; } } private static function recordingStatusText(int $status): string { $map = [ 0 => '无录制', 1 => '录制中', 2 => '已生成', 3 => '录制失败', ]; return $map[$status] ?? '未知'; } /** * @notes 获取TRTC配置 * @return array|null */ private static function getTrtcConfig(): ?array { // 从配置文件或数据库读取 // 这里使用配置文件方式 $config = config('project.trtc'); if (empty($config['sdkAppId']) || empty($config['secretKey'])) { return null; } return $config; } /** * @notes 生成UserSig * @param int $sdkAppId * @param string $secretKey * @param string $userId * @param int $expire * @return string|false */ private static function generateUserSig(int $sdkAppId, string $secretKey, string $userId, int $expire = 86400) { try { // 使用官方的TLSSigAPIv2类生成UserSig $api = new \app\common\service\TLSSigAPIv2($sdkAppId, $secretKey); $userSig = $api->genUserSig($userId, $expire); return $userSig; } catch (\Exception $e) { \think\facade\Log::error('生成UserSig失败: ' . $e->getMessage()); return false; } } /** * @notes 格式化通话时长 * @param int $seconds * @return string */ private static function formatDuration(int $seconds): string { if ($seconds < 60) { return $seconds . '秒'; } elseif ($seconds < 3600) { $minutes = floor($seconds / 60); $secs = $seconds % 60; return $minutes . '分' . $secs . '秒'; } else { $hours = floor($seconds / 3600); $minutes = floor(($seconds % 3600) / 60); $secs = $seconds % 60; return $hours . '小时' . $minutes . '分' . $secs . '秒'; } } /** * @notes 为患者创建 TRTC 账号(自动调用) * @param int $patientId * @return bool */ private static function createPatientTrtcAccount(int $patientId): bool { try { $config = self::getTrtcConfig(); if (!$config || !$config['enable']) { // 如果 TRTC 未启用,不创建账号 return false; } $userId = 'patient_' . $patientId; // 检查是否已存在 $existingAccount = \app\common\model\tcm\PatientTrtc::where('patient_id', $patientId) ->where('delete_time', null) ->find(); // 生成 UserSig(长期有效,180天) $expireTime = 86400 * 180; $userSig = self::generateUserSig($config['sdkAppId'], $config['secretKey'], $userId, $expireTime); if (!$userSig) { return false; } $data = [ 'patient_id' => $patientId, 'user_id' => $userId, 'user_sig' => $userSig, 'sdk_app_id' => $config['sdkAppId'], 'expire_time' => time() + $expireTime, 'is_active' => 1 ]; if ($existingAccount) { // 更新现有账号 $existingAccount->save($data); \think\facade\Log::info('更新患者 TRTC 账号 - patient_id: ' . $patientId . ', user_id: ' . $userId); } else { // 创建新账号 \app\common\model\tcm\PatientTrtc::create($data); \think\facade\Log::info('创建患者 TRTC 账号 - patient_id: ' . $patientId . ', user_id: ' . $userId); } // 导入账号到腾讯云IM(用于TUICallKit) self::importAccountToTencentIm($patientId, $userId); return true; } catch (\Exception $e) { \think\facade\Log::error('创建患者 TRTC 账号失败 - patient_id: ' . $patientId . ', error: ' . $e->getMessage()); return false; } } /** * @notes 导入账号到腾讯云IM * @param int $patientId * @param string $userId * @return bool */ private static function importAccountToTencentIm(int $patientId, string $userId): bool { try { // 获取患者信息 $diagnosis = Diagnosis::where('patient_id', $patientId) ->order('id', 'desc') ->find(); $nick = $diagnosis ? $diagnosis->patient_name : '患者' . $patientId; // 调用IM服务导入账号 $imService = new \app\common\service\TencentImService(); $result = $imService->importAccount($userId, $nick); if ($result['success']) { \think\facade\Log::info('导入患者IM账号成功 - patient_id: ' . $patientId . ', user_id: ' . $userId . ', nick: ' . $nick); return true; } else { \think\facade\Log::warning('导入患者IM账号失败 - patient_id: ' . $patientId . ', user_id: ' . $userId . ', error: ' . $result['message']); return false; } } catch (\Exception $e) { \think\facade\Log::error('导入患者IM账号异常 - patient_id: ' . $patientId . ', user_id: ' . $userId . ', error: ' . $e->getMessage()); return false; } } /** * @notes 获取医助签名(供小程序调用) * @param int $patientId * @return array|bool */ public static function getDoctorSignature(int $patientId) { try { // 获取配置 $config = self::getTrtcConfig(); if (!$config) { self::setError('请先配置腾讯云TRTC参数'); return false; } // 医助userId(使用 doctor_ 前缀) $doctorUserId = 'doctor_' . $patientId; // 生成医助的 UserSig $userSig = self::generateUserSig( $config['sdkAppId'], $config['secretKey'], $doctorUserId ); if (!$userSig) { self::setError('生成签名失败'); return false; } // 确保医助账号已导入IM(使用正确的方法) self::importDoctorAccountToIm($patientId, $doctorUserId); \think\facade\Log::info('获取医助签名成功 - admin_id: ' . $patientId . ', user_id: ' . $doctorUserId); return [ 'sdkAppId' => (int)$config['sdkAppId'], 'userId' => $doctorUserId, 'userSig' => $userSig, 'expireTime' => 86400 ]; } catch (\Exception $e) { \think\facade\Log::error('获取医助签名失败 - admin_id: ' . $patientId . ', error: ' . $e->getMessage()); self::setError($e->getMessage()); return false; } } /** * @notes 获取患者签名(供小程序调用) * @param int $patientId * @return array|bool */ public static function getPatientSignature(int $patientId) { try { // 获取配置 $config = self::getTrtcConfig(); if (!$config) { self::setError('请先配置腾讯云TRTC参数'); return false; } // 患者userId $patientUserId = 'patient_' . $patientId; // 生成患者的 UserSig $userSig = self::generateUserSig( $config['sdkAppId'], $config['secretKey'], $patientUserId ); if (!$userSig) { self::setError('生成签名失败'); return false; } //添加患者资料 // 确保患者账号已导入IM self::ensurePatientImAccount($patientId, $patientUserId); \think\facade\Log::info('小程序获取患者签名成功 - patient_id: ' . $patientId . ', user_id: ' . $patientUserId); $data= Diagnosis::where('id', $patientId)->find(); return [ 'sdkAppId' => (int)$config['sdkAppId'], 'userId' => $patientUserId, 'userSig' => $userSig, 'data'=>$data, 'expireTime' => 86400 ]; } catch (\Exception $e) { \think\facade\Log::error('获取患者签名失败 - patient_id: ' . $patientId . ', error: ' . $e->getMessage()); self::setError($e->getMessage()); return false; } } /** * @notes 获取医助列表 * @return array */ public static function getAssistants() { try { // 通过中间表查询角色ID为2的管理员(医助) $assistants = \app\common\model\auth\Admin::alias('a') ->join('admin_role ar', 'a.id = ar.admin_id') ->where('ar.role_id', 2) ->where('a.disable', 0) ->field(['a.id', 'a.name', 'a.account']) ->order('a.id', 'asc') ->select() ->toArray(); return $assistants; } catch (\Exception $e) { \think\facade\Log::error('获取医助列表失败: ' . $e->getMessage()); return []; } } /** * @notes 获取医生列表 * @return array */ public static function getDoctors() { try { // 通过中间表查询角色ID为1的管理员(医生) $doctors = \app\common\model\auth\Admin::alias('a') ->join('admin_role ar', 'a.id = ar.admin_id') ->where('ar.role_id', 1) ->where('a.disable', 0) ->field(['a.id', 'a.name', 'a.account']) ->order('a.id', 'asc') ->select() ->toArray(); return $doctors; } catch (\Exception $e) { \think\facade\Log::error('获取医生列表失败: ' . $e->getMessage()); return []; } } /** * @notes 生成小程序码 * @param array $params * @return array|false */ public static function generateMiniProgramQrcode(array $params) { try { $diagnosisId = $params['diagnosis_id'] ?? ''; $doctorId = $params['doctor_id'] ?? ''; $patientId = $params['patient_id']; $shareUserId = $params['share_user_id']; // 视频登录页(pages/login/login)需要传挂号医生id,其余场景传诊单id $page = !empty($params['mini_program_path']) ? $params['mini_program_path'] : 'pages/order/monad/monad'; $sceneId = ($page === 'pages/login/login' && !empty($doctorId)) ? $doctorId : $diagnosisId; if (empty($sceneId)) { throw new \Exception($page === 'pages/login/login' ? '挂号医生ID不能为空' : '诊单ID不能为空'); } // 获取小程序配置 $config = self::getMiniProgramConfig(); if (!$config) { throw new \Exception('小程序配置未设置'); } // 小程序 onLoad 解析 scene,须与前端 parsePageParams 约定一致(微信 scene 总长勿超过 32 字符) $scene = "id={$sceneId}&sa={$shareUserId}&did={$doctorId}"; // 调用微信接口生成小程序码 $qrcodeUrl = self::generateWxQrcode($config, $page, $scene); if (!$qrcodeUrl) { throw new \Exception('生成小程序码失败: ' . self::getError()); } return [ 'qrcode_url' => $qrcodeUrl, 'diagnosis_id' => $diagnosisId, 'patient_id' => $patientId ]; } catch (\Exception $e) { self::setError($e->getMessage()); return false; } } /** * @notes 生成订单小程序码 */ public static function generateOrderQrcode(array $params) { try { $orderNo = $params['order_no'] ?? ''; if (empty($orderNo)) { throw new \Exception('订单号不能为空'); } // 查询订单ID(使用ID代替订单号,更短) $order = \app\common\model\Order::where('order_no', $orderNo)->find(); if (!$order) { throw new \Exception('订单不存在'); } $config = self::getMiniProgramConfig(); if (!$config) { throw new \Exception('小程序配置未设置'); } $page = 'pages/order/order'; // 微信小程序 scene 参数最大长度为32字符,使用订单ID代替订单号 $scene = "id={$order->id}"; $qrcodeUrl = self::generateWxQrcode($config, $page, $scene); if (!$qrcodeUrl) { throw new \Exception('生成小程序码失败: ' . self::getError()); } return [ 'qrcode_url' => $qrcodeUrl, 'order_no' => $orderNo ]; } catch (\Exception $e) { self::setError($e->getMessage()); return false; } } /** * @notes 获取小程序配置 * @return array|null */ private static function getMiniProgramConfig(): ?array { try { // 从配置表获取小程序配置 $appId = \app\common\service\ConfigService::get('mnp_setting', 'app_id', ''); $appSecret = \app\common\service\ConfigService::get('mnp_setting', 'app_secret', ''); if (empty($appId) || empty($appSecret)) { return null; } return [ 'app_id' => $appId, 'app_secret' => $appSecret ]; } catch (\Exception $e) { return null; } } /** * @notes 生成微信小程序码 * @param array $config * @param string $page * @param string $scene * @return string|false */ private static function generateWxQrcode(array $config, string $page, string $scene, int $retryCount = 0) { try { // 记录请求参数 \think\facade\Log::info('开始生成小程序码', [ 'page' => $page, 'scene' => $scene, 'app_id' => $config['app_id'], 'retry_count' => $retryCount ]); // 获取access_token(如果是重试,强制刷新) $forceRefresh = $retryCount > 0; $accessToken = self::getWxAccessToken($config['app_id'], $config['app_secret'], $forceRefresh); if (!$accessToken) { throw new \Exception('获取access_token失败: ' . self::getError()); } // 调用微信接口生成小程序码 $url = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={$accessToken}"; // 记录 scene 长度 \think\facade\Log::info('Scene参数信息', [ 'scene' => $scene, 'scene_length' => strlen($scene), 'scene_urlencode_length' => strlen(urlencode($scene)) ]); $data = [ 'scene' => $scene, 'page' => $page, 'check_path' => false, 'env_version' => 'release', 'width' => 280 ]; $response = self::httpPost($url, json_encode($data)); // 检查是否返回错误(JSON格式) $result = json_decode($response, true); if (is_array($result) && isset($result['errcode']) && $result['errcode'] != 0) { $errorMsg = "生成小程序码失败: errcode={$result['errcode']}, errmsg=" . ($result['errmsg'] ?? '未知错误'); \think\facade\Log::error($errorMsg); // 如果是AccessToken错误且未重试过,自动重试一次 if (($result['errcode'] == 40001 || $result['errcode'] == 42001) && $retryCount < 1) { \think\facade\Log::info('AccessToken失效,自动重试(第' . ($retryCount + 1) . '次)'); // 清除缓存 $cacheKey = 'wx_access_token_' . $config['app_id']; cache($cacheKey, null); // 递归调用,重试一次 return self::generateWxQrcode($config, $page, $scene, $retryCount + 1); } throw new \Exception($errorMsg); } // 如果不是JSON错误,说明返回的是图片二进制数据 // 保存图片 $filename = 'qrcode_' . $config['app_id'] . '_' . md5($scene) . '_' . time() . '.png'; $savePath = 'uploads/qrcode/' . date('Ymd') . '/'; $fullPath = public_path() . $savePath; if (!is_dir($fullPath)) { mkdir($fullPath, 0755, true); } $saved = file_put_contents($fullPath . $filename, $response); if (!$saved) { throw new \Exception('保存二维码图片失败'); } $qrcodeUrl = request()->domain() . '/' . $savePath . $filename; \think\facade\Log::info('小程序码生成成功', ['url' => $qrcodeUrl, 'retry_count' => $retryCount]); return $qrcodeUrl; } catch (\Exception $e) { \think\facade\Log::error('generateWxQrcode异常: ' . $e->getMessage()); self::setError($e->getMessage()); return false; } } /** * @notes 获取微信access_token * @param string $appId * @param string $appSecret * @param bool $forceRefresh 是否强制刷新(不使用缓存) * @return string|false */ private static function getWxAccessToken(string $appId, string $appSecret, bool $forceRefresh = false) { try { $cacheKey = 'wx_access_token_' . $appId; // 如果不是强制刷新,尝试从缓存获取 if (!$forceRefresh) { $accessToken = cache($cacheKey); if ($accessToken) { \think\facade\Log::info('使用缓存的AccessToken'); return $accessToken; } } else { \think\facade\Log::info('强制刷新AccessToken,清除缓存'); cache($cacheKey, null); } // 从微信服务器获取 $url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={$appId}&secret={$appSecret}"; $response = self::httpGet($url); // 记录原始响应用于调试 \think\facade\Log::info('微信AccessToken响应: ' . $response); $result = json_decode($response, true); // 检查是否有错误 if (isset($result['errcode']) && $result['errcode'] != 0) { $errorMsg = "获取AccessToken失败: errcode={$result['errcode']}, errmsg=" . ($result['errmsg'] ?? '未知错误'); \think\facade\Log::error($errorMsg); throw new \Exception($errorMsg); } if (isset($result['access_token'])) { // 缓存access_token,有效期7000秒(微信官方7200秒,提前200秒过期) cache($cacheKey, $result['access_token'], 7000); \think\facade\Log::info('AccessToken获取成功并已缓存'); return $result['access_token']; } throw new \Exception('获取access_token失败: 响应中没有access_token字段'); } catch (\Exception $e) { \think\facade\Log::error('getWxAccessToken异常: ' . $e->getMessage()); self::setError($e->getMessage()); return false; } } /** * @notes HTTP GET请求 * @param string $url * @return string|false */ private static function httpGet(string $url) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); $response = curl_exec($ch); curl_close($ch); return $response; } /** * @notes HTTP POST请求 * @param string $url * @param string $data * @return string|false */ private static function httpPost(string $url, string $data) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $data); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); $response = curl_exec($ch); curl_close($ch); return $response; } /** * @notes 获取诊单详情并记录查看 * @param array $params * @return array|false */ public static function diagnosisDetailWithRecord(array $params) { try { $diagnosisId = $params['id']; $userId = $params['user_id'] ?? 0; $shareUserId = $params['share_user_id'] ?? 0; // 获取诊单详情 $diagnosis = self::detail(['id' => $diagnosisId,'user_id' => $userId]); if (!$diagnosis) { throw new \Exception('诊单不存在'); } // 记录查看行为 // self::recordDiagnosisView([ // 'user_id' => $userId, // 'diagnosis_id' => $diagnosisId, // 'patient_id' => $diagnosis['patient_id'] ?? 0, // 'share_user_id' => $shareUserId // ]); return $diagnosis; } catch (\Exception $e) { self::setError($e->getMessage()); return false; } } /** * @notes 记录诊单查看 * @param array $params * @return bool */ private static function recordDiagnosisView(array $params) { try { $userId = $params['user_id']; $diagnosisId = $params['diagnosis_id']; $patientId = $params['patient_id']; $shareUserId = $params['share_user_id']; // 查找是否已有记录 $record = \app\common\model\DiagnosisViewRecord::where([ 'user_id' => $userId, 'diagnosis_id' => $diagnosisId ])->find(); $now = time(); if ($record) { // 更新记录 $record->view_count = $record->view_count + 1; $record->last_view_time = $now; $record->update_time = $now; $record->save(); } else { // 创建新记录 \app\common\model\DiagnosisViewRecord::create([ 'user_id' => $userId, 'diagnosis_id' => $diagnosisId, 'patient_id' => $patientId, 'share_user_id' => $shareUserId, 'view_count' => 1, 'first_view_time' => $now, 'last_view_time' => $now, 'is_confirmed' => 0, 'create_time' => $now, 'update_time' => $now ]); } return true; } catch (\Exception $e) { \think\facade\Log::error('记录诊单查看失败: ' . $e->getMessage()); return false; } } /** * @notes 确认诊单 * @param array $params * @return bool */ public static function confirmDiagnosisRecord(array $params) { try { $userId = $params['user_id'] ?? 0; $diagnosisId = $params['id']; $shareUserId = $params['share_user_id'] ?? 0; if(!$userId){ throw new \Exception('请重新登录!'); } // 查找查看记录 $record = \app\common\model\DiagnosisViewRecord::where([ 'user_id' => $userId, 'diagnosis_id' => $diagnosisId ])->find(); $now = time(); if ($record) { // 更新确认状态 $record->is_confirmed = 1; $record->confirmed_time = $now; $record->update_time = $now; $record->save(); } else { // 如果没有查看记录,创建一条已确认的记录 $diagnosis = Diagnosis::find($diagnosisId); if (!$diagnosis) { throw new \Exception('诊单不存在'); } // 再次检查是否存在记录,防止并发创建重复记录 $existingRecord = \app\common\model\DiagnosisViewRecord::where([ 'user_id' => $userId, 'diagnosis_id' => $diagnosisId ])->find(); if (!$existingRecord) { \app\common\model\DiagnosisViewRecord::create([ 'user_id' => $userId, 'diagnosis_id' => $diagnosisId, 'patient_id' => $diagnosis->patient_id ?? 0, 'share_user_id' => $shareUserId, 'view_count' => 1, 'first_view_time' => $now, 'last_view_time' => $now, 'is_confirmed' => 1, 'confirmed_time' => $now, 'create_time' => $now, 'update_time' => $now ]); } } return true; } catch (\Exception $e) { self::setError($e->getMessage()); return false; } } /** * @notes 获取就诊卡列表(供小程序调用) * @param int $patientId * @return array|false */ public static function getCardList(int $user_id) { try { if (!$user_id) { self::setError('用户ID不能为空'); return false; } // 从诊断查看记录表中获取该用户查看过的诊断ID列表 $viewRecords = \think\facade\Db::name('diagnosis_view_records') ->where('user_id', $user_id) ->where('delete_time', null) ->column('diagnosis_id'); if (empty($viewRecords)) { return []; } // 根据诊断ID列表查询诊单详情 $cardList = Diagnosis::whereIn('id', $viewRecords) ->where('delete_time', null) ->field([ 'id', 'patient_id', 'patient_name', 'gender', 'age', 'diagnosis_date', 'diagnosis_type', 'syndrome_type', 'status', 'create_time', 'update_time' ]) ->order('create_time', 'desc') ->select() ->toArray(); // 处理数据格式 foreach ($cardList as &$card) { // 转换诊断日期为时间戳和格式化文本 if ($card['diagnosis_date']) { $card['diagnosis_date_text'] = date('Y-m-d', $card['diagnosis_date']); } else { $card['diagnosis_date_text'] = '-'; } // 添加性别描述 $card['gender_desc'] = $card['gender'] == 1 ? '男' : '女'; // 添加状态描述 $card['status_desc'] = $card['status'] == 1 ? '启用' : '禁用'; // 格式化创建时间 if ($card['create_time']) { $card['create_time'] = date('Y-m-d H:i:s', $card['create_time']); } // 格式化更新时间 if ($card['update_time']) { $card['update_time'] = date('Y-m-d H:i:s', $card['update_time']); } } return $cardList; } catch (\Exception $e) { \think\facade\Log::error('获取就诊卡列表失败 - user_id: ' . $user_id . ', error: ' . $e->getMessage()); self::setError($e->getMessage()); return false; } } /** * @notes 新建就诊卡(供小程序调用) * patient_id 可选:有则复用(同患者多张卡),无则自动生成(新患者首张卡) * @param array $params * @return array|bool 成功返回 ['id'=>诊单ID, 'patient_id'=>患者ID],失败返回false */ public static function addCard(array $params) { try { $userId = $params['user_id'] ?? 0; unset($params['user_id']); // 诊单表无此字段,仅用于创建 view_record $patientId = $params['patient_id'] ?? 0; if (!$patientId) { $params['patient_id'] = self::generatePatientId(); } $params['status'] = 1; // 处理既往史数组 if (isset($params['past_history']) && is_array($params['past_history'])) { $params['past_history'] = implode(',', $params['past_history']); } $multiSelectFields = [ 'diet_condition', 'body_feeling', 'sleep_condition', 'eye_condition', 'head_feeling', 'sweat_condition', 'skin_condition', 'urine_condition', 'stool_condition', 'kidney_condition', 'local_hospital_diagnosis' ]; foreach ($multiSelectFields as $field) { if (isset($params[$field]) && is_array($params[$field])) { $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); } if (isset($params['diagnosis_date'])) { $params['diagnosis_date'] = is_numeric($params['diagnosis_date']) ? $params['diagnosis_date'] : strtotime($params['diagnosis_date']); } $model = Diagnosis::create($params); self::createPatientTrtcAccount($model->patient_id); // 关联 diagnosis_view_records,便于用户通过 User::with('diagnosis') 获取就诊卡 if ($userId) { $now = time(); \app\common\model\DiagnosisViewRecord::create([ 'user_id' => $userId, 'diagnosis_id' => $model->id, 'patient_id' => $model->patient_id, 'share_user_id' => 0, 'view_count' => 1, 'first_view_time' => $now, 'last_view_time' => $now, 'is_confirmed' => 0, 'create_time' => $now, 'update_time' => $now ]); } return ['id' => $model->id, 'patient_id' => $model->patient_id]; } catch (\Exception $e) { self::setError($e->getMessage()); return false; } } /** * @notes 更新就诊卡(供小程序调用) * @param array $params * @return bool */ public static function updateCard(array $params) { try { $diagnosisId = $params['id'] ?? 0; $patientId = $params['patient_id'] ?? 0; if (!$diagnosisId) { self::setError('诊单ID不能为空'); return false; } if (!$patientId) { self::setError('患者ID不能为空'); return false; } // 获取诊单信息 $diagnosis = Diagnosis::find($diagnosisId); if (!$diagnosis) { self::setError('诊单不存在'); return false; } // 权限验证:只能修改自己的诊单 if ($diagnosis->patient_id != $patientId) { \think\facade\Log::warning('非法修改诊单 - diagnosis_id: ' . $diagnosisId . ', patient_id: ' . $patientId . ', actual_patient_id: ' . $diagnosis->patient_id); self::setError('无权限修改此诊单'); return false; } // 准备更新数据 $updateData = [ 'patient_name' => $params['patient_name'] ?? '', 'gender' => $params['gender'] ?? 1, 'age' => !empty($params['age']) ? intval($params['age']) : 0, 'id_card' => $params['id_card'] ?? '', 'height' => !empty($params['height']) ? floatval($params['height']) : null, 'weight' => !empty($params['weight']) ? floatval($params['weight']) : null, 'systolic_pressure' => !empty($params['systolic_pressure']) ? intval($params['systolic_pressure']) : null, 'diastolic_pressure' => !empty($params['diastolic_pressure']) ? intval($params['diastolic_pressure']) : null, 'fasting_blood_sugar' => !empty($params['fasting_blood_sugar']) ? floatval($params['fasting_blood_sugar']) : null, 'diabetes_discovery_year' => !empty($params['diabetes_discovery_year']) ? intval($params['diabetes_discovery_year']) : null, 'local_hospital_diagnosis' => $params['local_hospital_diagnosis'] ?? '', 'local_hospital_name' => $params['local_hospital_name'] ?? '', 'local_hospital_visit_date' => $params['local_hospital_visit_date'] ?? '', 'diagnosis_type' => $params['diagnosis_type'] ?? '', 'syndrome_type' => $params['syndrome_type'] ?? '', 'diabetes_type' => $params['diabetes_type'] ?? '', // 现病史字段 'appetite' => $params['appetite'] ?? '', 'water_intake' => $params['water_intake'] ?? '', 'diet_condition' => $params['diet_condition'] ?? '', 'weight_change' => $params['weight_change'] ?? '', 'body_feeling' => $params['body_feeling'] ?? '', 'sleep_condition' => $params['sleep_condition'] ?? '', 'eye_condition' => $params['eye_condition'] ?? '', 'head_feeling' => $params['head_feeling'] ?? '', 'sweat_condition' => $params['sweat_condition'] ?? '', 'skin_condition' => $params['skin_condition'] ?? '', 'urine_condition' => $params['urine_condition'] ?? '', 'stool_condition' => $params['stool_condition'] ?? '', 'kidney_condition' => $params['kidney_condition'] ?? '', 'fatty_liver_degree' => $params['fatty_liver_degree'] ?? '', // 既往史字段 'past_history' => $params['past_history'] ?? '', 'trauma_history' => isset($params['trauma_history']) ? intval($params['trauma_history']) : 0, 'surgery_history' => isset($params['surgery_history']) ? intval($params['surgery_history']) : 0, 'allergy_history' => isset($params['allergy_history']) ? intval($params['allergy_history']) : 0, 'family_history' => isset($params['family_history']) ? intval($params['family_history']) : 0, 'pregnancy_history' => isset($params['pregnancy_history']) ? intval($params['pregnancy_history']) : 0, // 临床信息 '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'] ?? '', 'doctor_advice' => $params['doctor_advice'] ?? '', 'remark' => $params['remark'] ?? '', 'update_time' => time() ]; // 处理诊断日期 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); \think\facade\Log::info('诊单更新成功 - diagnosis_id: ' . $diagnosisId . ', patient_id: ' . $patientId); return true; } catch (\Exception $e) { \think\facade\Log::error('更新诊单失败 - error: ' . $e->getMessage()); self::setError($e->getMessage()); return false; } } /** * @notes 获取患者的企微信息(基于会话内容存档接口) */ public static function getWechatExternalContact(int $patientId) { try { $diagnosis = \app\common\model\tcm\Diagnosis::where('patient_id', $patientId) ->field(['id', 'patient_name', 'phone', 'external_userid']) ->findOrEmpty(); if ($diagnosis->isEmpty()) { $diagnosis = \app\common\model\tcm\Diagnosis::where('id', $patientId) ->field(['id', 'patient_name', 'phone', 'external_userid']) ->findOrEmpty(); } if ($diagnosis->isEmpty()) { self::setError('未找到患者信息'); return false; } $corpId = env('work_wechat.corp_id', ''); $msgauditSecret = env('work_wechat.msgaudit_secret', ''); $result = [ 'patient_name' => $diagnosis->patient_name, 'phone' => $diagnosis->phone, 'external_userid' => $diagnosis->external_userid ?? '', 'msgaudit_enabled' => !empty($corpId) && !empty($msgauditSecret), 'permit_users' => [], 'msgaudit_agree' => [], ]; if (empty($corpId) || empty($msgauditSecret)) { return $result; } $accessToken = self::getAccessToken($corpId, $msgauditSecret, 'msgaudit'); if (!$accessToken) { return $result; } // 获取开启会话存档的成员列表 $permitUsers = self::fetchMsgAuditPermitUsers($accessToken); $result['permit_users'] = $permitUsers; // 如果有 external_userid,检查同意存档情况 if (!empty($diagnosis->external_userid) && !empty($permitUsers)) { $agreeInfo = self::checkMsgAuditAgree($accessToken, $permitUsers, $diagnosis->external_userid); $result['msgaudit_agree'] = $agreeInfo; } return $result; } catch (\Exception $e) { self::setError($e->getMessage()); return false; } } // ========== 通用 access_token 获取 ========== private static function getAccessToken(string $corpId, string $secret, string $type = 'app') { $cacheKey = "work_wechat_{$type}_token_" . md5($corpId . $secret); $cached = \think\facade\Cache::get($cacheKey); if ($cached) return $cached; $url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={$corpId}&corpsecret={$secret}"; $response = self::wechatHttpGet($url); if (!$response || ($response['errcode'] ?? -1) != 0) { \think\facade\Log::error("获取企微access_token失败[{$type}]: " . json_encode($response)); return false; } \think\facade\Cache::set($cacheKey, $response['access_token'], $response['expires_in'] - 200); return $response['access_token']; } // ========== 会话内容存档 msgaudit 接口 ========== /** * @notes 获取会话内容存档开启成员列表 */ public static function getMsgAuditPermitUsers() { try { $corpId = env('work_wechat.corp_id', ''); $msgauditSecret = env('work_wechat.msgaudit_secret', ''); if (empty($corpId) || empty($msgauditSecret)) { self::setError('会话内容存档Secret未配置(MSGAUDIT_SECRET)'); return false; } $accessToken = self::getAccessToken($corpId, $msgauditSecret, 'msgaudit'); if (!$accessToken) { self::setError('获取会话内容存档access_token失败'); return false; } $ids = self::fetchMsgAuditPermitUsers($accessToken); return ['ids' => $ids]; } catch (\Exception $e) { self::setError($e->getMessage()); return false; } } private static function fetchMsgAuditPermitUsers(string $accessToken): array { $url = "https://qyapi.weixin.qq.com/cgi-bin/msgaudit/get_permit_user_list?access_token={$accessToken}"; $response = self::wechatHttpPost($url, '{}'); if (!$response || ($response['errcode'] ?? -1) != 0) { return []; } return $response['ids'] ?? []; } /** * @notes 检查单聊会话同意存档情况 */ private static function checkMsgAuditAgree(string $accessToken, array $permitUserIds, string $externalUserId) { $infoList = []; foreach ($permitUserIds as $userId) { $infoList[] = [ 'userid' => $userId, 'exteranalopenid' => $externalUserId, ]; } $url = "https://qyapi.weixin.qq.com/cgi-bin/msgaudit/check_single_agree?access_token={$accessToken}"; $response = self::wechatHttpPost($url, json_encode(['info' => $infoList])); if (!$response || ($response['errcode'] ?? -1) != 0) { return []; } return $response['agreeinfo'] ?? []; } // ========== HTTP 工具方法 ========== private static function wechatHttpGet(string $url) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_TIMEOUT, 10); $result = curl_exec($ch); curl_close($ch); return $result ? json_decode($result, true) : null; } private static function wechatHttpPost(string $url, string $jsonBody) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonBody); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); $result = curl_exec($ch); curl_close($ch); return $result ? json_decode($result, true) : null; } /** * @notes 医助理诊单统计(按部门维度,按人统计创建数量) * @param array $params start_time, end_time, days(可选,默认7) * @return array */ public static function assistantDiagnosisStats(array $params = []) { $days = isset($params['days']) ? (int)$params['days'] : 7; $endTime = !empty($params['end_time']) ? strtotime($params['end_time']) : time(); if ($days === 0) { $startTime = strtotime(date('Y-m-d')); } else { $days = $days > 0 ? min($days, 90) : 7; $startTime = !empty($params['start_time']) ? strtotime($params['start_time']) : strtotime("-{$days} days", $endTime); if ($startTime > $endTime) { $startTime = strtotime("-{$days} days", $endTime); } } $depts = \app\common\model\dept\Dept::where('status', 1) ->whereNull('delete_time') ->order('sort desc, id asc') ->column('name', 'id'); $assistantIds = \app\common\model\auth\AdminRole::where('role_id', 2) ->column('admin_id'); if (empty($assistantIds)) { return [ 'date_range' => [date('Y-m-d', $startTime), date('Y-m-d', $endTime)], 'today_total' => 0, 'today_ranking' => [], 'departments' => [], 'ranking' => [], 'chart' => ['xAxis' => [], 'series' => []], ]; } $adminDepts = \app\common\model\auth\AdminDept::whereIn('admin_id', $assistantIds) ->select() ->toArray(); $adminToDepts = []; foreach ($adminDepts as $ad) { $adminId = $ad['admin_id']; if (!isset($adminToDepts[$adminId])) { $adminToDepts[$adminId] = []; } $adminToDepts[$adminId][] = (int)$ad['dept_id']; } $admins = \app\common\model\auth\Admin::whereIn('id', $assistantIds) ->whereNull('delete_time') ->column('name', 'id'); $countRows = Diagnosis::where('assistant_id', 'in', $assistantIds) ->where('create_time', 'between', [$startTime, $endTime]) ->whereNull('delete_time') ->field('assistant_id, count(*) as cnt') ->group('assistant_id') ->select() ->toArray(); $counts = []; foreach ($countRows as $row) { $counts[$row['assistant_id']] = (int)$row['cnt']; } $todayStart = strtotime(date('Y-m-d')); $todayEnd = time(); $todayCountRows = Diagnosis::where('assistant_id', 'in', $assistantIds) ->where('create_time', 'between', [$todayStart, $todayEnd]) ->whereNull('delete_time') ->field('assistant_id, count(*) as cnt') ->group('assistant_id') ->select() ->toArray(); $todayCounts = []; foreach ($todayCountRows as $row) { $todayCounts[$row['assistant_id']] = (int)$row['cnt']; } $todayTotal = array_sum($todayCounts); $todayRanking = []; foreach ($assistantIds as $adminId) { $cnt = (int)($todayCounts[$adminId] ?? 0); $todayRanking[] = [ 'id' => (int)$adminId, 'name' => $admins[$adminId] ?? '未知', 'count' => $cnt, ]; } usort($todayRanking, fn($a, $b) => $b['count'] <=> $a['count']); $todayRanking = array_values(array_filter($todayRanking, fn($a) => $a['count'] > 0)); $deptData = []; $assignedAdminIds = []; foreach ($depts as $deptId => $deptName) { $assistants = []; foreach ($adminToDepts as $adminId => $deptIds) { if (!in_array((int)$deptId, $deptIds)) { continue; } $assignedAdminIds[] = $adminId; $cnt = (int)($counts[$adminId] ?? 0); $assistants[] = [ 'id' => (int)$adminId, 'name' => $admins[$adminId] ?? '未知', 'count' => $cnt, ]; } $deptData[] = [ 'dept_id' => (int)$deptId, 'dept_name' => $deptName, 'assistants' => $assistants, 'total' => array_sum(array_column($assistants, 'count')), ]; } $unassignedIds = array_diff($assistantIds, array_unique($assignedAdminIds)); if (!empty($unassignedIds)) { $assistants = []; foreach ($unassignedIds as $adminId) { $cnt = (int)($counts[$adminId] ?? 0); $assistants[] = [ 'id' => (int)$adminId, 'name' => $admins[$adminId] ?? '未知', 'count' => $cnt, ]; } $deptData[] = [ 'dept_id' => 0, 'dept_name' => '未分配部门', 'assistants' => $assistants, 'total' => array_sum(array_column($assistants, 'count')), ]; } $ranking = []; foreach ($assistantIds as $adminId) { $ranking[] = [ 'id' => (int)$adminId, 'name' => $admins[$adminId] ?? '未知', 'count' => (int)($counts[$adminId] ?? 0), ]; } usort($ranking, fn($a, $b) => $b['count'] <=> $a['count']); $allAssistantNames = []; $allAssistantData = []; foreach ($ranking as $i => $a) { $allAssistantNames[] = ($i + 1) . '.' . $a['name']; $allAssistantData[] = $a['count']; } return [ 'date_range' => [date('Y-m-d', $startTime), date('Y-m-d', $endTime)], 'today_total' => $todayTotal, 'today_ranking' => $todayRanking, 'departments' => $deptData, 'ranking' => $ranking, 'chart' => [ 'xAxis' => $allAssistantNames, 'series' => [['name' => '诊单数', 'data' => $allAssistantData]], ], ]; } }