Files
zyt/server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php
T
2026-04-23 15:56:01 +08:00

2125 lines
78 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\adminapi\logic\auth\AuthLogic;
use app\adminapi\logic\order\OrderLogic;
use app\common\model\Order;
use app\common\model\tcm\Diagnosis;
use app\common\model\tcm\Prescription;
use app\common\model\tcm\PrescriptionOrder;
use app\common\model\tcm\PrescriptionOrderLog;
use app\common\model\tcm\PrescriptionOrderPayOrder;
use app\common\model\auth\Admin;
use app\common\model\auth\AdminDept;
use app\common\model\dept\Dept;
use app\common\service\ExpressTrackService;
use app\common\service\gancao\GancaoScmRecipelService;
use think\facade\Config;
use think\facade\Log;
class PrescriptionOrderLogic
{
private static string $error = '';
public static function setError(string $msg): void
{
self::$error = $msg;
}
public static function getError(): string
{
return self::$error;
}
/**
* 部门自底向上链式名称,含父级(如:总部 / 华东 / 上海门诊)
*/
private static function buildDeptPath(int $deptId): string
{
if ($deptId <= 0) {
return '';
}
$names = [];
$id = $deptId;
for ($i = 0; $i < 50; $i++) {
$row = Dept::where('id', $id)->whereNull('delete_time')->field(['id', 'name', 'pid'])->find();
if ($row === null) {
break;
}
$data = $row->toArray();
array_unshift($names, (string) ($data['name'] ?? ''));
$pid = (int) ($data['pid'] ?? 0);
if ($pid <= 0) {
break;
}
$id = $pid;
}
if ($names === []) {
return '';
}
$names = array_filter($names, static fn (string $n): bool => $n !== '');
return implode(' / ', $names);
}
/**
* @param int[] $allowRoleIds
*/
private static function roleIntersect(array $adminInfo, array $allowRoleIds): bool
{
$myRoles = array_map('intval', $adminInfo['role_id'] ?? []);
$allow = array_map('intval', $allowRoleIds);
return count(array_intersect($myRoles, $allow)) > 0;
}
/** 与 order 列表一致:超管或 order_edit_all_roles 可见全部业务订单 */
public static function canSeeAllPrescriptionOrders(array $adminInfo): bool
{
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
return true;
}
$roles = Config::get('project.order_edit_all_roles', [0, 3]);
return self::roleIntersect($adminInfo, is_array($roles) ? $roles : []);
}
/**
* @param PrescriptionOrder $row
*/
public static function canAccessOrder($row, int $adminId, array $adminInfo): bool
{
if (self::canSeeAllPrescriptionOrders($adminInfo)) {
return true;
}
if ((int) $row->creator_id === $adminId) {
return true;
}
$aid = (int) Diagnosis::where('id', (int) $row->diagnosis_id)->whereNull('delete_time')->value('assistant_id');
return $aid === $adminId;
}
/**
* @param PrescriptionOrder $row
*/
public static function canWithdrawOrder($row, int $adminId, array $adminInfo): bool
{
return self::canAccessOrder($row, $adminId, $adminInfo);
}
public static function canViewInternalCost(array $adminInfo): bool
{
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
return true;
}
$allow = Config::get('project.prescription_order_finance_roles', [0, 3]);
$allow = array_map('intval', is_array($allow) ? $allow : []);
$myRoles = array_map('intval', $adminInfo['role_id'] ?? []);
return count(array_intersect($myRoles, $allow)) > 0;
}
/**
* 业务订单详情内是否返回/展示处方「药材明细」(菜单权限 tcm.prescriptionOrder/viewRxHerbs
*/
private static function canViewPrescriptionHerbsInOrderDetail(array $adminInfo): bool
{
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
return true;
}
$perms = AuthLogic::getAuthByAdminId((int) ($adminInfo['admin_id'] ?? 0));
return in_array('tcm.prescriptionOrder/viewRxHerbs', $perms, true);
}
public static function canAuditPrescriptionOrder(array $adminInfo): bool
{
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
return true;
}
$allow = Config::get('project.prescription_audit_roles', [0, 3, 6]);
return self::roleIntersect($adminInfo, is_array($allow) ? $allow : []);
}
public static function canAuditPaymentSlipOrder(array $adminInfo): bool
{
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
return true;
}
$allow = Config::get('project.prescription_order_payment_audit_roles', [0, 3]);
return self::roleIntersect($adminInfo, is_array($allow) ? $allow : []);
}
/**
* 列表顶栏金额统计:与「关联支付单」审核白名单同档的角色可见「全量」统计(不缩小到本人/医助诊单域)
*/
public static function canViewOrderListStatsAllScope(array $adminInfo): bool
{
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
return true;
}
$allow = Config::get('project.prescription_order_payment_audit_roles', [0, 3, 4, 9, 6]);
if (!is_array($allow)) {
$allow = [0, 3];
}
return self::roleIntersect($adminInfo, $allow);
}
/**
* @param array<string,mixed> $row
*/
public static function maskInternalCostIfNeeded(array &$row, array $adminInfo): void
{
if (!self::canViewInternalCost($adminInfo)) {
unset($row['internal_cost']);
}
}
/** 定金门槛(元),0 表示不校验 */
public static function depositMinAmount(): float
{
return round((float) Config::get('prescription_order.link_pay_min_amount', 0), 2);
}
/**
* @param int[] $payOrderIds
*/
public static function assertDepositAndLinkPayRules(float $orderAmount, array $payOrderIds): ?string
{
$min = self::depositMinAmount();
if ($min <= 0) {
return null;
}
if ($orderAmount < $min) {
return '订单金额须大于等于定金门槛 ¥' . $min . '(当前 ¥' . $orderAmount . '';
}
if ($payOrderIds === []) {
return '未关联支付单,关联支付单总金额须大于等于定金门槛 ¥' . $min;
}
// 检查关联支付单的总金额
$rows = Order::whereIn('id', $payOrderIds)->whereNull('delete_time')->column('amount', 'id');
$totalAmount = 0.0;
foreach ($payOrderIds as $pid) {
$pid = (int) $pid;
if ($pid <= 0) {
continue;
}
$amt = round((float) ($rows[$pid] ?? 0), 2);
$totalAmount += $amt;
}
if ($totalAmount < $min) {
return '关联支付单总金额须大于等于定金门槛 ¥' . $min . '(当前总金额 ¥' . $totalAmount . '';
}
return null;
}
public static function generateOrderNo(): string
{
return 'PO' . date('YmdHis') . mt_rand(100000, 999999);
}
private static function normalizeExpressCompany($raw): string
{
$ec = strtolower(trim((string) ($raw ?? '')));
if ($ec === '') {
return 'auto';
}
if (!in_array($ec, ['sf', 'jd', 'jt', 'jtexpress', 'auto'], true)) {
return 'auto';
}
// 统一 jtexpress 为 jt
if ($ec === 'jtexpress') {
return 'jt';
}
return $ec;
}
/** 业务订单「处方审核」驳回后,允许医生继续编辑已通过的消费者处方 */
public static function allowDoctorEditApprovedPrescriptionDueToBusinessReject(int $prescriptionId): bool
{
return PrescriptionOrder::where('prescription_id', $prescriptionId)
->where('prescription_audit_status', 2)
->whereNull('delete_time')
->where('fulfillment_status', '<>', 4)
->count() > 0;
}
/**
* 医生保存消费者处方后:重置关联业务订单的处方/支付单审核为待审(因处方内容已变)
*/
public static function onConsumerPrescriptionSaved(int $prescriptionId): void
{
$rows = PrescriptionOrder::where('prescription_id', $prescriptionId)
->where('prescription_audit_status', 2)
->whereNull('delete_time')
->where('fulfillment_status', '<>', 4)
->select();
foreach ($rows as $order) {
$order->prescription_audit_status = 0;
$order->prescription_audit_remark = '';
$order->payment_slip_audit_status = 0;
$order->payment_slip_audit_remark = '';
self::syncFulfillmentStatus($order);
$order->save();
}
}
private static function syncFulfillmentStatus(PrescriptionOrder $order): void
{
$fs = (int) $order->fulfillment_status;
if ($fs === 3 || $fs === 4) {
return;
}
$pa = (int) $order->prescription_audit_status;
$pay = (int) $order->payment_slip_audit_status;
if ($pa === 1 && $pay === 1) {
$order->fulfillment_status = 2;
} elseif ($pa === 2 || $pay === 2) {
$order->fulfillment_status = 1;
}
}
/**
* @param mixed $raw
* @return int[]
*/
private static function normalizePayOrderIds($raw): array
{
if (!is_array($raw)) {
return [];
}
$ids = array_map('intval', $raw);
return array_values(array_unique(array_filter($ids, static fn (int $v): bool => $v > 0)));
}
/**
* 解除业务订单与支付单的关联(撤回/驳回后支付单可再次被选用)
*/
private static function clearPayOrderLinks(PrescriptionOrder $order): void
{
PrescriptionOrderPayOrder::where('prescription_order_id', (int) $order->id)->delete();
$order->linked_pay_order_id = null;
}
/**
* 支付单不可同时关联多条「有效」业务订单(未删除且未撤回/取消);$exceptPrescriptionOrderId 为当前编辑单 ID 时跳过自检
*
* @param int[] $payOrderIds
*/
private static function assertPayOrdersExclusive(array $payOrderIds, ?int $exceptPrescriptionOrderId): ?string
{
$ids = array_values(array_unique(array_filter(array_map('intval', $payOrderIds), static fn (int $v): bool => $v > 0)));
if ($ids === []) {
return null;
}
$activePoIds = PrescriptionOrder::whereNull('delete_time')
->where('fulfillment_status', '<>', 4)
->column('id');
if ($activePoIds === []) {
return null;
}
$activeSet = array_fill_keys(array_map('intval', $activePoIds), true);
$links = PrescriptionOrderPayOrder::whereIn('pay_order_id', $ids)->select();
foreach ($links as $link) {
$poId = (int) $link->prescription_order_id;
if ($exceptPrescriptionOrderId !== null && $poId === $exceptPrescriptionOrderId) {
continue;
}
if (!isset($activeSet[$poId])) {
continue;
}
return '支付单 #' . (int) $link->pay_order_id . ' 已关联其他有效业务订单,不可重复关联';
}
return null;
}
/**
* @param int[] $payOrderIds
*/
private static function replacePayOrderLinks(int $prescriptionOrderId, array $payOrderIds): void
{
PrescriptionOrderPayOrder::where('prescription_order_id', $prescriptionOrderId)->delete();
$t = time();
foreach ($payOrderIds as $pid) {
$link = new PrescriptionOrderPayOrder();
$link->prescription_order_id = $prescriptionOrderId;
$link->pay_order_id = $pid;
$link->create_time = $t;
$link->save();
}
}
/**
* @return int[]
*/
private static function linkedPayOrderIdList(int $prescriptionOrderId): array
{
return array_map(
'intval',
PrescriptionOrderPayOrder::where('prescription_order_id', $prescriptionOrderId)->order('id', 'asc')->column('pay_order_id')
);
}
/**
* @return array<int, array<string, mixed>>
*/
private static function linkedPayOrdersPayload(array $ids): array
{
if ($ids === []) {
return [];
}
$rows = Order::whereIn('id', $ids)->whereNull('delete_time')
->field(['id', 'order_no', 'order_type', 'amount', 'status', 'create_time', 'creator_id', 'remark'])
->order('id', 'asc')
->select()
->toArray();
$creatorIds = array_values(array_unique(array_filter(array_map('intval', array_column($rows, 'creator_id')))));
$creatorNames = [];
if ($creatorIds !== []) {
$creatorNames = \app\common\model\auth\Admin::whereIn('id', $creatorIds)->column('name', 'id');
}
$typeMap = [
1 => '挂号费',
2 => '问诊费',
3 => '药品费用',
4 => '首付费用',
5 => '尾款费用',
6 => '其他费用',
7 => '全部费用',
];
$statusMap = [
1 => '待支付',
2 => '已支付',
3 => '已取消',
4 => '已退款',
5 => '待审核',
];
foreach ($rows as &$r) {
$ot = (int) ($r['order_type'] ?? 0);
$st = (int) ($r['status'] ?? 0);
$r['order_type_desc'] = $typeMap[$ot] ?? '—';
$r['status_desc'] = $statusMap[$st] ?? '—';
$cid = (int) ($r['creator_id'] ?? 0);
$r['creator_name'] = $cid > 0 ? (string) ($creatorNames[$cid] ?? '') : '';
}
unset($r);
return $rows;
}
/**
* @param array<string,mixed> $arr
*/
private static function attachLinkedPayOrders(array &$arr): void
{
$id = (int) ($arr['id'] ?? 0);
if ($id <= 0) {
$arr['pay_order_ids'] = [];
$arr['linked_pay_orders'] = [];
$arr['linked_pay_paid_total'] = 0.0;
$arr['deposit_min_amount'] = self::depositMinAmount();
return;
}
$ids = self::linkedPayOrderIdList($id);
$arr['pay_order_ids'] = $ids;
$arr['linked_pay_orders'] = self::linkedPayOrdersPayload($ids);
$sum = 0.0;
foreach ($arr['linked_pay_orders'] as $o) {
$sum += (float) ($o['amount'] ?? 0);
}
$arr['linked_pay_paid_total'] = round($sum, 2);
$arr['deposit_min_amount'] = self::depositMinAmount();
}
/**
* @param array<string,mixed> $params
* @return array<string,mixed>|false
*/
public static function create(array $params, int $adminId, array $adminInfo)
{
self::$error = '';
$rxId = (int) $params['prescription_id'];
$diagId = (int) $params['diagnosis_id'];
$payOrderIds = self::normalizePayOrderIds($params['pay_order_ids'] ?? []);
$rx = PrescriptionLogic::detail($rxId, $adminId, $adminInfo);
if ($rx === null) {
$err = PrescriptionLogic::getError();
self::$error = $err !== '' ? $err : '处方不存在';
return false;
}
if ((int) ($rx['diagnosis_id'] ?? 0) !== $diagId) {
self::$error = '诊单与处方不匹配';
return false;
}
if ((int) ($rx['void_status'] ?? 0) === 1) {
self::$error = '已作废处方不可创建业务订单';
return false;
}
if (PrescriptionOrder::where('prescription_id', $rxId)->whereNull('delete_time')->where('fulfillment_status', '<>', 4)->count() > 0) {
self::$error = '该处方已存在有效业务订单(未撤回前不可重复创建)';
return false;
}
$diag = Diagnosis::where('id', $diagId)->whereNull('delete_time')->find();
if (!$diag) {
self::$error = '诊单不存在';
return false;
}
if ($payOrderIds !== []) {
$linkErr = OrderLogic::assertPayOrdersLinkable($payOrderIds, $diagId, $adminId, $adminInfo);
if ($linkErr !== null) {
self::$error = $linkErr;
return false;
}
$exErr = self::assertPayOrdersExclusive($payOrderIds, null);
if ($exErr !== null) {
self::$error = $exErr;
return false;
}
}
$orderAmtPreview = round((float) ($params['amount'] ?? 0), 2);
$depErrCreate = self::assertDepositAndLinkPayRules($orderAmtPreview, $payOrderIds);
if ($depErrCreate !== null) {
self::$error = $depErrCreate;
return false;
}
$internalCost = null;
if (isset($params['internal_cost']) && $params['internal_cost'] !== '' && $params['internal_cost'] !== null) {
if (self::canViewInternalCost($adminInfo)) {
$internalCost = round((float) $params['internal_cost'], 2);
}
}
$medDays = $params['medication_days'] ?? null;
$medDays = $medDays === '' || $medDays === null ? null : (int) $medDays;
$order = new PrescriptionOrder();
$order->order_no = self::generateOrderNo();
$order->prescription_id = $rxId;
$order->diagnosis_id = $diagId;
$order->creator_id = $adminId;
$order->recipient_name = (string) $params['recipient_name'];
$order->recipient_phone = (string) $params['recipient_phone'];
// 处理省市区字段
if (isset($params['shipping_province'])) {
$order->shipping_province = (string) $params['shipping_province'];
}
if (isset($params['shipping_city'])) {
$order->shipping_city = (string) $params['shipping_city'];
}
if (isset($params['shipping_district'])) {
$order->shipping_district = (string) $params['shipping_district'];
}
$order->shipping_address = (string) $params['shipping_address'];
$order->is_follow_up = (int) ($params['is_follow_up'] ?? 0) === 1 ? 1 : 0;
$order->medication_days = $medDays > 0 ? $medDays : null;
$order->prev_staff = (string) ($params['prev_staff'] ?? '');
$order->service_channel = (string) ($params['service_channel'] ?? '');
$order->service_package = (string) ($params['service_package'] ?? '');
$order->tracking_number = (string) ($params['tracking_number'] ?? '');
$order->express_company = self::normalizeExpressCompany($params['express_company'] ?? 'auto');
$order->fee_type = (int) $params['fee_type'];
$order->amount = round((float) $params['amount'], 2);
$order->internal_cost = $internalCost;
$order->remark_extra = (string) ($params['remark_extra'] ?? '');
$order->linked_pay_order_id = $payOrderIds !== [] ? $payOrderIds[0] : null;
$order->prescription_audit_status = 0;
$order->prescription_audit_remark = '';
$order->payment_slip_audit_status = 0;
$order->payment_slip_audit_remark = '';
$order->fulfillment_status = 1;
try {
$order->save();
} catch (\Throwable $e) {
self::$error = $e->getMessage();
return false;
}
self::writeLog((int) $order->id, $adminId, $adminInfo, 'create', '创建业务订单');
if ($payOrderIds !== []) {
self::replacePayOrderLinks((int) $order->id, $payOrderIds);
}
$out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
public static function detail(int $id, int $adminId, array $adminInfo): ?array
{
self::$error = '';
$row = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
if (!$row) {
self::$error = '订单不存在';
return null;
}
if (!self::canAccessOrder($row, $adminId, $adminInfo)) {
self::$error = '无权限查看';
return null;
}
$arr = $row->toArray();
self::maskInternalCostIfNeeded($arr, $adminInfo);
self::attachLinkedPayOrders($arr);
// 添加创建人姓名
$creatorId = (int) ($arr['creator_id'] ?? 0);
if ($creatorId > 0) {
$creatorName = Admin::where('id', $creatorId)->value('name');
$arr['creator_name'] = $creatorName ? (string) $creatorName : '';
} else {
$arr['creator_name'] = '';
}
$rxId = (int) ($arr['prescription_id'] ?? 0);
$arr['prescription'] = null;
$arr['prescription_detail_error'] = '';
$arr['doctor_name'] = '';
if ($rxId > 0) {
$rxDetail = PrescriptionLogic::detail($rxId, $adminId, $adminInfo);
if ($rxDetail !== null) {
$arr['prescription'] = $rxDetail;
// 添加开方人姓名(优先使用处方的 doctor_name,否则从 creator_id 获取)
$doctorName = (string) ($rxDetail['doctor_name'] ?? '');
if ($doctorName === '') {
$rxCreatorId = (int) ($rxDetail['creator_id'] ?? 0);
if ($rxCreatorId > 0) {
$doctorName = (string) (Admin::where('id', $rxCreatorId)->value('name') ?? '');
}
}
$arr['doctor_name'] = $doctorName;
} else {
$err = PrescriptionLogic::getError();
$arr['prescription_detail_error'] = $err !== '' ? $err : '处方不存在';
}
}
$canHerbs = self::canViewPrescriptionHerbsInOrderDetail($adminInfo);
$arr['prescription_detail_herbs_visible'] = $canHerbs;
if (! $canHerbs && isset($arr['prescription']) && is_array($arr['prescription'])) {
unset($arr['prescription']['herbs']);
}
// 诊单医助 / 诊单创建人 及部门(多部门、含父级路径用「;」与「 / 」分隔);创建人为 tcm_diagnosis.admin_id
$arr['assistant_id'] = 0;
$arr['assistant_name'] = '';
$arr['assistant_dept_path'] = '';
$arr['diagnosis_creator_id'] = 0;
$arr['diagnosis_creator_name'] = '';
$arr['diagnosis_creator_dept_path'] = '';
$diagIdForMeta = (int) ($arr['diagnosis_id'] ?? 0);
if ($diagIdForMeta > 0) {
// 勿在 field 中写 admin_id:未执行建表补充列时会报 Unknown column
$dg = Diagnosis::where('id', $diagIdForMeta)->whereNull('delete_time')->find();
if ($dg !== null) {
$drow = $dg->toArray();
$astId = (int) ($drow['assistant_id'] ?? 0);
$arr['assistant_id'] = $astId;
if ($astId > 0) {
$an = Admin::where('id', $astId)->whereNull('delete_time')->value('name');
$arr['assistant_name'] = $an !== null && (string) $an !== '' ? (string) $an : '';
$deptIds = AdminDept::where('admin_id', $astId)->column('dept_id');
$pathSegments = [];
foreach ($deptIds as $did) {
$p = self::buildDeptPath((int) $did);
if ($p !== '') {
$pathSegments[] = $p;
}
}
$pathSegments = array_values(array_unique($pathSegments));
$arr['assistant_dept_path'] = $pathSegments === [] ? '' : implode('', $pathSegments);
}
$creId = (int) ($drow['admin_id'] ?? 0);
$arr['diagnosis_creator_id'] = $creId;
if ($creId > 0) {
$cn = Admin::where('id', $creId)->whereNull('delete_time')->value('name');
$arr['diagnosis_creator_name'] = $cn !== null && (string) $cn !== '' ? (string) $cn : '';
$deptIdsC = AdminDept::where('admin_id', $creId)->column('dept_id');
$pathSegmentsC = [];
foreach ($deptIdsC as $didC) {
$pc = self::buildDeptPath((int) $didC);
if ($pc !== '') {
$pathSegmentsC[] = $pc;
}
}
$pathSegmentsC = array_values(array_unique($pathSegmentsC));
$arr['diagnosis_creator_dept_path'] = $pathSegmentsC === [] ? '' : implode('', $pathSegmentsC);
}
}
}
return $arr;
}
/**
* 物流轨迹(顺丰/京东等,优先快递100;未配置密钥时返回官网链接)
*
* @return array<string,mixed>|null
*/
public static function logisticsTrace(int $id, string $expressCompanyOverride, int $adminId, array $adminInfo): ?array
{
self::$error = '';
$row = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
if (!$row) {
self::$error = '订单不存在';
return null;
}
if (!self::canAccessOrder($row, $adminId, $adminInfo)) {
self::$error = '无权限查看';
return null;
}
$num = trim((string) ($row->tracking_number ?? ''));
if ($num === '') {
self::$error = '未填写快递单号';
return null;
}
$ec = trim($expressCompanyOverride) !== '' ? self::normalizeExpressCompany($expressCompanyOverride) : self::normalizeExpressCompany($row->express_company ?? 'auto');
// 优先从数据库查询物流追踪信息
$trackingData = \app\common\service\ExpressTrackingService::getDetailByTrackingNumber($num);
if ($trackingData && !empty($trackingData['traces'])) {
// 从数据库获取到数据,直接返回
$payload = [
'carrier' => $trackingData['express_company'],
'carrier_label' => $trackingData['express_company_name'],
'kuaidi_com' => $trackingData['express_company'],
'traces' => array_map(function($trace) {
return [
'time' => $trace['trace_time'],
'ftime' => $trace['trace_time'],
'context' => $trace['trace_context'],
'location' => $trace['location'],
'status' => $trace['status'],
'statusCode' => $trace['status_code'],
];
}, $trackingData['traces']),
'state' => $trackingData['current_state'],
'state_text' => $trackingData['current_state_text'],
'source' => 'database',
'hint' => '',
'official_url' => '',
'last_query_time' => date('Y-m-d H:i:s', $trackingData['last_query_time']),
'query_count' => $trackingData['query_count'],
];
} else {
// 数据库没有数据,调用快递100 API
$payload = ExpressTrackService::query($ec, $num, (string) ($row->recipient_phone ?? ''));
$payload['source'] = 'api';
}
$payload['official_urls'] = ExpressTrackService::officialUrls($num);
$payload['tracking_number'] = $num;
$payload['express_company_used'] = $ec;
return $payload;
}
/**
* @param array<string,mixed> $params
* @return array<string,mixed>|false
*/
public static function edit(array $params, int $adminId, array $adminInfo)
{
self::$error = '';
$id = (int) $params['id'];
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
if (!$order) {
self::$error = '订单不存在';
return false;
}
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
self::$error = '无权限编辑';
return false;
}
$fs = (int) $order->fulfillment_status;
if ($fs === 3 || $fs === 4) {
self::$error = '已完成或已取消的订单不可编辑';
return false;
}
// 已成功提交甘草 SCM(生成了甘草处方单号 或 submit_time>0),再改本地信息会与甘草侧不一致,禁止编辑
$gcOrderNo = trim((string) $order->gancao_reciperl_order_no);
$gcSubmitTime = (int) $order->gancao_submit_time;
if ($gcOrderNo !== '' || $gcSubmitTime > 0) {
self::$error = sprintf(
'订单已提交甘草(处方单号:%s),不可再编辑;如需修改请先走取消流程',
$gcOrderNo !== '' ? $gcOrderNo : '—'
);
return false;
}
$medDays = $params['medication_days'] ?? null;
$medDays = $medDays === '' || $medDays === null ? null : (int) $medDays;
$order->recipient_name = (string) $params['recipient_name'];
$order->recipient_phone = (string) $params['recipient_phone'];
// 处理省市区字段
if (isset($params['shipping_province'])) {
$order->shipping_province = (string) $params['shipping_province'];
}
if (isset($params['shipping_city'])) {
$order->shipping_city = (string) $params['shipping_city'];
}
if (isset($params['shipping_district'])) {
$order->shipping_district = (string) $params['shipping_district'];
}
$order->shipping_address = (string) $params['shipping_address'];
$order->is_follow_up = (int) ($params['is_follow_up'] ?? 0) === 1 ? 1 : 0;
$order->medication_days = $medDays > 0 ? $medDays : null;
$order->dose_unit = (string) ($params['dose_unit'] ?? '剂');
$order->dose_count = isset($params['dose_count']) && (int)$params['dose_count'] > 0 ? (int)$params['dose_count'] : 1;
$order->prev_staff = (string) ($params['prev_staff'] ?? '');
$order->service_channel = (string) ($params['service_channel'] ?? '');
$order->service_package = (string) ($params['service_package'] ?? '');
$order->tracking_number = (string) ($params['tracking_number'] ?? '');
if (array_key_exists('express_company', $params)) {
$order->express_company = self::normalizeExpressCompany($params['express_company']);
}
$order->fee_type = (int) $params['fee_type'];
$order->amount = round((float) $params['amount'], 2);
$order->remark_extra = (string) ($params['remark_extra'] ?? '');
$diagId = (int) $order->diagnosis_id;
if (array_key_exists('pay_order_ids', $params)) {
$payOrderIds = self::normalizePayOrderIds($params['pay_order_ids']);
// 过滤掉已删除的支付单ID(拒绝审核时可能软删除了待审核支付单)
if (!empty($payOrderIds)) {
$existingIds = Order::whereIn('id', $payOrderIds)
->whereNull('delete_time')
->column('id');
$payOrderIds = array_values(array_intersect($payOrderIds, $existingIds));
}
$linkErr = OrderLogic::assertPayOrdersLinkable($payOrderIds, $diagId, $adminId, $adminInfo, (int) $order->id);
if ($linkErr !== null) {
self::$error = $linkErr;
return false;
}
$exErr = self::assertPayOrdersExclusive($payOrderIds, (int) $order->id);
if ($exErr !== null) {
self::$error = $exErr;
return false;
}
self::replacePayOrderLinks((int) $order->id, $payOrderIds);
$order->linked_pay_order_id = $payOrderIds !== [] ? $payOrderIds[0] : null;
} elseif (array_key_exists('linked_pay_order_id', $params)) {
$lp = $params['linked_pay_order_id'];
if ($lp === '' || $lp === null || (int) $lp <= 0) {
self::replacePayOrderLinks((int) $order->id, []);
$order->linked_pay_order_id = null;
} else {
$single = [(int) $lp];
$linkErr = OrderLogic::assertPayOrdersLinkable($single, $diagId, $adminId, $adminInfo, (int) $order->id);
if ($linkErr !== null) {
self::$error = $linkErr;
return false;
}
$exErr = self::assertPayOrdersExclusive($single, (int) $order->id);
if ($exErr !== null) {
self::$error = $exErr;
return false;
}
self::replacePayOrderLinks((int) $order->id, $single);
$order->linked_pay_order_id = $single[0];
}
}
if (array_key_exists('internal_cost', $params) && self::canViewInternalCost($adminInfo)) {
$raw = $params['internal_cost'];
if ($raw === '' || $raw === null) {
$order->internal_cost = null;
} else {
$order->internal_cost = round((float) $raw, 2);
}
}
$finalPayIds = self::linkedPayOrderIdList((int) $order->id);
$depErr = self::assertDepositAndLinkPayRules(round((float) $order->amount, 2), $finalPayIds);
if ($depErr !== null) {
self::$error = $depErr;
return false;
}
// 编辑后重置支付审核状态为待审核(需要重新审核)
// 但如果当前状态是"已发货"(5),则不重置审核状态
if ((int) $order->fulfillment_status !== 5) {
$order->payment_slip_audit_status = 0;
$order->payment_slip_audit_remark = '';
}
self::syncFulfillmentStatus($order);
try {
$order->save();
} catch (\Throwable $e) {
self::$error = $e->getMessage();
return false;
}
self::writeLog((int) $order->id, $adminId, $adminInfo, 'edit', '编辑了业务订单及相关信息');
$out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
/**
* 确认发货:更新快递信息并将 fulfillment_status 从 2(履约中)推进到 5(已发货)
*
* @return array<string,mixed>|false
*/
public static function ship(int $id, string $expressCompany, string $trackingNumber, int $adminId, array $adminInfo)
{
self::$error = '';
$trackingNumber = trim($trackingNumber);
if ($trackingNumber === '') {
self::$error = '快递单号不能为空';
return false;
}
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
if (!$order) {
self::$error = '订单不存在';
return false;
}
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
self::$error = '无权限操作';
return false;
}
if ((int) $order->fulfillment_status !== 2 && (int) $order->fulfillment_status !== 5) {
self::$error = '仅「履约中」或「已发货」状态的订单可更新快递信息';
return false;
}
$order->express_company = self::normalizeExpressCompany($expressCompany);
$order->tracking_number = $trackingNumber;
// 仅履约中(2)时才推进到已发货(5),已发货(5)则只更新快递信息保持状态
$isFirstShip = false;
if ((int) $order->fulfillment_status === 2) {
$order->fulfillment_status = 5;
$isFirstShip = true;
}
try {
$order->save();
} catch (\Throwable $e) {
self::$error = $e->getMessage();
return false;
}
if ($isFirstShip) {
self::writeLog((int) $order->id, $adminId, $adminInfo, 'ship', '确认发货,运单:' . $trackingNumber . '' . $expressCompany . '');
} else {
self::writeLog((int) $order->id, $adminId, $adminInfo, 'fill_tracking', '更新发货信息,运单:' . $trackingNumber . '' . $expressCompany . '');
}
$out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
/**
* @return array<string,mixed>|false
*/
public static function withdraw(int $id, int $adminId, array $adminInfo)
{
self::$error = '';
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
if (!$order) {
self::$error = '订单不存在';
return false;
}
if (!self::canWithdrawOrder($order, $adminId, $adminInfo)) {
self::$error = '无权限撤回';
return false;
}
if ((int) $order->fulfillment_status !== 1) {
self::$error = '仅「待双审通过」状态可撤回;双审通过后请走履约/完成流程';
return false;
}
$order->fulfillment_status = 4;
self::clearPayOrderLinks($order);
try {
$order->save();
// 撤回订单后,将关联的处方审核状态改回"待审核"
$prescriptionId = (int) $order->prescription_id;
if ($prescriptionId > 0) {
Prescription::where('id', $prescriptionId)
->whereNull('delete_time')
->update([
'audit_status' => 0, // 0=待审核
'audit_time' => null,
'audit_by' => null,
'audit_by_name' => '',
'audit_remark' => '',
'update_time' => time(),
]);
}
} catch (\Throwable $e) {
self::$error = $e->getMessage();
return false;
}
self::writeLog((int) $order->id, $adminId, $adminInfo, 'withdraw', '撤回订单,状态变更为「已取消」');
$out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
/**
* @return array<string,mixed>|false
*/
public static function auditPrescription(int $id, string $action, string $remark, int $adminId, array $adminInfo)
{
self::$error = '';
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
if (!$order) {
self::$error = '订单不存在';
return false;
}
if (!self::canAuditPrescriptionOrder($adminInfo)) {
self::$error = '无处方审核权限';
return false;
}
if ((int) $order->prescription_audit_status !== 0) {
self::$error = '当前无需处方审核';
return false;
}
$fs = (int) $order->fulfillment_status;
if ($fs === 3 || $fs === 4) {
self::$error = '订单已结束';
return false;
}
if ($action === 'reject' && trim($remark) === '') {
self::$error = '驳回时请填写审核意见';
return false;
}
if ($action === 'approve') {
$order->prescription_audit_status = 1;
$order->prescription_audit_remark = mb_substr(trim($remark), 0, 500);
} else {
$order->prescription_audit_status = 2;
$order->prescription_audit_remark = mb_substr(trim($remark), 0, 500);
$order->payment_slip_audit_status = 0;
$order->payment_slip_audit_remark = '';
}
self::syncFulfillmentStatus($order);
if ($action === 'reject') {
self::clearPayOrderLinks($order);
}
try {
$order->save();
} catch (\Throwable $e) {
self::$error = $e->getMessage();
return false;
}
if ($action === 'approve') {
$syncRemark = trim($remark) !== '' ? trim($remark) : '业务订单处方审核通过';
PrescriptionLogic::syncApproveConsumerPrescriptionIfPending(
(int) $order->prescription_id,
$adminId,
$adminInfo,
$syncRemark
);
}
$actionName = $action === 'approve' ? 'audit_rx_approve' : 'audit_rx_reject';
$summary = $action === 'approve' ? '处方审核通过' . ($remark ? ',意见:' . $remark : '') : '处方审核驳回,意见:' . $remark;
self::writeLog((int) $order->id, $adminId, $adminInfo, $actionName, $summary);
$out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
/**
* @return array<string,mixed>|false
*/
public static function auditPaymentSlip(int $id, string $action, string $remark, int $adminId, array $adminInfo)
{
self::$error = '';
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
if (!$order) {
self::$error = '订单不存在';
return false;
}
if (!self::canAuditPaymentSlipOrder($adminInfo)) {
self::$error = '无支付单审核权限';
return false;
}
if ((int) $order->prescription_audit_status !== 1) {
self::$error = '请先通过处方审核';
return false;
}
if ((int) $order->payment_slip_audit_status !== 0) {
self::$error = '当前无需支付单审核';
return false;
}
$fs = (int) $order->fulfillment_status;
if ($fs === 3 || $fs === 4) {
self::$error = '订单已结束';
return false;
}
// 已发货状态(5)允许重新发起支付单审核(新增支付单场景)
if ($action === 'reject' && trim($remark) === '') {
self::$error = '驳回时请填写审核意见';
return false;
}
if ($action === 'approve') {
$order->payment_slip_audit_status = 1;
$order->payment_slip_audit_remark = mb_substr(trim($remark), 0, 500);
// 将关联的待审核支付单(status=5)更新为已支付(status=2
$payOrderIds = self::linkedPayOrderIdList($id);
if (!empty($payOrderIds)) {
Order::whereIn('id', $payOrderIds)
->where('status', 5) // 只更新待审核状态的
->update([
'status' => 2, // 已支付
'payment_time' => date('Y-m-d H:i:s')
]);
}
// 如果有完单申请,自动完成订单
if ((int) $order->completion_request === 1 && (int) $order->fulfillment_status === 5) {
$order->fulfillment_status = 3; // 已完成
$order->completion_request = 0; // 清除完单申请标记
self::writeLog((int) $order->id, $adminId, $adminInfo, 'auto_complete',
'支付审核通过,根据完单申请自动完成订单');
}
} else {
$order->payment_slip_audit_status = 2;
$order->payment_slip_audit_remark = mb_substr(trim($remark), 0, 500);
// 拒绝审核时,软删除待审核状态的关联支付单(status=5)
$payOrderIds = self::linkedPayOrderIdList($id);
if (!empty($payOrderIds)) {
Order::whereIn('id', $payOrderIds)
->where('status', 5) // 只删除待审核状态的
->update([
'delete_time' => date('Y-m-d H:i:s')
]);
}
// 拒绝审核时,清除完单申请
if ((int) $order->completion_request === 1) {
$order->completion_request = 0;
$order->completion_request_time = 0;
$order->completion_request_by = 0;
$order->completion_request_by_name = '';
}
}
// 状态同步和关联清除逻辑:
// - 如果是"已发货"状态(5),无论同意还是拒绝,都保持状态不变
// - 如果不是"已发货"状态,同步状态;拒绝时清除所有支付单关联关系
$currentStatus = (int) $order->fulfillment_status;
if ($currentStatus === 5) {
// 已发货状态:保持已发货状态不变
// 不调用 syncFulfillmentStatus
// 拒绝时也不清除关联(只删除了待审核支付单)
} else {
self::syncFulfillmentStatus($order);
if ($action === 'reject') {
self::clearPayOrderLinks($order);
}
}
try {
$order->save();
} catch (\Throwable $e) {
self::$error = $e->getMessage();
return false;
}
$actionName = $action === 'approve' ? 'audit_pay_approve' : 'audit_pay_reject';
$summary = $action === 'approve' ? '支付单审核通过' . ($remark ? ',意见:' . $remark : '') : '支付单审核驳回,意见:' . $remark;
self::writeLog((int) $order->id, $adminId, $adminInfo, $actionName, $summary);
$out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
/**
* 撤回处方审核:将已通过/已驳回的处方审核重置为待审核
*
* @return array<string,mixed>|false
*/
public static function revokeRxAudit(int $id, int $adminId, array $adminInfo)
{
self::$error = '';
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
if (!$order) {
self::$error = '订单不存在';
return false;
}
if (!self::canAuditPrescriptionOrder($adminInfo)) {
self::$error = '无处方审核权限';
return false;
}
$rxStatus = (int) $order->prescription_audit_status;
if ($rxStatus === 0) {
self::$error = '当前已是待审核状态,无需撤回';
return false;
}
$fs = (int) $order->fulfillment_status;
if ($fs === 3 || $fs === 4) {
self::$error = '订单已结束,不可撤回审核';
return false;
}
// 如果处方审核已通过,且支付审核也已通过或已驳回,不允许撤回处方审核
if ($rxStatus === 1 && (int) $order->payment_slip_audit_status !== 0) {
self::$error = '支付单审核已进行,请先撤回支付单审核';
return false;
}
$order->prescription_audit_status = 0;
$order->prescription_audit_remark = '';
// 如果支付审核不是待审核,也一并重置
if ((int) $order->payment_slip_audit_status !== 0) {
$order->payment_slip_audit_status = 0;
$order->payment_slip_audit_remark = '';
}
self::syncFulfillmentStatus($order);
try {
$order->save();
} catch (\Throwable $e) {
self::$error = $e->getMessage();
return false;
}
// 同步撤回处方表的审核状态
$prescriptionId = (int) $order->prescription_id;
if ($prescriptionId > 0) {
try {
$prescription = Prescription::where('id', $prescriptionId)->whereNull('delete_time')->find();
if ($prescription) {
$prescription->audit_status = 0;
$prescription->audit_remark = '';
$prescription->audit_time = null;
$prescription->audit_by = null;
$prescription->audit_by_name = '';
$prescription->save();
}
} catch (\Throwable $e) {
// 记录日志但不影响主流程
Log::error('撤回处方审核时同步处方表失败: ' . $e->getMessage(), [
'prescription_id' => $prescriptionId,
'order_id' => $id
]);
}
}
self::writeLog((int) $order->id, $adminId, $adminInfo, 'revoke_rx_audit', '撤回处方审核,状态重置为待审核');
$out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
/**
* 撤回支付单审核:将已通过/已驳回的支付单审核重置为待审核
*
* @return array<string,mixed>|false
*/
public static function revokePayAudit(int $id, int $adminId, array $adminInfo)
{
self::$error = '';
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
if (!$order) {
self::$error = '订单不存在';
return false;
}
if (!self::canAuditPaymentSlipOrder($adminInfo)) {
self::$error = '无支付单审核权限';
return false;
}
if ((int) $order->prescription_audit_status !== 1) {
self::$error = '处方审核未通过,无法撤回支付单审核';
return false;
}
$payStatus = (int) $order->payment_slip_audit_status;
if ($payStatus === 0) {
self::$error = '当前已是待审核状态,无需撤回';
return false;
}
$fs = (int) $order->fulfillment_status;
if ($fs === 3 || $fs === 4) {
self::$error = '订单已结束,不可撤回审核';
return false;
}
$order->payment_slip_audit_status = 0;
$order->payment_slip_audit_remark = '';
self::syncFulfillmentStatus($order);
// 将关联的已支付支付单(status=2,且payment_method='manual')恢复为待审核(status=5
$payOrderIds = self::linkedPayOrderIdList($id);
if (!empty($payOrderIds)) {
Order::whereIn('id', $payOrderIds)
->where('status', 2) // 只恢复已支付状态的
->where('payment_method', 'manual') // 只恢复手动创建的
->update([
'status' => 5, // 待审核
'payment_time' => null
]);
}
try {
$order->save();
} catch (\Throwable $e) {
self::$error = $e->getMessage();
return false;
}
self::writeLog((int) $order->id, $adminId, $adminInfo, 'revoke_pay_audit', '撤回支付单审核,状态重置为待审核');
$out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
/**
* @return array<int, array<string, mixed>>
*/
public static function getLogs(int $orderId, int $adminId, array $adminInfo): array
{
self::$error = '';
$order = PrescriptionOrder::where('id', $orderId)->whereNull('delete_time')->find();
if (!$order) {
self::$error = '订单不存在';
return [];
}
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
self::$error = '无权限查看';
return [];
}
return PrescriptionOrderLog::where('prescription_order_id', $orderId)
->order('id', 'desc')
->select()
->toArray();
}
/**
* 为「已发货」(fulfillment_status=5) 的业务订单新增一条关联支付单(zyt_order),
* 创建后将支付单链接到业务订单,并将处方/支付审核状态重置为待审核以启动再次审核流程。
*
* @param array<string,mixed> $params id, order_type, amount, remark
* @return array<string,mixed>|false
*/
public static function addPayOrder(array $params, int $adminId, array $adminInfo)
{
self::$error = '';
$id = (int) $params['id'];
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
if (!$order) {
self::$error = '订单不存在';
return false;
}
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
self::$error = '无权限操作';
return false;
}
if ((int) $order->fulfillment_status !== 5) {
self::$error = '仅「已发货」状态的订单可新增支付单';
return false;
}
$orderType = (int) ($params['order_type'] ?? 3);
$amount = round((float) ($params['pay_amount'] ?? 0), 2);
$remark = mb_substr(trim((string) ($params['pay_remark'] ?? '')), 0, 200);
$completionRequest = (int) ($params['completion_request'] ?? 0);
if ($amount <= 0) {
self::$error = '支付单金额须大于 0';
return false;
}
// 创建 zyt_order 并标记为待审核(需要审核后才算已支付)
$payOrder = new Order();
$payOrder->order_no = OrderLogic::generateOrderNo();
$payOrder->patient_id = (int) $order->diagnosis_id;
$payOrder->creator_id = $adminId;
$payOrder->order_type = $orderType;
$payOrder->amount = $amount;
$payOrder->status = 5; // 待审核
$payOrder->payment_method = 'manual';
$payOrder->payment_time = null; // 审核通过后再设置支付时间
$payOrder->remark = $remark;
try {
$payOrder->save();
} catch (\Throwable $e) {
self::$error = $e->getMessage();
return false;
}
// 将新支付单追加到业务订单关联列表
$existingIds = self::linkedPayOrderIdList($id);
$newIds = array_unique(array_merge($existingIds, [(int) $payOrder->id]));
self::replacePayOrderLinks($id, $newIds);
$order->linked_pay_order_id = $newIds[0];
// 重置支付单审核为待审核,以启动新一轮审核(处方审核保持通过状态不变)
$order->payment_slip_audit_status = 0;
$order->payment_slip_audit_remark = '';
// 处理完单申请
if ($completionRequest === 1) {
$order->completion_request = 1;
$order->completion_request_time = time();
$order->completion_request_by = $adminId;
$order->completion_request_by_name = (string) ($adminInfo['name'] ?? '');
}
try {
$order->save();
} catch (\Throwable $e) {
self::$error = $e->getMessage();
return false;
}
$logMsg = '新增支付单 #' . $payOrder->id . '(¥' . $amount . '),类型:' . $orderType . ',等待支付审核';
if ($completionRequest === 1) {
$logMsg .= ',并申请完成订单';
}
self::writeLog($id, $adminId, $adminInfo, 'add_pay_order', $logMsg);
$out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
/**
* 为「已发货」(fulfillment_status=5) 的业务订单关联已有支付单,
* 关联后将支付审核状态重置为待审核以启动再次审核流程。
*
* @param array<string,mixed> $params id, pay_order_id
* @return array<string,mixed>|false
*/
public static function linkPayOrder(array $params, int $adminId, array $adminInfo)
{
self::$error = '';
$id = (int) $params['id'];
$completionRequest = (int) ($params['completion_request'] ?? 0);
// 支持单个或多个支付单ID
$payOrderIds = [];
if (isset($params['pay_order_ids']) && is_array($params['pay_order_ids'])) {
// 多个支付单(数组)
$payOrderIds = array_values(array_unique(array_filter(array_map('intval', $params['pay_order_ids']))));
} elseif (isset($params['pay_order_id'])) {
// 单个支付单(兼容旧接口)
$payOrderIds = [(int) $params['pay_order_id']];
}
if (empty($payOrderIds)) {
self::$error = '请选择要关联的支付单';
return false;
}
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
if (!$order) {
self::$error = '订单不存在';
return false;
}
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
self::$error = '无权限操作';
return false;
}
if ((int) $order->fulfillment_status !== 5) {
self::$error = '仅「已发货」状态的订单可关联支付单';
return false;
}
$diagId = (int) $order->diagnosis_id;
// 验证所有支付单是否可关联
$linkErr = OrderLogic::assertPayOrdersLinkable($payOrderIds, $diagId, $adminId, $adminInfo);
if ($linkErr !== null) {
self::$error = $linkErr;
return false;
}
// 验证所有支付单是否已被其他业务订单占用
$exErr = self::assertPayOrdersExclusive($payOrderIds, $id);
if ($exErr !== null) {
self::$error = $exErr;
return false;
}
// 将支付单追加到业务订单关联列表
$existingIds = self::linkedPayOrderIdList($id);
$newIds = array_unique(array_merge($existingIds, $payOrderIds));
self::replacePayOrderLinks($id, $newIds);
$order->linked_pay_order_id = $newIds[0];
// 重置支付单审核为待审核,以启动新一轮审核(处方审核保持通过状态不变)
$order->payment_slip_audit_status = 0;
$order->payment_slip_audit_remark = '';
// 处理完单申请
if ($completionRequest === 1) {
$order->completion_request = 1;
$order->completion_request_time = time();
$order->completion_request_by = $adminId;
$order->completion_request_by_name = (string) ($adminInfo['name'] ?? '');
}
try {
$order->save();
} catch (\Throwable $e) {
self::$error = $e->getMessage();
return false;
}
// 计算总金额
$totalAmount = 0;
$payOrders = Order::whereIn('id', $payOrderIds)->select();
foreach ($payOrders as $po) {
$totalAmount += round((float) $po->amount, 2);
}
$logMsg = '关联支付单 ' . count($payOrderIds) . ' 笔(共¥' . $totalAmount . '),等待支付审核';
if ($completionRequest === 1) {
$logMsg .= ',并申请完成订单';
}
self::writeLog($id, $adminId, $adminInfo, 'link_pay_order', $logMsg);
$out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
/** 完成订单时可选的履约终态:3=已完成,7-12=业务结案类状态 */
private const COMPLETE_FULFILLMENT_TARGETS = [3, 7, 8, 9, 10, 11, 12];
/** 完成时是否按「实付=关联支付单金额」重算并清空诊单医助(与 clearDiagnosisAssistantOnComplete 的终态判断一致) */
private const COMPLETE_FULFILLMENT_TERMINAL = [3, 8, 9, 10, 11, 12];
/**
* 将「已发货/已签收」且支付审核已通过的订单结案:可选「已完成」或 7-12 业务状态。
*
* @return array<string,mixed>|false
*/
public static function complete(int $id, int $adminId, array $adminInfo, int $targetFulfillmentStatus = 3)
{
self::$error = '';
if (!in_array($targetFulfillmentStatus, self::COMPLETE_FULFILLMENT_TARGETS, true)) {
self::$error = '无效的完成状态';
return false;
}
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
if (!$order) {
self::$error = '订单不存在';
return false;
}
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
self::$error = '无权限操作';
return false;
}
$curFs = (int) $order->fulfillment_status;
if ($curFs !== 5 && $curFs !== 6) {
self::$error = '仅「已发货」或「已签收」状态可完成订单';
return false;
}
if ((int) $order->payment_slip_audit_status !== 1) {
self::$error = '支付单审核尚未通过,不可完成订单';
return false;
}
$label = self::fulfillmentStatusLabel($targetFulfillmentStatus);
$linkedPayOrderIds = self::linkedPayOrderIdList($id);
$linkedPayOrders = self::linkedPayOrdersPayload($linkedPayOrderIds);
$linkedPayPaidTotal = 0.0;
foreach ($linkedPayOrders as $o) {
$linkedPayPaidTotal += (float) ($o['amount'] ?? 0);
}
$linkedPayPaidTotal = round($linkedPayPaidTotal, 2);
if ($targetFulfillmentStatus === 3) {
$order->fulfillment_status = 3;
$order->paid = $linkedPayPaidTotal;
} else {
$order->fulfillment_status = $targetFulfillmentStatus;
}
try {
$order->save();
} catch (\Throwable $e) {
self::$error = $e->getMessage();
return false;
}
$unassignSummary = '';
if (in_array($targetFulfillmentStatus, self::COMPLETE_FULFILLMENT_TERMINAL, true)) {
$unassignSummary = self::clearDiagnosisAssistantOnComplete((int) $order->diagnosis_id);
}
if ($targetFulfillmentStatus === 3) {
$logLine = '订单完成,状态变更为「' . $label . '」,实付金额更新为 ¥' . $linkedPayPaidTotal . $unassignSummary;
} else {
$logLine = '订单处理完成,状态变更为「' . $label . '」' . $unassignSummary;
}
self::writeLog(
$id,
$adminId,
$adminInfo,
'complete',
$logLine
);
$out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
private static function fulfillmentStatusLabel(int $fs): string
{
$m = [
1 => '待双审通过',
2 => '待发货',
3 => '已完成',
4 => '已取消',
5 => '已发货',
6 => '已签收',
7 => '进行中',
8 => '暂不制药',
9 => '拒收',
10 => '退款',
11 => '保留药方',
12 => '制药缓发',
];
return $m[$fs] ?? ('状态' . (string) $fs);
}
/**
* 完成订单时把关联诊单的 assistant_id 清空,让患者回到「待分配」状态
* 仅当该患者没有其它进行中的处方订单时才清空,避免误覆盖
*
* @return string 追加到操作日志的描述(无变更则返回空串)
*/
private static function clearDiagnosisAssistantOnComplete(int $diagnosisId): string
{
if ($diagnosisId <= 0) {
return '';
}
try {
$diag = Diagnosis::where('id', $diagnosisId)->whereNull('delete_time')->find();
if (!$diag) {
return '';
}
$oldAssistant = (string) ($diag['assistant_id'] ?? '');
if ($oldAssistant === '' || $oldAssistant === '0') {
return '';
}
// 同一诊单下若仍有未完成/未取消的处方订单,则不清空
$pendingCount = PrescriptionOrder::where('diagnosis_id', $diagnosisId)
->whereNull('delete_time')
->whereNotIn('fulfillment_status', [3, 4, 8, 9, 10, 11, 12])
->count();
if ($pendingCount > 0) {
return '';
}
Diagnosis::where('id', $diagnosisId)
->whereNull('delete_time')
->update(['assistant_id' => '']);
return ';并已清空诊单医助分配(原 assistant_id=' . $oldAssistant . '),患者进入待分配状态';
} catch (\Throwable $e) {
Log::warning('clearDiagnosisAssistantOnComplete failed: ' . $e->getMessage(), [
'diagnosis_id' => $diagnosisId,
]);
return '';
}
}
/**
* 列表行是否展示「上传甘草药方」(需配置甘草、处方审核通过、未上传过、有权限;不要求支付审核)。
*
* @param array<string,mixed> $item
* @param array<int, int|string> $assistantByDiag diagnosis_id => assistant_id
*/
public static function canUploadGancaoRecipel(array $item, int $adminId, array $adminInfo, array $assistantByDiag = []): bool
{
if (!GancaoScmRecipelService::isConfigured()) {
return false;
}
$pa = (int) ($item['prescription_audit_status'] ?? 0);
$fs = (int) ($item['fulfillment_status'] ?? 0);
if ($pa !== 1) {
return false;
}
if (in_array($fs, [3, 4, 8, 9, 10, 11, 12], true)) {
return false;
}
if (trim((string) ($item['gancao_reciperl_order_no'] ?? '')) !== '') {
return false;
}
if (self::canSeeAllPrescriptionOrders($adminInfo)) {
return true;
}
if ((int) ($item['creator_id'] ?? 0) === $adminId) {
return true;
}
$did = (int) ($item['diagnosis_id'] ?? 0);
$aid = (int) ($assistantByDiag[$did] ?? 0);
return $aid === $adminId && $adminId > 0;
}
/**
* 将当前业务订单关联处方提交至甘草药管家(CTM_PREVIEW → CTM_SUBMIT_RECIPEL)。
*
* @return array<string,mixed>|false 成功返回甘草 result 主要字段
*/
public static function submitGancaoRecipel(int $id, int $adminId, array $adminInfo)
{
self::$error = '';
if (!GancaoScmRecipelService::isConfigured()) {
$why = GancaoScmRecipelService::whyNotConfigured();
self::$error = $why !== '' ? $why : '甘草处方上传未启用或未配置完整,请检查 .env(GANCAO_SCM_*)须写在任意 [分区] 之前,或使用 [GANCAO_SCM] 分区';
return false;
}
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
if (!$order) {
self::$error = '订单不存在';
return false;
}
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
self::$error = '无权限操作';
return false;
}
$item = $order->toArray();
$assistantByDiag = Diagnosis::where('id', (int) $order->diagnosis_id)->whereNull('delete_time')->column('assistant_id', 'id');
if (!self::canUploadGancaoRecipel($item, $adminId, $adminInfo, $assistantByDiag)) {
self::$error = '须处方审核通过后方可上传甘草;已完成/已取消订单不可上传;同一订单仅可上传一次';
return false;
}
$rxId = (int) $order->prescription_id;
if ($rxId <= 0) {
self::$error = '订单未关联处方';
return false;
}
$rx = PrescriptionLogic::detail($rxId, $adminId, $adminInfo);
if ($rx === null) {
self::$error = PrescriptionLogic::getError() !== '' ? PrescriptionLogic::getError() : '处方不存在';
return false;
}
$c = Config::get('gancao_scm', []);
$nameMap = is_array($c['herb_id_map'] ?? null) ? $c['herb_id_map'] : [];
$herbs = is_array($rx['herbs'] ?? null) ? $rx['herbs'] : [];
$docGidMap = GancaoScmRecipelService::doctorMedicineGidMapForHerbs($herbs);
[, $missing] = GancaoScmRecipelService::buildMList($herbs, $nameMap, $docGidMap);
if ($missing !== []) {
self::$error = '以下药材无法匹配甘草药ID(请核对医师药品库 zyt_doctor_medicine 中同名字段的 gid,或在处方 herbs 中写 gc_id / config 的 herb_id_map):' . implode('、', $missing);
return false;
}
$token = GancaoScmRecipelService::getToken(false);
if ($token === null || $token === '') {
$why = GancaoScmRecipelService::getLastGetTokenError();
self::$error = $why !== '' ? $why : '获取甘草开放平台 token 失败,请检查 GANCAO_SCM_GATEWAY_*(网关 OpenAPI)与 GANCAO_SCM_BIZ_*(业务账号,常与网关不同)、网关地址及服务器时间';
return false;
}
$orderArr = $order->toArray();
// dump($rx);
// dump($orderArr);
$previewPayload = GancaoScmRecipelService::buildPreviewPayload($rx, $orderArr, $token);
$prevRet = GancaoScmRecipelService::ctmPreview($previewPayload);
if ((int) ($prevRet['state'] ?? 0) !== 1) {
self::$error = '甘草预检查通信失败:' . (string) ($prevRet['msg'] ?? '');
return false;
}
$prevBody = $prevRet['body'] ?? [];
if (!GancaoScmRecipelService::isApiSuccess($prevBody)) {
self::$error = '甘草预检查未通过:' . GancaoScmRecipelService::apiStatusMessage($prevBody);
return false;
}
$result = $prevBody['result'] ?? [];
if (is_array($result)) {
$rules = $result['rule_check'] ?? [];
if (is_array($rules)) {
foreach ($rules as $ru) {
if (!is_array($ru)) {
continue;
}
$t = (int) ($ru['type'] ?? 0);
if ($t >= 2) {
self::$error = '甘草预检查规则拦截:' . (string) ($ru['msg'] ?? '') . 'code:' . (string) ($ru['code'] ?? '') . '';
return false;
}
}
}
$mListOut = $result['m_list'] ?? [];
if (is_array($mListOut)) {
foreach ($mListOut as $rowM) {
if (is_array($rowM) && isset($rowM['is_available']) && (int) $rowM['is_available'] === 0) {
$tn = (string) ($rowM['title'] ?? '');
self::$error = '甘草药库缺药,请调整处方:' . ($tn !== '' ? $tn : '未知药材');
return false;
}
}
}
}
$addrParts = GancaoScmRecipelService::splitCnAddress((string) $order->shipping_address);
$appNo = (string) $order->order_no;
$submitPayload = GancaoScmRecipelService::buildSubmitPayload($rx, $orderArr, $token, $addrParts, $appNo);
$subRet = GancaoScmRecipelService::ctmSubmit($submitPayload);
if ((int) ($subRet['state'] ?? 0) !== 1) {
$errMsg = (string) ($subRet['msg'] ?? '');
$response = isset($subRet['response']) ? mb_substr((string) $subRet['response'], 0, 500) : '';
self::$error = '甘草下单通信失败:' . $errMsg . ($response !== '' ? ';响应:' . $response : '');
Log::error('Gancao CTM_SUBMIT failed', [
'subRet' => $subRet,
'payload' => json_encode(GancaoScmRecipelService::maskSensitiveData($submitPayload), JSON_UNESCAPED_UNICODE)
]);
return false;
}
$subBody = $subRet['body'] ?? [];
if (!GancaoScmRecipelService::isApiSuccess($subBody)) {
self::$error = '甘草下单失败:' . GancaoScmRecipelService::apiStatusMessage($subBody);
Log::error('Gancao CTM_SUBMIT api error', [
'body' => $subBody,
'payload' => json_encode(GancaoScmRecipelService::maskSensitiveData($submitPayload), JSON_UNESCAPED_UNICODE)
]);
return false;
}
$gcNo = (string) (($subBody['result'] ?? [])['recipel_order_no'] ?? '');
if ($gcNo === '') {
self::$error = '甘草返回缺少 recipel_order_no';
return false;
}
$order->gancao_reciperl_order_no = mb_substr($gcNo, 0, 32);
$order->gancao_submit_time = time();
try {
$order->save();
} catch (\Throwable $e) {
self::$error = '本地保存甘草单号失败:' . $e->getMessage();
return false;
}
self::writeLog($id, $adminId, $adminInfo, 'gancao_submit', '上传甘草药方成功 ' . $gcNo);
$fee = $subBody['result']['fee'] ?? [];
$appNo = $submitPayload['app_order_no'] ?? ('PO' . (string) $order->id);
return [
'recipel_order_no' => $gcNo,
'app_order_no' => $appNo,
'fee' => is_array($fee) ? $fee : [],
];
}
/**
* 甘草预下单测试(仅 CTM_PREVIEW,不提交订单)
* 用于在编辑订单时测试价格和配置
*
* @param array $params 包含 id 和可选的 dose_count
* @return array<string,mixed>|false 成功返回预览结果(包含价格信息)
*/
public static function previewGancaoRecipel(int $id, int $adminId, array $adminInfo, array $params = [])
{
self::$error = '';
if (!GancaoScmRecipelService::isConfigured()) {
$why = GancaoScmRecipelService::whyNotConfigured();
self::$error = $why !== '' ? $why : '甘草处方上传未启用或未配置完整';
return false;
}
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
if (!$order) {
self::$error = '订单不存在';
return false;
}
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
self::$error = '无权限操作';
return false;
}
$rxId = (int) $order->prescription_id;
if ($rxId <= 0) {
self::$error = '订单未关联处方';
return false;
}
$rx = PrescriptionLogic::detail($rxId, $adminId, $adminInfo);
if ($rx === null) {
self::$error = PrescriptionLogic::getError() !== '' ? PrescriptionLogic::getError() : '处方不存在';
return false;
}
$c = Config::get('gancao_scm', []);
$nameMap = is_array($c['herb_id_map'] ?? null) ? $c['herb_id_map'] : [];
$herbs = is_array($rx['herbs'] ?? null) ? $rx['herbs'] : [];
$docGidMap = GancaoScmRecipelService::doctorMedicineGidMapForHerbs($herbs);
[, $missing] = GancaoScmRecipelService::buildMList($herbs, $nameMap, $docGidMap);
if ($missing !== []) {
self::$error = '以下药材无法匹配甘草药ID:' . implode('、', $missing);
return false;
}
$token = GancaoScmRecipelService::getToken(false);
if ($token === null || $token === '') {
$why = GancaoScmRecipelService::getLastGetTokenError();
self::$error = $why !== '' ? $why : '获取甘草开放平台 token 失败';
return false;
}
$orderArr = $order->toArray();
// 如果传递了 dose_count 参数,使用传递的值覆盖订单中的值
if (isset($params['dose_count']) && (int)$params['dose_count'] > 0) {
$orderArr['dose_count'] = (int)$params['dose_count'];
}
// 如果传递了 medication_days 参数,使用传递的值覆盖订单中的值
if (isset($params['medication_days']) && (int)$params['medication_days'] > 0) {
$orderArr['medication_days'] = (int)$params['medication_days'];
}
$previewPayload = GancaoScmRecipelService::buildPreviewPayload($rx, $orderArr, $token);
$prevRet = GancaoScmRecipelService::ctmPreview($previewPayload);
if ((int) ($prevRet['state'] ?? 0) !== 1) {
self::$error = '甘草预检查通信失败:' . (string) ($prevRet['msg'] ?? '');
return false;
}
$prevBody = $prevRet['body'] ?? [];
if (!GancaoScmRecipelService::isApiSuccess($prevBody)) {
self::$error = '甘草预检查未通过:' . GancaoScmRecipelService::apiStatusMessage($prevBody);
return false;
}
$result = $prevBody['result'] ?? [];
// 检查规则拦截
if (is_array($result)) {
$rules = $result['rule_check'] ?? [];
if (is_array($rules)) {
foreach ($rules as $ru) {
if (!is_array($ru)) continue;
$t = (int) ($ru['type'] ?? 0);
if ($t >= 2) {
self::$error = '甘草预检查规则拦截:' . (string) ($ru['msg'] ?? '');
return false;
}
}
}
// 检查药材可用性
$mListOut = $result['m_list'] ?? [];
if (is_array($mListOut)) {
foreach ($mListOut as $rowM) {
if (is_array($rowM) && isset($rowM['is_available']) && (int) $rowM['is_available'] === 0) {
$tn = (string) ($rowM['title'] ?? '');
self::$error = '甘草药库缺药:' . ($tn !== '' ? $tn : '未知药材');
return false;
}
}
}
}
// 返回预览结果
$fee = $result['fee'] ?? [];
return [
'success' => true,
'fee' => is_array($fee) ? $fee : [],
'result' => $result,
'order_no' => (string) $order->order_no,
'dose_count' => (int) ($orderArr['dose_count'] ?? $rx['dose_count'] ?? 0),
];
}
private static function writeLog(int $orderId, int $adminId, array $adminInfo, string $action, string $summary): void
{
$adminName = $adminInfo['name'] ?? '';
if ($adminName === '' && $adminId > 0) {
$adminName = (string) \app\common\model\auth\Admin::where('id', $adminId)->value('name');
}
$log = new PrescriptionOrderLog();
$log->prescription_order_id = $orderId;
$log->admin_id = $adminId;
$log->admin_name = mb_substr($adminName, 0, 64);
$log->action = mb_substr($action, 0, 32);
$log->summary = mb_substr($summary, 0, 500);
$log->create_time = time();
try {
$log->save();
} catch (\Throwable $e) {
// 忽略日志写入错误
}
}
}