更新
This commit is contained in:
@@ -60,6 +60,7 @@ class LoginLogic extends BaseLogic
|
||||
'avatar' => $avatar,
|
||||
'role_name' => $adminInfo['role_name'],
|
||||
'token' => $adminInfo['token'],
|
||||
'is_paw' => $admin->is_paw ?? 1, // 0=需要修改密码,1=正常
|
||||
];
|
||||
}
|
||||
|
||||
@@ -133,6 +134,7 @@ class LoginLogic extends BaseLogic
|
||||
'avatar' => $avatar,
|
||||
'role_name' => $adminInfo['role_name'],
|
||||
'token' => $adminInfo['token'],
|
||||
'is_paw' => $admin->is_paw ?? 1, // 0=需要修改密码,1=正常
|
||||
];
|
||||
}
|
||||
|
||||
@@ -211,4 +213,48 @@ class LoginLogic extends BaseLogic
|
||||
//设置token过期
|
||||
return AdminTokenService::expireToken($adminInfo['token']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 首次登录修改密码
|
||||
* @param string $token
|
||||
* @param string $password
|
||||
* @return bool
|
||||
*/
|
||||
public function changeFirstPassword($token, $password)
|
||||
{
|
||||
try {
|
||||
// 通过 token 获取管理员信息
|
||||
$adminSession = \app\common\model\auth\AdminSession::where('token', '=', $token)
|
||||
->where('expire_time', '>', time())
|
||||
->find();
|
||||
|
||||
if (!$adminSession) {
|
||||
self::setError('登录已过期,请重新登录');
|
||||
return false;
|
||||
}
|
||||
|
||||
$admin = Admin::find($adminSession->admin_id);
|
||||
if (!$admin) {
|
||||
self::setError('管理员不存在');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 使用系统的密码加密方式
|
||||
$passwordSalt = Config::get('project.unique_identification');
|
||||
$encryptedPassword = create_password($password, $passwordSalt);
|
||||
|
||||
// 更新密码和 is_paw 标记
|
||||
$admin->password = $encryptedPassword;
|
||||
$admin->is_paw = 1;
|
||||
$admin->save();
|
||||
|
||||
// 使当前 token 过期,要求重新登录
|
||||
AdminTokenService::expireToken($token);
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
self::setError($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -281,7 +281,7 @@ class AdminLogic extends BaseLogic
|
||||
{
|
||||
$admin = Admin::field([
|
||||
'id', 'account', 'name', 'disable', 'root',
|
||||
'multipoint_login', 'avatar',
|
||||
'multipoint_login', 'avatar', 'is_paw',
|
||||
'gender', 'age', 'phone', 'title', 'department',
|
||||
'specialty', 'education', 'experience', 'honors',
|
||||
'license_no', 'qualification_images', 'enable_image_consult', 'enable_video_consult', 'enable_charge',
|
||||
@@ -311,6 +311,9 @@ class AdminLogic extends BaseLogic
|
||||
return $admin;
|
||||
}
|
||||
|
||||
$authRoleIds = AdminRole::where('admin_id', $params['id'])->column('role_id');
|
||||
$admin['role_ids'] = array_values(array_map('intval', $authRoleIds));
|
||||
|
||||
$result['user'] = $admin;
|
||||
// 当前管理员角色拥有的菜单
|
||||
$result['menu'] = MenuLogic::getMenuByAdminId($params['id']);
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace app\adminapi\logic\doctor;
|
||||
use app\common\logic\BaseLogic;
|
||||
use app\common\model\doctor\Appointment;
|
||||
use app\common\model\doctor\Roster;
|
||||
use app\common\service\doctor\RosterSegmentService;
|
||||
use app\common\model\tcm\Diagnosis;
|
||||
use app\common\model\auth\Admin;
|
||||
use app\api\logic\ChatNotifyLogic;
|
||||
@@ -70,72 +71,46 @@ class AppointmentLogic extends BaseLogic
|
||||
return ['slots' => []];
|
||||
}
|
||||
|
||||
// 2. 根据排班时段生成时间段
|
||||
// 2. 按每段排班的起止时间与号源间隔生成时间段
|
||||
$slots = [];
|
||||
|
||||
|
||||
foreach ($rosters as $roster) {
|
||||
// 再次确认状态为出诊(双重检查)
|
||||
if ($roster['status'] != 1) {
|
||||
if ((int) $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
|
||||
'status' => $roster['status'],
|
||||
]);
|
||||
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
|
||||
];
|
||||
}
|
||||
$window = RosterSegmentService::resolveWindow($roster);
|
||||
if ($window === null) {
|
||||
\think\facade\Log::warning('无法解析排班起止时间,跳过', [
|
||||
'roster_id' => $roster['id'],
|
||||
'period' => $roster['period'] ?? null,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
[$startHm, $endHm] = $window;
|
||||
$slotMinutes = RosterSegmentService::normalizeSlotMinutes($roster['slot_minutes'] ?? 15);
|
||||
$times = RosterSegmentService::generateSlotTimes($startHm, $endHm, $slotMinutes);
|
||||
$times = RosterSegmentService::applyQuotaCap($times, (int) ($roster['quota'] ?? 0));
|
||||
|
||||
\think\facade\Log::info('处理排班时段', [
|
||||
'roster_id' => $roster['id'],
|
||||
'window' => $window,
|
||||
'slot_minutes' => $slotMinutes,
|
||||
'slot_count' => count($times),
|
||||
]);
|
||||
|
||||
foreach ($times as $time) {
|
||||
$slots[] = [
|
||||
'time' => $time,
|
||||
'available' => true,
|
||||
'quota' => 1,
|
||||
'period' => $roster['period'] ?? 'segment',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,14 +139,8 @@ class AppointmentLogic extends BaseLogic
|
||||
|
||||
\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)
|
||||
'last_3_slots' => array_slice($slots, -3),
|
||||
]);
|
||||
|
||||
// 4. 查询已预约的时间段
|
||||
@@ -237,6 +206,7 @@ class AppointmentLogic extends BaseLogic
|
||||
}
|
||||
$data = [
|
||||
'patient_id' => $params['patient_id'],
|
||||
'assistant_id'=>$params['assistant_id'],
|
||||
'doctor_id' => $params['doctor_id'],
|
||||
'roster_id' => 0, // 不再关联排班表
|
||||
'appointment_date' => $params['appointment_date'],
|
||||
@@ -244,6 +214,7 @@ class AppointmentLogic extends BaseLogic
|
||||
'appointment_time' => $appointmentTime,
|
||||
'appointment_type' => $params['appointment_type'] ?? 'video',
|
||||
'remark' => $params['remark'] ?? '',
|
||||
'channel_source' => $params['channel_source'] ?? '',
|
||||
'status' => 1,
|
||||
'create_time' => time(),
|
||||
'update_time' => time(),
|
||||
@@ -339,28 +310,41 @@ class AppointmentLogic extends BaseLogic
|
||||
public static function getDoctorAvailability($doctorId, $date)
|
||||
{
|
||||
try {
|
||||
$totalAvailable = 0;
|
||||
|
||||
// 查询上午和下午的排班
|
||||
$rosters = Roster::where([
|
||||
'doctor_id' => $doctorId,
|
||||
'date' => $date,
|
||||
'status' => 1 // 出诊
|
||||
])->select();
|
||||
'status' => 1,
|
||||
])->select()->toArray();
|
||||
|
||||
if (empty($rosters)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$bookedHm = Appointment::where([
|
||||
'doctor_id' => $doctorId,
|
||||
'appointment_date' => $date,
|
||||
'status' => 1,
|
||||
])->column('appointment_time');
|
||||
$bookedHm = array_map(static function ($t) {
|
||||
return substr((string) $t, 0, 5);
|
||||
}, $bookedHm);
|
||||
$bookedSet = array_fill_keys($bookedHm, true);
|
||||
|
||||
$totalAvailable = 0;
|
||||
|
||||
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;
|
||||
$window = RosterSegmentService::resolveWindow($roster);
|
||||
if ($window === null) {
|
||||
continue;
|
||||
}
|
||||
[$startHm, $endHm] = $window;
|
||||
$slotMinutes = RosterSegmentService::normalizeSlotMinutes($roster['slot_minutes'] ?? 15);
|
||||
$times = RosterSegmentService::generateSlotTimes($startHm, $endHm, $slotMinutes);
|
||||
$times = RosterSegmentService::applyQuotaCap($times, (int) ($roster['quota'] ?? 0));
|
||||
foreach ($times as $t) {
|
||||
if (empty($bookedSet[$t])) {
|
||||
++$totalAvailable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace app\adminapi\logic\doctor;
|
||||
|
||||
use app\common\logic\BaseLogic;
|
||||
use app\common\model\doctor\Medicine;
|
||||
use app\common\service\doctor\MedicineNameAbbrService;
|
||||
|
||||
/**
|
||||
* 药品库逻辑层
|
||||
*/
|
||||
class MedicineLogic extends BaseLogic
|
||||
{
|
||||
/**
|
||||
* 添加药品
|
||||
*/
|
||||
public static function add(array $params): bool
|
||||
{
|
||||
try {
|
||||
Medicine::create([
|
||||
'name' => $params['name'],
|
||||
'name_pinyin_abbr' => MedicineNameAbbrService::build($params['name']),
|
||||
'supplier' => $params['supplier'],
|
||||
'unit' => $params['unit'],
|
||||
'settlement_price' => $params['settlement_price'],
|
||||
'retail_price' => $params['retail_price'],
|
||||
'stock' => $params['stock'] ?? 0,
|
||||
'image' => $params['image'] ?? '',
|
||||
'status' => $params['status'] ?? 1,
|
||||
'remark' => $params['remark'] ?? '',
|
||||
]);
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
self::setError($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑药品
|
||||
*/
|
||||
public static function edit(array $params): bool
|
||||
{
|
||||
try {
|
||||
Medicine::update([
|
||||
'id' => $params['id'],
|
||||
'name' => $params['name'],
|
||||
'name_pinyin_abbr' => MedicineNameAbbrService::build($params['name']),
|
||||
'supplier' => $params['supplier'],
|
||||
'unit' => $params['unit'],
|
||||
'settlement_price' => $params['settlement_price'],
|
||||
'retail_price' => $params['retail_price'],
|
||||
'stock' => $params['stock'] ?? 0,
|
||||
'image' => $params['image'] ?? '',
|
||||
'status' => $params['status'] ?? 1,
|
||||
'remark' => $params['remark'] ?? '',
|
||||
]);
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
self::setError($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除药品
|
||||
*/
|
||||
public static function delete(array $params): bool
|
||||
{
|
||||
Medicine::destroy($params['id']);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 药品详情
|
||||
*/
|
||||
public static function detail(array $params): array
|
||||
{
|
||||
return Medicine::findOrEmpty($params['id'])->toArray();
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace app\adminapi\logic\doctor;
|
||||
|
||||
use app\common\logic\BaseLogic;
|
||||
use app\common\model\doctor\Roster;
|
||||
use app\common\service\doctor\RosterSegmentService;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
@@ -13,6 +14,80 @@ use think\facade\Db;
|
||||
*/
|
||||
class RosterLogic extends BaseLogic
|
||||
{
|
||||
/**
|
||||
* 组装单条排班数据(period 缺省为 segment)
|
||||
*/
|
||||
protected static function buildRowData(array $params): array
|
||||
{
|
||||
$period = $params['period'] ?? '';
|
||||
if (!in_array($period, ['morning', 'afternoon', 'night', 'segment'], true)) {
|
||||
$period = 'segment';
|
||||
}
|
||||
|
||||
$start = trim((string) ($params['start_time'] ?? ''));
|
||||
$end = trim((string) ($params['end_time'] ?? ''));
|
||||
$status = (int) $params['status'];
|
||||
$slotMinutes = RosterSegmentService::normalizeSlotMinutes($params['slot_minutes'] ?? 15);
|
||||
|
||||
if ($status === 1) {
|
||||
if ($start === '' || $end === '') {
|
||||
throw new \InvalidArgumentException('出诊须填写接诊开始与结束时间');
|
||||
}
|
||||
if ($start >= $end) {
|
||||
throw new \InvalidArgumentException('结束时间须晚于开始时间');
|
||||
}
|
||||
} else {
|
||||
if ($start === '' || $end === '') {
|
||||
throw new \InvalidArgumentException('请填写时段开始与结束时间');
|
||||
}
|
||||
if ($start >= $end) {
|
||||
throw new \InvalidArgumentException('结束时间须晚于开始时间');
|
||||
}
|
||||
}
|
||||
|
||||
$shiftType = $params['shift_type'] ?? '';
|
||||
if ($shiftType !== '' && !in_array($shiftType, ['day', 'night'], true)) {
|
||||
$shiftType = '';
|
||||
}
|
||||
|
||||
$quota = (int) ($params['quota'] ?? 0);
|
||||
if ($status !== 1) {
|
||||
$quota = 0;
|
||||
}
|
||||
|
||||
return [
|
||||
'doctor_id' => (int) $params['doctor_id'],
|
||||
'date' => $params['date'],
|
||||
'period' => $period,
|
||||
'start_time' => $start,
|
||||
'end_time' => $end,
|
||||
'shift_type' => $shiftType !== '' ? $shiftType : null,
|
||||
'slot_minutes' => $slotMinutes,
|
||||
'status' => $status,
|
||||
'quota' => $quota,
|
||||
'max_patients' => $status === 1 ? (int) ($params['max_patients'] ?? 0) : 0,
|
||||
'remark' => (string) ($params['remark'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否存在相同医生、日期、起止时间的记录(排除指定 id)
|
||||
*/
|
||||
protected static function duplicateExists(int $doctorId, string $date, string $start, string $end, ?int $excludeId = null): bool
|
||||
{
|
||||
$q = Roster::where([
|
||||
['doctor_id', '=', $doctorId],
|
||||
['date', '=', $date],
|
||||
['start_time', '=', $start],
|
||||
['end_time', '=', $end],
|
||||
]);
|
||||
if ($excludeId) {
|
||||
$q->where('id', '<>', $excludeId);
|
||||
}
|
||||
|
||||
return (bool) $q->find();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 保存排班
|
||||
* @param array $params
|
||||
@@ -21,30 +96,33 @@ class RosterLogic extends BaseLogic
|
||||
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'] ?? '',
|
||||
];
|
||||
$data = self::buildRowData($params);
|
||||
|
||||
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];
|
||||
if (self::duplicateExists($data['doctor_id'], $data['date'], $data['start_time'], $data['end_time'], !empty($params['id']) ? (int) $params['id'] : null)) {
|
||||
self::setError('该医生在同一天已存在相同的接诊时段');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!empty($params['id'])) {
|
||||
$data['update_time'] = time();
|
||||
Roster::where('id', (int) $params['id'])->update($data);
|
||||
|
||||
return ['id' => (int) $params['id']];
|
||||
}
|
||||
|
||||
$data['create_time'] = time();
|
||||
$data['update_time'] = time();
|
||||
$roster = Roster::create($data);
|
||||
|
||||
return ['id' => $roster->id];
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
self::setError($e->getMessage());
|
||||
|
||||
return false;
|
||||
} catch (\Exception $e) {
|
||||
self::setError($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -58,9 +136,11 @@ class RosterLogic extends BaseLogic
|
||||
{
|
||||
try {
|
||||
Roster::destroy($params['id']);
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
self::setError($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -84,53 +164,50 @@ class RosterLogic extends BaseLogic
|
||||
{
|
||||
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'] ?? '',
|
||||
];
|
||||
|
||||
// 检查是否已存在
|
||||
foreach ($params['rosters'] as $roster) {
|
||||
$data = self::buildRowData($roster);
|
||||
|
||||
$exists = Roster::where([
|
||||
['doctor_id', '=', $data['doctor_id']],
|
||||
['date', '=', $data['date']],
|
||||
['period', '=', $data['period']],
|
||||
['start_time', '=', $data['start_time']],
|
||||
['end_time', '=', $data['end_time']],
|
||||
])->find();
|
||||
|
||||
if ($exists) {
|
||||
// 更新
|
||||
$data['update_time'] = time();
|
||||
$exists->save($data);
|
||||
$updateCount++;
|
||||
++$updateCount;
|
||||
} else {
|
||||
// 新增
|
||||
$data['create_time'] = time();
|
||||
$data['update_time'] = time();
|
||||
Roster::create($data);
|
||||
$createCount++;
|
||||
++$createCount;
|
||||
}
|
||||
$successCount++;
|
||||
++$successCount;
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
|
||||
return [
|
||||
'success_count' => $successCount,
|
||||
'create_count' => $createCount,
|
||||
'update_count' => $updateCount,
|
||||
];
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
Db::rollback();
|
||||
self::setError($e->getMessage());
|
||||
|
||||
return false;
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
self::setError($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -145,28 +222,29 @@ class RosterLogic extends BaseLogic
|
||||
try {
|
||||
Db::startTrans();
|
||||
|
||||
// 获取源排班数据
|
||||
$where = [
|
||||
['date', 'between', [$params['source_start_date'], $params['source_end_date']]]
|
||||
['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,
|
||||
'start_time' => $roster->start_time,
|
||||
'end_time' => $roster->end_time,
|
||||
'shift_type' => $roster->shift_type,
|
||||
'slot_minutes' => $roster->slot_minutes ?: 15,
|
||||
'status' => $roster->status,
|
||||
'quota' => $roster->quota,
|
||||
'max_patients' => $roster->max_patients,
|
||||
@@ -175,23 +253,29 @@ class RosterLogic extends BaseLogic
|
||||
'update_time' => time(),
|
||||
];
|
||||
|
||||
// 检查目标日期是否已存在排班
|
||||
$exists = Roster::where([
|
||||
if (empty($data['start_time']) || empty($data['end_time'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$dup = Roster::where([
|
||||
['doctor_id', '=', $data['doctor_id']],
|
||||
['date', '=', $data['date']],
|
||||
['period', '=', $data['period']],
|
||||
['start_time', '=', $data['start_time']],
|
||||
['end_time', '=', $data['end_time']],
|
||||
])->find();
|
||||
|
||||
if (!$exists) {
|
||||
if (!$dup) {
|
||||
Roster::create($data);
|
||||
}
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
self::setError($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,9 @@ namespace app\adminapi\logic\tcm;
|
||||
|
||||
use app\common\logic\BaseLogic;
|
||||
use app\common\model\tcm\Diagnosis;
|
||||
use app\common\model\tcm\ImChatMessage;
|
||||
use app\common\model\DiagnosisViewRecord;
|
||||
use think\facade\Db;
|
||||
/**
|
||||
* 中医辨房病因诊单逻辑
|
||||
* Class DiagnosisLogic
|
||||
@@ -499,7 +501,10 @@ class DiagnosisLogic extends BaseLogic
|
||||
$doctorUserId = 'doctor_' . $adminId;
|
||||
$query = Diagnosis::where('id', $patientId)
|
||||
->where('delete_time', null)->find();
|
||||
|
||||
if(!$query){
|
||||
self::setError('患者诊单已被删除');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 患者userId(必须与小程序端一致)
|
||||
$patientUserId = 'patient_' . $patientId;
|
||||
@@ -524,7 +529,9 @@ class DiagnosisLogic extends BaseLogic
|
||||
'userSig' => $userSig,
|
||||
'assistant_id'=>$query->assistant_id?'doctor_'.$query->assistant_id:'',
|
||||
'patientUserId' => $patientUserId, // 患者的userId(用于发起通话)
|
||||
'expireTime' => 86400 // 24小时
|
||||
'expireTime' => 86400, // 24小时
|
||||
// 与 .env [trtc] ISLOCHOSTVOD 一致:true 允许浏览器本地录制并上传
|
||||
'isLochostVod' => (bool)config('trtc.is_lochost_vod', false),
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
self::setError($e->getMessage());
|
||||
@@ -620,16 +627,11 @@ class DiagnosisLogic extends BaseLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 拉取与本诊单相关的腾讯云 IM 单聊记录(C2C:patient_{患者ID} 与 全部医生/医助账号 doctor_* 分别会话后合并)
|
||||
* @notes 拉取与本诊单相关的 IM 单聊记录:本地归档 + 腾讯云漫游合并(归档突破云端约 7 天限制)
|
||||
*/
|
||||
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('诊单不存在');
|
||||
@@ -641,6 +643,24 @@ class DiagnosisLogic extends BaseLogic
|
||||
return false;
|
||||
}
|
||||
$patientImId = 'patient_' . $patientId;
|
||||
$archived = self::loadArchivedImChatRows($diagnosisId);
|
||||
|
||||
$config = self::getTrtcConfig();
|
||||
if (!$config) {
|
||||
if (empty($archived)) {
|
||||
self::setError('请先配置腾讯云 TRTC / IM 参数');
|
||||
return false;
|
||||
}
|
||||
$lists = self::enrichImMessagesWithStaffNames($archived);
|
||||
|
||||
return [
|
||||
'lists' => $lists,
|
||||
'patient_im_id' => $patientImId,
|
||||
'patient_name' => $diag['patient_name'] ?? '',
|
||||
'doctor_accounts_queried' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$doctorAccounts = self::collectAllDoctorImPeerAccounts();
|
||||
$assistantId = isset($diag['assistant_id']) ? (int)$diag['assistant_id'] : 0;
|
||||
if ($assistantId > 0) {
|
||||
@@ -652,34 +672,12 @@ class DiagnosisLogic extends BaseLogic
|
||||
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);
|
||||
$live = self::pullLiveImChatMessagesForDiagnosis($diag);
|
||||
$merged = self::mergeImMessagesByMsgId($archived, $live);
|
||||
$merged = self::enrichImMessagesWithStaffNames($merged);
|
||||
|
||||
return [
|
||||
'lists' => $unique,
|
||||
'lists' => $merged,
|
||||
'patient_im_id' => $patientImId,
|
||||
'patient_name' => $diag['patient_name'] ?? '',
|
||||
'doctor_accounts_queried' => $doctorAccounts,
|
||||
@@ -690,6 +688,239 @@ class DiagnosisLogic extends BaseLogic
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时任务:从腾讯云拉取漫游消息写入归档表
|
||||
*
|
||||
* @return array{inserted:int, skipped_live_empty:bool, error?:string}
|
||||
*/
|
||||
public static function syncImChatArchiveForDiagnosis(int $diagnosisId): array
|
||||
{
|
||||
$out = ['inserted' => 0, 'skipped_live_empty' => false];
|
||||
try {
|
||||
if (!self::getTrtcConfig()) {
|
||||
$out['error'] = 'TRTC/IM 未配置';
|
||||
return $out;
|
||||
}
|
||||
$diag = Diagnosis::where('id', $diagnosisId)->where('delete_time', null)->find();
|
||||
if (!$diag) {
|
||||
$out['error'] = '诊单不存在';
|
||||
return $out;
|
||||
}
|
||||
$live = self::pullLiveImChatMessagesForDiagnosis($diag);
|
||||
if (empty($live)) {
|
||||
$out['skipped_live_empty'] = true;
|
||||
return $out;
|
||||
}
|
||||
$live = self::enrichImMessagesWithStaffNames($live);
|
||||
$patientId = (int)$diag['patient_id'];
|
||||
$out['inserted'] = self::persistImChatArchiveRows($diagnosisId, $patientId, $live);
|
||||
return $out;
|
||||
} catch (\Exception $e) {
|
||||
$out['error'] = $e->getMessage();
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{diagnoses:int, inserted:int, errors:array<int, string>}
|
||||
*/
|
||||
public static function syncImChatArchiveBatch(int $sinceDays, int $limit, ?int $onlyDiagnosisId): array
|
||||
{
|
||||
$stats = ['diagnoses' => 0, 'inserted' => 0, 'errors' => []];
|
||||
$limit = max(1, min(500, $limit));
|
||||
$q = Diagnosis::where('delete_time', null);
|
||||
if ($onlyDiagnosisId !== null && $onlyDiagnosisId > 0) {
|
||||
$q->where('id', $onlyDiagnosisId);
|
||||
} elseif ($sinceDays > 0) {
|
||||
$q->where('update_time', '>=', time() - $sinceDays * 86400);
|
||||
}
|
||||
$ids = $q->order('id', 'desc')->limit($limit)->column('id');
|
||||
foreach ($ids as $id) {
|
||||
$stats['diagnoses']++;
|
||||
$r = self::syncImChatArchiveForDiagnosis((int)$id);
|
||||
if (!empty($r['error'])) {
|
||||
$stats['errors'][] = 'diagnosis ' . $id . ': ' . $r['error'];
|
||||
continue;
|
||||
}
|
||||
$stats['inserted'] += (int)($r['inserted'] ?? 0);
|
||||
}
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private static function loadArchivedImChatRows(int $diagnosisId): array
|
||||
{
|
||||
if ($diagnosisId <= 0) {
|
||||
return [];
|
||||
}
|
||||
$list = ImChatMessage::where('diagnosis_id', $diagnosisId)
|
||||
->order('msg_time', 'asc')
|
||||
->order('id', 'asc')
|
||||
->select()
|
||||
->toArray();
|
||||
$out = [];
|
||||
foreach ($list as $row) {
|
||||
$out[] = [
|
||||
'msg_id' => (string)($row['msg_id'] ?? ''),
|
||||
'from_account' => (string)($row['from_account'] ?? ''),
|
||||
'to_account' => (string)($row['to_account'] ?? ''),
|
||||
'time' => (int)($row['msg_time'] ?? 0),
|
||||
'is_from_doctor' => !empty($row['is_from_doctor']),
|
||||
'msg_type' => (string)($row['msg_type'] ?? ''),
|
||||
'text' => (string)($row['text'] ?? ''),
|
||||
'image_url' => (string)($row['image_url'] ?? ''),
|
||||
'file_url' => (string)($row['file_url'] ?? ''),
|
||||
'file_name' => (string)($row['file_name'] ?? ''),
|
||||
'raw_elem_type' => (string)($row['raw_elem_type'] ?? ''),
|
||||
'from_staff_name' => (string)($row['from_staff_name'] ?? ''),
|
||||
'doctor_peer_account' => (string)($row['doctor_peer_account'] ?? ''),
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $archived
|
||||
* @param array<int, array<string, mixed>> $live
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private static function mergeImMessagesByMsgId(array $archived, array $live): array
|
||||
{
|
||||
$map = [];
|
||||
$tail = [];
|
||||
foreach ($archived as $r) {
|
||||
$k = (string)($r['msg_id'] ?? '');
|
||||
if ($k !== '') {
|
||||
$map[$k] = $r;
|
||||
} else {
|
||||
$tail[] = $r;
|
||||
}
|
||||
}
|
||||
foreach ($live as $r) {
|
||||
$k = (string)($r['msg_id'] ?? '');
|
||||
if ($k !== '') {
|
||||
$map[$k] = $r;
|
||||
} else {
|
||||
$tail[] = $r;
|
||||
}
|
||||
}
|
||||
$merged = array_values($map);
|
||||
$merged = array_merge($merged, $tail);
|
||||
usort($merged, function ($a, $b) {
|
||||
return ($a['time'] ?? 0) <=> ($b['time'] ?? 0);
|
||||
});
|
||||
return $merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Diagnosis|array<string, mixed> $diag
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private static function pullLiveImChatMessagesForDiagnosis($diag): array
|
||||
{
|
||||
$patientId = (int)$diag['patient_id'];
|
||||
if ($patientId <= 0) {
|
||||
return [];
|
||||
}
|
||||
$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)) {
|
||||
return [];
|
||||
}
|
||||
$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;
|
||||
}
|
||||
return $unique;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $rows
|
||||
*/
|
||||
private static function persistImChatArchiveRows(int $diagnosisId, int $patientId, array $rows): int
|
||||
{
|
||||
$now = time();
|
||||
$chunks = [];
|
||||
foreach ($rows as $r) {
|
||||
$msgId = (string)($r['msg_id'] ?? '');
|
||||
if ($msgId === '') {
|
||||
continue;
|
||||
}
|
||||
$chunks[] = [
|
||||
'diagnosis_id' => $diagnosisId,
|
||||
'patient_id' => $patientId,
|
||||
'msg_id' => $msgId,
|
||||
'from_account' => (string)($r['from_account'] ?? ''),
|
||||
'to_account' => (string)($r['to_account'] ?? ''),
|
||||
'msg_time' => (int)($r['time'] ?? 0),
|
||||
'is_from_doctor' => !empty($r['is_from_doctor']) ? 1 : 0,
|
||||
'msg_type' => (string)($r['msg_type'] ?? ''),
|
||||
'text' => (string)($r['text'] ?? ''),
|
||||
'image_url' => (string)($r['image_url'] ?? ''),
|
||||
'file_url' => (string)($r['file_url'] ?? ''),
|
||||
'file_name' => (string)($r['file_name'] ?? ''),
|
||||
'raw_elem_type' => (string)($r['raw_elem_type'] ?? ''),
|
||||
'from_staff_name' => (string)($r['from_staff_name'] ?? ''),
|
||||
'doctor_peer_account' => (string)($r['doctor_peer_account'] ?? ''),
|
||||
'create_time' => $now,
|
||||
];
|
||||
}
|
||||
$inserted = 0;
|
||||
foreach (array_chunk($chunks, 80) as $chunk) {
|
||||
$inserted += self::insertIgnoreImChatBatch($chunk);
|
||||
}
|
||||
return $inserted;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $chunk
|
||||
*/
|
||||
private static function insertIgnoreImChatBatch(array $chunk): int
|
||||
{
|
||||
if (empty($chunk)) {
|
||||
return 0;
|
||||
}
|
||||
$table = (new ImChatMessage())->getTable();
|
||||
$cols = array_keys($chunk[0]);
|
||||
$colSql = '`' . implode('`,`', $cols) . '`';
|
||||
$rowPh = '(' . implode(',', array_fill(0, count($cols), '?')) . ')';
|
||||
$allPh = implode(',', array_fill(0, count($chunk), $rowPh));
|
||||
$flat = [];
|
||||
foreach ($chunk as $row) {
|
||||
foreach ($cols as $c) {
|
||||
$flat[] = $row[$c];
|
||||
}
|
||||
}
|
||||
$sql = 'INSERT IGNORE INTO `' . $table . '` (' . $colSql . ') VALUES ' . $allPh;
|
||||
return (int)Db::execute($sql, $flat);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为 from_account = doctor_{id} 的消息补充后台姓名,便于前端区分不同医生
|
||||
*
|
||||
@@ -747,7 +978,9 @@ class DiagnosisLogic extends BaseLogic
|
||||
if (!is_array($raw)) {
|
||||
continue;
|
||||
}
|
||||
$out[] = self::normalizeTimMessage($raw);
|
||||
$normalized = self::normalizeTimMessage($raw);
|
||||
$normalized['doctor_peer_account'] = $operator;
|
||||
$out[] = $normalized;
|
||||
}
|
||||
$complete = (int)$res['complete'];
|
||||
if ($complete === 1) {
|
||||
|
||||
@@ -5,13 +5,34 @@ declare(strict_types=1);
|
||||
namespace app\adminapi\logic\tcm;
|
||||
|
||||
use app\common\logic\BaseLogic;
|
||||
use app\common\model\auth\AdminRole;
|
||||
use app\common\model\tcm\PrescriptionLibrary;
|
||||
use think\facade\Config;
|
||||
|
||||
/**
|
||||
* 处方库逻辑层
|
||||
*/
|
||||
class PrescriptionLibraryLogic extends BaseLogic
|
||||
{
|
||||
/**
|
||||
* @notes 是否可管理全部处方(超级管理员 或 配置中的管理员角色)
|
||||
*/
|
||||
public static function canManageAllPrescriptions(int $adminId, array $adminInfo): bool
|
||||
{
|
||||
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$allowRoles = Config::get('project.prescription_library_manage_all_roles', []);
|
||||
if ($allowRoles === [] || $allowRoles === null) {
|
||||
$allowRoles = Config::get('project.order_edit_all_roles', [0, 3]);
|
||||
}
|
||||
|
||||
$myRoles = AdminRole::where('admin_id', $adminId)->column('role_id');
|
||||
|
||||
return count(array_intersect($myRoles, $allowRoles)) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 添加处方库
|
||||
*/
|
||||
@@ -24,7 +45,7 @@ class PrescriptionLibraryLogic extends BaseLogic
|
||||
}
|
||||
|
||||
$model = PrescriptionLibrary::create($params);
|
||||
return $model->id;
|
||||
return (int) $model->id;
|
||||
} catch (\Exception $e) {
|
||||
self::setError($e->getMessage());
|
||||
return null;
|
||||
@@ -34,7 +55,7 @@ class PrescriptionLibraryLogic extends BaseLogic
|
||||
/**
|
||||
* @notes 编辑处方库
|
||||
*/
|
||||
public static function edit(array $params, int $adminId): bool
|
||||
public static function edit(array $params, int $adminId, bool $canManageAll = false): bool
|
||||
{
|
||||
try {
|
||||
$model = PrescriptionLibrary::findOrEmpty($params['id']);
|
||||
@@ -43,8 +64,7 @@ class PrescriptionLibraryLogic extends BaseLogic
|
||||
return false;
|
||||
}
|
||||
|
||||
// 权限检查:只有创建者或公开的处方才能编辑
|
||||
if ($model->creator_id != $adminId && $model->is_public != 1) {
|
||||
if (!$canManageAll && (int) $model->creator_id !== $adminId) {
|
||||
self::setError('无权限编辑此处方');
|
||||
return false;
|
||||
}
|
||||
@@ -65,7 +85,7 @@ class PrescriptionLibraryLogic extends BaseLogic
|
||||
/**
|
||||
* @notes 删除处方库
|
||||
*/
|
||||
public static function delete(int $id, int $adminId): bool
|
||||
public static function delete(int $id, int $adminId, bool $canManageAll = false): bool
|
||||
{
|
||||
try {
|
||||
$model = PrescriptionLibrary::findOrEmpty($id);
|
||||
@@ -74,8 +94,7 @@ class PrescriptionLibraryLogic extends BaseLogic
|
||||
return false;
|
||||
}
|
||||
|
||||
// 权限检查:只有创建者才能删除
|
||||
if ($model->creator_id != $adminId) {
|
||||
if (!$canManageAll && (int) $model->creator_id !== $adminId) {
|
||||
self::setError('无权限删除此处方');
|
||||
return false;
|
||||
}
|
||||
@@ -91,7 +110,7 @@ class PrescriptionLibraryLogic extends BaseLogic
|
||||
/**
|
||||
* @notes 处方库详情
|
||||
*/
|
||||
public static function detail(int $id, int $adminId): ?array
|
||||
public static function detail(int $id, int $adminId, bool $canManageAll = false): ?array
|
||||
{
|
||||
try {
|
||||
$model = PrescriptionLibrary::findOrEmpty($id);
|
||||
@@ -99,8 +118,7 @@ class PrescriptionLibraryLogic extends BaseLogic
|
||||
return null;
|
||||
}
|
||||
|
||||
// 权限检查:只有创建者或公开的处方才能查看
|
||||
if ($model->creator_id != $adminId && $model->is_public != 1) {
|
||||
if (!$canManageAll && (int) $model->creator_id !== $adminId && (int) $model->is_public !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user