Files
zyt/server/app/adminapi/logic/tcm/PrescriptionLogic.php
T
2026-04-07 18:13:03 +08:00

636 lines
24 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace app\adminapi\logic\tcm;
use app\common\model\auth\Admin;
use app\common\model\tcm\Prescription;
use app\common\model\tcm\PrescriptionOrder;
use app\common\model\tcm\Diagnosis;
use app\common\service\wechat\WechatWorkAppMessageService;
use think\facade\Config;
use think\facade\Log;
class PrescriptionLogic
{
private static $error = '';
/** @var array{ok: bool, message?: string, errcode?: int}|null 最近一次审核成功后的企微通知结果 */
private static ?array $lastAuditWecomNotify = null;
public static function setError(string $msg): void
{
self::$error = $msg;
}
public static function getError(): string
{
return self::$error;
}
/**
* 可见角色 ID 存库为逗号分隔字符串
*
* @param array|string $raw
*/
public static function normalizeVisibleRoleIds($raw): string
{
if (is_string($raw)) {
$parts = array_filter(array_map('intval', explode(',', $raw)));
} elseif (is_array($raw)) {
$parts = array_filter(array_map('intval', $raw));
} else {
$parts = [];
}
return implode(',', array_values(array_unique($parts)));
}
/**
* 当前管理员是否可审核处方(通过/驳回)
*/
public static function canAuditPrescription(int $adminId, array $adminInfo): bool
{
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
return true;
}
$allow = Config::get('project.prescription_audit_roles', []);
if ($allow === [] || $allow === null) {
$allow = Config::get('project.prescription_library_manage_all_roles', [0, 3]);
}
$myRoles = array_map('intval', $adminInfo['role_id'] ?? []);
$allow = array_map('intval', $allow);
return count(array_intersect($myRoles, $allow)) > 0;
}
/**
* 是否可查看该处方(列表/详情)
*
* @param Prescription|array<string,mixed> $row
*/
public static function canViewPrescription($row, int $adminId, array $adminInfo): bool
{
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
return true;
}
$creatorId = is_array($row) ? (int) ($row['creator_id'] ?? 0) : (int) $row->creator_id;
if ($creatorId === $adminId) {
return true;
}
$assistantId = is_array($row) ? (int) ($row['assistant_id'] ?? 0) : (int) ($row->assistant_id ?? 0);
if ($assistantId > 0 && $assistantId === $adminId) {
return true;
}
$isShared = is_array($row) ? (int) ($row['is_shared'] ?? 0) : (int) $row->is_shared;
if ($isShared === 1) {
return true;
}
$vis = is_array($row) ? (string) ($row['visible_role_ids'] ?? '') : (string) ($row->visible_role_ids ?? '');
$allowed = array_filter(array_map('intval', explode(',', $vis)));
$myRoles = array_map('intval', $adminInfo['role_id'] ?? []);
return count(array_intersect($myRoles, $allowed)) > 0;
}
private static function generateSn(): string
{
return 'RX' . date('Ymd') . str_pad((string)mt_rand(1, 9999), 4, '0', STR_PAD_LEFT);
}
/**
* 处方日期统一为 Y-m-d(与库表 prescription_date 一致)
*
* @param mixed $raw
*/
private static function normalizePrescriptionDate($raw): string
{
if ($raw === null || $raw === '') {
return date('Y-m-d');
}
if (is_numeric($raw) && (float) $raw > 1e9) {
return date('Y-m-d', (int) $raw);
}
$s = (string) $raw;
if (preg_match('/^\d{4}-\d{2}-\d{2}/', $s)) {
return substr($s, 0, 10);
}
$ts = strtotime($s);
return $ts ? date('Y-m-d', $ts) : date('Y-m-d');
}
/**
* 同一诊单、同一开方人、同一处方日期:仅允许一张「未作废」的未删除记录;已作废的可再新开一张。
*
* @param int|null $excludeId 编辑时排除自身 id
*/
private static function assertUniquePrescriptionPerDiagnosisDay(int $diagnosisId, int $creatorId, string $prescriptionDateYmd, ?int $excludeId): bool
{
if ($diagnosisId <= 0 || $creatorId <= 0) {
return true;
}
$q = Prescription::where('diagnosis_id', $diagnosisId)
->where('creator_id', $creatorId)
->where('prescription_date', $prescriptionDateYmd)
->where('void_status', 0)
->whereNull('delete_time');
if ($excludeId !== null && $excludeId > 0) {
$q->where('id', '<>', $excludeId);
}
if ($q->count() > 0) {
self::setError('同一诊单同一天已存在未作废处方,请先作废后再新开');
return false;
}
return true;
}
/**
* 添加处方
*/
public static function add(array $params, int $adminId): ?int
{
// 如果没有诊单ID,允许直接创建处方模板
if (!empty($params['diagnosis_id'])) {
$diagnosis = Diagnosis::find($params['diagnosis_id']);
if (!$diagnosis) {
self::setError('诊单不存在');
return null;
}
}
$dateYmd = self::normalizePrescriptionDate($params['prescription_date'] ?? date('Y-m-d'));
$diagnosisIdRule = (int) ($params['diagnosis_id'] ?? 0);
if ($diagnosisIdRule > 0 && !self::assertUniquePrescriptionPerDiagnosisDay($diagnosisIdRule, $adminId, $dateYmd, null)) {
return null;
}
$herbs = $params['herbs'] ?? [];
if (empty($herbs) || !is_array($herbs)) {
self::setError('请添加中药');
return null;
}
foreach ($herbs as $h) {
if (empty($h['name'])) {
self::setError('中药名称不能为空');
return null;
}
}
$sn = self::generateSn();
while (Prescription::where('sn', $sn)->find()) {
$sn = self::generateSn();
}
$assistantIdForRx = 0;
if ($diagnosisIdRule > 0) {
$diagAssistant = Diagnosis::where('id', $diagnosisIdRule)->value('assistant_id');
$assistantIdForRx = (int) ($diagAssistant ?? 0);
}
$data = [
'sn' => $sn,
'prescription_name' => $params['prescription_name'] ?? '',
'prescription_type' => $params['prescription_type'] ?? '浓缩水丸',
'diagnosis_id' => (int)($params['diagnosis_id'] ?? 0),
'appointment_id' => (int)($params['appointment_id'] ?? 0),
'patient_id' => (int)($params['patient_id'] ?? 0),
'patient_name' => $params['patient_name'] ?? '',
'gender' => (int)($params['gender'] ?? 1),
'age' => (int)($params['age'] ?? 0),
'phone' => $params['phone'] ?? '',
'visit_no' => $params['visit_no'] ?? $sn,
'prescription_date' => $dateYmd,
'pulse' => $params['pulse'] ?? '',
'pulse_condition' => $params['pulse_condition'] ?? '',
'tongue' => $params['tongue'] ?? '',
'tongue_image' => $params['tongue_image'] ?? '',
'clinical_diagnosis' => $params['clinical_diagnosis'] ?? '',
'case_record' => $params['case_record'] ?? null,
'herbs' => $herbs,
'dose_count' => (int)($params['dose_count'] ?? 1),
'dose_unit' => $params['dose_unit'] ?? '剂',
'usage_days' => (int)($params['usage_days'] ?? 7),
'usage_instruction' => $params['usage_instruction'] ?? '水煎服一日二次',
'usage_time' => $params['usage_time'] ?? '饭前',
'usage_way' => $params['usage_way'] ?? '温水送服',
'dietary_taboo' => is_array($params['dietary_taboo'] ?? null) ? implode(',', $params['dietary_taboo']) : ($params['dietary_taboo'] ?? ''),
'usage_notes' => $params['usage_notes'] ?? '',
'amount' => (float)($params['amount'] ?? 0),
'doctor_name' => $params['doctor_name'] ?? '',
'doctor_signature' => $params['doctor_signature'] ?? '',
'template_id' => (int)($params['template_id'] ?? 0),
'is_shared' => (int)($params['is_shared'] ?? 0),
'visible_role_ids' => self::normalizeVisibleRoleIds($params['visible_role_ids'] ?? []),
// 新开方一律待审核(忽略客户端传入的 audit_status,防止绕过审核)
'audit_status' => 0,
'audit_time' => null,
'audit_by' => null,
'audit_by_name' => '',
'audit_remark' => '',
'creator_id' => $adminId,
'assistant_id' => $assistantIdForRx,
];
$prescription = new Prescription();
$prescription->save($data);
return (int) $prescription->id;
}
/**
* 编辑处方
*/
public static function edit(array $params, int $adminId): bool
{
try {
$prescription = Prescription::find($params['id']);
if (!$prescription) {
self::setError('处方不存在');
return false;
}
// 已通过且未作废:不可编辑;例外:业务订单「处方审核」已驳回时允许医生改方,保存后业务订单侧审核会重置为待审
if ((int) ($prescription->audit_status ?? 1) === 1 && (int) ($prescription->void_status ?? 0) === 0) {
if (!PrescriptionOrderLogic::allowDoctorEditApprovedPrescriptionDueToBusinessReject((int) $params['id'])) {
self::setError('该处方已通过审核,不可编辑');
return false;
}
}
// 已驳回:仅当该诊单+当天+同一创建人下没有另一条「已通过且未作废」的处方时允许重新编辑(改后待审核、作废一并清除)
if ((int) ($prescription->audit_status ?? 1) === 2) {
$rejDiagnosisId = (int) ($params['diagnosis_id'] ?? $prescription->diagnosis_id);
$rejDateYmd = self::normalizePrescriptionDate($params['prescription_date'] ?? $prescription->prescription_date);
if ($rejDiagnosisId > 0) {
$hasOtherApproved = Prescription::where('diagnosis_id', $rejDiagnosisId)
->where('creator_id', (int) $prescription->creator_id)
->where('prescription_date', $rejDateYmd)
->where('audit_status', 1)
->where('void_status', 0)
->whereNull('delete_time')
->where('id', '<>', (int) $params['id'])
->count() > 0;
if ($hasOtherApproved) {
self::setError('该诊单当天已有审核通过的处方,不可再编辑本条已驳回记录');
return false;
}
}
}
// 检查权限:只有创建者或共享的处方才能编辑
if ($prescription->creator_id != $adminId && $prescription->is_shared != 1) {
self::setError('无权限编辑此处方');
return false;
}
$herbs = $params['herbs'] ?? [];
if (empty($herbs) || !is_array($herbs)) {
self::setError('请添加中药');
return false;
}
foreach ($herbs as $h) {
if (empty($h['name'])) {
self::setError('中药名称不能为空');
return false;
}
}
$newDiagnosisId = (int) ($params['diagnosis_id'] ?? $prescription->diagnosis_id);
$newDateYmd = self::normalizePrescriptionDate($params['prescription_date'] ?? $prescription->prescription_date);
if ($newDiagnosisId > 0 && !self::assertUniquePrescriptionPerDiagnosisDay($newDiagnosisId, (int) $prescription->creator_id, $newDateYmd, (int) $params['id'])) {
return false;
}
$wasVoid = (int) ($prescription->void_status ?? 0) === 1;
$assistantIdForRx = (int) ($prescription->assistant_id ?? 0);
if ($newDiagnosisId > 0) {
$diagAssistant = Diagnosis::where('id', $newDiagnosisId)->value('assistant_id');
$assistantIdForRx = (int) ($diagAssistant ?? 0);
} else {
$assistantIdForRx = 0;
}
$data = [
'diagnosis_id' => $newDiagnosisId,
'assistant_id' => $assistantIdForRx,
'prescription_name' => $params['prescription_name'] ?? $prescription->prescription_name,
'prescription_type' => $params['prescription_type'] ?? $prescription->prescription_type,
'patient_name' => $params['patient_name'] ?? $prescription->patient_name,
'gender' => (int)($params['gender'] ?? $prescription->gender),
'age' => (int)($params['age'] ?? $prescription->age),
'visit_no' => $params['visit_no'] ?? $prescription->visit_no,
'prescription_date' => $newDateYmd,
'tongue' => $params['tongue'] ?? $prescription->tongue,
'tongue_image' => $params['tongue_image'] ?? $prescription->tongue_image,
'pulse' => $params['pulse'] ?? $prescription->pulse,
'pulse_condition' => $params['pulse_condition'] ?? $prescription->pulse_condition,
'clinical_diagnosis' => $params['clinical_diagnosis'] ?? $prescription->clinical_diagnosis,
'herbs' => $herbs,
'dose_count' => (int)($params['dose_count'] ?? $prescription->dose_count),
'dose_unit' => $params['dose_unit'] ?? $prescription->dose_unit,
'usage_days' => (int)($params['usage_days'] ?? $prescription->usage_days),
'usage_instruction' => $params['usage_instruction'] ?? $prescription->usage_instruction,
'usage_time' => $params['usage_time'] ?? $prescription->usage_time,
'usage_way' => $params['usage_way'] ?? $prescription->usage_way,
'dietary_taboo' => is_array($params['dietary_taboo'] ?? null) ? implode(',', $params['dietary_taboo']) : ($params['dietary_taboo'] ?? $prescription->dietary_taboo),
'usage_notes' => $params['usage_notes'] ?? $prescription->usage_notes,
'doctor_name' => $params['doctor_name'] ?? $prescription->doctor_name,
'is_shared' => (int)($params['is_shared'] ?? $prescription->is_shared),
'visible_role_ids' => array_key_exists('visible_role_ids', $params)
? self::normalizeVisibleRoleIds($params['visible_role_ids'])
: (string) ($prescription->visible_role_ids ?? ''),
// 开方医生修改后重新进入待审核
'audit_status' => 0,
'audit_time' => null,
'audit_by' => null,
'audit_by_name' => '',
'audit_remark' => '',
];
if ($wasVoid) {
$data['void_status'] = 0;
$data['void_time'] = null;
$data['void_by'] = null;
$data['void_by_name'] = '';
}
$prescription->save($data);
PrescriptionOrderLogic::onConsumerPrescriptionSaved((int) $params['id']);
return true;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
/**
* 删除处方
*/
public static function delete(int $id): bool
{
try {
$prescription = Prescription::find($id);
if (!$prescription) {
self::setError('处方不存在');
return false;
}
if ((int) ($prescription->audit_status ?? 1) === 1 && (int) ($prescription->void_status ?? 0) === 0) {
self::setError('该处方已通过审核,不可删除');
return false;
}
$prescription->delete();
return true;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
/**
* 处方详情(带查看权限校验,供后台管理端)
*/
public static function detail(int $id, int $viewerAdminId, array $viewerAdminInfo): ?array
{
self::$error = '';
$row = Prescription::find($id);
if (!$row) {
return null;
}
if (!self::canViewPrescription($row, $viewerAdminId, $viewerAdminInfo)) {
self::setError('无权限查看此处方');
return null;
}
$arr = $row->toArray();
$arr['gender_desc'] = $arr['gender'] == 1 ? '男' : '女';
$arr['visible_role_ids'] = self::visibleRoleIdsToArray($arr['visible_role_ids'] ?? '');
$bizPo = PrescriptionOrder::where('prescription_id', $id)
->where('prescription_audit_status', 2)
->whereNull('delete_time')
->where('fulfillment_status', '<>', 4)
->order('id', 'desc')
->find();
$arr['business_prescription_audit_rejected'] = $bizPo ? 1 : 0;
$arr['business_prescription_audit_remark'] = $bizPo ? (string) ($bizPo->prescription_audit_remark ?? '') : '';
return $arr;
}
/**
* @return int[]
*/
public static function visibleRoleIdsToArray(string $csv): array
{
if ($csv === '') {
return [];
}
return array_values(array_unique(array_filter(array_map('intval', explode(',', $csv)))));
}
/**
* 审核:通过 / 驳回(驳回同时作废处方)
*/
public static function audit(int $id, string $action, string $remark, int $adminId, array $adminInfo): bool
{
self::$error = '';
self::$lastAuditWecomNotify = null;
if (!self::canAuditPrescription($adminId, $adminInfo)) {
self::setError('无审核权限');
return false;
}
$row = Prescription::find($id);
if (!$row) {
self::setError('处方不存在');
return false;
}
if (!self::canViewPrescription($row, $adminId, $adminInfo)) {
self::setError('无权限查看此处方,无法审核');
return false;
}
if ((int) ($row->audit_status ?? 1) !== 0) {
self::setError('当前状态不可审核');
return false;
}
$name = (string) ($adminInfo['name'] ?? '');
$now = time();
if ($action === 'approve') {
$row->audit_status = 1;
$row->audit_time = $now;
$row->audit_by = $adminId;
$row->audit_by_name = $name;
$row->audit_remark = $remark;
$ok = (bool) $row->save();
if ($ok) {
self::$lastAuditWecomNotify = self::notifyCreatorAuditResult($row, 'approve', $remark, $adminInfo);
}
return $ok;
}
if ($action === 'reject') {
if (trim($remark) === '') {
self::setError('驳回时请填写审核意见');
return false;
}
$row->audit_status = 2;
$row->audit_time = $now;
$row->audit_by = $adminId;
$row->audit_by_name = $name;
$row->audit_remark = $remark;
$row->void_status = 1;
$row->void_time = $now;
$row->void_by = $adminId;
$row->void_by_name = $name;
$ok = (bool) $row->save();
if ($ok) {
self::$lastAuditWecomNotify = self::notifyCreatorAuditResult($row, 'reject', $remark, $adminInfo);
}
return $ok;
}
self::setError('无效的审核操作');
return false;
}
/**
* 审核通过/驳回后,给企业微信中的开方人发一条应用消息(需绑定 work_wechat_userid
*
* @return array{ok: bool, message?: string, errcode?: int}
*/
private static function notifyCreatorAuditResult(Prescription $rx, string $action, string $remark, array $auditorInfo): array
{
try {
$creatorId = (int) ($rx->creator_id ?? 0);
if ($creatorId <= 0) {
return ['ok' => false, 'message' => '无法通知:处方无开方人信息'];
}
$creator = Admin::whereNull('delete_time')->find($creatorId);
if (!$creator) {
return ['ok' => false, 'message' => '无法通知:开方人账号不存在'];
}
$wxId = trim((string) ($creator->work_wechat_userid ?? ''));
if ($wxId === '') {
Log::warning("处方审核企业微信通知跳过: 开方人 admin_id={$creatorId} 未绑定 work_wechat_userid");
return [
'ok' => false,
'message' => '开方人未绑定企业微信账号,无法推送消息(请其在后台「扫码绑定企业微信」后再试)',
];
}
$sn = (string) ($rx->sn ?? '');
$patient = (string) ($rx->patient_name ?? '');
$auditorName = (string) ($auditorInfo['name'] ?? '');
if ($action === 'approve') {
$text = "【处方审核】您开具的处方已通过审核。\n编号:{$sn}\n患者:{$patient}\n审核人:{$auditorName}";
if (trim($remark) !== '') {
$text .= "\n备注:" . trim($remark);
}
} else {
$text = "【处方审核】您开具的处方已被驳回并已作废。\n编号:{$sn}\n患者:{$patient}\n审核人:{$auditorName}\n意见:" . trim($remark);
}
$result = WechatWorkAppMessageService::sendTextToUser($wxId, $text);
if (empty($result['ok'])) {
Log::warning('处方审核企业微信通知失败: ' . ($result['message'] ?? ''));
}
return $result;
} catch (\Throwable $e) {
Log::error('处方审核企业微信通知异常: ' . $e->getMessage());
return ['ok' => false, 'message' => '企业微信通知异常:' . $e->getMessage()];
}
}
/**
* 取出并清空最近一次审核后的企微通知结果(供接口返回给前端提示)
*
* @return array{ok: bool, message?: string, errcode?: int}|null
*/
public static function consumeLastAuditWecomNotify(): ?array
{
$v = self::$lastAuditWecomNotify;
self::$lastAuditWecomNotify = null;
return $v;
}
/**
* 根据诊单ID获取处方列表
*/
public static function listByDiagnosis(int $diagnosisId): array
{
return Prescription::where('diagnosis_id', $diagnosisId)
->whereNull('delete_time')
->order('id', 'desc')
->select()
->toArray();
}
/**
* 根据预约ID获取处方
*/
public static function getByAppointment(int $appointmentId): ?array
{
$row = Prescription::where('appointment_id', $appointmentId)
->whereNull('delete_time')
->order('id', 'desc')
->find();
return $row ? $row->toArray() : null;
}
/**
* 作废处方
*/
public static function void(int $id, int $adminId, string $adminName): bool
{
$row = Prescription::find($id);
if (!$row) {
self::setError('处方不存在');
return false;
}
if ((int)($row->void_status ?? 0) === 1) {
self::setError('该处方已作废');
return false;
}
$row->void_status = 1;
$row->void_time = time();
$row->void_by = $adminId;
$row->void_by_name = $adminName;
return $row->save();
}
}