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 */ 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 $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 $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 $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 $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; // 解除关联后刷新「需代收」固定快照(已付归零 → 需代收=总金额) self::refreshAgencyCollectSnapshot((int) $order->id); } /** * 支付单不可同时关联多条「有效」业务订单(未删除且未撤回/取消);$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> */ 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', 'payment_method', 'create_type']) ->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 $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(); } /** * 刷新「需代收」固定快照:按当前 总金额 − 关联已付总额(支付单 status ∈ {2 已支付, 5 待审核})写入 agency_collect_amount。 * 在关联/新增支付单、建单、改总金额、解除关联等写操作末尾调用;之后退款/审核状态变化不再影响该值。 */ private static function refreshAgencyCollectSnapshot(int $id): void { if ($id <= 0) { return; } $order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find(); if (!$order) { return; } $amount = round((float) $order->amount, 2); $paid = 0.0; $ids = self::linkedPayOrderIdList($id); if ($ids !== []) { $payOrders = Order::whereIn('id', $ids)->whereNull('delete_time')->field(['amount', 'status'])->select(); foreach ($payOrders as $po) { $st = (int) $po->status; if ($st === 2 || $st === 5) { $paid += (float) $po->amount; } } } $order->agency_collect_amount = round($amount - $paid, 2); $order->save(); } /** * @param array $params * @return array|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); } self::refreshAgencyCollectSnapshot((int) $order->id); $out = PrescriptionOrder::where('id', (int) $order->id)->find()->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) { // 须包含 id:Admin 模型 $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'])) { $rx = &$arr['prescription']; $herbs = is_array($rx['herbs'] ?? null) ? $rx['herbs'] : []; $hasAux = false; foreach ($herbs as $h) { if (is_array($h) && (string) ($h['formula_type'] ?? '') === '辅方') { $hasAux = true; break; } } $rx['has_aux_formula'] = $hasAux ? 1 : 0; unset($rx['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 */ private static function channelSourceDictMap(): array { try { /** @var array $m */ $m = Db::name('dict_data')->where('type_value', 'channels')->column('name', 'value'); return $m; } catch (\Throwable) { return []; } } /** * @param array $row doctor_appointment 行 * @param array $channelDict * * @return array */ 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|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> $lists * @param array $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 $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|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; } /** * 直接调用京东官方物流接口刷新轨迹并落库(后台「京东接口更新」按钮)。 * * @return array{success:bool, message:string, traces_count:int, state:string}|null */ public static function logisticsJdUpdate(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; } $num = trim((string) ($row->tracking_number ?? '')); if ($num === '') { self::$error = '未填写快递单号'; return null; } $ret = \app\common\service\ExpressTrackingService::queryJdOfficialAndUpdate($num); if (empty($ret['success'])) { self::$error = (string) ($ret['message'] ?? '京东物流更新失败'); return null; } return [ 'success' => true, 'message' => (string) ($ret['message'] ?? '京东物流轨迹已更新'), 'traces_count' => (int) ($ret['traces_count'] ?? 0), 'state' => (string) ($ret['state'] ?? ''), ]; } /** * @param array $params * @return array|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', '编辑了业务订单及相关信息'); self::refreshAgencyCollectSnapshot((int) $order->id); $out = PrescriptionOrder::where('id', (int) $order->id)->find()->toArray(); self::maskInternalCostIfNeeded($out, $adminInfo); self::maskRemarkExtraIfNeeded($out, $adminInfo); self::attachLinkedPayOrders($out); return $out; } /** * 甘草已提交后仅更新物流字段(tracking_number、express_company),忽略金额/地址等其它请求参数。 * * @param array $params * @return array|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|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|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|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|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|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|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); // 将关联的已支付补齐支付单恢复为待审核:兼容旧 manual 与新 fubei/create_type=fubei 两种标记 $payOrderIds = self::linkedPayOrderIdList($id); if (!empty($payOrderIds)) { Order::whereIn('id', $payOrderIds) ->where('status', 2) // 只恢复已支付状态的 ->where(function ($query) { $query->where('payment_method', 'manual') ->whereOr('payment_method', 'fubei') ->whereOr('create_type', 'fubei'); }) ->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> */ 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 $params id, order_type, amount, remark * @return array|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 = 'fubei'; // 补齐支付单按「付呗」记账 $payOrder->create_type = 'fubei'; $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); self::refreshAgencyCollectSnapshot($id); $out = PrescriptionOrder::where('id', $id)->find()->toArray(); self::maskInternalCostIfNeeded($out, $adminInfo); self::maskRemarkExtraIfNeeded($out, $adminInfo); self::attachLinkedPayOrders($out); return $out; } /** * 为「已发货/已签收」(fulfillment_status=5/6) 的业务订单关联已有支付单, * 关联后将支付审核状态重置为待审核以启动再次审核流程。 * * @param array $params id, pay_order_id * @return array|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); self::refreshAgencyCollectSnapshot($id); $out = PrescriptionOrder::where('id', $id)->find()->toArray(); self::maskInternalCostIfNeeded($out, $adminInfo); self::maskRemarkExtraIfNeeded($out, $adminInfo); self::attachLinkedPayOrders($out); return $out; } /** * 已发货/已签收订单:不新增/关联支付单,仅提交完单申请并将支付审核置为待审核。 * * @param array $params id * @return array|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|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> */ private static function linkedPayRefundableRows(int $prescriptionOrderId): array { $ids = self::linkedPayOrderIdList($prescriptionOrderId); if ($ids === []) { return []; } return Order::whereIn('id', $ids) ->whereNull('delete_time') ->whereIn('status', [2, 5]) ->field(['id', 'amount']) ->order('id', 'asc') ->select() ->toArray(); } private static function linkedPayRefundableTotal(int $prescriptionOrderId): float { $sum = 0.0; foreach (self::linkedPayRefundableRows($prescriptionOrderId) as $row) { $sum += (float) ($row['amount'] ?? 0); } return round($sum, 2); } /** * 业务订单退款:须填写原因;可指定退款金额(部分退)或退满可退上限(全额)。 * * @param float|null $refundAmount 为空或 0 表示退满可退上限 * @return array|false */ public static function refund(int $id, string $reason, int $adminId, array $adminInfo, ?float $refundAmount = null) { 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 (!in_array($curFs, [3, 5, 6, 9], true)) { self::$error = '仅「已发货」「已签收」「已完成」或「拒收」状态可退款'; return false; } if ((int) $order->payment_slip_audit_status !== 1) { self::$error = '支付单审核尚未通过,不可退款'; return false; } $maxRefundable = self::linkedPayRefundableTotal($id); $currentPaid = round((float) $order->paid, 2); $cap = $maxRefundable > 0 ? $maxRefundable : $currentPaid; if ($cap <= 0) { self::$error = '当前无可退金额'; return false; } if ($refundAmount === null || $refundAmount <= 0) { $refundAmount = $cap; } else { $refundAmount = round($refundAmount, 2); if ($refundAmount > $cap + 0.009) { self::$error = '退款金额不能超过可退上限 ¥' . $cap; return false; } } $isFullRefund = $refundAmount >= $cap - 0.009; $toRefundRows = self::linkedPayRefundableRows($id); $refundedPayCount = 0; $markedPayTotal = 0.0; try { if ($isFullRefund && $toRefundRows !== []) { $ids = array_map('intval', array_column($toRefundRows, 'id')); $refundedPayCount = Order::whereIn('id', $ids) ->whereNull('delete_time') ->update(['status' => 4]); foreach ($toRefundRows as $pr) { $markedPayTotal += (float) ($pr['amount'] ?? 0); } $markedPayTotal = round($markedPayTotal, 2); } elseif (!$isFullRefund && $toRefundRows !== []) { $remaining = $refundAmount; foreach ($toRefundRows as $pr) { if ($remaining < 0.01) { break; } $amt = round((float) ($pr['amount'] ?? 0), 2); if ($amt <= $remaining + 0.001) { Order::where('id', (int) $pr['id']) ->whereNull('delete_time') ->update(['status' => 4]); $remaining -= $amt; $markedPayTotal += $amt; $refundedPayCount++; } } $markedPayTotal = round($markedPayTotal, 2); } if ($isFullRefund) { $order->fulfillment_status = 10; $order->paid = 0; } else { $order->paid = round(max(0, $currentPaid - $refundAmount), 2); if ($order->paid <= 0.009) { $order->paid = 0; $order->fulfillment_status = 10; } } $prevStoredRefund = round((float) ($order->refund_amount ?? 0), 2); $order->refund_amount = round($prevStoredRefund + $refundAmount, 2); $order->save(); } catch (\Throwable $e) { self::$error = $e->getMessage(); return false; } $unassignSummary = ''; if ($isFullRefund || (float) $order->paid <= 0.009) { $unassignSummary = self::clearDiagnosisAssistantOnComplete((int) $order->diagnosis_id); } if ($isFullRefund) { $logLine = '订单全额退款,状态变更为「退款」;原因:' . $reason . ';退款金额 ¥' . $refundAmount; } else { $logLine = '订单部分退款;原因:' . $reason . ';本次退款 ¥' . $refundAmount . ';已付余额 ¥' . $order->paid; } if ($refundedPayCount > 0) { $logLine .= ';关联收款单 ' . $refundedPayCount . ' 笔已标记为已退款'; if ($markedPayTotal > 0) { $logLine .= '(收款侧合计 ¥' . $markedPayTotal . ')'; } } elseif ($toRefundRows !== []) { $logLine .= ';关联收款单无匹配的已支付/待审核记录可标记'; } if ($isFullRefund) { $logLine .= ';已付总额清零' . $unassignSummary; } self::writeLog($id, $adminId, $adminInfo, 'refund', $logLine); $out = $order->toArray(); $out['this_refund_amount'] = $refundAmount; $out['refund_full'] = $isFullRefund; $out['refundable_cap'] = $cap; 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> $diagById * @return array 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 $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 $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 $apRow doctor_appointment 一行 * @param array $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> $lists * @param array $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 $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', 'prescription_name', 'aux_usage', 'herbs', 'creator_id']) ->select() ->toArray(); foreach ($rxRows as $xr) { $rxById[(int) $xr['id']] = $xr; } } // 药方名称反查:处方库(开方医师 creator_id + 公开模板),按 formula_type + 药材集合匹配, // 复刻前端 resolveSlipAuxLibraryName 口径(主方/辅方均如此;处方记录 prescription_name 仅做兜底)。 [$libByDoctor, $libPublic] = self::buildPrescriptionLibraryNameIndexForExport($rxById); 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 $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 : []); // 药方名称:主方/辅方均反查处方库取名称(与处方笺一致),带标签;甘草/无匹配留空 [$mainRxName, $auxRxName] = self::resolvePrescriptionNamesForExport( \is_array($rx) ? $rx : [], $libByDoctor, $libPublic ); $rxNameParts = []; if ($mainRxName !== '') { $rxNameParts[] = '主方:' . $mainRxName; } if ($auxRxName !== '') { $rxNameParts[] = '辅方:' . $auxRxName; } $item['export_prescription_name'] = implode(' ', $rxNameParts); $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, '.', ''); $refundStored = round((float) ($item['refund_amount'] ?? 0), 2); $item['export_refund_amount'] = $refundStored > 0 ? number_format($refundStored, 2, '.', '') : ''; // 需代收:优先用固定快照值 agency_collect_amount;历史单为 NULL 时回退实时 amt − paid $agencySnap = $item['agency_collect_amount'] ?? null; $item['export_agency_collect'] = ($agencySnap !== null && $agencySnap !== '') ? number_format(round((float) $agencySnap, 2), 2, '.', '') : 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); } /** * 导出药方名称反查:批量加载相关开方医师处方库(creator_id 命中 + 公开模板), * 建立 [creator_id][formula_type][herbKey] => name 与 公开 [formula_type][herbKey] => name 两级索引。 * * @param array $rxById 处方 id => 处方行(需含 herbs / creator_id) * * @return array{0: array>>, 1: array>} */ private static function buildPrescriptionLibraryNameIndexForExport(array $rxById): array { $libByDoctor = []; $libPublic = []; if ($rxById === []) { return [$libByDoctor, $libPublic]; } $doctorIds = []; foreach ($rxById as $rx) { $cid = (int) ($rx['creator_id'] ?? 0); if ($cid > 0) { $doctorIds[$cid] = true; } } $doctorIdList = array_keys($doctorIds); try { $libRows = PrescriptionLibrary::whereNull('delete_time') ->where(function ($q) use ($doctorIdList) { if ($doctorIdList !== []) { $q->whereIn('creator_id', $doctorIdList)->whereOr('is_public', 1); } else { $q->where('is_public', 1); } }) ->field(['prescription_name', 'formula_type', 'herbs', 'creator_id', 'is_public']) ->select() ->toArray(); } catch (\Throwable $e) { Log::warning('buildPrescriptionLibraryNameIndexForExport failed: ' . $e->getMessage()); return [$libByDoctor, $libPublic]; } foreach ($libRows as $lib) { $name = trim((string) ($lib['prescription_name'] ?? '')); if ($name === '') { continue; } $herbsRaw = $lib['herbs'] ?? null; if (\is_string($herbsRaw) && $herbsRaw !== '') { $decoded = json_decode($herbsRaw, true); $herbsRaw = \is_array($decoded) ? $decoded : []; } if (!\is_array($herbsRaw) || $herbsRaw === []) { continue; } $key = self::herbSetMatchKeyForExport($herbsRaw); if ($key === '') { continue; } $ft = ((string) ($lib['formula_type'] ?? '')) === '辅方' ? '辅方' : '主方'; if ((int) ($lib['is_public'] ?? 0) === 1) { $libPublic[$ft][$key] ??= $name; } $cid = (int) ($lib['creator_id'] ?? 0); if ($cid > 0) { $libByDoctor[$cid][$ft][$key] ??= $name; } } return [$libByDoctor, $libPublic]; } /** * 反查单张处方的主方 / 辅方名称(与处方笺展示一致): * - 主方:主方药材集合匹配开方医师/公开处方库(formula_type=主方);兜底取处方 prescription_name。 * - 辅方:优先 aux_usage.prescription_name / library_name,否则辅方药材集合匹配(formula_type=辅方)。 * * @param array $rx 处方行(含 herbs / creator_id / prescription_name / aux_usage) * @param array>> $libByDoctor * @param array> $libPublic * * @return array{0: string, 1: string} [主方名称, 辅方名称] */ private static function resolvePrescriptionNamesForExport(array $rx, array $libByDoctor, array $libPublic): array { if ($rx === []) { return ['', '']; } $doctorId = (int) ($rx['creator_id'] ?? 0); $herbs = $rx['herbs'] ?? null; if (\is_string($herbs) && $herbs !== '') { $decoded = json_decode($herbs, true); $herbs = \is_array($decoded) ? $decoded : []; } if (!\is_array($herbs)) { $herbs = []; } $mainHerbs = []; $auxHerbs = []; foreach ($herbs as $h) { if (!\is_array($h)) { continue; } if (((string) ($h['formula_type'] ?? '')) === '辅方') { $auxHerbs[] = $h; } else { $mainHerbs[] = $h; } } $lookup = static function (string $ft, array $hs) use ($doctorId, $libByDoctor, $libPublic): string { if ($hs === []) { return ''; } $key = self::herbSetMatchKeyForExport($hs); if ($key === '') { return ''; } if ($doctorId > 0 && isset($libByDoctor[$doctorId][$ft][$key])) { return $libByDoctor[$doctorId][$ft][$key]; } return (string) ($libPublic[$ft][$key] ?? ''); }; // 主方 $mainName = $lookup('主方', $mainHerbs); if ($mainName === '') { $mainName = trim((string) ($rx['prescription_name'] ?? '')); } // 辅方:先读持久化字段 $auxName = ''; $auxUsage = $rx['aux_usage'] ?? null; if (\is_string($auxUsage) && $auxUsage !== '') { $decodedAux = json_decode($auxUsage, true); $auxUsage = \is_array($decodedAux) ? $decodedAux : null; } if (\is_array($auxUsage)) { $auxName = trim((string) ($auxUsage['prescription_name'] ?? ($auxUsage['library_name'] ?? ''))); } if ($auxName === '') { $auxName = $lookup('辅方', $auxHerbs); } return [$mainName, $auxName]; } /** * 药材集合匹配 key:每味 name:dosage(dosage 规整去尾零)排序后拼接,与处方库反查双方同口径。 */ private static function herbSetMatchKeyForExport(array $herbs): string { $parts = []; foreach ($herbs as $h) { if (!\is_array($h)) { continue; } $name = trim((string) ($h['name'] ?? '')); if ($name === '') { continue; } $dosage = (float) ($h['dosage'] ?? 0); if (floor($dosage) === $dosage) { $dStr = (string) (int) $dosage; } else { $dStr = rtrim(rtrim(number_format($dosage, 4, '.', ''), '0'), '.'); } $parts[] = $name . ':' . $dStr; } sort($parts); return implode('|', $parts); } /** * 完成订单时把关联诊单的 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 $item * @param array $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|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|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|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; } // 总金额变化后刷新「需代收」固定快照,保持与新总额一致 self::refreshAgencyCollectSnapshot($id); // 记录日志 $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); } }