新增功能

This commit is contained in:
Your Name
2026-03-04 15:32:30 +08:00
parent a07e844c47
commit ab77f5488d
2266 changed files with 177942 additions and 3444 deletions
@@ -0,0 +1,106 @@
<?php
namespace app\adminapi\controller\doctor;
use app\adminapi\controller\BaseAdminController;
use app\adminapi\lists\doctor\AppointmentLists;
use app\adminapi\logic\doctor\AppointmentLogic;
use app\adminapi\validate\doctor\AppointmentValidate;
/**
* 医生预约控制器
* Class AppointmentController
* @package app\adminapi\controller\doctor
*/
class AppointmentController extends BaseAdminController
{
/**
* @notes 获取可用时间段
* @return \think\response\Json
*/
public function availableSlots()
{
$params = (new AppointmentValidate())->goCheck('availableSlots');
$result = AppointmentLogic::getAvailableSlots($params);
return $this->data($result);
}
/**
* @notes 创建预约
* @return \think\response\Json
*/
public function create()
{
$params = (new AppointmentValidate())->post()->goCheck('create');
$result = AppointmentLogic::create($params);
if ($result === false) {
return $this->fail(AppointmentLogic::getError());
}
return $this->success('预约成功', $result);
}
/**
* @notes 取消预约
* @return \think\response\Json
*/
public function cancel()
{
$params = (new AppointmentValidate())->post()->goCheck('cancel');
$result = AppointmentLogic::cancel($params);
if ($result === false) {
return $this->fail(AppointmentLogic::getError());
}
return $this->success('取消成功');
}
/**
* @notes 预约列表
* @return \think\response\Json
*/
public function lists()
{
return $this->dataLists(new AppointmentLists());
}
/**
* @notes 预约详情
* @return \think\response\Json
*/
public function detail()
{
$params = (new AppointmentValidate())->goCheck('detail');
$result = AppointmentLogic::detail($params);
return $this->data($result);
}
/**
* @notes 获取医生可用号源数
* @return \think\response\Json
*/
public function doctorAvailability()
{
$doctorId = $this->request->get('doctor_id');
$date = $this->request->get('date');
if (!$doctorId || !$date) {
return $this->fail('参数错误');
}
$availableCount = AppointmentLogic::getDoctorAvailability($doctorId, $date);
return $this->data(['available_count' => $availableCount]);
}
/**
* @notes 完成预约
* @return \think\response\Json
*/
public function complete()
{
$params = (new AppointmentValidate())->post()->goCheck('complete');
$result = AppointmentLogic::complete($params);
if ($result === false) {
return $this->fail(AppointmentLogic::getError());
}
return $this->success('操作成功');
}
}
@@ -0,0 +1,86 @@
<?php
namespace app\adminapi\controller\doctor;
use app\adminapi\controller\BaseAdminController;
use app\adminapi\lists\doctor\RosterLists;
use app\adminapi\logic\doctor\RosterLogic;
use app\adminapi\validate\doctor\RosterValidate;
/**
* 医生排班控制器
* Class RosterController
* @package app\adminapi\controller\doctor
*/
class RosterController extends BaseAdminController
{
/**
* @notes 排班列表
* @return \think\response\Json
*/
public function lists()
{
return $this->dataLists(new RosterLists());
}
/**
* @notes 保存排班(新增或更新)
* @return \think\response\Json
*/
public function save()
{
$params = (new RosterValidate())->post()->goCheck('save');
$result = RosterLogic::save($params);
return $this->success('保存成功', $result);
}
/**
* @notes 删除排班
* @return \think\response\Json
*/
public function delete()
{
$params = (new RosterValidate())->post()->goCheck('delete');
RosterLogic::delete($params);
return $this->success('删除成功');
}
/**
* @notes 排班详情
* @return \think\response\Json
*/
public function detail()
{
$params = (new RosterValidate())->goCheck('detail');
$result = RosterLogic::detail($params);
return $this->data($result);
}
/**
* @notes 批量保存排班
* @return \think\response\Json
*/
public function batchSave()
{
$params = (new RosterValidate())->post()->goCheck('batchSave');
$result = RosterLogic::batchSave($params);
if ($result === false) {
return $this->fail(RosterLogic::getError());
}
return $this->success('批量保存成功', $result);
}
/**
* @notes 复制排班
* @return \think\response\Json
*/
public function copy()
{
$params = (new RosterValidate())->post()->goCheck('copy');
$result = RosterLogic::copy($params);
if ($result === false) {
return $this->fail(RosterLogic::getError());
}
return $this->success('复制成功', []);
}
}
@@ -0,0 +1,81 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
namespace app\adminapi\controller\tcm;
use app\adminapi\controller\BaseAdminController;
use app\adminapi\logic\tcm\BloodRecordLogic;
/**
* 血糖血压记录控制器
* Class BloodRecordController
* @package app\adminapi\controller\tcm
*/
class BloodRecordController extends BaseAdminController
{
/**
* @notes 添加记录
* @return \think\response\Json
*/
public function add()
{
$params = $this->request->post();
$result = BloodRecordLogic::add($params);
if ($result) {
return $this->success('添加成功', [], 1, 1);
}
return $this->fail(BloodRecordLogic::getError());
}
/**
* @notes 编辑记录
* @return \think\response\Json
*/
public function edit()
{
$params = $this->request->post();
$result = BloodRecordLogic::edit($params);
if ($result) {
return $this->success('编辑成功', [], 1, 1);
}
return $this->fail(BloodRecordLogic::getError());
}
/**
* @notes 删除记录
* @return \think\response\Json
*/
public function delete()
{
$params = $this->request->post();
$result = BloodRecordLogic::delete($params);
if ($result) {
return $this->success('删除成功', [], 1, 1);
}
return $this->fail(BloodRecordLogic::getError());
}
/**
* @notes 记录详情
* @return \think\response\Json
*/
public function detail()
{
$params = $this->request->get();
$result = BloodRecordLogic::detail($params);
return $this->data($result);
}
/**
* @notes 获取患者的记录列表
* @return \think\response\Json
*/
public function getRecordsByPatient()
{
$params = $this->request->get();
$result = BloodRecordLogic::getRecordsByPatient($params);
return $this->data($result);
}
}
@@ -0,0 +1,275 @@
<?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\controller\tcm;
use app\adminapi\controller\BaseAdminController;
use app\adminapi\lists\tcm\DiagnosisLists;
use app\adminapi\logic\tcm\DiagnosisLogic;
use app\adminapi\validate\tcm\DiagnosisValidate;
/**
* 中医辨房病因诊单控制器
* Class DiagnosisController
* @package app\adminapi\controller\tcm
*/
class DiagnosisController extends BaseAdminController
{
/**
* @notes 测试接口
* @return \think\response\Json
*/
public function test()
{
return $this->success('中医诊单控制器可以访问', [
'controller' => 'TcmDiagnosisController',
'namespace' => __NAMESPACE__,
'class' => __CLASS__,
'method' => __METHOD__
]);
}
/**
* @notes 诊单列表
* @return \think\response\Json
*/
public function lists()
{
return $this->dataLists(new DiagnosisLists());
}
/**
* @notes 添加诊单
* @return \think\response\Json
*/
public function add()
{
$params = (new DiagnosisValidate())->post()->goCheck('add');
$result = DiagnosisLogic::add($params);
if ($result) {
return $this->success('添加成功', ['id' => $result], 1, 1);
}
return $this->fail(DiagnosisLogic::getError());
}
/**
* @notes 编辑诊单
* @return \think\response\Json
*/
public function edit()
{
$params = (new DiagnosisValidate())->post()->goCheck('edit');
$result = DiagnosisLogic::edit($params);
if ($result) {
return $this->success('编辑成功', [], 1, 1);
}
return $this->fail(DiagnosisLogic::getError());
}
/**
* @notes 删除诊单
* @return \think\response\Json
*/
public function delete()
{
$params = (new DiagnosisValidate())->post()->goCheck('id');
$result = DiagnosisLogic::delete($params);
if ($result) {
return $this->success('删除成功', [], 1, 1);
}
return $this->fail(DiagnosisLogic::getError());
}
/**
* @notes 诊单详情
* @return \think\response\Json
*/
public function detail()
{
$params = (new DiagnosisValidate())->goCheck('id');
$result = DiagnosisLogic::detail($params);
return $this->data($result);
}
/**
* @notes 检查手机号是否重复
* @return \think\response\Json
*/
public function checkPhone()
{
$params = $this->request->post();
$result = DiagnosisLogic::checkPhone($params);
return $this->data($result);
}
/**
* @notes 检查身份证号是否重复
* @return \think\response\Json
*/
public function checkIdCard()
{
$params = $this->request->post();
$result = DiagnosisLogic::checkIdCard($params);
return $this->data($result);
}
/**
* @notes 指派医助
* @return \think\response\Json
*/
public function assign()
{
$params = $this->request->post();
// 验证参数
if (empty($params['id'])) {
return $this->fail('诊单ID不能为空');
}
if (!isset($params['assistant_id'])) {
return $this->fail('请选择医助');
}
$result = DiagnosisLogic::assign($params);
if ($result) {
return $this->success('指派成功', [], 1, 1);
}
return $this->fail(DiagnosisLogic::getError());
}
/**
* @notes 获取通话签名
* @return \think\response\Json
*/
public function getCallSignature()
{
$params = $this->request->post();
if (empty($params['diagnosis_id'])) {
return $this->fail('诊单ID不能为空');
}
if (empty($params['patient_id'])) {
return $this->fail('患者ID不能为空');
}
// 传递当前管理员ID
$params['admin_id'] = $this->adminId;
$result = DiagnosisLogic::getCallSignature($params);
if ($result) {
return $this->data($result);
}
return $this->fail(DiagnosisLogic::getError());
}
/**
* @notes 发起通话
* @return \think\response\Json
*/
public function startCall()
{
$params = $this->request->post();
if (empty($params['diagnosis_id'])) {
return $this->fail('诊单ID不能为空');
}
// 传递当前管理员ID
$params['admin_id'] = $this->adminId;
$result = DiagnosisLogic::startCall($params);
if ($result) {
return $this->success('发起通话成功', [], 1, 1);
}
return $this->fail(DiagnosisLogic::getError());
}
/**
* @notes 结束通话
* @return \think\response\Json
*/
public function endCall()
{
$params = $this->request->post();
if (empty($params['diagnosis_id'])) {
return $this->fail('诊单ID不能为空');
}
// 传递当前管理员ID
$params['admin_id'] = $this->adminId;
$result = DiagnosisLogic::endCall($params);
if ($result) {
return $this->success('结束通话成功', [], 1, 1);
}
return $this->fail(DiagnosisLogic::getError());
}
/**
* @notes 获取通话记录
* @return \think\response\Json
*/
public function getCallRecords()
{
$params = $this->request->get();
if (empty($params['diagnosis_id'])) {
return $this->fail('诊单ID不能为空');
}
$result = DiagnosisLogic::getCallRecords($params);
return $this->data($result);
}
/**
* @notes 获取患者通话签名(用于测试)
* @return \think\response\Json
*/
public function getPatientSignature()
{
$params = $this->request->get();
if (empty($params['patient_id'])) {
return $this->fail('患者ID不能为空');
}
$result = DiagnosisLogic::getPatientSignature((int)$params['patient_id']);
if ($result) {
return $this->data($result);
}
return $this->fail(DiagnosisLogic::getError());
}
/**
* @notes 获取医助列表
* @return \think\response\Json
*/
public function getAssistants()
{
$result = DiagnosisLogic::getAssistants();
return $this->data($result);
}
/**
* @notes 获取医生列表
* @return \think\response\Json
*/
public function getDoctors()
{
$result = DiagnosisLogic::getDoctors();
return $this->data($result);
}
}
@@ -0,0 +1,102 @@
<?php
namespace app\adminapi\lists\doctor;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\model\doctor\Appointment;
use app\common\lists\ListsSearchInterface;
/**
* 医生预约列表
* Class AppointmentLists
* @package app\adminapi\lists\doctor
*/
class AppointmentLists extends BaseAdminDataLists implements ListsSearchInterface
{
/**
* @notes 设置搜索条件
* @return array
*/
public function setSearch(): array
{
$allowSearch = [
'=' => ['a.patient_id', 'a.doctor_id', 'a.status', 'a.appointment_date'],
];
// 处理日期范围搜索
if (!empty($this->params['start_date']) && !empty($this->params['end_date'])) {
$this->searchWhere[] = ['a.appointment_date', 'between', [$this->params['start_date'], $this->params['end_date']]];
}
// 处理患者姓名搜索
if (!empty($this->params['patient_name'])) {
$this->searchWhere[] = ['u.patient_name', 'like', '%' . $this->params['patient_name'] . '%'];
}
// 处理医生姓名搜索
if (!empty($this->params['doctor_name'])) {
$this->searchWhere[] = ['ad.name', 'like', '%' . $this->params['doctor_name'] . '%'];
}
return $allowSearch;
}
/**
* @notes 获取列表
* @return array
*/
public function lists(): array
{
$lists = Appointment::alias('a')
->leftJoin('tcm_diagnosis u', 'a.patient_id = u.id')
->leftJoin('admin ad', 'a.doctor_id = ad.id')
->field('a.*, u.patient_name as patient_name, u.phone as patient_phone, ad.name as doctor_name')
->where($this->searchWhere)
->order('a.id', 'desc')
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
// 添加状态和类型描述
foreach ($lists as &$item) {
$statusMap = [
1 => '已预约',
2 => '已取消',
3 => '已完成',
];
$item['status_desc'] = $statusMap[$item['status']] ?? '未知';
$typeMap = [
'video' => '视频问诊',
'text' => '图文问诊',
'phone' => '电话问诊',
];
$item['appointment_type_desc'] = $typeMap[$item['appointment_type']] ?? '未知';
// 格式化时间戳为日期时间
if (isset($item['create_time']) && is_numeric($item['create_time'])) {
$item['create_time'] = date('Y-m-d H:i:s', $item['create_time']);
}
if (isset($item['update_time']) && is_numeric($item['update_time'])) {
$item['update_time'] = date('Y-m-d H:i:s', $item['update_time']);
}
}
return $lists;
}
/**
* @notes 获取数量
* @return int
*/
public function count(): int
{
$query = Appointment::alias('a')
->leftJoin('tcm_diagnosis u', 'a.patient_id = u.id')
->leftJoin('admin ad', 'a.doctor_id = ad.id')
->where($this->searchWhere);
return $query->count();
}
}
@@ -0,0 +1,68 @@
<?php
namespace app\adminapi\lists\doctor;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\model\doctor\Roster;
use app\common\lists\ListsSearchInterface;
/**
* 医生排班列表
* Class RosterLists
* @package app\adminapi\lists\doctor
*/
class RosterLists extends BaseAdminDataLists implements ListsSearchInterface
{
/**
* @notes 设置搜索条件
* @return array
*/
public function setSearch(): array
{
return [
'=' => ['doctor_id', 'period', 'status'],
];
}
/**
* @notes 获取列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function lists(): array
{
$where = $this->searchWhere;
// 处理日期范围搜索
if (!empty($this->params['start_date']) && !empty($this->params['end_date'])) {
$where[] = ['date', 'between', [$this->params['start_date'], $this->params['end_date']]];
}
$lists = Roster::where($where)
->field(['id', 'doctor_id', 'date', 'period', 'status', 'quota', 'max_patients', 'booked_count', 'remark', 'create_time', 'update_time'])
->order(['date' => 'asc', 'period' => 'asc'])
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
return $lists;
}
/**
* @notes 获取数量
* @return int
*/
public function count(): int
{
$where = $this->searchWhere;
// 处理日期范围搜索
if (!empty($this->params['start_date']) && !empty($this->params['end_date'])) {
$where[] = ['date', 'between', [$this->params['start_date'], $this->params['end_date']]];
}
return Roster::where($where)->count();
}
}
@@ -0,0 +1,69 @@
<?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\lists\tcm;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\model\tcm\Diagnosis;
use app\common\lists\ListsSearchInterface;
/**
* 中医辨房病因诊单列表
* Class DiagnosisLists
* @package app\adminapi\lists\tcm
*/
class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
{
/**
* @notes 设置搜索条件
* @return array
*/
public function setSearch(): array
{
return [
'%like%' => ['patient_name'],
'=' => ['patient_id', 'gender', 'diagnosis_type', 'syndrome_type', 'assistant_id', 'status'],
'between_time' => ['diagnosis_date'],
];
}
/**
* @notes 获取列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function lists(): array
{
$lists = Diagnosis::where($this->searchWhere)
->field(['id', 'patient_id', 'patient_name', 'id_card', 'phone', 'gender', 'age', 'diagnosis_date', 'diagnosis_type', 'syndrome_type', 'assistant_id', 'status', 'create_time', 'update_time'])
->append(['gender_desc', 'status_desc', 'diagnosis_date_text'])
->order(['id' => 'desc'])
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
return $lists;
}
/**
* @notes 获取数量
* @return int
*/
public function count(): int
{
return Diagnosis::where($this->searchWhere)->count();
}
}
@@ -24,8 +24,10 @@ use app\common\model\auth\AdminRole;
use app\common\model\auth\AdminSession;
use app\common\cache\AdminTokenCache;
use app\common\service\FileService;
use app\common\service\TencentImService;
use think\facade\Config;
use think\facade\Db;
use think\facade\Log;
/**
* 管理员逻辑
@@ -67,6 +69,9 @@ class AdminLogic extends BaseLogic
// 岗位
self::insertJobs($admin['id'], $params['jobs_id'] ?? []);
// 导入医生账号到腾讯云IM
self::importDoctorAccountToIm($admin['id'], $params['name']);
Db::commit();
return true;
} catch (\Exception $e) {
@@ -134,6 +139,9 @@ class AdminLogic extends BaseLogic
// 岗位
self::insertJobs($params['id'], $params['jobs_id'] ?? []);
// 导入医生账号到腾讯云IM
self::importDoctorAccountToIm($params['id'], $params['name']);
Db::commit();
return true;
} catch (\Exception $e) {
@@ -334,4 +342,33 @@ class AdminLogic extends BaseLogic
}
}
/**
* @notes 导入医生账号到腾讯云IM
* @param int $adminId 管理员ID
* @param string $name 管理员名称
* @return void
* @author AI Assistant
* @date 2026/03/02
*/
public static function importDoctorAccountToIm($adminId, $name)
{
try {
$userId = 'doctor_' . $adminId;
Log::info('开始导入医生IM账号 - admin_id: ' . $adminId . ', user_id: ' . $userId . ', name: ' . $name);
$imService = new TencentImService();
$result = $imService->importAccount($userId, $name);
if ($result) {
Log::info('医生IM账号导入成功 - admin_id: ' . $adminId . ', user_id: ' . $userId);
} else {
Log::warning('医生IM账号导入失败 - admin_id: ' . $adminId . ', user_id: ' . $userId);
}
} catch (\Exception $e) {
Log::error('导入医生IM账号异常 - admin_id: ' . $adminId . ', error: ' . $e->getMessage());
}
}
}
@@ -0,0 +1,399 @@
<?php
namespace app\adminapi\logic\doctor;
use app\common\logic\BaseLogic;
use app\common\model\doctor\Appointment;
use app\common\model\doctor\Roster;
use think\facade\Db;
/**
* 医生预约逻辑
* Class AppointmentLogic
* @package app\adminapi\logic\doctor
*/
class AppointmentLogic extends BaseLogic
{
/**
* @notes 获取可用时间段
* @param array $params
* @return array
*/
public static function getAvailableSlots(array $params)
{
try {
$doctorId = $params['doctor_id'];
$date = $params['appointment_date'];
$period = $params['period'] ?? 'all';
// 记录请求参数
\think\facade\Log::info('获取可用时段 - 请求参数', [
'doctor_id' => $doctorId,
'date' => $date,
'period' => $period
]);
// 1. 检查医生在该日期是否有出诊排班(只查询status=1的记录)
$rosters = Roster::where([
'doctor_id' => $doctorId,
'date' => $date,
'status' => 1 // 只查询出诊状态
])->select()->toArray();
// 记录查询到的排班
\think\facade\Log::info('查询到的排班数据', [
'doctor_id' => $doctorId,
'date' => $date,
'count' => count($rosters),
'rosters' => $rosters
]);
// 额外记录:查询该日期所有排班(用于调试)
$allRosters = Roster::where([
'doctor_id' => $doctorId,
'date' => $date
])->select()->toArray();
\think\facade\Log::info('该日期所有排班数据(包括非出诊)', [
'count' => count($allRosters),
'all_rosters' => $allRosters
]);
// 如果没有出诊排班,返回空数组
if (empty($rosters)) {
\think\facade\Log::warning('没有找到出诊排班', [
'doctor_id' => $doctorId,
'date' => $date
]);
return ['slots' => []];
}
// 2. 根据排班时段生成时间段
$slots = [];
foreach ($rosters as $roster) {
// 再次确认状态为出诊(双重检查)
if ($roster['status'] != 1) {
\think\facade\Log::warning('跳过非出诊排班', [
'roster_id' => $roster['id'],
'status' => $roster['status']
]);
continue;
}
$periodValue = $roster['period'];
\think\facade\Log::info('处理排班时段', [
'roster_id' => $roster['id'],
'period' => $periodValue,
'status' => $roster['status'],
'period_type' => gettype($periodValue)
]);
// 根据时段生成时间段(支持数字和字符串格式)
$startHour = null;
$endHour = null;
if ($periodValue == 1 || $periodValue === 'morning' || $periodValue === '上午') {
// 上午:9:00-12:00
$startHour = 9;
$endHour = 12;
\think\facade\Log::info('识别为上午时段', ['period' => $periodValue]);
} elseif ($periodValue == 2 || $periodValue === 'afternoon' || $periodValue === '下午') {
// 下午:14:00-18:00
$startHour = 14;
$endHour = 18;
\think\facade\Log::info('识别为下午时段', ['period' => $periodValue]);
} else {
\think\facade\Log::warning('未知的时段值,跳过此记录', [
'roster_id' => $roster['id'],
'period' => $periodValue,
'period_type' => gettype($periodValue)
]);
continue;
}
// 确保startHour和endHour已设置
if ($startHour === null || $endHour === null) {
\think\facade\Log::error('时段参数未正确设置', [
'roster_id' => $roster['id'],
'period' => $periodValue
]);
continue;
}
// 生成15分钟间隔的时间段
for ($hour = $startHour; $hour < $endHour; $hour++) {
for ($minute = 0; $minute < 60; $minute += 15) {
$time = sprintf('%02d:%02d', $hour, $minute);
$slots[] = [
'time' => $time,
'available' => true,
'quota' => 1,
'period' => $periodValue
];
}
}
}
\think\facade\Log::info('生成的时间段数量(去重前)', [
'count' => count($slots),
'sample_slots' => array_slice($slots, 0, 3)
]);
// 如果没有生成任何时间段,返回空数组
if (empty($slots)) {
\think\facade\Log::warning('没有生成任何时间段');
return ['slots' => []];
}
// 3. 去重:按时间去重(因为可能有重复的排班记录)
$uniqueSlots = [];
$timeMap = [];
foreach ($slots as $slot) {
$time = $slot['time'];
if (!isset($timeMap[$time])) {
$timeMap[$time] = true;
$uniqueSlots[] = $slot;
}
}
$slots = $uniqueSlots;
\think\facade\Log::info('生成的时间段数量(去重后)', [
'count' => count($slots),
'morning_slots' => count(array_filter($slots, function($s) {
return $s['time'] < '12:00';
})),
'afternoon_slots' => count(array_filter($slots, function($s) {
return $s['time'] >= '14:00';
})),
'first_3_slots' => array_slice($slots, 0, 3),
'last_3_slots' => array_slice($slots, -3)
]);
// 4. 查询已预约的时间段
$appointmentTimes = Appointment::where([
'doctor_id' => $doctorId,
'appointment_date' => $date,
'status' => 1 // 只查询有效预约
])->column('appointment_time');
// 将时间格式统一为 HH:MM(去掉秒)
$appointments = array_map(function($time) {
// 如果是 HH:MM:SS 格式,截取前5位
return substr($time, 0, 5);
}, $appointmentTimes);
// 4. 标记已占用的时间段
foreach ($slots as &$slot) {
if (in_array($slot['time'], $appointments)) {
$slot['available'] = false;
$slot['quota'] = 0;
}
}
// 5. 按时间排序
usort($slots, function($a, $b) {
return strcmp($a['time'], $b['time']);
});
return ['slots' => $slots];
} catch (\Exception $e) {
self::setError($e->getMessage());
return ['slots' => []];
}
}
/**
* @notes 创建预约
* @param array $params
* @return array|bool
*/
public static function create(array $params)
{
try {
Db::startTrans();
// 1. 检查时间段是否已被预约(每个时间段只能预约1次)
// 统一时间格式为 HH:MM:SS
$appointmentTime = strlen($params['appointment_time']) == 5
? $params['appointment_time'] . ':00'
: $params['appointment_time'];
$exists = Appointment::where([
'doctor_id' => $params['doctor_id'],
'appointment_date' => $params['appointment_date'],
'status' => 1
])->where('appointment_time', $appointmentTime)
->find();
if ($exists) {
self::setError('该时间段已被预约');
Db::rollback();
return false;
}
$data = [
'patient_id' => $params['patient_id'],
'doctor_id' => $params['doctor_id'],
'roster_id' => 0, // 不再关联排班表
'appointment_date' => $params['appointment_date'],
'period' => $params['period'] ?? 'all',
'appointment_time' => $appointmentTime,
'appointment_type' => $params['appointment_type'] ?? 'video',
'remark' => $params['remark'] ?? '',
'status' => 1,
'create_time' => time(),
'update_time' => time(),
];
$appointment = Appointment::create($data);
Db::commit();
return ['id' => $appointment->id];
} catch (\Exception $e) {
Db::rollback();
self::setError($e->getMessage());
return false;
}
}
/**
* @notes 取消预约
* @param array $params
* @return bool
*/
public static function cancel(array $params)
{
try {
$appointment = Appointment::findOrEmpty($params['id']);
if ($appointment->isEmpty()) {
self::setError('预约记录不存在');
return false;
}
$appointment->status = 2; // 已取消
$appointment->update_time = time();
$appointment->save();
return true;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
/**
* @notes 预约详情
* @param array $params
* @return array
*/
public static function detail(array $params)
{
$appointment = Appointment::alias('a')
->leftJoin('la_user u', 'a.patient_id = u.id')
->leftJoin('la_admin ad', 'a.doctor_id = ad.id')
->field('a.*, u.nickname as patient_name, u.mobile as patient_phone, ad.name as doctor_name')
->where('a.id', $params['id'])
->findOrEmpty()
->toArray();
if (empty($appointment)) {
return [];
}
// 添加状态和类型描述
$statusMap = [
1 => '已预约',
2 => '已取消',
3 => '已完成',
];
$appointment['status_desc'] = $statusMap[$appointment['status']] ?? '未知';
$typeMap = [
'video' => '视频问诊',
'text' => '图文问诊',
'phone' => '电话问诊',
];
$appointment['appointment_type_desc'] = $typeMap[$appointment['appointment_type']] ?? '未知';
// 格式化时间戳为日期时间
if (isset($appointment['create_time']) && is_numeric($appointment['create_time'])) {
$appointment['create_time'] = date('Y-m-d H:i:s', $appointment['create_time']);
}
if (isset($appointment['update_time']) && is_numeric($appointment['update_time'])) {
$appointment['update_time'] = date('Y-m-d H:i:s', $appointment['update_time']);
}
return $appointment;
}
/**
* @notes 获取医生某天的可用号源总数
* @param int $doctorId
* @param string $date
* @return int
*/
public static function getDoctorAvailability($doctorId, $date)
{
try {
$totalAvailable = 0;
// 查询上午和下午的排班
$rosters = Roster::where([
'doctor_id' => $doctorId,
'date' => $date,
'status' => 1 // 出诊
])->select();
foreach ($rosters as $roster) {
// 查询该时段已预约数量
$appointmentCount = Appointment::where([
'doctor_id' => $doctorId,
'appointment_date' => $date,
'period' => $roster->period,
'status' => 1
])->count();
// 计算剩余号源
$remaining = $roster->quota - $appointmentCount;
if ($remaining > 0) {
$totalAvailable += $remaining;
}
}
return $totalAvailable;
} catch (\Exception $e) {
return 0;
}
}
/**
* @notes 完成预约
* @param array $params
* @return bool
*/
public static function complete(array $params)
{
try {
$appointment = Appointment::findOrEmpty($params['id']);
if ($appointment->isEmpty()) {
self::setError('预约记录不存在');
return false;
}
if ($appointment->status != 1) {
self::setError('该预约已处理,无法完成');
return false;
}
$appointment->status = 3; // 已完成
$appointment->update_time = time();
$appointment->save();
return true;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
}
@@ -0,0 +1,198 @@
<?php
namespace app\adminapi\logic\doctor;
use app\common\logic\BaseLogic;
use app\common\model\doctor\Roster;
use think\facade\Db;
/**
* 医生排班逻辑
* Class RosterLogic
* @package app\adminapi\logic\doctor
*/
class RosterLogic extends BaseLogic
{
/**
* @notes 保存排班
* @param array $params
* @return array|bool
*/
public static function save(array $params)
{
try {
$data = [
'doctor_id' => $params['doctor_id'],
'date' => $params['date'],
'period' => $params['period'],
'status' => $params['status'],
'quota' => $params['quota'] ?? 0,
'max_patients' => $params['max_patients'] ?? 0,
'remark' => $params['remark'] ?? '',
];
if (isset($params['id']) && $params['id']) {
// 更新
$data['update_time'] = time();
Roster::where('id', $params['id'])->update($data);
return ['id' => $params['id']];
} else {
// 新增
$data['create_time'] = time();
$data['update_time'] = time();
$roster = Roster::create($data);
return ['id' => $roster->id];
}
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
/**
* @notes 删除排班
* @param array $params
* @return bool
*/
public static function delete(array $params)
{
try {
Roster::destroy($params['id']);
return true;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
/**
* @notes 排班详情
* @param array $params
* @return array
*/
public static function detail(array $params)
{
return Roster::findOrEmpty($params['id'])->toArray();
}
/**
* @notes 批量保存排班
* @param array $params
* @return array|bool
*/
public static function batchSave(array $params)
{
try {
Db::startTrans();
$successCount = 0;
$updateCount = 0;
$createCount = 0;
foreach ($params['rosters'] as $roster) {
$data = [
'doctor_id' => $roster['doctor_id'],
'date' => $roster['date'],
'period' => $roster['period'],
'status' => $roster['status'],
'quota' => $roster['quota'] ?? 0,
'max_patients' => $roster['max_patients'] ?? 0,
'remark' => $roster['remark'] ?? '',
];
// 检查是否已存在
$exists = Roster::where([
['doctor_id', '=', $data['doctor_id']],
['date', '=', $data['date']],
['period', '=', $data['period']],
])->find();
if ($exists) {
// 更新
$data['update_time'] = time();
$exists->save($data);
$updateCount++;
} else {
// 新增
$data['create_time'] = time();
$data['update_time'] = time();
Roster::create($data);
$createCount++;
}
$successCount++;
}
Db::commit();
return [
'success_count' => $successCount,
'create_count' => $createCount,
'update_count' => $updateCount,
];
} catch (\Exception $e) {
Db::rollback();
self::setError($e->getMessage());
return false;
}
}
/**
* @notes 复制排班
* @param array $params
* @return bool
*/
public static function copy(array $params)
{
try {
Db::startTrans();
// 获取源排班数据
$where = [
['date', 'between', [$params['source_start_date'], $params['source_end_date']]]
];
if (isset($params['doctor_id']) && $params['doctor_id']) {
$where[] = ['doctor_id', '=', $params['doctor_id']];
}
$sourceRosters = Roster::where($where)->select();
// 计算日期差
$sourceDays = (strtotime($params['source_end_date']) - strtotime($params['source_start_date'])) / 86400;
$targetDays = (strtotime($params['target_start_date']) - strtotime($params['source_start_date'])) / 86400;
foreach ($sourceRosters as $roster) {
$newDate = date('Y-m-d', strtotime($roster->date) + ($targetDays * 86400));
$data = [
'doctor_id' => $roster->doctor_id,
'date' => $newDate,
'period' => $roster->period,
'status' => $roster->status,
'quota' => $roster->quota,
'max_patients' => $roster->max_patients,
'remark' => $roster->remark,
'create_time' => time(),
'update_time' => time(),
];
// 检查目标日期是否已存在排班
$exists = Roster::where([
['doctor_id', '=', $data['doctor_id']],
['date', '=', $data['date']],
['period', '=', $data['period']],
])->find();
if (!$exists) {
Roster::create($data);
}
}
Db::commit();
return true;
} catch (\Exception $e) {
Db::rollback();
self::setError($e->getMessage());
return false;
}
}
}
@@ -0,0 +1,126 @@
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
namespace app\adminapi\logic\tcm;
use app\common\logic\BaseLogic;
use app\common\model\tcm\BloodRecord;
/**
* 血糖血压记录逻辑
* Class BloodRecordLogic
* @package app\adminapi\logic\tcm
*/
class BloodRecordLogic extends BaseLogic
{
/**
* @notes 添加记录
* @param array $params
* @return bool
*/
public static function add(array $params): bool
{
try {
// 处理记录日期
if (isset($params['record_date'])) {
$params['record_date'] = strtotime($params['record_date']);
}
BloodRecord::create($params);
return true;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
/**
* @notes 编辑记录
* @param array $params
* @return bool
*/
public static function edit(array $params): bool
{
try {
// 处理记录日期
if (isset($params['record_date'])) {
$params['record_date'] = strtotime($params['record_date']);
}
BloodRecord::update($params);
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 {
BloodRecord::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
{
$record = BloodRecord::findOrEmpty($params['id'])->toArray();
// 处理记录日期格式
if (!empty($record['record_date'])) {
$record['record_date'] = date('Y-m-d', $record['record_date']);
}
return $record;
}
/**
* @notes 获取患者的血糖血压记录列表
* @param array $params
* @return array
*/
public static function getRecordsByPatient(array $params): array
{
$where = [];
if (isset($params['diagnosis_id'])) {
$where[] = ['diagnosis_id', '=', $params['diagnosis_id']];
}
if (isset($params['patient_id'])) {
$where[] = ['patient_id', '=', $params['patient_id']];
}
$records = BloodRecord::where($where)
->where('delete_time', null)
->order('record_date', 'desc')
->order('record_time', 'desc')
->select()
->toArray();
// 格式化日期
foreach ($records as &$record) {
if (!empty($record['record_date'])) {
$record['record_date'] = date('Y-m-d', $record['record_date']);
}
}
return $records;
}
}
@@ -0,0 +1,803 @@
<?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;
/**
* 中医辨房病因诊单逻辑
* 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'
];
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'
];
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'
];
foreach ($multiSelectFields as $field) {
if (!empty($diagnosis[$field])) {
$diagnosis[$field] = explode(',', $diagnosis[$field]);
} else {
$diagnosis[$field] = [];
}
}
// 处理舌苔照片(JSON转数组)
if (!empty($diagnosis['tongue_images'])) {
$diagnosis['tongue_images'] = json_decode($diagnosis['tongue_images'], true) ?: [];
} else {
$diagnosis['tongue_images'] = [];
}
// 处理检查报告(JSON转数组)
if (!empty($diagnosis['report_files'])) {
$diagnosis['report_files'] = json_decode($diagnosis['report_files'], true) ?: [];
} else {
$diagnosis['report_files'] = [];
}
// 处理诊断日期格式
if (!empty($diagnosis['diagnosis_date'])) {
$diagnosis['diagnosis_date'] = date('Y-m-d', $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;
// 患者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,
'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 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 [];
}
}
}
@@ -0,0 +1,86 @@
<?php
namespace app\adminapi\validate\doctor;
use app\common\validate\BaseValidate;
/**
* 医生预约验证器
* Class AppointmentValidate
* @package app\adminapi\validate\doctor
*/
class AppointmentValidate extends BaseValidate
{
/**
* 设置校验规则
* @var string[]
*/
protected $rule = [
'id' => 'require',
'patient_id' => 'require|integer',
'doctor_id' => 'require|integer',
'appointment_date' => 'require|date',
'period' => 'in:morning,afternoon,all',
'appointment_time' => 'require',
'appointment_type' => 'in:video,text,phone',
];
/**
* 参数描述
* @var string[]
*/
protected $field = [
'id' => '预约ID',
'patient_id' => '患者ID',
'doctor_id' => '医生ID',
'appointment_date' => '预约日期',
'period' => '时段',
'appointment_time' => '预约时间',
'appointment_type' => '预约类型',
];
/**
* @notes 创建预约场景
* @return AppointmentValidate
*/
public function sceneCreate()
{
return $this->only(['patient_id', 'doctor_id', 'appointment_date', 'period', 'appointment_time', 'appointment_type']);
}
/**
* @notes 取消预约场景
* @return AppointmentValidate
*/
public function sceneCancel()
{
return $this->only(['id']);
}
/**
* @notes 预约详情场景
* @return AppointmentValidate
*/
public function sceneDetail()
{
return $this->only(['id']);
}
/**
* @notes 可用时间段场景
* @return AppointmentValidate
*/
public function sceneAvailableSlots()
{
return $this->only(['doctor_id', 'appointment_date', 'period']);
}
/**
* @notes 完成预约场景
* @return AppointmentValidate
*/
public function sceneComplete()
{
return $this->only(['id']);
}
}
@@ -0,0 +1,80 @@
<?php
namespace app\adminapi\validate\doctor;
use app\common\validate\BaseValidate;
/**
* 医生排班验证器
* Class RosterValidate
* @package app\adminapi\validate\doctor
*/
class RosterValidate extends BaseValidate
{
protected $rule = [
'id' => 'require',
'doctor_id' => 'require',
'date' => 'require|date',
'period' => 'require|in:morning,afternoon',
'status' => 'require|in:1,2,3,4',
'quota' => 'number',
'max_patients' => 'number',
];
protected $message = [
'id.require' => '请选择排班',
'doctor_id.require' => '请选择医生',
'date.require' => '请选择日期',
'date.date' => '日期格式不正确',
'period.require' => '请选择时段',
'period.in' => '时段参数错误',
'status.require' => '请选择状态',
'status.in' => '状态参数错误',
'quota.number' => '号源数必须是数字',
'max_patients.number' => '最大接诊数必须是数字',
];
/**
* @notes 保存场景
*/
public function sceneSave()
{
return $this->only(['doctor_id', 'date', 'period', 'status', 'quota', 'max_patients', 'remark']);
}
/**
* @notes 删除场景
*/
public function sceneDelete()
{
return $this->only(['id']);
}
/**
* @notes 详情场景
*/
public function sceneDetail()
{
return $this->only(['id']);
}
/**
* @notes 批量保存场景
*/
public function sceneBatchSave()
{
return $this->only(['rosters'])
->append('rosters', 'require|array');
}
/**
* @notes 复制场景
*/
public function sceneCopy()
{
return $this->only(['source_start_date', 'source_end_date', 'target_start_date'])
->append('source_start_date', 'require|date')
->append('source_end_date', 'require|date')
->append('target_start_date', 'require|date');
}
}
@@ -0,0 +1,81 @@
<?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\validate\tcm;
use app\common\model\tcm\Diagnosis;
use app\common\validate\BaseValidate;
/**
* 中医辨房病因诊单验证
* Class DiagnosisValidate
* @package app\adminapi\validate\tcm
*/
class DiagnosisValidate extends BaseValidate
{
protected $rule = [
'id' => 'require|checkDiagnosis',
'patient_name' => 'require|length:1,50',
'id_card' => 'length:15,18',
'phone' => 'require|mobile',
'gender' => 'require|in:0,1',
'age' => 'require|number|between:0,150',
'diagnosis_date' => 'require',
'diagnosis_type' => 'require',
'syndrome_type' => 'require',
'status' => 'in:0,1',
];
protected $message = [
'id.require' => '参数缺失',
'patient_name.require' => '请输入患者姓名',
'patient_name.length' => '患者姓名长度须在1-50位字符',
'id_card.length' => '身份证号长度不正确',
'phone.require' => '请输入手机号',
'phone.mobile' => '手机号格式不正确',
'gender.require' => '请选择性别',
'gender.in' => '性别参数错误',
'age.require' => '请输入年龄',
'age.number' => '年龄必须为数字',
'age.between' => '年龄范围0-150',
'diagnosis_date.require' => '请选择诊断日期',
'diagnosis_type.require' => '请选择诊断类型',
'syndrome_type.require' => '请选择证型',
'status.in' => '状态参数错误',
];
public function sceneAdd()
{
return $this->remove('id', true);
}
public function sceneEdit()
{
return $this->only(['id', 'patient_name', 'id_card', 'phone', 'gender', 'age', 'diagnosis_date', 'diagnosis_type', 'syndrome_type', 'past_history', 'symptoms', 'tongue_coating', 'pulse', 'treatment_principle', 'prescription', 'doctor_advice', 'remark', 'status']);
}
public function sceneId()
{
return $this->only(['id']);
}
protected function checkDiagnosis($value)
{
$diagnosis = Diagnosis::findOrEmpty($value);
if ($diagnosis->isEmpty()) {
return '诊单不存在';
}
return true;
}
}