更新
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user