This commit is contained in:
Your Name
2026-03-23 18:02:16 +08:00
parent 18fee2e8f8
commit 250d173c2f
525 changed files with 10659 additions and 793 deletions
@@ -591,6 +591,289 @@ class DiagnosisLogic extends BaseLogic
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 发起通话
@@ -685,6 +968,15 @@ class DiagnosisLogic extends BaseLogic
$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;
@@ -693,6 +985,58 @@ class DiagnosisLogic extends BaseLogic
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配置
@@ -1020,8 +1364,9 @@ class DiagnosisLogic extends BaseLogic
throw new \Exception('小程序配置未设置');
}
$scene = "id={$sceneId}&share_user={$shareUserId}&doctor_id={$doctorId}";
// 小程序 onLoad 解析 scene,须与前端 parsePageParams 约定一致(微信 scene 总长勿超过 32 字符)
$scene = "id={$sceneId}&sa={$shareUserId}&did={$doctorId}";
// 调用微信接口生成小程序码
$qrcodeUrl = self::generateWxQrcode($config, $page, $scene);
@@ -1051,13 +1396,20 @@ class DiagnosisLogic extends BaseLogic
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 = "order_no={$orderNo}";
// 微信小程序 scene 参数最大长度为32字符,使用订单ID代替订单号
$scene = "id={$order->id}";
$qrcodeUrl = self::generateWxQrcode($config, $page, $scene);
@@ -1128,6 +1480,13 @@ class DiagnosisLogic extends BaseLogic
// 调用微信接口生成小程序码
$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,