1727 lines
62 KiB
PHP
1727 lines
62 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
|
||
* @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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @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']);
|
||
}
|
||
|
||
return $records;
|
||
} catch (\Exception $e) {
|
||
self::setError($e->getMessage());
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @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);
|
||
|
||
return [
|
||
'sdkAppId' => (int)$config['sdkAppId'],
|
||
'userId' => $patientUserId,
|
||
'userSig' => $userSig,
|
||
'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'];
|
||
$patientId = $params['patient_id'];
|
||
$shareUserId = $params['share_user_id'];
|
||
|
||
// 获取小程序配置
|
||
$config = self::getMiniProgramConfig();
|
||
if (!$config) {
|
||
throw new \Exception('小程序配置未设置');
|
||
}
|
||
|
||
// 构建小程序路径和参数(前端可传 mini_program_path 覆盖默认路径)
|
||
$page = !empty($params['mini_program_path']) ? $params['mini_program_path'] : 'pages/order/monad/monad';
|
||
$scene = "id={$diagnosisId}&share_user={$shareUserId}";
|
||
|
||
// 调用微信接口生成小程序码
|
||
$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('订单号不能为空');
|
||
}
|
||
|
||
$config = self::getMiniProgramConfig();
|
||
if (!$config) {
|
||
throw new \Exception('小程序配置未设置');
|
||
}
|
||
|
||
$page = 'pages/order/order';
|
||
$scene = "order_no={$orderNo}";
|
||
|
||
$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}";
|
||
|
||
$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 $patientId)
|
||
{
|
||
try {
|
||
if (!$patientId) {
|
||
self::setError('患者ID不能为空');
|
||
return false;
|
||
}
|
||
|
||
// 查询该患者的所有诊单
|
||
$cardList = Diagnosis::where('patient_id', $patientId)
|
||
->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('获取就诊卡列表失败 - patient_id: ' . $patientId . ', error: ' . $e->getMessage());
|
||
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;
|
||
}
|
||
}
|