1021 lines
38 KiB
PHP
1021 lines
38 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\adminapi\logic\tcm;
|
||
|
||
use app\common\model\auth\Admin;
|
||
use app\common\model\doctor\Appointment;
|
||
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 app\adminapi\logic\auth\AuthLogic;
|
||
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'] ?? []);
|
||
if (count(array_intersect($myRoles, $allowed)) > 0) {
|
||
return true;
|
||
}
|
||
|
||
// 有开方权限的医生可以查看所有处方(只读)
|
||
// 这样其他医生可以查看患者的历史处方记录
|
||
$permissions = $adminInfo['permissions'] ?? [];
|
||
if (in_array('tcm.diagnosis/kaifang', $permissions, true)) {
|
||
return true;
|
||
}
|
||
|
||
// 有业务订单管理权限的角色(如药师)可以查看处方(只读)
|
||
// 这样药师可以在业务订单中查看关联的处方信息
|
||
$orderEditAllRoles = Config::get('project.order_edit_all_roles', [0, 3]);
|
||
$myRoles = array_map('intval', $adminInfo['role_id'] ?? []);
|
||
$allowedRoles = array_map('intval', is_array($orderEditAllRoles) ? $orderEditAllRoles : []);
|
||
if (count(array_intersect($myRoles, $allowedRoles)) > 0) {
|
||
return true;
|
||
}
|
||
|
||
// 有业务订单详情权限的用户可以查看关联处方(只读)
|
||
$perms = AuthLogic::getAuthByAdminId($adminId);
|
||
if (in_array('tcm.prescriptionOrder/detail', $perms, true)) {
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
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');
|
||
}
|
||
|
||
private static function normalizeDosageBagCount($raw): int
|
||
{
|
||
$count = (int) $raw;
|
||
if ($count < 1) {
|
||
return 1;
|
||
}
|
||
if ($count > 5) {
|
||
return 5;
|
||
}
|
||
|
||
return $count;
|
||
}
|
||
|
||
/**
|
||
* 同一诊单、同一开方人、同一处方日期:仅允许一张「未作废」的未删除记录;已作废的可再新开一张。
|
||
*
|
||
* @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'] ?? '浓缩水丸',
|
||
'dosage_amount' => isset($params['dosage_amount']) ? (float)$params['dosage_amount'] : null,
|
||
'dosage_unit' => $params['dosage_unit'] ?? '',
|
||
'dosage_bag_count' => self::normalizeDosageBagCount($params['dosage_bag_count'] ?? 1),
|
||
'need_decoction' => (int)($params['need_decoction'] ?? 0),
|
||
'bags_per_dose' => isset($params['bags_per_dose']) ? (int)$params['bags_per_dose'] : 1,
|
||
'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),
|
||
'times_per_day' => (int)($params['times_per_day'] ?? 2),
|
||
'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) {
|
||
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,
|
||
'dosage_amount' => isset($params['dosage_amount']) ? (float)$params['dosage_amount'] : $prescription->dosage_amount,
|
||
'dosage_unit' => $params['dosage_unit'] ?? $prescription->dosage_unit,
|
||
'dosage_bag_count' => self::normalizeDosageBagCount($params['dosage_bag_count'] ?? ($prescription->dosage_bag_count ?? 1)),
|
||
'need_decoction' => isset($params['need_decoction']) ? (int)$params['need_decoction'] : (int)($prescription->need_decoction ?? 0),
|
||
'bags_per_dose' => isset($params['bags_per_dose']) ? (int)$params['bags_per_dose'] : (int)($prescription->bags_per_dose ?? 1),
|
||
'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),
|
||
'times_per_day' => (int)($params['times_per_day'] ?? $prescription->times_per_day ?? 2),
|
||
'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,
|
||
'doctor_signature' => $params['doctor_signature'] ?? $prescription->doctor_signature,
|
||
'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 ?? ''),
|
||
// 后台编辑保存后视为手工处方(原系统代开标记清除)
|
||
'is_system_auto' => 0,
|
||
// 开方医生修改后重新进入待审核
|
||
'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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 仅修正处方笺展示用患者姓名、手机号与性别(zyt_tcm_prescription),不改变审核状态与其它字段
|
||
*/
|
||
public static function patchPatientContact(int $rxId, string $patientName, string $phone, int $gender, int $adminId, array $adminInfo): bool
|
||
{
|
||
self::$error = '';
|
||
$prescription = Prescription::find($rxId);
|
||
if (!$prescription) {
|
||
self::setError('处方不存在');
|
||
|
||
return false;
|
||
}
|
||
if (!self::canViewPrescription($prescription, $adminId, $adminInfo)) {
|
||
self::setError('无权限修改此处方');
|
||
|
||
return false;
|
||
}
|
||
|
||
$patientName = trim($patientName);
|
||
$phone = trim($phone);
|
||
if ($patientName === '') {
|
||
self::setError('患者姓名不能为空');
|
||
|
||
return false;
|
||
}
|
||
if (mb_strlen($patientName) > 50) {
|
||
self::setError('患者姓名过长');
|
||
|
||
return false;
|
||
}
|
||
if ($phone === '') {
|
||
self::setError('手机号不能为空');
|
||
|
||
return false;
|
||
}
|
||
if (strlen($phone) > 20) {
|
||
self::setError('手机号过长');
|
||
|
||
return false;
|
||
}
|
||
if (!in_array($gender, [0, 1], true)) {
|
||
self::setError('性别无效');
|
||
|
||
return false;
|
||
}
|
||
|
||
$oldName = trim((string) ($prescription->patient_name ?? ''));
|
||
$oldPhone = trim((string) ($prescription->phone ?? ''));
|
||
$oldGender = (int) ($prescription->gender ?? 0);
|
||
|
||
if ($oldName === $patientName && $oldPhone === $phone && $oldGender === $gender) {
|
||
self::setError('信息未变更');
|
||
|
||
return false;
|
||
}
|
||
|
||
$genderText = static function (int $g): string {
|
||
if ($g === 1) {
|
||
return '男';
|
||
}
|
||
if ($g === 0) {
|
||
return '女';
|
||
}
|
||
|
||
return '未知';
|
||
};
|
||
|
||
try {
|
||
$prescription->save([
|
||
'patient_name' => $patientName,
|
||
'phone' => $phone,
|
||
'gender' => $gender,
|
||
]);
|
||
|
||
$latestPo = PrescriptionOrder::where('prescription_id', $rxId)
|
||
->whereNull('delete_time')
|
||
->order('id', 'desc')
|
||
->find();
|
||
$logOrderId = $latestPo ? (int) $latestPo->id : 0;
|
||
|
||
$nameFrom = $oldName === '' ? '—' : $oldName;
|
||
$phoneFrom = $oldPhone === '' ? '—' : $oldPhone;
|
||
$genderFrom = $genderText($oldGender);
|
||
$genderTo = $genderText($gender);
|
||
$summary = sprintf(
|
||
'处方 #%d:患者姓名「%s」→「%s」,手机「%s」→「%s」,性别「%s」→「%s」',
|
||
$rxId,
|
||
$nameFrom,
|
||
$patientName,
|
||
$phoneFrom,
|
||
$phone,
|
||
$genderFrom,
|
||
$genderTo
|
||
);
|
||
PrescriptionOrderLogic::appendRxPatientPatchLog($logOrderId, $adminId, $adminInfo, $summary);
|
||
|
||
return true;
|
||
} catch (\Throwable $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 ?? '') : '';
|
||
|
||
// 与 PrescriptionLists「业务订单」角标一致:未删除且非已取消(4) 即视为存在有效业务订单
|
||
$hasBizOrder = PrescriptionOrder::where('prescription_id', $id)
|
||
->whereNull('delete_time')
|
||
->where('fulfillment_status', '<>', 4)
|
||
->count() > 0;
|
||
$arr['has_prescription_order'] = $hasBizOrder ? 1 : 0;
|
||
|
||
/**
|
||
* 处方笺展示所需的"收件信息/订单号/合计"等字段,
|
||
* 来源是该处方关联的「业务处方订单」(zyt_tcm_prescription_order)。
|
||
* 取最近一条未删除且未取消的订单:
|
||
* - recipient_name / recipient_phone / shipping_address 用于「收件信息」一行
|
||
* - order_no 用于处方笺右上角「编号」
|
||
* - amount 作为「合计」金额回填(前端再做 0 兜底展示)
|
||
*/
|
||
$latestPo = PrescriptionOrder::where('prescription_id', $id)
|
||
->whereNull('delete_time')
|
||
->where('fulfillment_status', '<>', 4)
|
||
->order('id', 'desc')
|
||
->find();
|
||
if ($latestPo) {
|
||
$arr['recipient_name'] = (string) ($latestPo->recipient_name ?? '');
|
||
$arr['recipient_phone'] = (string) ($latestPo->recipient_phone ?? '');
|
||
$arr['shipping_address'] = (string) ($latestPo->shipping_address ?? '');
|
||
$arr['order_no'] = (string) ($latestPo->order_no ?? '');
|
||
$arr['order_amount'] = round((float) ($latestPo->amount ?? 0), 2);
|
||
}
|
||
|
||
return $arr;
|
||
}
|
||
|
||
/**
|
||
* 业务订单「处方审核」通过时:消费者处方仍为待审核则一并记为已通过,
|
||
* 避免消费者处方列表筛选「未通过」仍包含该条、与业务侧已通过不一致。
|
||
*/
|
||
public static function syncApproveConsumerPrescriptionIfPending(
|
||
int $prescriptionId,
|
||
int $adminId,
|
||
array $adminInfo,
|
||
string $remark
|
||
): void {
|
||
if ($prescriptionId <= 0) {
|
||
return;
|
||
}
|
||
$row = Prescription::find($prescriptionId);
|
||
if (!$row) {
|
||
return;
|
||
}
|
||
if ((int) ($row->void_status ?? 0) === 1) {
|
||
return;
|
||
}
|
||
if ((int) ($row->audit_status ?? 1) !== 0) {
|
||
return;
|
||
}
|
||
|
||
self::$lastAuditWecomNotify = null;
|
||
$name = (string) ($adminInfo['name'] ?? '');
|
||
$now = time();
|
||
$finalRemark = trim($remark) !== '' ? trim($remark) : '业务订单处方审核通过';
|
||
$finalRemark = mb_substr($finalRemark, 0, 500);
|
||
|
||
$row->audit_status = 1;
|
||
$row->audit_time = $now;
|
||
$row->audit_by = $adminId;
|
||
$row->audit_by_name = $name;
|
||
$row->audit_remark = $finalRemark;
|
||
|
||
try {
|
||
if ($row->save()) {
|
||
self::$lastAuditWecomNotify = self::notifyCreatorAuditResult($row, 'approve', $finalRemark, $adminInfo);
|
||
}
|
||
} catch (\Throwable $e) {
|
||
Log::error('syncApproveConsumerPrescriptionIfPending: ' . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @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, int $viewerAdminId, array $viewerAdminInfo): ?array
|
||
{
|
||
self::$error = '';
|
||
$row = Prescription::where('appointment_id', $appointmentId)
|
||
->where('creator_id',$viewerAdminId)
|
||
->whereNull('delete_time')
|
||
->order('id', 'desc')
|
||
->find();
|
||
|
||
if (!$row) {
|
||
return null;
|
||
}
|
||
|
||
// 权限检查:只返回当前用户有权限查看的处方
|
||
if (!self::canViewPrescription($row, $viewerAdminId, $viewerAdminInfo)) {
|
||
self::setError('无权限查看此处方');
|
||
return null;
|
||
}
|
||
|
||
return $row->toArray();
|
||
}
|
||
|
||
/**
|
||
* 挂号标记「完成」后自动生成处方:无药材配方,is_system_auto=1,其余字段与常规开方一致(患者/诊单来自预约关联诊单)。
|
||
* 若该预约已有未作废处方、或同日同诊单同医师已存在未作废处方,则跳过(不抛错)。
|
||
*
|
||
* @return int|null 新建处方 id;跳过返回 null
|
||
*/
|
||
public static function createSystemAutoFromCompletedAppointment(int $appointmentId): ?int
|
||
{
|
||
if ($appointmentId <= 0) {
|
||
return null;
|
||
}
|
||
|
||
$appointment = Appointment::find($appointmentId);
|
||
if (!$appointment) {
|
||
return null;
|
||
}
|
||
|
||
$diagnosisId = (int) ($appointment->patient_id ?? 0);
|
||
if ($diagnosisId <= 0) {
|
||
Log::info('createSystemAutoFromCompletedAppointment: no diagnosis (patient_id), skip appt=' . $appointmentId);
|
||
|
||
return null;
|
||
}
|
||
|
||
$doctorId = (int) ($appointment->doctor_id ?? 0);
|
||
if ($doctorId <= 0) {
|
||
return null;
|
||
}
|
||
|
||
$dupAppt = Prescription::where('appointment_id', $appointmentId)
|
||
->where('void_status', 0)
|
||
->whereNull('delete_time')
|
||
->find();
|
||
if ($dupAppt) {
|
||
return null;
|
||
}
|
||
|
||
$diagnosis = Diagnosis::find($diagnosisId);
|
||
if (!$diagnosis) {
|
||
self::setError('诊单不存在');
|
||
|
||
return null;
|
||
}
|
||
|
||
$apptDateRaw = $appointment->appointment_date ?? '';
|
||
$dateYmd = is_string($apptDateRaw) && preg_match('/^\d{4}-\d{2}-\d{2}/', trim($apptDateRaw))
|
||
? substr(trim($apptDateRaw), 0, 10)
|
||
: self::normalizePrescriptionDate(date('Y-m-d'));
|
||
|
||
if (!self::assertUniquePrescriptionPerDiagnosisDay($diagnosisId, $doctorId, $dateYmd, null)) {
|
||
Log::info("createSystemAutoFromCompletedAppointment: unique rule skip diagnosis={$diagnosisId} doctor={$doctorId} date={$dateYmd}");
|
||
|
||
return null;
|
||
}
|
||
|
||
$sn = self::generateSn();
|
||
while (Prescription::where('sn', $sn)->find()) {
|
||
$sn = self::generateSn();
|
||
}
|
||
|
||
$clinical = trim((string) ($diagnosis->symptoms ?? ''));
|
||
if ($clinical === '') {
|
||
$clinical = '就诊完成系统生成(暂无药材配方,请医师补充诊断与药材)';
|
||
}
|
||
|
||
$doctorName = (string) (Admin::where('id', $doctorId)->value('name') ?? '');
|
||
$assistantIdForRx = (int) ($diagnosis->assistant_id ?? 0);
|
||
|
||
$visitNo = '1K' . str_pad((string) $diagnosisId, 8, '0', STR_PAD_LEFT);
|
||
|
||
$herbs = [];
|
||
|
||
$data = [
|
||
'sn' => $sn,
|
||
'prescription_name' => '系统代开',
|
||
'prescription_type' => '浓缩水丸',
|
||
'dosage_amount' => 1.0,
|
||
'dosage_unit' => 'g',
|
||
'dosage_bag_count' => 1,
|
||
'need_decoction' => 0,
|
||
'bags_per_dose' => 1,
|
||
'diagnosis_id' => $diagnosisId,
|
||
'appointment_id' => $appointmentId,
|
||
'patient_id' => 0,
|
||
'patient_name' => (string) ($diagnosis->patient_name ?? ''),
|
||
'gender' => (int) ($diagnosis->gender ?? 0),
|
||
'age' => (int) ($diagnosis->age ?? 0),
|
||
'phone' => (string) ($diagnosis->phone ?? ''),
|
||
'visit_no' => $visitNo,
|
||
'prescription_date' => $dateYmd,
|
||
'pulse' => (string) ($diagnosis->pulse ?? ''),
|
||
'pulse_condition' => '',
|
||
'tongue' => (string) ($diagnosis->tongue ?? ''),
|
||
'tongue_image' => '',
|
||
'clinical_diagnosis' => $clinical,
|
||
'case_record' => [],
|
||
'herbs' => $herbs,
|
||
'dose_count' => 1,
|
||
'dose_unit' => '剂',
|
||
'usage_days' => 7,
|
||
'times_per_day' => 2,
|
||
'usage_instruction' => '',
|
||
'usage_time' => '饭后',
|
||
'usage_way' => '温水送服',
|
||
'dietary_taboo' => '',
|
||
'usage_notes' => '',
|
||
'amount' => 0.0,
|
||
'doctor_name' => $doctorName,
|
||
'doctor_signature' => '',
|
||
'template_id' => 0,
|
||
'is_shared' => 0,
|
||
'is_system_auto' => 1,
|
||
'visible_role_ids' => self::normalizeVisibleRoleIds([]),
|
||
'audit_status' => 0,
|
||
'audit_time' => null,
|
||
'audit_by' => null,
|
||
'audit_by_name' => '',
|
||
'audit_remark' => '',
|
||
'creator_id' => $doctorId,
|
||
'assistant_id' => $assistantIdForRx,
|
||
];
|
||
|
||
$prescription = new Prescription();
|
||
$prescription->save($data);
|
||
|
||
return (int) $prescription->id;
|
||
}
|
||
|
||
/**
|
||
* 作废处方
|
||
*/
|
||
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;
|
||
}
|
||
$rxId = (int) ($row->id ?? 0);
|
||
$bizCount = (int) PrescriptionOrder::where('prescription_id', $rxId)
|
||
->whereNull('delete_time')
|
||
->where('fulfillment_status', '<>', 4)
|
||
->count();
|
||
if ($bizCount > 0) {
|
||
self::setError('该处方已存在业务订单,无法作废');
|
||
|
||
return false;
|
||
}
|
||
$row->void_status = 1;
|
||
$row->void_time = time();
|
||
$row->void_by = $adminId;
|
||
$row->void_by_name = $adminName;
|
||
return $row->save();
|
||
}
|
||
}
|