新增功能
This commit is contained in:
@@ -314,6 +314,7 @@ class DiagnosisController extends BaseAdminController
|
||||
return $this->fail('房间号不能为空');
|
||||
}
|
||||
|
||||
$params['admin_id'] = (int)$this->adminId;
|
||||
$result = DiagnosisLogic::bindCallRoom($params);
|
||||
if ($result) {
|
||||
return $this->success('绑定成功', [], 1, 1);
|
||||
@@ -321,6 +322,69 @@ class DiagnosisController extends BaseAdminController
|
||||
return $this->fail(DiagnosisLogic::getError());
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 接通后发起腾讯云云端混流录制(需配置 CAM 与云点播)
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function startCloudRecording()
|
||||
{
|
||||
$params = $this->request->post();
|
||||
if (empty($params['diagnosis_id'])) {
|
||||
return $this->fail('诊单ID不能为空');
|
||||
}
|
||||
$params['admin_id'] = (int)$this->adminId;
|
||||
$result = DiagnosisLogic::startCloudRecording($params);
|
||||
if ($result === false) {
|
||||
return $this->fail(DiagnosisLogic::getError());
|
||||
}
|
||||
|
||||
return $this->data($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 浏览器本地上传通话录制后,关联到通话记录(合并 recording_urls)
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function attachLocalCallRecording()
|
||||
{
|
||||
$params = $this->request->post();
|
||||
if (empty($params['diagnosis_id'])) {
|
||||
return $this->fail('诊单ID不能为空');
|
||||
}
|
||||
if (empty($params['file_url'])) {
|
||||
return $this->fail('文件地址不能为空');
|
||||
}
|
||||
$params['admin_id'] = (int)$this->adminId;
|
||||
$result = DiagnosisLogic::attachLocalCallRecording($params);
|
||||
if ($result) {
|
||||
return $this->success('已关联录制', [], 1, 1);
|
||||
}
|
||||
|
||||
return $this->fail(DiagnosisLogic::getError());
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 医助旁观诊单当前视频通话(TRTC 进房参数,仅拉流)
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function watchCall()
|
||||
{
|
||||
$diagnosisId = (int)$this->request->get('diagnosis_id', 0);
|
||||
if ($diagnosisId <= 0) {
|
||||
return $this->fail('诊单ID不能为空');
|
||||
}
|
||||
|
||||
$result = DiagnosisLogic::getAssistantWatchRoomParams([
|
||||
'diagnosis_id' => $diagnosisId,
|
||||
'admin_id' => (int)$this->adminId,
|
||||
]);
|
||||
if ($result === false) {
|
||||
return $this->fail(DiagnosisLogic::getError());
|
||||
}
|
||||
|
||||
return $this->data($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取患者通话签名(用于测试)
|
||||
* @return \think\response\Json
|
||||
|
||||
@@ -19,6 +19,7 @@ use app\common\model\tcm\Diagnosis;
|
||||
use app\common\model\DiagnosisViewRecord;
|
||||
use app\common\model\doctor\Appointment;
|
||||
use app\common\model\tcm\Prescription;
|
||||
use app\common\model\tcm\CallRecord;
|
||||
use app\common\model\auth\Admin;
|
||||
use app\common\model\auth\AdminRole;
|
||||
use app\common\lists\ListsSearchInterface;
|
||||
@@ -171,9 +172,76 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
$item['has_prescription'] = isset($prescriptionMap[$item['id']]) ? 1 : 0;
|
||||
}
|
||||
|
||||
// 最近一条通话记录状态(列表行展示:通话中 / 已结束等)
|
||||
$latestCallByDiag = [];
|
||||
if (!empty($diagnosisIds)) {
|
||||
$agg = CallRecord::whereIn('diagnosis_id', $diagnosisIds)
|
||||
->field('diagnosis_id, MAX(id) AS max_id')
|
||||
->group('diagnosis_id')
|
||||
->select()
|
||||
->toArray();
|
||||
$maxIds = array_filter(array_column($agg, 'max_id'));
|
||||
if (!empty($maxIds)) {
|
||||
$recList = CallRecord::whereIn('id', $maxIds)->select()->toArray();
|
||||
foreach ($recList as $r) {
|
||||
$latestCallByDiag[(int)$r['diagnosis_id']] = $r;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($lists as &$item) {
|
||||
$item['video_call_hint'] = $this->buildVideoCallHint($latestCallByDiag[$item['id']] ?? null);
|
||||
}
|
||||
|
||||
return $lists;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed>|null $rec tcm_call_record 一行
|
||||
* @return array{state:string,label:string,end_time?:int,start_time?:int}
|
||||
*/
|
||||
private function buildVideoCallHint(?array $rec): array
|
||||
{
|
||||
if (!$rec) {
|
||||
return ['state' => 'none', 'label' => ''];
|
||||
}
|
||||
$st = (int)($rec['status'] ?? 0);
|
||||
$roomOk = trim((string)($rec['room_id'] ?? '')) !== '';
|
||||
|
||||
if ($st === 1 && $roomOk) {
|
||||
$t = (int)($rec['start_time'] ?? 0);
|
||||
|
||||
return [
|
||||
'state' => 'live',
|
||||
'label' => '视频通话进行中',
|
||||
'start_time' => $t,
|
||||
];
|
||||
}
|
||||
if ($st === 1 && !$roomOk) {
|
||||
return [
|
||||
'state' => 'pending_room',
|
||||
'label' => '通话发起中,待同步房间',
|
||||
];
|
||||
}
|
||||
if ($st === 2) {
|
||||
$end = (int)($rec['end_time'] ?? 0);
|
||||
$timePart = $end > 0 ? date('m-d H:i', $end) : '';
|
||||
|
||||
return [
|
||||
'state' => 'ended',
|
||||
'label' => $timePart !== '' ? ('视频已结束 · ' . $timePart) : '视频已结束',
|
||||
'end_time' => $end,
|
||||
];
|
||||
}
|
||||
if ($st === 3) {
|
||||
return ['state' => 'missed', 'label' => '上次通话未接听'];
|
||||
}
|
||||
if ($st === 4) {
|
||||
return ['state' => 'cancelled', 'label' => '上次通话已取消'];
|
||||
}
|
||||
|
||||
return ['state' => 'none', 'label' => ''];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取数量
|
||||
* @return int
|
||||
|
||||
@@ -934,13 +934,22 @@ class DiagnosisLogic extends BaseLogic
|
||||
->find();
|
||||
|
||||
if ($record) {
|
||||
$taskId = trim((string)($record['cloud_recording_task_id'] ?? ''));
|
||||
if ($taskId !== '') {
|
||||
\app\common\service\TrtcCloudRecordingService::stopRecording(
|
||||
\app\common\service\TrtcCloudRecordingService::trtcSdkAppId(),
|
||||
$taskId
|
||||
);
|
||||
}
|
||||
$endTime = time();
|
||||
$duration = $endTime - $record->start_time;
|
||||
|
||||
$record->save([
|
||||
'status' => 2, // 2-已结束
|
||||
'end_time' => $endTime,
|
||||
'duration' => $duration
|
||||
'duration' => $duration,
|
||||
'cloud_recording_task_id' => '',
|
||||
'update_time' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1019,6 +1028,14 @@ class DiagnosisLogic extends BaseLogic
|
||||
'update_time' => time(),
|
||||
]);
|
||||
|
||||
$adminId = (int)($params['admin_id'] ?? 0);
|
||||
if ($adminId > 0) {
|
||||
self::startCloudRecording([
|
||||
'diagnosis_id' => $diagnosisId,
|
||||
'admin_id' => $adminId,
|
||||
], true);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
self::setError($e->getMessage());
|
||||
@@ -1026,6 +1043,227 @@ class DiagnosisLogic extends BaseLogic
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 医生接通并绑定房间后:发起腾讯云云端混流录制(需 CAM 密钥 + 控制台开通录制 + 云点播)
|
||||
* @param array $params diagnosis_id、admin_id
|
||||
* @return array|false
|
||||
*/
|
||||
/**
|
||||
* @param bool $silent 为 true 时不写入 BaseLogic 错误(供 bindCallRoom 内自动调用,避免污染绑定接口)
|
||||
*/
|
||||
public static function startCloudRecording(array $params, bool $silent = false)
|
||||
{
|
||||
try {
|
||||
$diagnosisId = (int)($params['diagnosis_id'] ?? 0);
|
||||
$adminId = (int)($params['admin_id'] ?? 0);
|
||||
if ($diagnosisId <= 0 || $adminId <= 0) {
|
||||
if (!$silent) {
|
||||
self::setError('参数错误');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$record = \app\common\model\tcm\CallRecord::where('diagnosis_id', $diagnosisId)
|
||||
->where('caller_id', $adminId)
|
||||
->where('status', 1)
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
if (!$record) {
|
||||
if (!$silent) {
|
||||
self::setError('没有进行中的通话记录');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
$roomId = trim((string)($record['room_id'] ?? ''));
|
||||
if ($roomId === '') {
|
||||
if (!$silent) {
|
||||
self::setError('尚未同步房间号,无法开启云端录制');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
$existingTask = trim((string)($record['cloud_recording_task_id'] ?? ''));
|
||||
if ($existingTask !== '') {
|
||||
return [
|
||||
'started' => true,
|
||||
'task_id' => $existingTask,
|
||||
'message' => '已在录制中',
|
||||
];
|
||||
}
|
||||
|
||||
$prefix = (string)config('trtc.recording_bot_prefix', 'recorder_');
|
||||
$botUserId = $prefix . $diagnosisId . '_' . (int)$record['id'];
|
||||
try {
|
||||
$imService = new \app\common\service\TencentImService();
|
||||
$imService->importAccount($botUserId, '云端录制');
|
||||
} catch (\Throwable $e) {
|
||||
// 导入失败不阻断,部分环境仅 TRTC 亦可进房
|
||||
}
|
||||
$botSig = \app\common\service\TrtcCloudRecordingService::makeBotUserSig($botUserId);
|
||||
$sdkAppId = \app\common\service\TrtcCloudRecordingService::trtcSdkAppId();
|
||||
|
||||
$r = \app\common\service\TrtcCloudRecordingService::startMixRecording(
|
||||
$sdkAppId,
|
||||
$roomId,
|
||||
$botUserId,
|
||||
$botSig
|
||||
);
|
||||
if (!$r['ok']) {
|
||||
return [
|
||||
'started' => false,
|
||||
'message' => $r['message'] ?? '开启失败',
|
||||
];
|
||||
}
|
||||
|
||||
$record->save([
|
||||
'cloud_recording_task_id' => $r['task_id'],
|
||||
'recording_status' => 1,
|
||||
'update_time' => time(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'started' => true,
|
||||
'task_id' => $r['task_id'],
|
||||
'message' => '已发起云端录制',
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
if (!$silent) {
|
||||
self::setError($e->getMessage());
|
||||
}
|
||||
\think\facade\Log::warning('startCloudRecording: ' . $e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 医生端浏览器本地录制上传后,将文件访问地址合并写入当前诊单下该医生的最近一条通话记录
|
||||
*/
|
||||
public static function attachLocalCallRecording(array $params): bool
|
||||
{
|
||||
try {
|
||||
$diagnosisId = (int)($params['diagnosis_id'] ?? 0);
|
||||
$adminId = (int)($params['admin_id'] ?? 0);
|
||||
$fileUrl = trim((string)($params['file_url'] ?? ''));
|
||||
if ($diagnosisId <= 0 || $adminId <= 0 || $fileUrl === '') {
|
||||
self::setError('参数错误');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$record = \app\common\model\tcm\CallRecord::where('diagnosis_id', $diagnosisId)
|
||||
->where('caller_id', $adminId)
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
// 与 startCall 的 caller_id 不一致或竞态时,回退到该诊单最新一条通话记录
|
||||
if (!$record) {
|
||||
$record = \app\common\model\tcm\CallRecord::where('diagnosis_id', $diagnosisId)
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
}
|
||||
if (!$record) {
|
||||
self::setError('未找到通话记录');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$prev = [];
|
||||
if (!empty($record->recording_urls)) {
|
||||
$prev = json_decode((string)$record->recording_urls, true);
|
||||
if (!is_array($prev)) {
|
||||
$prev = [];
|
||||
}
|
||||
}
|
||||
$merged = array_values(array_unique(array_merge($prev, [$fileUrl])));
|
||||
|
||||
$record->save([
|
||||
'recording_urls' => json_encode($merged, JSON_UNESCAPED_UNICODE),
|
||||
'recording_status' => 2,
|
||||
'update_time' => time(),
|
||||
]);
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
self::setError($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 医助进入当前诊单 TRTC 房间旁观(仅拉流,与医生端 doctor_{id} 账号体系一致)
|
||||
* @param array $params diagnosis_id、admin_id(当前登录后台用户)
|
||||
* @return array|false
|
||||
*/
|
||||
public static function getAssistantWatchRoomParams(array $params)
|
||||
{
|
||||
try {
|
||||
$diagnosisId = (int)($params['diagnosis_id'] ?? 0);
|
||||
$adminId = (int)($params['admin_id'] ?? 0);
|
||||
if ($diagnosisId <= 0 || $adminId <= 0) {
|
||||
self::setError('参数错误');
|
||||
return false;
|
||||
}
|
||||
|
||||
$diag = Diagnosis::where('id', $diagnosisId)->where('delete_time', null)->find();
|
||||
if (!$diag) {
|
||||
self::setError('诊单不存在');
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((int)($diag['assistant_id'] ?? 0) !== $adminId) {
|
||||
self::setError('仅本诊单指派的医助可旁观该通话');
|
||||
return false;
|
||||
}
|
||||
|
||||
$record = \app\common\model\tcm\CallRecord::where('diagnosis_id', $diagnosisId)
|
||||
->where('status', 1)
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
|
||||
$roomRaw = $record ? trim((string)$record['room_id']) : '';
|
||||
if ($roomRaw === '') {
|
||||
self::setError('当前无进行中的通话或尚未同步房间号,请待医生接通后再试');
|
||||
return false;
|
||||
}
|
||||
|
||||
$config = self::getTrtcConfig();
|
||||
if (!$config) {
|
||||
self::setError('请先配置腾讯云TRTC参数');
|
||||
return false;
|
||||
}
|
||||
|
||||
$doctorUserId = 'doctor_' . $adminId;
|
||||
self::importDoctorAccountToIm($adminId, $doctorUserId);
|
||||
|
||||
$userSig = self::generateUserSig($config['sdkAppId'], $config['secretKey'], $doctorUserId);
|
||||
if (!$userSig) {
|
||||
self::setError('生成签名失败');
|
||||
return false;
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'sdkAppId' => (int)$config['sdkAppId'],
|
||||
'userId' => $doctorUserId,
|
||||
'userSig' => $userSig,
|
||||
'patientName' => (string)($diag['patient_name'] ?? ''),
|
||||
];
|
||||
|
||||
if (ctype_digit($roomRaw)) {
|
||||
$payload['roomId'] = (int)$roomRaw;
|
||||
} else {
|
||||
$payload['strRoomId'] = $roomRaw;
|
||||
}
|
||||
|
||||
return $payload;
|
||||
} catch (\Exception $e) {
|
||||
self::setError($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static function recordingStatusText(int $status): string
|
||||
{
|
||||
$map = [
|
||||
|
||||
Reference in New Issue
Block a user