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
+23 -4
View File
@@ -15,6 +15,7 @@
namespace app\adminapi\logic;
use app\common\enum\FileEnum;
use app\common\logic\BaseLogic;
use app\common\model\file\File;
use app\common\model\file\FileCate;
@@ -36,7 +37,10 @@ class FileLogic extends BaseLogic
*/
public static function move($params)
{
$adminId = (int)(request()->adminId ?? 0);
(new File())->whereIn('id', $params['ids'])
->where('source', FileEnum::SOURCE_ADMIN)
->where('source_id', $adminId)
->update([
'cid' => $params['cid'],
'update_time' => time()
@@ -51,7 +55,10 @@ class FileLogic extends BaseLogic
*/
public static function rename($params)
{
$adminId = (int)(request()->adminId ?? 0);
(new File())->where('id', $params['id'])
->where('source', FileEnum::SOURCE_ADMIN)
->where('source_id', $adminId)
->update([
'name' => $params['name'],
'update_time' => time()
@@ -66,7 +73,15 @@ class FileLogic extends BaseLogic
*/
public static function delete($params)
{
$result = File::whereIn('id', $params['ids'])->select();
$adminId = (int)(request()->adminId ?? 0);
$result = File::whereIn('id', $params['ids'])
->where('source', FileEnum::SOURCE_ADMIN)
->where('source_id', $adminId)
->select();
$ids = $result->column('id');
if (empty($ids)) {
return;
}
$StorageDriver = new StorageDriver([
'default' => ConfigService::get('storage', 'default', 'local'),
'engine' => ConfigService::get('storage') ?? ['local'=>[]],
@@ -74,7 +89,7 @@ class FileLogic extends BaseLogic
foreach ($result as $item) {
$StorageDriver->delete($item['uri']);
}
File::destroy($params['ids']);
File::destroy($ids);
}
/**
@@ -123,8 +138,12 @@ class FileLogic extends BaseLogic
// 删除分类及子分类
$cateModel->whereIn('id', $cateIds)->update(['delete_time' => time()]);
// 删除文件
$fileIds = $fileModel->whereIn('cid', $cateIds)->column('id');
// 删除文件(仅当前管理员在该分类下的素材)
$adminId = (int)(request()->adminId ?? 0);
$fileIds = $fileModel->whereIn('cid', $cateIds)
->where('source', FileEnum::SOURCE_ADMIN)
->where('source_id', $adminId)
->column('id');
if (!empty($fileIds)) {
self::delete(['ids' => $fileIds]);
@@ -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,
@@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace app\adminapi\logic\tcm;
use app\common\logic\BaseLogic;
use app\common\model\tcm\PrescriptionLibrary;
/**
* 处方库逻辑层
*/
class PrescriptionLibraryLogic extends BaseLogic
{
/**
* @notes 添加处方库
*/
public static function add(array $params): ?int
{
try {
// 处理药材数据
if (isset($params['herbs']) && is_array($params['herbs'])) {
$params['herbs'] = json_encode($params['herbs'], JSON_UNESCAPED_UNICODE);
}
$model = PrescriptionLibrary::create($params);
return $model->id;
} catch (\Exception $e) {
self::setError($e->getMessage());
return null;
}
}
/**
* @notes 编辑处方库
*/
public static function edit(array $params, int $adminId): bool
{
try {
$model = PrescriptionLibrary::findOrEmpty($params['id']);
if ($model->isEmpty()) {
self::setError('处方不存在');
return false;
}
// 权限检查:只有创建者或公开的处方才能编辑
if ($model->creator_id != $adminId && $model->is_public != 1) {
self::setError('无权限编辑此处方');
return false;
}
// 处理药材数据
if (isset($params['herbs']) && is_array($params['herbs'])) {
$params['herbs'] = json_encode($params['herbs'], JSON_UNESCAPED_UNICODE);
}
$model->save($params);
return true;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
/**
* @notes 删除处方库
*/
public static function delete(int $id, int $adminId): bool
{
try {
$model = PrescriptionLibrary::findOrEmpty($id);
if ($model->isEmpty()) {
self::setError('处方不存在');
return false;
}
// 权限检查:只有创建者才能删除
if ($model->creator_id != $adminId) {
self::setError('无权限删除此处方');
return false;
}
$model->delete();
return true;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
/**
* @notes 处方库详情
*/
public static function detail(int $id, int $adminId): ?array
{
try {
$model = PrescriptionLibrary::findOrEmpty($id);
if ($model->isEmpty()) {
return null;
}
// 权限检查:只有创建者或公开的处方才能查看
if ($model->creator_id != $adminId && $model->is_public != 1) {
return null;
}
$data = $model->toArray();
// 解析药材JSON
if (!empty($data['herbs'])) {
$data['herbs'] = json_decode($data['herbs'], true);
} else {
$data['herbs'] = [];
}
return $data;
} catch (\Exception $e) {
self::setError($e->getMessage());
return null;
}
}
}
@@ -31,10 +31,13 @@ class PrescriptionLogic
*/
public static function add(array $params, int $adminId): ?int
{
$diagnosis = Diagnosis::find($params['diagnosis_id']);
if (!$diagnosis) {
self::setError('诊单不存在');
return null;
// 如果没有诊单ID,允许直接创建处方模板
if (!empty($params['diagnosis_id'])) {
$diagnosis = Diagnosis::find($params['diagnosis_id']);
if (!$diagnosis) {
self::setError('诊单不存在');
return null;
}
}
$herbs = $params['herbs'] ?? [];
@@ -57,27 +60,37 @@ class PrescriptionLogic
$data = [
'sn' => $sn,
'diagnosis_id' => (int)$params['diagnosis_id'],
'prescription_name' => $params['prescription_name'] ?? '',
'prescription_type' => $params['prescription_type'] ?? '浓缩水丸',
'diagnosis_id' => (int)($params['diagnosis_id'] ?? 0),
'appointment_id' => (int)($params['appointment_id'] ?? 0),
'patient_id' => (int)$diagnosis->patient_id,
'patient_name' => $params['patient_name'] ?? $diagnosis->patient_name,
'gender' => (int)($params['gender'] ?? $diagnosis->gender),
'age' => (int)($params['age'] ?? $diagnosis->age ?? 0),
'phone' => $params['phone'] ?? $diagnosis->phone ?? '',
'patient_id' => (int)($params['patient_id'] ?? 0),
'patient_name' => $params['patient_name'] ?? '',
'gender' => (int)($params['gender'] ?? 1),
'age' => (int)($params['age'] ?? 0),
'phone' => $params['phone'] ?? '',
'visit_no' => $params['visit_no'] ?? $sn,
'prescription_date' => $params['prescription_date'] ?? date('Y-m-d'),
'pulse' => $params['pulse'] ?? $diagnosis->pulse ?? '',
'tongue' => $params['tongue'] ?? $diagnosis->tongue_coating ?? '',
'pulse' => $params['pulse'] ?? '',
'pulse_condition' => $params['pulse_condition'] ?? '',
'tongue' => $params['tongue'] ?? '',
'tongue_image' => $params['tongue_image'] ?? '',
'clinical_diagnosis' => $params['clinical_diagnosis'] ?? '',
'case_record' => $params['case_record'] ?? null,
'herbs' => $herbs,
'dose_count' => (int)($params['dose_count'] ?? 1),
'dose_unit' => $params['dose_unit'] ?? '剂',
'usage_days' => (int)($params['usage_days'] ?? 7),
'usage_instruction' => $params['usage_instruction'] ?? '水煎服一日二次',
'usage_time' => $params['usage_time'] ?? '饭前',
'usage_way' => $params['usage_way'] ?? '温水送服',
'dietary_taboo' => is_array($params['dietary_taboo'] ?? null) ? implode(',', $params['dietary_taboo']) : ($params['dietary_taboo'] ?? ''),
'usage_notes' => $params['usage_notes'] ?? '',
'amount' => (float)($params['amount'] ?? 0),
'doctor_name' => $params['doctor_name'] ?? '',
'doctor_signature' => $params['doctor_signature'] ?? '',
'template_id' => (int)($params['template_id'] ?? 0),
'is_shared' => (int)($params['is_shared'] ?? 0),
'creator_id' => $adminId,
];
@@ -86,6 +99,91 @@ class PrescriptionLogic
return (int)$prescription->id;
}
/**
* 编辑处方
*/
public static function edit(array $params, int $adminId): bool
{
try {
$prescription = Prescription::find($params['id']);
if (!$prescription) {
self::setError('处方不存在');
return false;
}
// 检查权限:只有创建者或共享的处方才能编辑
if ($prescription->creator_id != $adminId && $prescription->is_shared != 1) {
self::setError('无权限编辑此处方');
return false;
}
$herbs = $params['herbs'] ?? [];
if (empty($herbs) || !is_array($herbs)) {
self::setError('请添加中药');
return false;
}
foreach ($herbs as $h) {
if (empty($h['name'])) {
self::setError('中药名称不能为空');
return false;
}
}
$data = [
'prescription_name' => $params['prescription_name'] ?? $prescription->prescription_name,
'prescription_type' => $params['prescription_type'] ?? $prescription->prescription_type,
'patient_name' => $params['patient_name'] ?? $prescription->patient_name,
'gender' => (int)($params['gender'] ?? $prescription->gender),
'age' => (int)($params['age'] ?? $prescription->age),
'visit_no' => $params['visit_no'] ?? $prescription->visit_no,
'prescription_date' => $params['prescription_date'] ?? $prescription->prescription_date,
'tongue' => $params['tongue'] ?? $prescription->tongue,
'tongue_image' => $params['tongue_image'] ?? $prescription->tongue_image,
'pulse' => $params['pulse'] ?? $prescription->pulse,
'pulse_condition' => $params['pulse_condition'] ?? $prescription->pulse_condition,
'clinical_diagnosis' => $params['clinical_diagnosis'] ?? $prescription->clinical_diagnosis,
'herbs' => $herbs,
'dose_count' => (int)($params['dose_count'] ?? $prescription->dose_count),
'dose_unit' => $params['dose_unit'] ?? $prescription->dose_unit,
'usage_days' => (int)($params['usage_days'] ?? $prescription->usage_days),
'usage_instruction' => $params['usage_instruction'] ?? $prescription->usage_instruction,
'usage_time' => $params['usage_time'] ?? $prescription->usage_time,
'usage_way' => $params['usage_way'] ?? $prescription->usage_way,
'dietary_taboo' => is_array($params['dietary_taboo'] ?? null) ? implode(',', $params['dietary_taboo']) : ($params['dietary_taboo'] ?? $prescription->dietary_taboo),
'usage_notes' => $params['usage_notes'] ?? $prescription->usage_notes,
'doctor_name' => $params['doctor_name'] ?? $prescription->doctor_name,
'is_shared' => (int)($params['is_shared'] ?? $prescription->is_shared),
];
$prescription->save($data);
return true;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
/**
* 删除处方
*/
public static function delete(int $id): bool
{
try {
$prescription = Prescription::find($id);
if (!$prescription) {
self::setError('处方不存在');
return false;
}
$prescription->delete();
return true;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
/**
* 处方详情
*/