2381 lines
86 KiB
PHP
2381 lines
86 KiB
PHP
<?php
|
||
// +----------------------------------------------------------------------
|
||
// | likeadmin快速开发前后端分离管理后台(PHP版)
|
||
// +----------------------------------------------------------------------
|
||
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
|
||
// | 开源版本可自由商用,可去除界面版权logo
|
||
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
|
||
// | github下载:https://github.com/likeshop-github/likeadmin
|
||
// | 访问官网:https://www.likeadmin.cn
|
||
// | likeadmin团队 版权所有 拥有最终解释权
|
||
// +----------------------------------------------------------------------
|
||
// | author: likeadminTeam
|
||
// +----------------------------------------------------------------------
|
||
|
||
namespace app\adminapi\logic\tcm;
|
||
|
||
use app\common\logic\BaseLogic;
|
||
use app\common\model\tcm\Diagnosis;
|
||
use app\common\model\DiagnosisViewRecord;
|
||
/**
|
||
* 中医辨房病因诊单逻辑
|
||
* Class DiagnosisLogic
|
||
* @package app\adminapi\logic\tcm
|
||
*/
|
||
class DiagnosisLogic extends BaseLogic
|
||
{
|
||
/**
|
||
* @notes 添加诊单
|
||
* @param array $params
|
||
* @return int|bool 成功返回ID,失败返回false
|
||
*/
|
||
public static function add(array $params)
|
||
{
|
||
try {
|
||
// 检查手机号是否重复
|
||
if (!empty($params['phone'])) {
|
||
$exists = Diagnosis::where('phone', $params['phone'])
|
||
->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<int, string> 如 ['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<int, array<string, mixed>> $rows
|
||
* @return array<int, array<string, mixed>>
|
||
*/
|
||
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<int, array<string, mixed>>
|
||
*/
|
||
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<string, mixed> $raw
|
||
* @return array<string, mixed>
|
||
*/
|
||
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<string, mixed>
|
||
*/
|
||
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]],
|
||
],
|
||
];
|
||
}
|
||
}
|