Files
zyt/server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php
T
2026-06-02 16:47:50 +08:00

3829 lines
142 KiB
PHP
Executable File
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\stats\YejiStatsLogic;
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\DiagnosisAssignLog;
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\dict\DictData;
use app\common\model\doctor\Appointment;
use app\common\model\auth\Admin;
use app\common\service\DataScope\DataScopeService;
use app\common\service\ExpressTrackService;
use app\common\service\ExpressTrackingService;
use app\common\service\gancao\GancaoScmRecipelService;
use think\db\Query;
use think\facade\Config;
use think\facade\Db;
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;
}
/**
* 部门自底向上链式名称,含父级(如:总部 / 华东 / 上海门诊)
* 使用 Db 直查 zyt_dept:避免 Dept 软删除全局作用域导致有 dept_id 仍拼不出路径
*/
private static function buildDeptPath(int $deptId): string
{
if ($deptId <= 0) {
return '';
}
$names = [];
$id = $deptId;
for ($i = 0; $i < 50; $i++) {
$row = Db::name('dept')->where('id', $id)->field(['id', 'name', 'pid'])->find();
if (empty($row)) {
break;
}
$r = is_array($row) ? $row : $row->toArray();
array_unshift($names, (string) ($r['name'] ?? ''));
$pid = (int) ($r['pid'] ?? 0);
if ($pid <= 0) {
break;
}
$id = $pid;
}
if ($names === []) {
return '';
}
$names = array_filter($names, static fn (string $n): bool => $n !== '');
return implode(' / ', $names);
}
/**
* 管理员在 zyt_admin_dept 中的部门路径(多部门用「;」、含父级「 / 」)
*/
private static function buildAdminDeptPathJoined(int $adminId): string
{
if ($adminId <= 0) {
return '';
}
$deptIds = Db::name('admin_dept')->where('admin_id', $adminId)->column('dept_id');
$pathSegmentsC = [];
foreach ($deptIds as $didC) {
$pc = self::buildDeptPath((int) $didC);
if ($pc !== '') {
$pathSegmentsC[] = $pc;
}
}
$pathSegmentsC = array_values(array_unique($pathSegmentsC));
return $pathSegmentsC === [] ? '' : implode('', $pathSegmentsC);
}
/**
* @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 : []);
}
/**
* 「下单」角色:列表「审核人」筛选仅允许查本人审核过的订单(经理/管理员等豁免)
*/
public static function isOrderPlacerAuditFilterRestricted(array $adminInfo): bool
{
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
return false;
}
$placer = Config::get('project.prescription_order_placer_role_ids', [6]);
if (!self::roleIntersect($adminInfo, is_array($placer) ? $placer : [])) {
return false;
}
$exempt = Config::get('project.prescription_order_placer_exempt_role_ids', [3, 8]);
return !self::roleIntersect($adminInfo, is_array($exempt) ? $exempt : []);
}
/** @return string[] */
public static function prescriptionOrderAuditLogActions(): array
{
return ['audit_rx_approve', 'audit_rx_reject', 'audit_pay_approve', 'audit_pay_reject'];
}
/** 是否展示「下单人」筛选(下单 / 经理 / 管理员 / 超管) */
public static function shouldShowAuditAdminFilter(array $adminInfo): bool
{
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
return true;
}
if (self::isOrderPlacerAuditFilterRestricted($adminInfo)) {
return true;
}
$exempt = Config::get('project.prescription_order_placer_exempt_role_ids', [3, 8]);
return self::roleIntersect($adminInfo, is_array($exempt) ? $exempt : []);
}
/**
* 下单人下拉:直接取「下单」角色下的后台用户(project.prescription_order_placer_role_ids
*
* @return array<int, array{id: int, name: string}>
*/
public static function listAuditAdminOptions(int $adminId, array $adminInfo): array
{
if ($adminId <= 0 || !self::shouldShowAuditAdminFilter($adminInfo)) {
return [];
}
$roleIds = Config::get('project.prescription_order_placer_role_ids', [6]);
$roleIds = array_values(array_unique(array_filter(array_map(
'intval',
is_array($roleIds) ? $roleIds : []
), static fn(int $id): bool => $id > 0)));
if ($roleIds === []) {
$roleIds = [6];
}
if (self::isOrderPlacerAuditFilterRestricted($adminInfo)) {
$name = trim((string) ($adminInfo['name'] ?? ''));
return [
[
'id' => $adminId,
'name' => $name !== '' ? $name : ('管理员#' . $adminId),
],
];
}
$rows = Db::name('admin')
->alias('a')
->join('admin_role ar', 'a.id = ar.admin_id')
->whereIn('ar.role_id', $roleIds)
->where('a.disable', 0)
->whereNull('a.delete_time')
->field(['a.id', 'a.name'])
->distinct(true)
->order('a.name', 'asc')
->select()
->toArray();
$out = [];
foreach ($rows as $r) {
$id = (int) ($r['id'] ?? 0);
if ($id <= 0) {
continue;
}
$name = trim((string) ($r['name'] ?? ''));
if ($name === '') {
$name = '管理员#' . $id;
}
$out[] = ['id' => $id, 'name' => $name];
}
return $out;
}
/** 指定后台账号是否拥有「下单」角色 */
public static function adminHasPlacerRole(int $adminId): bool
{
if ($adminId <= 0) {
return false;
}
$roleIds = Config::get('project.prescription_order_placer_role_ids', [6]);
$roleIds = array_values(array_unique(array_filter(array_map(
'intval',
is_array($roleIds) ? $roleIds : []
), static fn(int $id): bool => $id > 0)));
if ($roleIds === []) {
$roleIds = [6];
}
return Db::name('admin_role')
->where('admin_id', $adminId)
->whereIn('role_id', $roleIds)
->count() > 0;
}
/**
* 菜单权限:非全量角色可额外查看「关联处方开方人为本账号」的业务订单(医助代建单)
*
* perms: tcm.prescriptionOrder/viewOrdersForOwnPrescription
*/
public static function canViewOrdersForOwnPrescription(array $adminInfo): bool
{
$perms = AuthLogic::getAuthByAdminId((int) ($adminInfo['admin_id'] ?? 0));
return in_array('tcm.prescriptionOrder/viewOrdersForOwnPrescription', $perms, true);
}
/**
* 业务订单关联处方的开方人是否为指定管理员(处方 creator_id
*/
public static function isPrescriptionOrderPrescriber(int $prescriptionId, int $adminId): bool
{
if ($prescriptionId <= 0 || $adminId <= 0) {
return false;
}
$cid = (int) Prescription::where('id', $prescriptionId)->whereNull('delete_time')->value('creator_id');
return $cid === $adminId;
}
/**
* 与 PrescriptionOrderLists 数据域一致:订单创建人属于当前账号可见 admin 集合
*/
private static function orderWithinDataScopeOwners(PrescriptionOrder $row, int $adminId, array $adminInfo): bool
{
if (!DataScopeService::isEnabled()) {
return false;
}
$ids = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
if ($ids === null || $ids === []) {
return false;
}
return in_array((int) $row->creator_id, $ids, true);
}
/**
* 是否已分配「处方业务订单」相关菜单/按钮权限(lists、detail、审核等任一子权限即可)
*/
public static function hasPrescriptionOrderMenuAccess(array $adminInfo): bool
{
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
return true;
}
$perms = AuthLogic::getAuthByAdminId((int) ($adminInfo['admin_id'] ?? 0));
foreach ($perms as $p) {
if (is_string($p) && str_starts_with($p, 'tcm.prescriptionOrder/')) {
return true;
}
}
return false;
}
/**
* @param PrescriptionOrder $row
*/
public static function canAccessOrder($row, int $adminId, array $adminInfo): bool
{
if (self::canSeeAllPrescriptionOrders($adminInfo)) {
return true;
}
if (self::hasPrescriptionOrderMenuAccess($adminInfo)) {
return true;
}
if ((int) $row->creator_id === $adminId) {
return true;
}
if (self::canViewOrdersForOwnPrescription($adminInfo)) {
if (self::isPrescriptionOrderPrescriber((int) $row->prescription_id, $adminId)) {
return true;
}
}
if (self::orderWithinDataScopeOwners($row, $adminId, $adminInfo)) {
return true;
}
return false;
}
/**
* 撤回:仅创建人或全量角色(避免开方医生仅「查看医助代建单」权限误撤他人单子)
*
* @param PrescriptionOrder $row
*/
public static function canWithdrawOrder($row, int $adminId, array $adminInfo): bool
{
if (self::canSeeAllPrescriptionOrders($adminInfo)) {
return true;
}
return (int) $row->creator_id === $adminId;
}
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);
}
/**
* 与业务订单列表金额统计一致:命中「统计医助」角色且非农单支付审核档时,业务侧按「仅限本人关联诊单的业绩域」收口。
* 提成结算等对业绩归因 SQL 收窄时需与之对齐。
*/
public static function statsRestrictedToOwnAssistantPerformanceDomain(array $adminInfo): bool
{
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
return false;
}
if (self::canViewOrderListStatsAllScope($adminInfo)) {
return false;
}
$assistantRid = (int) Config::get('project.prescription_order_stats_assistant_role_id', 2);
return $assistantRid > 0 && self::roleIntersect($adminInfo, [$assistantRid]);
}
/**
* 提成结算等与列表对齐:按开方医生 / 订单创建人 筛选。
*
* @param array<string, mixed> $params
*/
public static function applyCommissionOrderDoctorAssistantFilters(Query $query, string $alias, array $params): void
{
if (isset($params['doctor_id']) && (int) $params['doctor_id'] > 0) {
$doctorId = (int) $params['doctor_id'];
$rxTbl = (new Prescription())->getTable();
$query->whereExists(
"SELECT 1 FROM `{$rxTbl}` rx WHERE rx.`id` = `{$alias}`.`prescription_id` "
. "AND rx.`delete_time` IS NULL AND rx.`creator_id` = {$doctorId}"
);
}
if (isset($params['assistant_id']) && (int) $params['assistant_id'] > 0) {
$query->where("{$alias}.creator_id", (int) $params['assistant_id']);
}
}
/**
* 与处方订单列表 HasDataScopeFilter + applyCreatorOrOwnPrescriptionVisibility 等价;主业务订单别名 $alias。
*
* @param array<string, mixed> $params 可含 assistant_id(显式收窄时校验数据域)
*/
public static function applyPrescriptionOrderListAccessForCommissionQuery(
Query $query,
string $alias,
int $viewerAdminId,
array $viewerAdminInfo,
array $params = []
): void {
if ($viewerAdminId <= 0) {
return;
}
self::applyCommissionOrderDoctorAssistantFilters($query, $alias, $params);
if (self::canSeeAllPrescriptionOrders($viewerAdminInfo)) {
self::applyCommissionOrderDataScope($query, $alias, $viewerAdminId, $viewerAdminInfo);
return;
}
$rxTbl = (new Prescription())->getTable();
$explicitAssistant = (int) ($params['assistant_id'] ?? 0);
$allowExplicitAssistantVisibility = false;
if ($explicitAssistant > 0) {
if (!DataScopeService::isEnabled()) {
$allowExplicitAssistantVisibility = true;
} else {
$visibleIds = DataScopeService::getVisibleAdminIds($viewerAdminId, $viewerAdminInfo);
if ($visibleIds === null || in_array($explicitAssistant, $visibleIds, true)) {
$allowExplicitAssistantVisibility = true;
}
}
}
$query->where(function ($q) use (
$alias,
$rxTbl,
$viewerAdminId,
$explicitAssistant,
$allowExplicitAssistantVisibility,
$viewerAdminInfo
): void {
if (self::canViewOrdersForOwnPrescription($viewerAdminInfo)) {
$q->where(function ($qq) use ($alias, $rxTbl, $viewerAdminId): void {
$qq->where("{$alias}.creator_id", $viewerAdminId);
$qq->whereOrRaw(
"EXISTS (SELECT 1 FROM `{$rxTbl}` rx WHERE rx.`id` = `{$alias}`.`prescription_id`"
. " AND rx.`delete_time` IS NULL AND rx.`creator_id` = {$viewerAdminId})"
);
});
} else {
$q->where("{$alias}.creator_id", $viewerAdminId);
}
if ($allowExplicitAssistantVisibility) {
$q->whereOr("{$alias}.creator_id", $explicitAssistant);
}
});
self::applyCommissionOrderDataScope($query, $alias, $viewerAdminId, $viewerAdminInfo);
}
/**
* 列表数据域:订单创建人 ∈ 可见 admin。
*/
private static function applyCommissionOrderDataScope(
Query $query,
string $alias,
int $viewerAdminId,
array $viewerAdminInfo
): void {
if (!DataScopeService::isEnabled()) {
return;
}
$ids = DataScopeService::getVisibleAdminIds($viewerAdminId, $viewerAdminInfo);
if ($ids === null) {
return;
}
if ($ids === []) {
$query->whereRaw('0 = 1');
return;
}
$query->whereIn("{$alias}.creator_id", $ids);
}
/**
* @param array<string,mixed> $row
*/
public static function maskInternalCostIfNeeded(array &$row, array $adminInfo): void
{
if (!self::canViewInternalCost($adminInfo)) {
unset($row['internal_cost']);
}
}
/**
* 菜单权限:查看/编辑业务订单「备注」(对内 remark_extra
*
* perms: tcm.prescriptionOrder/editRemarkExtra
*/
public static function canEditOrderRemarkExtra(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/editRemarkExtra', $perms, true);
}
/**
* @param array<string,mixed> $row
*/
public static function maskRemarkExtraIfNeeded(array &$row, array $adminInfo): void
{
if (!self::canEditOrderRemarkExtra($adminInfo)) {
unset($row['remark_extra']);
}
}
/** 定金门槛(元),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;
}
// 豁免权:关联的支付单中有豁免单则跳过金额校验
$hasExempt = Order::whereIn('id', $payOrderIds)
->whereNull('delete_time')
->where('is_exempt', 1)
->count();
if ($hasExempt > 0) {
return null;
}
// 检查关联支付单的总金额
$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', 'is_exempt'])
->order('id', 'asc')
->whereIn('status', [2, 4, 5])
->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 => '全部费用',
8 => '驼奶费用',
];
$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) {
$st = (int) ($o['status'] ?? 0);
if ($st === 2 || $st === 5) {
$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 = self::canEditOrderRemarkExtra($adminInfo)
? (string) ($params['remark_extra'] ?? '')
: '';
$order->remark_assistant = (string) ($params['remark_assistant'] ?? '');
$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::maskRemarkExtraIfNeeded($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::maskRemarkExtraIfNeeded($arr, $adminInfo);
self::attachLinkedPayOrders($arr);
// 添加创建人姓名、登录账号(业务订单 creator_id;la_admin 无手机列则不发手机号)
$creatorId = (int) ($arr['creator_id'] ?? 0);
$arr['creator_name'] = '';
$arr['creator_account'] = '';
$arr['creator_mobile'] = '';
if ($creatorId > 0) {
// 须包含 idAdmin 模型 $append 在 toArray 时会依赖 data['id']
$adm = Admin::where('id', $creatorId)->whereNull('delete_time')->field(['id', 'name', 'account'])->find();
if ($adm !== null) {
$a = $adm->toArray();
$arr['creator_name'] = (string) ($a['name'] ?? '');
$arr['creator_account'] = (string) ($a['account'] ?? '');
}
}
$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);部门见下方:优先业务订单 creator_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 = Db::name('admin_dept')->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 : '';
}
}
}
// 「创建人所属部门」:优先 zyt_tcm_prescription_order.creator_id;无部门时再试诊单 admin_id(非处方开方人)
$deptPath = self::buildAdminDeptPathJoined((int) ($arr['creator_id'] ?? 0));
if ($deptPath === '') {
$dgAdmin = (int) ($arr['diagnosis_creator_id'] ?? 0);
if ($dgAdmin > 0) {
$deptPath = self::buildAdminDeptPathJoined($dgAdmin);
}
}
$arr['diagnosis_creator_dept_path'] = $deptPath;
/** 与 diagnosis_creator_dept_path 相同语义:业务订单创建人所属部门(多部门「;」、路径「 / 」),便于前端字段自解释 */
$arr['order_creator_dept_path'] = $deptPath;
// 挂号预约摘要:优先处方 appointment_id;否则按诊单 id 匹配预约表 patient_id(诊单)
$arr['linked_appointment'] = null;
$rxApptId = 0;
if (isset($arr['prescription']) && is_array($arr['prescription'])) {
$rxApptId = (int) ($arr['prescription']['appointment_id'] ?? 0);
}
$resolvedApptId = $rxApptId;
if ($resolvedApptId <= 0 && $diagIdForMeta > 0) {
$apFallback = Appointment::where('patient_id', $diagIdForMeta)->order('id', 'desc')->find();
if ($apFallback !== null) {
$resolvedApptId = (int) $apFallback->id;
}
}
if ($resolvedApptId > 0) {
$brief = self::buildAppointmentBrief($resolvedApptId);
if ($brief !== null) {
$brief['resolved_from'] = $rxApptId === $resolvedApptId ? 'prescription' : 'diagnosis';
$arr['linked_appointment'] = $brief;
}
}
return $arr;
}
/**
* 业务订单详情场景:仅更新关联处方的患者姓名与手机号(不重置消费者处方审核、不改变其它处方字段)
*/
public static function patchPrescriptionPatient(
int $prescriptionOrderId,
string $patientName,
string $phone,
int $adminId,
array $adminInfo
): bool {
self::$error = '';
$order = PrescriptionOrder::where('id', $prescriptionOrderId)->whereNull('delete_time')->find();
if (!$order) {
self::setError('订单不存在');
return false;
}
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
self::setError('无权限操作');
return false;
}
$rxId = (int) ($order->prescription_id ?? 0);
if ($rxId <= 0) {
self::setError('该订单未关联处方');
return false;
}
$rx = Prescription::where('id', $rxId)->whereNull('delete_time')->find();
if (!$rx) {
self::setError('处方不存在');
return false;
}
if (!PrescriptionLogic::canViewPrescription($rx, $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;
}
$oldName = trim((string) ($rx->patient_name ?? ''));
$oldPhone = trim((string) ($rx->phone ?? ''));
try {
$rx->save([
'patient_name' => $patientName,
'phone' => $phone,
]);
$nameFrom = $oldName === '' ? '—' : $oldName;
$phoneFrom = $oldPhone === '' ? '—' : $oldPhone;
$summary = sprintf(
'关联处方 #%d:患者姓名「%s」→「%s」,手机「%s」→「%s」',
$rxId,
$nameFrom,
$patientName,
$phoneFrom,
$phone
);
self::appendRxPatientPatchLog($prescriptionOrderId, $adminId, $adminInfo, $summary);
return true;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
/**
* 渠道字典 value => 名称(与挂号 channel_source 存值一致)
*
* @return array<string, string>
*/
private static function channelSourceDictMap(): array
{
try {
/** @var array<string, string> $m */
$m = Db::name('dict_data')->where('type_value', 'channels')->column('name', 'value');
return $m;
} catch (\Throwable) {
return [];
}
}
/**
* @param array<string, mixed> $row doctor_appointment 行
* @param array<string, string> $channelDict
*
* @return array<string, mixed>
*/
private static function formatAppointmentBriefFromRow(array $row, string $doctorName, array $channelDict = []): array
{
$apptId = (int) ($row['id'] ?? 0);
$statusMap = [
1 => '已预约',
2 => '已取消',
3 => '已完成',
4 => '已过号',
];
$typeMap = [
'video' => '视频问诊',
'text' => '图文问诊',
'phone' => '电话问诊',
];
$periodRaw = (string) ($row['period'] ?? '');
$period = $periodRaw === 'afternoon' ? '下午' : ($periodRaw === 'morning' ? '上午' : $periodRaw);
$timePart = (string) ($row['appointment_time'] ?? '');
if (strlen($timePart) > 5) {
$timePart = substr($timePart, 0, 5);
}
$status = (int) ($row['status'] ?? 0);
$atype = (string) ($row['appointment_type'] ?? '');
$chSrc = trim((string) ($row['channel_source'] ?? ''));
if ($chSrc === '') {
$legacy = $row['channels'] ?? null;
if ($legacy !== null && $legacy !== '') {
$chSrc = is_numeric($legacy)
? (string) (int) $legacy
: trim((string) $legacy);
}
}
$chDesc = '';
if ($chSrc !== '') {
$chDesc = (string) ($channelDict[$chSrc] ?? '');
if ($chDesc === '' && is_numeric($chSrc)) {
$chDesc = (string) ($channelDict[(string) (int) $chSrc] ?? '');
}
if ($chDesc === '') {
$chDesc = $chSrc;
}
}
$channelDictValue = null;
if ($chSrc !== '' && is_numeric($chSrc)) {
$channelDictValue = (int) $chSrc;
}
return [
'id' => $apptId,
'appointment_date' => (string) ($row['appointment_date'] ?? ''),
'period' => $period,
'appointment_time' => $timePart,
'appointment_type' => $atype,
'appointment_type_desc' => $typeMap[$atype] ?? '未知',
'status' => $status,
'status_desc' => $statusMap[$status] ?? '未知',
'doctor_name' => $doctorName,
'channel_source' => $chSrc,
'channel_source_desc' => $chDesc,
/** 字典 type_value=channels 的 value(整型);与业绩看板挂号 channels 收窄、进线统计所用字典为同套 */
'channel_dict_value' => $channelDictValue,
'remark' => (string) ($row['remark'] ?? ''),
];
}
/**
* 挂号预约简要信息(业务订单详情展示)
*
* @return array<string, mixed>|null
*/
private static function buildAppointmentBrief(int $appointmentId): ?array
{
if ($appointmentId <= 0) {
return null;
}
$ap = Appointment::find($appointmentId);
if ($ap === null) {
return null;
}
$row = $ap->toArray();
$doctorName = '';
$did = (int) ($row['doctor_id'] ?? 0);
if ($did > 0) {
$doctorName = (string) (Admin::where('id', $did)->value('name') ?? '');
}
$channelDict = self::channelSourceDictMap();
return self::formatAppointmentBriefFromRow($row, $doctorName, $channelDict);
}
/**
* 业务订单列表行批量附加 linked_appointment(解析规则与详情一致:优先处方 appointment_id,否则诊单最近一条预约;
* 业绩侧栏且带 channel_code 时优先展示与渠道收窄同源的已完成挂号,避免列表行属自媒体2Q 但展示关联为自媒体1)
*
* @param array<int, array<string, mixed>> $lists
* @param array<string, mixed> $listsParams PrescriptionOrderLists 请求参数(可选)
*/
public static function appendLinkedAppointmentsToListRows(array &$lists, array $listsParams = []): void
{
if ($lists === []) {
return;
}
foreach ($lists as &$item) {
$item['linked_appointment'] = null;
}
unset($item);
$yejiChannelHighlightByDiag = [];
if (
(int) ($listsParams['yeji_order_drawer'] ?? 0) === 1
&& trim((string) ($listsParams['channel_code'] ?? '')) !== ''
) {
$allDiagIdsForYeji = array_values(array_unique(array_filter(
array_map(static fn ($x): int => (int) ($x['diagnosis_id'] ?? 0), $lists),
static fn (int $id): bool => $id > 0
)));
if ($allDiagIdsForYeji !== []) {
$yejiChannelHighlightByDiag = YejiStatsLogic::batchResolveYejiChannelDrawerHighlightAppointmentIds(
$allDiagIdsForYeji,
$listsParams
);
}
}
$rxIds = array_values(array_unique(array_filter(array_map(
static fn ($x) => (int) ($x['prescription_id'] ?? 0),
$lists
))));
$rxApptByRx = [];
if ($rxIds !== []) {
$rxApptByRx = Prescription::whereIn('id', $rxIds)->whereNull('delete_time')->column('appointment_id', 'id');
}
$diagNeedFallback = [];
foreach ($lists as $item) {
$rxId = (int) ($item['prescription_id'] ?? 0);
$dgId = (int) ($item['diagnosis_id'] ?? 0);
$apFromRx = $rxId > 0 ? (int) ($rxApptByRx[$rxId] ?? 0) : 0;
if ($apFromRx <= 0 && $dgId > 0) {
$diagNeedFallback[$dgId] = true;
}
}
$diagIds = array_keys($diagNeedFallback);
$latestApptByDiag = [];
if ($diagIds !== []) {
try {
$agg = Db::name('doctor_appointment')
->whereIn('patient_id', $diagIds)
->fieldRaw('patient_id, MAX(id) as max_id')
->group('patient_id')
->select()
->toArray();
foreach ($agg as $r) {
$pid = (int) ($r['patient_id'] ?? 0);
$mid = (int) ($r['max_id'] ?? 0);
if ($pid > 0 && $mid > 0) {
$latestApptByDiag[$pid] = $mid;
}
}
} catch (\Throwable) {
$latestApptByDiag = [];
}
}
/** @var array<int, array{0: int, 1: int}> $resolvedPairs index => [ resolvedApptId, rxAppointmentId ] */
$resolvedPairs = [];
foreach ($lists as $idx => $item) {
$rxId = (int) ($item['prescription_id'] ?? 0);
$dgId = (int) ($item['diagnosis_id'] ?? 0);
$rxApptId = $rxId > 0 ? (int) ($rxApptByRx[$rxId] ?? 0) : 0;
$resolved = $rxApptId;
if ($resolved <= 0 && $dgId > 0) {
$resolved = (int) ($latestApptByDiag[$dgId] ?? 0);
}
$preferredYeji = $dgId > 0 ? (int) ($yejiChannelHighlightByDiag[$dgId] ?? 0) : 0;
if ($preferredYeji > 0) {
$resolved = $preferredYeji;
}
if ($resolved > 0) {
$resolvedPairs[$idx] = [$resolved, $rxApptId];
}
}
if ($resolvedPairs === []) {
return;
}
$allApptIds = array_values(array_unique(array_map(static fn ($p) => $p[0], $resolvedPairs)));
$appts = Appointment::whereIn('id', $allApptIds)->select()->toArray();
$apptById = [];
foreach ($appts as $a) {
$aid = (int) ($a['id'] ?? 0);
if ($aid > 0) {
$apptById[$aid] = $a;
}
}
$doctorIds = array_values(array_unique(array_filter(array_map(
static fn ($a) => (int) ($a['doctor_id'] ?? 0),
$appts
))));
$doctorNames = [];
if ($doctorIds !== []) {
$doctorNames = Admin::whereIn('id', $doctorIds)->column('name', 'id');
}
$channelDict = self::channelSourceDictMap();
foreach ($resolvedPairs as $idx => $pair) {
$resolvedId = $pair[0];
$rxApptId = $pair[1];
$row = $apptById[$resolvedId] ?? null;
if ($row === null || !is_array($row)) {
continue;
}
$did = (int) ($row['doctor_id'] ?? 0);
$dname = $did > 0 ? (string) ($doctorNames[$did] ?? '') : '';
$brief = self::formatAppointmentBriefFromRow($row, $dname, $channelDict);
$brief['resolved_from'] = $rxApptId === $resolvedId ? 'prescription' : 'diagnosis';
$lists[$idx]['linked_appointment'] = $brief;
}
}
/**
* 物流轨迹(顺丰/京东等,优先快递100;未配置密钥时返回官网链接)
*
* @return array<string,mixed>|null
*/
public static function logisticsTrace(int $id, string $expressCompanyOverride, int $adminId, array $adminInfo, string $phoneTail = ''): ?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 ?? ''), $phoneTail);
$payload['source'] = 'api';
}
$payload['official_urls'] = ExpressTrackService::officialUrls($num);
$payload['tracking_number'] = $num;
$payload['order_id'] = $id;
$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;
}
if (
(int) $order->creator_id === $adminId
&& (int) $order->prescription_audit_status === 1
&& (int) $order->payment_slip_audit_status === 1
&& !self::canSeeAllPrescriptionOrders($adminInfo)
) {
self::$error = '处方与支付单均已审核通过,创建人不可再编辑';
return false;
}
$fs = (int) $order->fulfillment_status;
if ($fs === 3 || $fs === 4) {
self::$error = '已完成或已取消的订单不可编辑';
return false;
}
if ($fs === 6) {
self::$error = '已签收订单不可修改快递单号';
return false;
}
// 已成功提交甘草 SCM:仅允许更新快递单号与承运商(与甘草侧地址/药方等仍以取消流程为准)
$gcOrderNo = trim((string) $order->gancao_reciperl_order_no);
$gcSubmitTime = (int) $order->gancao_submit_time;
if ($gcOrderNo !== '' || $gcSubmitTime > 0) {
return self::editGancaoLogisticsOnly($order, $params, $adminId, $adminInfo);
}
$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);
if (self::canEditOrderRemarkExtra($adminInfo)) {
$order->remark_extra = (string) ($params['remark_extra'] ?? '');
}
$order->remark_assistant = (string) ($params['remark_assistant'] ?? '');
$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::maskRemarkExtraIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
/**
* 甘草已提交后仅更新物流字段(tracking_number、express_company),忽略金额/地址等其它请求参数。
*
* @param array<string,mixed> $params
* @return array<string,mixed>|false
*/
private static function editGancaoLogisticsOnly(
PrescriptionOrder $order,
array $params,
int $adminId,
array $adminInfo
) {
$order->tracking_number = mb_substr(trim((string) ($params['tracking_number'] ?? '')), 0, 80);
if (array_key_exists('express_company', $params)) {
$order->express_company = self::normalizeExpressCompany($params['express_company']);
}
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::maskRemarkExtraIfNeeded($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, string $shipMode, 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;
}
$shipMode = self::normalizeShipMode($shipMode);
$shipModeText = $shipMode === 'direct' ? '药房直发' : '甘草药方发';
$order->express_company = self::normalizeExpressCompany($expressCompany);
$order->tracking_number = $trackingNumber;
$order->ship_mode = $shipMode;
// 仅履约中(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',
'确认发货(' . $shipModeText . '),运单:' . $trackingNumber . '' . $expressCompany . ''
);
} else {
self::writeLog(
(int) $order->id,
$adminId,
$adminInfo,
'fill_tracking',
'更新发货信息(' . $shipModeText . '),运单:' . $trackingNumber . '' . $expressCompany . ''
);
}
$out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo);
self::maskRemarkExtraIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
private static function normalizeShipMode(string $shipMode): string
{
$s = strtolower(trim($shipMode));
if ($s === 'direct') {
return 'direct';
}
return 'gancao';
}
/**
* @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::maskRemarkExtraIfNeeded($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::maskRemarkExtraIfNeeded($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;
}
$didAutoComplete = 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 && in_array((int) $order->fulfillment_status, [5, 6], true)) {
$linkedPayOrderIds = self::linkedPayOrderIdList($id);
$linkedPayOrders = self::linkedPayOrdersPayload($linkedPayOrderIds);
$linkedPayPaidTotal = 0.0;
foreach ($linkedPayOrders as $o) {
$linkedPayPaidTotal += (float) ($o['amount'] ?? 0);
}
$linkedPayPaidTotal = round($linkedPayPaidTotal, 2);
$order->fulfillment_status = 3;
$order->paid = $linkedPayPaidTotal;
$order->completion_request = 0;
$order->completion_request_time = 0;
$order->completion_request_by = 0;
$order->completion_request_by_name = '';
$didAutoComplete = true;
}
} 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)/已签收(6):不同步为「待发货」,拒绝时也不整单解绑(与原先仅已发货一致)
// - 其它状态:同步;拒绝时清除所有支付单关联关系
$currentStatus = (int) $order->fulfillment_status;
if (in_array($currentStatus, [5, 6], true)) {
// 保持履约节点不变
} else {
self::syncFulfillmentStatus($order);
if ($action === 'reject') {
self::clearPayOrderLinks($order);
}
}
try {
$order->save();
} catch (\Throwable $e) {
self::$error = $e->getMessage();
return false;
}
if ($didAutoComplete) {
$unassignSummary = self::clearDiagnosisAssistantOnComplete((int) $order->diagnosis_id);
$paidStr = number_format((float) $order->paid, 2, '.', '');
self::writeLog((int) $order->id, $adminId, $adminInfo, 'auto_complete',
'支付审核通过,根据完单申请自动完成订单,实付金额更新为 ¥' . $paidStr . $unassignSummary);
}
$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::maskRemarkExtraIfNeeded($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::maskRemarkExtraIfNeeded($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::maskRemarkExtraIfNeeded($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/6) 的业务订单新增一条关联支付单(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 (!in_array((int) $order->fulfillment_status, [5, 6], true)) {
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 = '支付单金额不能为负数';
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::maskRemarkExtraIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
/**
* 为「已发货/已签收」(fulfillment_status=5/6) 的业务订单关联已有支付单,
* 关联后将支付审核状态重置为待审核以启动再次审核流程。
*
* @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 (!in_array((int) $order->fulfillment_status, [5, 6], true)) {
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);
$duplicated = array_values(array_intersect($payOrderIds, $existingIds));
if ($duplicated !== []) {
self::$error = '支付单 #' . implode('、#', $duplicated) . ' 已绑定当前业务订单,请勿重复关联';
return false;
}
// 将支付单追加到业务订单关联列表
$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::maskRemarkExtraIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
/**
* 已发货/已签收订单:不新增/关联支付单,仅提交完单申请并将支付审核置为待审核。
*
* @param array<string,mixed> $params id
* @return array<string,mixed>|false
*/
public static function requestCompletion(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 (!in_array((int) $order->fulfillment_status, [5, 6], true)) {
self::$error = '仅「已发货」或「已签收」状态的订单可申请完单';
return false;
}
$order->completion_request = 1;
$order->completion_request_time = time();
$order->completion_request_by = $adminId;
$order->completion_request_by_name = (string) ($adminInfo['name'] ?? '');
$order->payment_slip_audit_status = 0;
$order->payment_slip_audit_remark = '';
try {
$order->save();
} catch (\Throwable $e) {
self::$error = $e->getMessage();
return false;
}
self::writeLog($id, $adminId, $adminInfo, 'completion_request', '提交完单申请(未新增/关联支付单),等待支付审核');
$out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo);
self::maskRemarkExtraIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
/** 完成订单时可选的履约终态:3=已完成,7-12 业务结案类状态(退款 10 走独立 refund 接口) */
private const COMPLETE_FULFILLMENT_TARGETS = [3, 7, 8, 9, 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) {
$st = (int) ($o['status'] ?? 0);
if ($st === 2 || $st === 5) {
$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::maskRemarkExtraIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
/**
* 业务订单退款:须填写原因;关联收款单标记为已退款,实付清零。
*
* @return array<string,mixed>|false
*/
public static function refund(int $id, string $reason, int $adminId, array $adminInfo)
{
self::$error = '';
$reason = trim($reason);
if ($reason === '') {
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;
}
$linkedPayOrderIds = self::linkedPayOrderIdList($id);
$refundedPayCount = 0;
try {
if ($linkedPayOrderIds !== []) {
$refundedPayCount = Order::whereIn('id', $linkedPayOrderIds)
->whereNull('delete_time')
->update(['status' => 4]);
}
$order->fulfillment_status = 10;
$order->paid = 0;
$order->save();
} catch (\Throwable $e) {
self::$error = $e->getMessage();
return false;
}
$unassignSummary = self::clearDiagnosisAssistantOnComplete((int) $order->diagnosis_id);
$logLine = '订单退款,状态变更为「退款」;原因:' . $reason;
if ($refundedPayCount > 0) {
$logLine .= ';关联支付单 ' . $refundedPayCount . ' 笔已标记为已退款';
}
$logLine .= ';已付总额清零' . $unassignSummary;
self::writeLog($id, $adminId, $adminInfo, 'refund', $logLine);
$out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo);
self::maskRemarkExtraIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
public static function fulfillmentStatusLabel(int $fs): string
{
$m = [
1 => '待双审通过',
2 => '待发货',
3 => '已完成',
4 => '已取消',
5 => '已发货',
6 => '已签收',
7 => '进行中',
8 => '暂不制药',
9 => '拒收',
10 => '退款',
11 => '保留药方',
12 => '制药缓发',
];
return $m[$fs] ?? ('状态' . (string) $fs);
}
/**
* 业务订单列表/导出:「诊单医助」admin id。
* 与 {@see PrescriptionLists} 一致优先 {@see Prescription::$assistant_id}(开方时从诊单快照到处方表);
* 为 0 时再取诊单当前 {@see Diagnosis::$assistant_id}(兼容历史处方或未写入快照列的数据)。
*/
public static function resolveAssistantAdminIdForPrescriptionOrderRow(
int $prescriptionAssistantId,
int $diagnosisAssistantId
): int {
if ($prescriptionAssistantId > 0) {
return $prescriptionAssistantId;
}
if ($diagnosisAssistantId > 0) {
return $diagnosisAssistantId;
}
return 0;
}
/**
* 复诊导出「初诊医助」:同 diagnosis_id 下最早 is_follow_up=0 的业务单医助(非 prev_staff 医生名)
*
* @param array<int, array<string, mixed>> $diagById
* @return array<int, string> diagnosis_id => 医助姓名
*/
private static function batchFirstVisitAssistantNameByDiagnosis(array $diagIds, array $diagById): array
{
if ($diagIds === []) {
return [];
}
$firstRows = PrescriptionOrder::whereIn('diagnosis_id', $diagIds)
->whereNull('delete_time')
->where('is_follow_up', 0)
->field(['id', 'diagnosis_id', 'prescription_id', 'creator_id', 'create_time'])
->order('create_time', 'asc')
->order('id', 'asc')
->select()
->toArray();
$firstByDiag = [];
$rxIds = [];
foreach ($firstRows as $row) {
$did = (int) ($row['diagnosis_id'] ?? 0);
if ($did <= 0 || isset($firstByDiag[$did])) {
continue;
}
$firstByDiag[$did] = $row;
$rxId = (int) ($row['prescription_id'] ?? 0);
if ($rxId > 0) {
$rxIds[$rxId] = true;
}
}
$rxAssistantById = [];
if ($rxIds !== []) {
$rxAssistantById = Prescription::whereIn('id', array_keys($rxIds))
->whereNull('delete_time')
->column('assistant_id', 'id');
}
$adminIds = [];
foreach ($firstByDiag as $did => $row) {
$diag = $diagById[$did] ?? [];
$rxId = (int) ($row['prescription_id'] ?? 0);
$rxAst = $rxId > 0 ? (int) ($rxAssistantById[$rxId] ?? 0) : 0;
$aid = self::resolveAssistantAdminIdForPrescriptionOrderRow(
$rxAst,
(int) ($diag['assistant_id'] ?? 0)
);
if ($aid <= 0) {
$aid = (int) ($row['creator_id'] ?? 0);
}
if ($aid > 0) {
$adminIds[$aid] = true;
}
}
$nameByAdminId = [];
if ($adminIds !== []) {
$admins = Admin::whereIn('id', array_keys($adminIds))
->field(['id', 'name'])
->select()
->toArray();
foreach ($admins as $a) {
$nameByAdminId[(int) $a['id']] = trim((string) ($a['name'] ?? ''));
}
}
$out = [];
foreach ($firstByDiag as $did => $row) {
$diag = $diagById[$did] ?? [];
$rxId = (int) ($row['prescription_id'] ?? 0);
$rxAst = $rxId > 0 ? (int) ($rxAssistantById[$rxId] ?? 0) : 0;
$aid = self::resolveAssistantAdminIdForPrescriptionOrderRow(
$rxAst,
(int) ($diag['assistant_id'] ?? 0)
);
if ($aid <= 0) {
$aid = (int) ($row['creator_id'] ?? 0);
}
$out[$did] = $aid > 0 ? (string) ($nameByAdminId[$aid] ?? '') : '';
}
return $out;
}
/**
* 导出用:管理员在 admin_dept 中的部门路径(多部门「;」、路径内「 / 」;业务订单导出取 creator_id
*/
public static function formatAssistantDeptPathForExport(int $assistantAdminId): string
{
if ($assistantAdminId <= 0) {
return '';
}
$deptIds = Db::name('admin_dept')->where('admin_id', $assistantAdminId)->column('dept_id');
$pathSegments = [];
foreach ($deptIds as $did) {
$p = self::buildDeptPath((int) $did);
if ($p !== '') {
$pathSegments[] = $p;
}
}
$pathSegments = array_values(array_unique($pathSegments));
return $pathSegments === [] ? '' : implode('', $pathSegments);
}
/**
* 导出用:服务套餐(与列表/详情 formatServicePackage 同口径,按 dict_data type_value=server_order 解析)
* 入参可能是逗号分隔字符串或数组;空串返回空,避免 Excel 显示「—」占位。
*
* @param mixed $value zyt_tcm_prescription_order.service_package
* @param array<string, string> $packageNameByValue dict_data server_order value=>name 字典缓存
*/
public static function formatServicePackageForExport($value, array $packageNameByValue): string
{
if ($value === null || $value === '' || $value === false) {
return '';
}
$items = [];
if (\is_array($value)) {
$items = $value;
} else {
$items = explode(',', (string) $value);
}
$names = [];
foreach ($items as $raw) {
$key = trim((string) $raw);
if ($key === '') {
continue;
}
$names[] = (string) ($packageNameByValue[$key] ?? $packageNameByValue[(string) (int) $key] ?? $key);
}
return $names === [] ? '' : implode('、', $names);
}
/**
* 导出用:药品形态(丸剂 / 饮片+代煎|自煎等)
*
* @param array<string, mixed> $rx
*/
public static function formatMedicationFormForExport(array $rx): string
{
$type = trim((string) ($rx['prescription_type'] ?? ''));
if ($type === '') {
return '';
}
if ($type === '饮片') {
$nd = (int) ($rx['need_decoction'] ?? 0);
return $type . ($nd === 1 ? '(代煎)' : '(自煎)');
}
return $type;
}
/**
* 导出列:挂号表渠道来源展示(与 AppointmentLists channel_source_desc 同字典口径)
*
* @param array<string, mixed> $apRow doctor_appointment 一行
* @param array<string, string> $channelNameByValue DictData channels value=>name
*/
private static function formatGuahaoChannelSourceForExport(array $apRow, array $channelNameByValue): string
{
$srcKey = trim((string) ($apRow['channel_source'] ?? ''));
if ($srcKey === '' && isset($apRow['channels']) && $apRow['channels'] !== '' && $apRow['channels'] !== null) {
$srcKey = trim((string) $apRow['channels']);
}
$label = '';
if ($srcKey !== '') {
$label = (string) ($channelNameByValue[$srcKey] ?? $channelNameByValue[(string) (int) $srcKey] ?? $srcKey);
}
$detail = trim((string) ($apRow['channel_source_detail'] ?? ''));
if ($label === '' && $detail === '') {
return '';
}
if ($detail !== '') {
return $label !== '' ? "{$label}{$detail}" : $detail;
}
return $label;
}
/**
* 挂号表渠道相关列是否存在(未加 channel_source 的老库仅查 channels 或整列留空)
*
* @return array{channel_source: bool, channel_source_detail: bool, channels: bool}
*/
private static function doctorAppointmentChannelColumnsForExport(): array
{
$out = [
'channel_source' => false,
'channel_source_detail' => false,
'channels' => false,
];
try {
$cols = Db::name('doctor_appointment')->getTableFields();
if (!is_array($cols)) {
return $out;
}
foreach (array_keys($out) as $k) {
$out[$k] = in_array($k, $cols, true);
}
} catch (\Throwable) {
// keep false
}
return $out;
}
/**
* 业务订单列表导出:写入与 PrescriptionOrderLists::setExcelFields 对应的字段(export=2 时由列表类调用)
* 「挂号渠道来源」取值与详情/列表解析一致:处方 appointment_id → 否则诊单下 MAX(挂号.id);业绩抽屉带渠道时与列表同源高亮。
*
* @param array<int, array<string, mixed>> $lists
* @param array<string, mixed> $listsParams PrescriptionOrderLists 请求参数(含 yeji_order_drawer、channel_code
*/
public static function appendPrescriptionOrderListExportRows(array &$lists, array $adminInfo, array $listsParams = []): void
{
if ($lists === []) {
return;
}
$diagIds = [];
$rxIds = [];
$poIds = [];
foreach ($lists as $row) {
$poIds[] = (int) ($row['id'] ?? 0);
$d = (int) ($row['diagnosis_id'] ?? 0);
if ($d > 0) {
$diagIds[$d] = true;
}
$r = (int) ($row['prescription_id'] ?? 0);
if ($r > 0) {
$rxIds[$r] = true;
}
}
$poIds = array_values(array_filter(array_unique($poIds), static fn (int $id): bool => $id > 0));
$diagIdList = array_keys($diagIds);
$rxIdList = array_keys($rxIds);
$diagById = [];
if ($diagIdList !== []) {
$diagRows = Diagnosis::whereIn('id', $diagIdList)->whereNull('delete_time')
->field(['id', 'patient_name', 'gender', 'age', 'phone', 'assistant_id'])
->select()
->toArray();
foreach ($diagRows as $dr) {
$diagById[(int) $dr['id']] = $dr;
}
}
$chCols = self::doctorAppointmentChannelColumnsForExport();
$needGuahaoChannel = $chCols['channel_source'] || $chCols['channels'] || $chCols['channel_source_detail'];
$channelNameByValue = [];
$apptById = [];
/** @var array<int, int> $resolvedApptIdByPoId 订单 id → 用于导出渠道的那条挂号 id */
$resolvedApptIdByPoId = [];
$rxById = [];
if ($rxIdList !== []) {
$rxRows = Prescription::whereIn('id', $rxIdList)->whereNull('delete_time')
->field(['id', 'prescription_type', 'need_decoction', 'dose_unit', 'assistant_id', 'appointment_id'])
->select()
->toArray();
foreach ($rxRows as $xr) {
$rxById[(int) $xr['id']] = $xr;
}
}
if ($needGuahaoChannel) {
$yejiChannelHighlightByDiag = [];
if (
(int) ($listsParams['yeji_order_drawer'] ?? 0) === 1
&& trim((string) ($listsParams['channel_code'] ?? '')) !== ''
) {
$allDiagIdsForYeji = array_values(array_unique(array_filter(
array_map(static fn ($x): int => (int) ($x['diagnosis_id'] ?? 0), $lists),
static fn (int $id): bool => $id > 0
)));
if ($allDiagIdsForYeji !== []) {
$yejiChannelHighlightByDiag = YejiStatsLogic::batchResolveYejiChannelDrawerHighlightAppointmentIds(
$allDiagIdsForYeji,
$listsParams
);
}
}
$diagNeedFallback = [];
foreach ($lists as $item) {
$rxId = (int) ($item['prescription_id'] ?? 0);
$dgId = (int) ($item['diagnosis_id'] ?? 0);
$apFromRx = $rxId > 0 ? (int) (($rxById[$rxId] ?? [])['appointment_id'] ?? 0) : 0;
if ($apFromRx <= 0 && $dgId > 0) {
$diagNeedFallback[$dgId] = true;
}
}
$fallbackDiagIds = array_keys($diagNeedFallback);
$latestApptByDiag = [];
if ($fallbackDiagIds !== []) {
try {
$agg = Db::name('doctor_appointment')
->whereIn('patient_id', $fallbackDiagIds)
->fieldRaw('patient_id, MAX(id) as max_id')
->group('patient_id')
->select()
->toArray();
foreach ($agg as $r) {
$pid = (int) ($r['patient_id'] ?? 0);
$mid = (int) ($r['max_id'] ?? 0);
if ($pid > 0 && $mid > 0) {
$latestApptByDiag[$pid] = $mid;
}
}
} catch (\Throwable) {
$latestApptByDiag = [];
}
}
foreach ($lists as $item) {
$poRowId = (int) ($item['id'] ?? 0);
if ($poRowId <= 0) {
continue;
}
$rxId = (int) ($item['prescription_id'] ?? 0);
$dgId = (int) ($item['diagnosis_id'] ?? 0);
$rxApptId = $rxId > 0 ? (int) (($rxById[$rxId] ?? [])['appointment_id'] ?? 0) : 0;
$resolved = $rxApptId;
if ($resolved <= 0 && $dgId > 0) {
$resolved = (int) ($latestApptByDiag[$dgId] ?? 0);
}
$preferredYeji = $dgId > 0 ? (int) ($yejiChannelHighlightByDiag[$dgId] ?? 0) : 0;
if ($preferredYeji > 0) {
$resolved = $preferredYeji;
}
if ($resolved > 0) {
$resolvedApptIdByPoId[$poRowId] = $resolved;
}
}
$uniqApptIds = array_values(array_unique(array_values($resolvedApptIdByPoId)));
if ($uniqApptIds !== []) {
$selectFields = ['id'];
if ($chCols['channel_source']) {
$selectFields[] = 'channel_source';
}
if ($chCols['channel_source_detail']) {
$selectFields[] = 'channel_source_detail';
}
if ($chCols['channels']) {
$selectFields[] = 'channels';
}
$apptRows = Appointment::whereIn('id', $uniqApptIds)->field($selectFields)->select()->toArray();
foreach ($apptRows as $ar) {
$aid = (int) ($ar['id'] ?? 0);
if ($aid > 0) {
$apptById[$aid] = $ar;
}
}
}
if ($chCols['channel_source'] || $chCols['channels']) {
$channelNameByValue = DictData::where('type_value', 'channels')->column('name', 'value');
}
}
$logRows = PrescriptionOrderLog::whereIn('prescription_order_id', $poIds)
->whereIn('action', ['audit_rx_approve', 'audit_rx_reject', 'revoke_rx_audit'])
->field(['prescription_order_id', 'id', 'action', 'admin_name', 'create_time'])
->order('id', 'asc')
->select()
->toArray();
$auditEffectiveByPo = [];
foreach ($logRows as $lg) {
$pid = (int) ($lg['prescription_order_id'] ?? 0);
if ($pid <= 0) {
continue;
}
$act = (string) ($lg['action'] ?? '');
if ($act === 'revoke_rx_audit') {
$auditEffectiveByPo[$pid] = null;
continue;
}
if ($act === 'audit_rx_approve' || $act === 'audit_rx_reject') {
$auditEffectiveByPo[$pid] = $lg;
}
}
// 服务套餐字典:与前端 formatServicePackage 同口径(dict_data type_value=server_order;只取启用项做兜底取原值)
$packageNameByValue = [];
$hasAnyPackage = false;
foreach ($lists as $row) {
$pkRaw = $row['service_package'] ?? '';
if (\is_array($pkRaw)) {
if ($pkRaw !== []) {
$hasAnyPackage = true;
break;
}
} elseif (trim((string) $pkRaw) !== '') {
$hasAnyPackage = true;
break;
}
}
if ($hasAnyPackage) {
try {
$packageNameByValue = DictData::where('type_value', 'server_order')->column('name', 'value');
} catch (\Throwable) {
$packageNameByValue = [];
}
}
// 签收时间:纯读物流库(express_tracking / express_trace),不在导出阶段发起任何快递100 HTTP。
// 签收时间的落库由 `php think tcm:backfill-sign-time` 与 `express:auto-update` 定时任务负责,
// 导出只读脚本/定时任务处理好的结果——避免逐单 HTTP 让导出变慢、耗时不可控。
/** @var array<int, int> $signTsByPoId order_id => Unix 秒 */
$signTsByPoId = [];
if ($poIds !== []) {
$signItems = [];
foreach ($lists as $row) {
$oid = (int) ($row['id'] ?? 0);
if ($oid <= 0) {
continue;
}
$signItems[] = [
'order_id' => $oid,
'tracking_number' => trim((string) ($row['tracking_number'] ?? '')),
];
}
try {
$signTsByPoId = ExpressTrackingService::batchResolveSignUnixFromDbForOrders($signItems);
} catch (\Throwable $e) {
Log::warning('appendPrescriptionOrderListExportRows batch sign_time failed: ' . $e->getMessage());
$signTsByPoId = [];
}
}
$firstVisitAssistantByDiag = self::batchFirstVisitAssistantNameByDiagnosis($diagIdList, $diagById);
$assistantDeptCache = [];
foreach ($lists as &$item) {
$poId = (int) ($item['id'] ?? 0);
$fs = (int) ($item['fulfillment_status'] ?? 0);
$item['export_fulfillment_status_text'] = self::fulfillmentStatusLabel($fs);
$ct = (int) ($item['create_time'] ?? 0);
$item['export_order_time'] = $ct > 0 ? date('Y-m-d H:i:s', $ct) : '';
$diagId = (int) ($item['diagnosis_id'] ?? 0);
$dg = $diagById[$diagId] ?? null;
$rxId = (int) ($item['prescription_id'] ?? 0);
$rx = $rxById[$rxId] ?? [];
$item['export_patient_name'] = \is_array($dg) ? (string) ($dg['patient_name'] ?? '') : '';
if (\is_array($dg)) {
$g = (int) ($dg['gender'] ?? 0);
$item['export_patient_gender'] = $g === 1 ? '男' : '女';
$item['export_patient_age'] = (string) ($dg['age'] ?? '');
$item['export_patient_phone'] = (string) ($dg['phone'] ?? '');
} else {
$item['export_patient_gender'] = '';
$item['export_patient_age'] = '';
$item['export_patient_phone'] = '';
}
// 导出「医助名字 / 医助部门」:与详情 order_creator 一致,按业务订单 creator_id(非诊单二诊 assistant_id
$creatorId = (int) ($item['creator_id'] ?? 0);
$item['assistant_name'] = (string) ($item['creator_name'] ?? '');
if ($creatorId > 0) {
if (!isset($assistantDeptCache[$creatorId])) {
$assistantDeptCache[$creatorId] = self::formatAssistantDeptPathForExport($creatorId);
}
$item['export_assistant_dept'] = $assistantDeptCache[$creatorId];
} else {
$item['export_assistant_dept'] = '';
}
$item['export_medication_form'] = self::formatMedicationFormForExport(\is_array($rx) ? $rx : []);
$item['export_service_package'] = self::formatServicePackageForExport(
$item['service_package'] ?? '',
$packageNameByValue
);
$item['export_channel'] = (string) ($item['service_channel'] ?? '');
$apptIdResolved = $resolvedApptIdByPoId[$poId] ?? 0;
$apCh = ($apptIdResolved > 0 && isset($apptById[$apptIdResolved])) ? $apptById[$apptIdResolved] : null;
$item['export_guahao_channel_source'] = \is_array($apCh)
? self::formatGuahaoChannelSourceForExport($apCh, $channelNameByValue)
: '';
$md = $item['medication_days'] ?? null;
$item['export_medication_days'] = $md !== null && $md !== '' ? (string) $md : '';
$amt = round((float) ($item['amount'] ?? 0), 2);
$paid = round((float) ($item['linked_pay_paid_total'] ?? 0), 2);
$item['export_amount'] = number_format($amt, 2, '.', '');
$item['export_paid_amount'] = number_format($paid, 2, '.', '');
$item['export_agency_collect'] = number_format(round($amt - $paid, 2), 2, '.', '');
$item['export_tracking_number'] = (string) ($item['tracking_number'] ?? '');
$signTs = $poId > 0 ? (int) ($signTsByPoId[$poId] ?? 0) : 0;
$item['export_sign_time'] = $signTs > 0 ? date('Y-m-d', $signTs) : '';
$isGc = trim((string) ($item['gancao_reciperl_order_no'] ?? '')) !== '';
$item['export_supply_mode'] = $isGc ? '甘草' : '自营';
// 「处方成本」列:与列表/详情一致,取订单 internal_cost(含「测试价格」预报价写入;甘草/自营均导出)
if (self::canViewInternalCost($adminInfo)) {
$rawCost = $item['internal_cost'] ?? null;
$item['export_gancao_prescription_cost'] = $rawCost !== null && $rawCost !== ''
? number_format(round((float) $rawCost, 2), 2, '.', '')
: '';
} else {
$item['export_gancao_prescription_cost'] = '';
}
$isFu = (int) ($item['is_follow_up'] ?? 0) === 1;
$item['export_first_visit_assistant'] = $isFu
? (string) ($firstVisitAssistantByDiag[(int) ($item['diagnosis_id'] ?? 0)] ?? '')
: '';
$hit = (int) ($item['po_assign_snapshot_hit'] ?? 0);
$rel = (int) ($item['po_assign_is_release'] ?? 0);
$er = (int) ($item['po_assign_to_er_center'] ?? 0);
if ($hit !== 1) {
$item['export_center_label'] = '—';
} elseif ($rel === 1) {
$item['export_center_label'] = '已释放';
} elseif ($er === 1) {
$item['export_center_label'] = '二中心';
} else {
$item['export_center_label'] = '一中心';
}
$paSt = (int) ($item['prescription_audit_status'] ?? 0);
$aud = $auditEffectiveByPo[$poId] ?? null;
if (($paSt === 1 || $paSt === 2) && \is_array($aud)) {
$at = (int) ($aud['create_time'] ?? 0);
$item['export_rx_audit_time'] = $at > 0 ? date('Y-m-d H:i:s', $at) : '';
$item['export_rx_auditor'] = (string) ($aud['admin_name'] ?? '');
} else {
$item['export_rx_audit_time'] = '';
$item['export_rx_auditor'] = '';
}
}
unset($item);
}
/**
* 完成订单时把关联诊单的 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 '';
}
// 存在医助指派日志时,保留当前医助分配,不自动清空
$assignLogCount = DiagnosisAssignLog::where('diagnosis_id', $diagnosisId)
->whereNull('delete_time')
->count();
if ($assignLogCount > 0) {
return ';检测到该患者已有医助指派记录,保留当前医助分配(assistant_id=' . $oldAssistant . '';
}
// 同一诊单下若仍有未完成/未取消的处方订单,则不清空
$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;
}
if (
self::canViewOrdersForOwnPrescription($adminInfo)
&& self::isPrescriptionOrderPrescriber((int) ($item['prescription_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),
/** 本业务订单金额(与甘草预报价合计对比占比用) */
'amount' => round((float) ($order->amount ?? 0), 2),
];
}
/**
* 设置发货类型(甘草 / 洛阳),与确认发货选项一致
*
* @return array<string,mixed>|false
*/
public static function setShipMode(int $id, string $shipMode, int $adminId, array $adminInfo)
{
self::$error = '';
$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;
}
$shipMode = self::normalizeShipMode($shipMode);
if ($shipMode === 'direct' && trim((string) $order->gancao_reciperl_order_no) !== '') {
self::$error = '已上传甘草药方,发货类型须为甘草药房';
return false;
}
$oldMode = self::normalizeShipMode((string) ($order->ship_mode ?? 'gancao'));
if ($oldMode === $shipMode) {
$out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo);
self::maskRemarkExtraIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
$order->ship_mode = $shipMode;
try {
$order->save();
} catch (\Throwable $e) {
self::$error = $e->getMessage();
return false;
}
$oldText = $oldMode === 'direct' ? '洛阳药房' : '甘草药房';
$newText = $shipMode === 'direct' ? '洛阳药房' : '甘草药房';
self::writeLog($id, $adminId, $adminInfo, 'set_ship_mode', '发货类型:' . $oldText . ' → ' . $newText);
$out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo);
self::maskRemarkExtraIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
/**
* 修改订单金额
*/
public static function updateAmount(array $params, int $adminId, array $adminInfo): bool
{
self::$error = '';
$id = (int) $params['id'];
$newAmount = round((float) $params['amount'], 2);
// 查询订单
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
if (!$order) {
self::setError('订单不存在');
return false;
}
// 数据权限检查
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
self::setError('无权限操作此订单');
return false;
}
// 状态检查:已完成(3)和已取消(4)不允许修改
if (in_array((int) $order->fulfillment_status, [3, 4], true)) {
self::setError('已完成或已取消的订单不允许修改金额');
return false;
}
$oldAmount = round((float) $order->amount, 2);
$oldAmountCents = (int) round($oldAmount * 100);
$newAmountCents = (int) round($newAmount * 100);
// 金额未变化
if ($newAmountCents === $oldAmountCents) {
self::setError('金额未发生变化');
return false;
}
// 更新金额
$order->amount = $newAmount;
try {
$order->save();
} catch (\Throwable $e) {
self::setError($e->getMessage());
return false;
}
// 记录日志
$summary = sprintf('将订单金额从 ¥%.2f 修改为 ¥%.2f', $oldAmount, $newAmount);
self::writeLog($id, $adminId, $adminInfo, 'update_amount', $summary);
return true;
}
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) {
// 忽略日志写入错误
}
}
/**
* 处方患者姓名/手机号修正日志(写入 zyt_tcm_prescription_order_log,与 patchPrescriptionPatient 同源)
*
* @param int $prescriptionOrderId 无关联订单时可传 0(若库表禁止则可能静默失败)
*/
public static function appendRxPatientPatchLog(int $prescriptionOrderId, int $adminId, array $adminInfo, string $summary): void
{
self::writeLog($prescriptionOrderId, $adminId, $adminInfo, 'patch_rx_patient', $summary);
}
}