This commit is contained in:
gr
2026-05-14 11:02:29 +08:00
316 changed files with 3430 additions and 447 deletions
@@ -9,6 +9,8 @@ use app\common\service\doctor\RosterSegmentService;
use app\common\model\tcm\Diagnosis;
use app\common\model\tcm\DiagnosisGuahaoLog;
use app\common\model\auth\Admin;
use app\common\model\auth\AdminRole;
use app\common\service\DataScope\DataScopeService;
use app\api\logic\ChatNotifyLogic;
use app\adminapi\logic\tcm\PrescriptionLogic;
use app\adminapi\logic\tcm\DiagnosisLogic;
@@ -42,6 +44,61 @@ class AppointmentLogic extends BaseLogic
return in_array($name, self::CHANNEL_NAMES_REQUIRING_SOURCE_DETAIL, true);
}
/**
* @param array<int|string, mixed> $cols Db::name('doctor_appointment')->getTableFields()
*/
private static function assertAppointmentChannelWritable(array $cols, string $chSrc): ?string
{
$hasChannelSource = in_array('channel_source', $cols, true);
$hasChannels = in_array('channels', $cols, true);
if (!$hasChannelSource && !$hasChannels) {
return '挂号表缺少渠道字段(channel_source 或 channels),无法保存渠道来源';
}
if (!$hasChannelSource && $hasChannels && $chSrc !== '' && !is_numeric($chSrc)) {
return '当前挂号表仅有数值型渠道字段 channels,无法写入该渠道;请在数据库增加 channel_source 列';
}
return null;
}
/**
* @param array<string, mixed> $row
* @param array<int|string, mixed> $cols
*/
private static function mergeAppointmentChannelIntoRow(array &$row, array $cols, string $chSrc, string $chDetail): void
{
$hasChannelSource = in_array('channel_source', $cols, true);
$hasChannelDetail = in_array('channel_source_detail', $cols, true);
$hasChannels = in_array('channels', $cols, true);
if ($hasChannelSource) {
$row['channel_source'] = $chSrc;
}
if ($hasChannelDetail) {
$row['channel_source_detail'] = $chDetail;
}
if ($hasChannels && is_numeric($chSrc)) {
$row['channels'] = (int) $chSrc;
}
}
/**
* @param array<string, mixed> $row
* @param array<int|string, mixed> $cols
*
* @return array<string, mixed>
*/
private static function filterAppointmentRowByExistingColumns(array $row, array $cols): array
{
$out = [];
foreach ($row as $k => $v) {
if (in_array((string) $k, $cols, true)) {
$out[$k] = $v;
}
}
return $out;
}
/**
* @notes 获取可用时间段
* @param array $params
@@ -262,25 +319,49 @@ class AppointmentLogic extends BaseLogic
}
}
$data = [
'patient_id' => $params['patient_id'],
'assistant_id'=>$params['assistant_id'],
'doctor_id' => $params['doctor_id'],
'roster_id' => 0, // 不再关联排班表
$cols = Db::name('doctor_appointment')->getTableFields();
$cols = is_array($cols) ? $cols : [];
$channelErr = self::assertAppointmentChannelWritable($cols, $chSrc);
if ($channelErr !== null) {
self::setError($channelErr);
Db::rollback();
return false;
}
$insert = [
'patient_id' => (int) $params['patient_id'],
'doctor_id' => (int) $params['doctor_id'],
'appointment_date' => $params['appointment_date'],
'period' => $params['period'] ?? 'all',
'appointment_time' => $appointmentTime,
'appointment_type' => $params['appointment_type'] ?? 'video',
'remark' => $params['remark'] ?? '',
// 表字段为 channel_source(与字典 type_value=channels 的 value 一致);勿再写入不存在的 channels 列
'channel_source' => $chSrc,
'channel_source_detail' => $chDetail,
'status' => 1,
'create_time' => time(),
'update_time' => time(),
];
if (in_array('roster_id', $cols, true)) {
$insert['roster_id'] = 0;
}
if (in_array('assistant_id', $cols, true)) {
$insert['assistant_id'] = (int) ($params['assistant_id'] ?? 0);
}
$periodVal = $params['period'] ?? 'all';
if (in_array('period', $cols, true)) {
$insert['period'] = $periodVal;
} elseif (in_array('type', $cols, true)) {
$insert['type'] = $periodVal;
}
self::mergeAppointmentChannelIntoRow($insert, $cols, $chSrc, $chDetail);
$appointment = Appointment::create($data);
$insert = self::filterAppointmentRowByExistingColumns($insert, $cols);
$newId = (int) Db::name('doctor_appointment')->insertGetId($insert);
if ($newId <= 0) {
self::setError('预约创建失败');
Db::rollback();
return false;
}
Db::commit();
@@ -288,21 +369,21 @@ class AppointmentLogic extends BaseLogic
$hm = substr((string) $appointmentTime, 0, 5);
$summary = sprintf(
'挂号 #%d:医生「%s」,%s %s',
(int) $appointment->id,
$newId,
$doctorName !== '' ? $doctorName : ('ID:' . (int) $params['doctor_id']),
(string) $params['appointment_date'],
$hm
);
self::appendDiagnosisGuahaoLog(
(int) $params['patient_id'],
(int) $appointment->id,
$newId,
$operatorAdminId,
$operatorAdminInfo,
'create',
$summary
);
return ['id' => $appointment->id];
return ['id' => $newId];
} catch (\Exception $e) {
Db::rollback();
self::setError($e->getMessage());
@@ -787,6 +868,244 @@ class AppointmentLogic extends BaseLogic
}
}
/**
* 与 AppointmentLists 一致的可见性(不含 progress_board / diag_scope_relax
*/
private static function appointmentRowManageableByAdmin(
Appointment $appointment,
?Diagnosis $diag,
int $adminId,
array $adminInfo
): bool {
$roleIds = array_map('intval', AdminRole::where('admin_id', $adminId)->column('role_id'));
if (in_array(1, $roleIds, true) && (int) $appointment->doctor_id !== $adminId) {
return false;
}
if (in_array(2, $roleIds, true)) {
$asst = $diag ? (int) $diag->assistant_id : 0;
if ($asst !== $adminId) {
return false;
}
}
if (!DataScopeService::isEnabled()) {
return true;
}
$ids = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
if ($ids === []) {
return false;
}
if ($ids === null) {
return true;
}
$docId = (int) $appointment->doctor_id;
$asstId = $diag ? (int) $diag->assistant_id : 0;
return in_array($docId, $ids, true)
|| ($asstId > 0 && in_array($asstId, $ids, true));
}
/**
* 后台编辑挂号(预约日期/时段/类型/状态/备注/医助)
*
* @param array<string, mixed> $params
*/
public static function adminEdit(array $params, int $adminId, array $adminInfo): bool
{
try {
$id = (int) ($params['id'] ?? 0);
if ($id <= 0) {
self::setError('参数错误');
return false;
}
$appointment = Appointment::findOrEmpty($id);
if ($appointment->isEmpty()) {
self::setError('预约记录不存在');
return false;
}
$diag = Diagnosis::where('id', (int) $appointment->patient_id)->whereNull('delete_time')->find();
if (!self::appointmentRowManageableByAdmin($appointment, $diag ?: null, $adminId, $adminInfo)) {
self::setError('无权限编辑');
return false;
}
$period = trim((string) ($params['period'] ?? ''));
if (!in_array($period, ['morning', 'afternoon', 'all'], true)) {
self::setError('时段无效');
return false;
}
$appointmentDate = trim((string) ($params['appointment_date'] ?? ''));
if ($appointmentDate === '') {
self::setError('预约日期不能为空');
return false;
}
$rawTime = trim((string) ($params['appointment_time'] ?? ''));
if ($rawTime === '') {
self::setError('预约时间不能为空');
return false;
}
$appointmentTime = strlen($rawTime) === 5 ? $rawTime . ':00' : $rawTime;
$status = (int) ($params['status'] ?? 0);
if ($status < 1 || $status > 4) {
self::setError('状态无效');
return false;
}
$appointmentType = trim((string) ($params['appointment_type'] ?? ''));
if (!in_array($appointmentType, ['video', 'text', 'phone'], true)) {
self::setError('预约类型无效');
return false;
}
$remark = isset($params['remark']) ? trim((string) $params['remark']) : '';
if (mb_strlen($remark) > 500) {
self::setError('备注过长');
return false;
}
$chSrc = trim((string) ($params['channel_source'] ?? ''));
if ($chSrc === '') {
self::setError('请选择渠道来源');
return false;
}
$chDetail = trim((string) ($params['channel_source_detail'] ?? ''));
$channelDictName = trim((string) (DictData::where('type_value', 'channels')
->where('value', $chSrc)
->value('name') ?? ''));
if ($channelDictName !== '' && self::channelDictNameRequiresSourceDetail($channelDictName)) {
if ($chDetail === '') {
self::setError('请填写自媒体渠道补充信息');
return false;
}
}
if (mb_strlen($chDetail) > 128) {
self::setError('渠道补充说明过长');
return false;
}
$postAssistant = array_key_exists('assistant_id', $params);
$assistantId = null;
if ($postAssistant) {
$assistantRaw = $params['assistant_id'];
if ($assistantRaw !== null && $assistantRaw !== '') {
$aid = (int) $assistantRaw;
$assistantId = $aid > 0 ? $aid : null;
}
}
$patientId = (int) $appointment->patient_id;
$doctorId = (int) $appointment->doctor_id;
if (in_array($status, [1, 4], true)) {
$patientDup = Appointment::where('patient_id', $patientId)
->where('appointment_date', $appointmentDate)
->whereIn('status', [1, 4])
->where('id', '<>', $id)
->find();
if ($patientDup) {
self::setError('该诊单在所选日期已有其他有效挂号(已预约/已过号)');
return false;
}
}
if ($status === 1) {
$slotDup = Appointment::where('doctor_id', $doctorId)
->where('appointment_date', $appointmentDate)
->where('appointment_time', $appointmentTime)
->where('status', 1)
->where('id', '<>', $id)
->find();
if ($slotDup) {
self::setError('该医生在所选时段已被占用');
return false;
}
}
$beforeDate = (string) ($appointment->appointment_date ?? '');
$beforeTimeRaw = (string) ($appointment->appointment_time ?? '');
$beforeHm = strlen($beforeTimeRaw) >= 5 ? substr($beforeTimeRaw, 0, 5) : $beforeTimeRaw;
$snap = $appointment->toArray();
$beforePeriod = (string) ($snap['period'] ?? $snap['type'] ?? '');
$beforeStatus = (int) $appointment->status;
$tblFields = Db::name('doctor_appointment')->getTableFields();
$cols = is_array($tblFields) ? $tblFields : [];
$channelErr = self::assertAppointmentChannelWritable($cols, $chSrc);
if ($channelErr !== null) {
self::setError($channelErr);
return false;
}
Db::startTrans();
$saveData = [
'appointment_date' => $appointmentDate,
'appointment_time' => $appointmentTime,
'appointment_type' => $appointmentType,
'status' => $status,
'remark' => $remark,
'update_time' => time(),
];
if (in_array('assistant_id', $cols, true) && $postAssistant) {
$saveData['assistant_id'] = $assistantId;
}
if (in_array('period', $cols, true)) {
$saveData['period'] = $period;
} elseif (in_array('type', $cols, true)) {
$saveData['type'] = $period;
}
self::mergeAppointmentChannelIntoRow($saveData, $cols, $chSrc, $chDetail);
Db::name('doctor_appointment')->where('id', $id)->update($saveData);
Db::commit();
$hm = substr($appointmentTime, 0, 5);
$summary = sprintf(
'后台修改挂号 #%d:日期 %s %s→%s %s,时段 %s→%s,状态 %d→%d',
$id,
$beforeDate,
$beforeHm,
$appointmentDate,
$hm,
$beforePeriod !== '' ? $beforePeriod : '—',
$period,
$beforeStatus,
$status
);
self::appendDiagnosisGuahaoLog($patientId, $id, $adminId, $adminInfo, 'admin_edit', $summary);
return true;
} catch (\Throwable $e) {
Db::rollback();
self::setError($e->getMessage());
return false;
}
}
/**
* 诊单列表维度:记录谁在后台发起挂号 / 取消挂号(写入失败不影响主流程)
*/