4149 lines
155 KiB
PHP
Executable File
4149 lines
155 KiB
PHP
Executable File
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\adminapi\logic\tcm;
|
||
|
||
use app\adminapi\logic\stats\YejiStatsLogic;
|
||
use app\adminapi\logic\auth\AuthLogic;
|
||
use app\adminapi\logic\order\OrderLogic;
|
||
use app\common\model\Order;
|
||
use app\common\model\tcm\Diagnosis;
|
||
use app\common\model\tcm\DiagnosisAssignLog;
|
||
use app\common\model\tcm\Prescription;
|
||
use app\common\model\tcm\PrescriptionLibrary;
|
||
use app\common\model\tcm\PrescriptionOrder;
|
||
use app\common\model\tcm\PrescriptionOrderLog;
|
||
use app\common\model\tcm\PrescriptionOrderPayOrder;
|
||
use app\common\model\dict\DictData;
|
||
use app\common\model\doctor\Appointment;
|
||
use app\common\model\auth\Admin;
|
||
use app\common\service\DataScope\DataScopeService;
|
||
use app\common\service\ExpressTrackService;
|
||
use app\common\service\ExpressTrackingService;
|
||
use app\common\service\gancao\GancaoScmRecipelService;
|
||
use think\db\Query;
|
||
use think\facade\Config;
|
||
use think\facade\Db;
|
||
use think\facade\Log;
|
||
|
||
class PrescriptionOrderLogic
|
||
{
|
||
private static string $error = '';
|
||
|
||
public static function setError(string $msg): void
|
||
{
|
||
self::$error = $msg;
|
||
}
|
||
|
||
public static function getError(): string
|
||
{
|
||
return self::$error;
|
||
}
|
||
|
||
/**
|
||
* 部门自底向上链式名称,含父级(如:总部 / 华东 / 上海门诊)
|
||
* 使用 Db 直查 zyt_dept:避免 Dept 软删除全局作用域导致有 dept_id 仍拼不出路径
|
||
*/
|
||
private static function buildDeptPath(int $deptId): string
|
||
{
|
||
if ($deptId <= 0) {
|
||
return '';
|
||
}
|
||
$names = [];
|
||
$id = $deptId;
|
||
for ($i = 0; $i < 50; $i++) {
|
||
$row = Db::name('dept')->where('id', $id)->field(['id', 'name', 'pid'])->find();
|
||
if (empty($row)) {
|
||
break;
|
||
}
|
||
$r = is_array($row) ? $row : $row->toArray();
|
||
array_unshift($names, (string) ($r['name'] ?? ''));
|
||
$pid = (int) ($r['pid'] ?? 0);
|
||
if ($pid <= 0) {
|
||
break;
|
||
}
|
||
$id = $pid;
|
||
}
|
||
|
||
if ($names === []) {
|
||
return '';
|
||
}
|
||
$names = array_filter($names, static fn (string $n): bool => $n !== '');
|
||
|
||
return implode(' / ', $names);
|
||
}
|
||
|
||
/**
|
||
* 管理员在 zyt_admin_dept 中的部门路径(多部门用「;」、含父级「 / 」)
|
||
*/
|
||
private static function buildAdminDeptPathJoined(int $adminId): string
|
||
{
|
||
if ($adminId <= 0) {
|
||
return '';
|
||
}
|
||
$deptIds = Db::name('admin_dept')->where('admin_id', $adminId)->column('dept_id');
|
||
$pathSegmentsC = [];
|
||
foreach ($deptIds as $didC) {
|
||
$pc = self::buildDeptPath((int) $didC);
|
||
if ($pc !== '') {
|
||
$pathSegmentsC[] = $pc;
|
||
}
|
||
}
|
||
$pathSegmentsC = array_values(array_unique($pathSegmentsC));
|
||
|
||
return $pathSegmentsC === [] ? '' : implode(';', $pathSegmentsC);
|
||
}
|
||
|
||
/**
|
||
* @param int[] $allowRoleIds
|
||
*/
|
||
private static function roleIntersect(array $adminInfo, array $allowRoleIds): bool
|
||
{
|
||
$myRoles = array_map('intval', $adminInfo['role_id'] ?? []);
|
||
$allow = array_map('intval', $allowRoleIds);
|
||
|
||
return count(array_intersect($myRoles, $allow)) > 0;
|
||
}
|
||
|
||
/** 与 order 列表一致:超管或 order_edit_all_roles 可见全部业务订单 */
|
||
public static function canSeeAllPrescriptionOrders(array $adminInfo): bool
|
||
{
|
||
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
|
||
return true;
|
||
}
|
||
$roles = Config::get('project.order_edit_all_roles', [0, 3]);
|
||
|
||
return self::roleIntersect($adminInfo, is_array($roles) ? $roles : []);
|
||
}
|
||
|
||
/**
|
||
* 「下单」角色:列表「审核人」筛选仅允许查本人审核过的订单(经理/管理员等豁免)
|
||
*/
|
||
public static function isOrderPlacerAuditFilterRestricted(array $adminInfo): bool
|
||
{
|
||
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
|
||
return false;
|
||
}
|
||
$placer = Config::get('project.prescription_order_placer_role_ids', [6]);
|
||
if (!self::roleIntersect($adminInfo, is_array($placer) ? $placer : [])) {
|
||
return false;
|
||
}
|
||
$exempt = Config::get('project.prescription_order_placer_exempt_role_ids', [3, 8]);
|
||
|
||
return !self::roleIntersect($adminInfo, is_array($exempt) ? $exempt : []);
|
||
}
|
||
|
||
/** @return string[] */
|
||
public static function prescriptionOrderAuditLogActions(): array
|
||
{
|
||
return ['audit_rx_approve', 'audit_rx_reject', 'audit_pay_approve', 'audit_pay_reject'];
|
||
}
|
||
|
||
/** 是否展示「下单人」筛选(下单 / 经理 / 管理员 / 超管) */
|
||
public static function shouldShowAuditAdminFilter(array $adminInfo): bool
|
||
{
|
||
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
|
||
return true;
|
||
}
|
||
if (self::isOrderPlacerAuditFilterRestricted($adminInfo)) {
|
||
return true;
|
||
}
|
||
$exempt = Config::get('project.prescription_order_placer_exempt_role_ids', [3, 8]);
|
||
|
||
return self::roleIntersect($adminInfo, is_array($exempt) ? $exempt : []);
|
||
}
|
||
|
||
/**
|
||
* 下单人下拉:直接取「下单」角色下的后台用户(project.prescription_order_placer_role_ids)
|
||
*
|
||
* @return array<int, array{id: int, name: string}>
|
||
*/
|
||
public static function listAuditAdminOptions(int $adminId, array $adminInfo): array
|
||
{
|
||
if ($adminId <= 0 || !self::shouldShowAuditAdminFilter($adminInfo)) {
|
||
return [];
|
||
}
|
||
|
||
$roleIds = Config::get('project.prescription_order_placer_role_ids', [6]);
|
||
$roleIds = array_values(array_unique(array_filter(array_map(
|
||
'intval',
|
||
is_array($roleIds) ? $roleIds : []
|
||
), static fn(int $id): bool => $id > 0)));
|
||
if ($roleIds === []) {
|
||
$roleIds = [6];
|
||
}
|
||
|
||
if (self::isOrderPlacerAuditFilterRestricted($adminInfo)) {
|
||
$name = trim((string) ($adminInfo['name'] ?? ''));
|
||
|
||
return [
|
||
[
|
||
'id' => $adminId,
|
||
'name' => $name !== '' ? $name : ('管理员#' . $adminId),
|
||
],
|
||
];
|
||
}
|
||
|
||
$rows = Db::name('admin')
|
||
->alias('a')
|
||
->join('admin_role ar', 'a.id = ar.admin_id')
|
||
->whereIn('ar.role_id', $roleIds)
|
||
->where('a.disable', 0)
|
||
->whereNull('a.delete_time')
|
||
->field(['a.id', 'a.name'])
|
||
->distinct(true)
|
||
->order('a.name', 'asc')
|
||
->select()
|
||
->toArray();
|
||
|
||
$out = [];
|
||
foreach ($rows as $r) {
|
||
$id = (int) ($r['id'] ?? 0);
|
||
if ($id <= 0) {
|
||
continue;
|
||
}
|
||
$name = trim((string) ($r['name'] ?? ''));
|
||
if ($name === '') {
|
||
$name = '管理员#' . $id;
|
||
}
|
||
$out[] = ['id' => $id, 'name' => $name];
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
/** 指定后台账号是否拥有「下单」角色 */
|
||
public static function adminHasPlacerRole(int $adminId): bool
|
||
{
|
||
if ($adminId <= 0) {
|
||
return false;
|
||
}
|
||
$roleIds = Config::get('project.prescription_order_placer_role_ids', [6]);
|
||
$roleIds = array_values(array_unique(array_filter(array_map(
|
||
'intval',
|
||
is_array($roleIds) ? $roleIds : []
|
||
), static fn(int $id): bool => $id > 0)));
|
||
if ($roleIds === []) {
|
||
$roleIds = [6];
|
||
}
|
||
|
||
return Db::name('admin_role')
|
||
->where('admin_id', $adminId)
|
||
->whereIn('role_id', $roleIds)
|
||
->count() > 0;
|
||
}
|
||
|
||
/**
|
||
* 菜单权限:非全量角色可额外查看「关联处方开方人为本账号」的业务订单(医助代建单)
|
||
*
|
||
* perms: tcm.prescriptionOrder/viewOrdersForOwnPrescription
|
||
*/
|
||
public static function canViewOrdersForOwnPrescription(array $adminInfo): bool
|
||
{
|
||
$perms = AuthLogic::getAuthByAdminId((int) ($adminInfo['admin_id'] ?? 0));
|
||
|
||
return in_array('tcm.prescriptionOrder/viewOrdersForOwnPrescription', $perms, true);
|
||
}
|
||
|
||
/**
|
||
* 业务订单关联处方的开方人是否为指定管理员(处方 creator_id)
|
||
*/
|
||
public static function isPrescriptionOrderPrescriber(int $prescriptionId, int $adminId): bool
|
||
{
|
||
if ($prescriptionId <= 0 || $adminId <= 0) {
|
||
return false;
|
||
}
|
||
$cid = (int) Prescription::where('id', $prescriptionId)->whereNull('delete_time')->value('creator_id');
|
||
|
||
return $cid === $adminId;
|
||
}
|
||
|
||
/**
|
||
* 与 PrescriptionOrderLists 数据域一致:订单创建人属于当前账号可见 admin 集合
|
||
*/
|
||
private static function orderWithinDataScopeOwners(PrescriptionOrder $row, int $adminId, array $adminInfo): bool
|
||
{
|
||
if (!DataScopeService::isEnabled()) {
|
||
return false;
|
||
}
|
||
$ids = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
|
||
if ($ids === null || $ids === []) {
|
||
return false;
|
||
}
|
||
|
||
return in_array((int) $row->creator_id, $ids, true);
|
||
}
|
||
|
||
/**
|
||
* 是否已分配「处方业务订单」相关菜单/按钮权限(lists、detail、审核等任一子权限即可)
|
||
*/
|
||
public static function hasPrescriptionOrderMenuAccess(array $adminInfo): bool
|
||
{
|
||
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
|
||
return true;
|
||
}
|
||
$perms = AuthLogic::getAuthByAdminId((int) ($adminInfo['admin_id'] ?? 0));
|
||
foreach ($perms as $p) {
|
||
if (is_string($p) && str_starts_with($p, 'tcm.prescriptionOrder/')) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* @param PrescriptionOrder $row
|
||
*/
|
||
public static function canAccessOrder($row, int $adminId, array $adminInfo): bool
|
||
{
|
||
if (self::canSeeAllPrescriptionOrders($adminInfo)) {
|
||
return true;
|
||
}
|
||
if (self::hasPrescriptionOrderMenuAccess($adminInfo)) {
|
||
return true;
|
||
}
|
||
if ((int) $row->creator_id === $adminId) {
|
||
return true;
|
||
}
|
||
if (self::canViewOrdersForOwnPrescription($adminInfo)) {
|
||
if (self::isPrescriptionOrderPrescriber((int) $row->prescription_id, $adminId)) {
|
||
return true;
|
||
}
|
||
}
|
||
if (self::orderWithinDataScopeOwners($row, $adminId, $adminInfo)) {
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 撤回:仅创建人或全量角色(避免开方医生仅「查看医助代建单」权限误撤他人单子)
|
||
*
|
||
* @param PrescriptionOrder $row
|
||
*/
|
||
public static function canWithdrawOrder($row, int $adminId, array $adminInfo): bool
|
||
{
|
||
if (self::canSeeAllPrescriptionOrders($adminInfo)) {
|
||
return true;
|
||
}
|
||
|
||
return (int) $row->creator_id === $adminId;
|
||
}
|
||
|
||
public static function canViewInternalCost(array $adminInfo): bool
|
||
{
|
||
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
|
||
return true;
|
||
}
|
||
$allow = Config::get('project.prescription_order_finance_roles', [0, 3]);
|
||
$allow = array_map('intval', is_array($allow) ? $allow : []);
|
||
$myRoles = array_map('intval', $adminInfo['role_id'] ?? []);
|
||
|
||
return count(array_intersect($myRoles, $allow)) > 0;
|
||
}
|
||
|
||
/**
|
||
* 业务订单详情内是否返回/展示处方「药材明细」(菜单权限 tcm.prescriptionOrder/viewRxHerbs)
|
||
*/
|
||
private static function canViewPrescriptionHerbsInOrderDetail(array $adminInfo): bool
|
||
{
|
||
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
|
||
return true;
|
||
}
|
||
|
||
$perms = AuthLogic::getAuthByAdminId((int) ($adminInfo['admin_id'] ?? 0));
|
||
|
||
return in_array('tcm.prescriptionOrder/viewRxHerbs', $perms, true);
|
||
}
|
||
|
||
public static function canAuditPrescriptionOrder(array $adminInfo): bool
|
||
{
|
||
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
|
||
return true;
|
||
}
|
||
$allow = Config::get('project.prescription_audit_roles', [0, 3, 6]);
|
||
|
||
return self::roleIntersect($adminInfo, is_array($allow) ? $allow : []);
|
||
}
|
||
|
||
public static function canAuditPaymentSlipOrder(array $adminInfo): bool
|
||
{
|
||
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
|
||
return true;
|
||
}
|
||
$allow = Config::get('project.prescription_order_payment_audit_roles', [0, 3]);
|
||
|
||
return self::roleIntersect($adminInfo, is_array($allow) ? $allow : []);
|
||
}
|
||
|
||
/**
|
||
* 列表顶栏金额统计:与「关联支付单」审核白名单同档的角色可见「全量」统计(不缩小到本人/医助诊单域)
|
||
*/
|
||
public static function canViewOrderListStatsAllScope(array $adminInfo): bool
|
||
{
|
||
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
|
||
return true;
|
||
}
|
||
$allow = Config::get('project.prescription_order_payment_audit_roles', [0, 3, 4, 9, 6]);
|
||
if (!is_array($allow)) {
|
||
$allow = [0, 3];
|
||
}
|
||
|
||
return self::roleIntersect($adminInfo, $allow);
|
||
}
|
||
|
||
/**
|
||
* 与业务订单列表金额统计一致:命中「统计医助」角色且非农单支付审核档时,业务侧按「仅限本人关联诊单的业绩域」收口。
|
||
* 提成结算等对业绩归因 SQL 收窄时需与之对齐。
|
||
*/
|
||
public static function statsRestrictedToOwnAssistantPerformanceDomain(array $adminInfo): bool
|
||
{
|
||
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
|
||
return false;
|
||
}
|
||
if (self::canViewOrderListStatsAllScope($adminInfo)) {
|
||
return false;
|
||
}
|
||
$assistantRid = (int) Config::get('project.prescription_order_stats_assistant_role_id', 2);
|
||
|
||
return $assistantRid > 0 && self::roleIntersect($adminInfo, [$assistantRid]);
|
||
}
|
||
|
||
/**
|
||
* 提成结算等与列表对齐:按开方医生 / 订单创建人 筛选。
|
||
*
|
||
* @param array<string, mixed> $params
|
||
*/
|
||
public static function applyCommissionOrderDoctorAssistantFilters(Query $query, string $alias, array $params): void
|
||
{
|
||
if (isset($params['doctor_id']) && (int) $params['doctor_id'] > 0) {
|
||
$doctorId = (int) $params['doctor_id'];
|
||
$rxTbl = (new Prescription())->getTable();
|
||
$query->whereExists(
|
||
"SELECT 1 FROM `{$rxTbl}` rx WHERE rx.`id` = `{$alias}`.`prescription_id` "
|
||
. "AND rx.`delete_time` IS NULL AND rx.`creator_id` = {$doctorId}"
|
||
);
|
||
}
|
||
|
||
if (isset($params['assistant_id']) && (int) $params['assistant_id'] > 0) {
|
||
$query->where("{$alias}.creator_id", (int) $params['assistant_id']);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 与处方订单列表 HasDataScopeFilter + applyCreatorOrOwnPrescriptionVisibility 等价;主业务订单别名 $alias。
|
||
*
|
||
* @param array<string, mixed> $params 可含 assistant_id(显式收窄时校验数据域)
|
||
*/
|
||
public static function applyPrescriptionOrderListAccessForCommissionQuery(
|
||
Query $query,
|
||
string $alias,
|
||
int $viewerAdminId,
|
||
array $viewerAdminInfo,
|
||
array $params = []
|
||
): void {
|
||
if ($viewerAdminId <= 0) {
|
||
return;
|
||
}
|
||
|
||
self::applyCommissionOrderDoctorAssistantFilters($query, $alias, $params);
|
||
|
||
if (self::canSeeAllPrescriptionOrders($viewerAdminInfo)) {
|
||
self::applyCommissionOrderDataScope($query, $alias, $viewerAdminId, $viewerAdminInfo);
|
||
|
||
return;
|
||
}
|
||
|
||
$rxTbl = (new Prescription())->getTable();
|
||
|
||
$explicitAssistant = (int) ($params['assistant_id'] ?? 0);
|
||
$allowExplicitAssistantVisibility = false;
|
||
if ($explicitAssistant > 0) {
|
||
if (!DataScopeService::isEnabled()) {
|
||
$allowExplicitAssistantVisibility = true;
|
||
} else {
|
||
$visibleIds = DataScopeService::getVisibleAdminIds($viewerAdminId, $viewerAdminInfo);
|
||
if ($visibleIds === null || in_array($explicitAssistant, $visibleIds, true)) {
|
||
$allowExplicitAssistantVisibility = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
$query->where(function ($q) use (
|
||
$alias,
|
||
$rxTbl,
|
||
$viewerAdminId,
|
||
$explicitAssistant,
|
||
$allowExplicitAssistantVisibility,
|
||
$viewerAdminInfo
|
||
): void {
|
||
if (self::canViewOrdersForOwnPrescription($viewerAdminInfo)) {
|
||
$q->where(function ($qq) use ($alias, $rxTbl, $viewerAdminId): void {
|
||
$qq->where("{$alias}.creator_id", $viewerAdminId);
|
||
$qq->whereOrRaw(
|
||
"EXISTS (SELECT 1 FROM `{$rxTbl}` rx WHERE rx.`id` = `{$alias}`.`prescription_id`"
|
||
. " AND rx.`delete_time` IS NULL AND rx.`creator_id` = {$viewerAdminId})"
|
||
);
|
||
});
|
||
} else {
|
||
$q->where("{$alias}.creator_id", $viewerAdminId);
|
||
}
|
||
if ($allowExplicitAssistantVisibility) {
|
||
$q->whereOr("{$alias}.creator_id", $explicitAssistant);
|
||
}
|
||
});
|
||
|
||
self::applyCommissionOrderDataScope($query, $alias, $viewerAdminId, $viewerAdminInfo);
|
||
}
|
||
|
||
/**
|
||
* 列表数据域:订单创建人 ∈ 可见 admin。
|
||
*/
|
||
private static function applyCommissionOrderDataScope(
|
||
Query $query,
|
||
string $alias,
|
||
int $viewerAdminId,
|
||
array $viewerAdminInfo
|
||
): void {
|
||
if (!DataScopeService::isEnabled()) {
|
||
return;
|
||
}
|
||
$ids = DataScopeService::getVisibleAdminIds($viewerAdminId, $viewerAdminInfo);
|
||
if ($ids === null) {
|
||
return;
|
||
}
|
||
if ($ids === []) {
|
||
$query->whereRaw('0 = 1');
|
||
|
||
return;
|
||
}
|
||
$query->whereIn("{$alias}.creator_id", $ids);
|
||
}
|
||
|
||
/**
|
||
* @param array<string,mixed> $row
|
||
*/
|
||
public static function maskInternalCostIfNeeded(array &$row, array $adminInfo): void
|
||
{
|
||
if (!self::canViewInternalCost($adminInfo)) {
|
||
unset($row['internal_cost']);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 菜单权限:查看/编辑业务订单「备注」(对内 remark_extra)
|
||
*
|
||
* perms: tcm.prescriptionOrder/editRemarkExtra
|
||
*/
|
||
public static function canEditOrderRemarkExtra(array $adminInfo): bool
|
||
{
|
||
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
|
||
return true;
|
||
}
|
||
$perms = AuthLogic::getAuthByAdminId((int) ($adminInfo['admin_id'] ?? 0));
|
||
|
||
return in_array('tcm.prescriptionOrder/editRemarkExtra', $perms, true);
|
||
}
|
||
|
||
/**
|
||
* @param array<string,mixed> $row
|
||
*/
|
||
public static function maskRemarkExtraIfNeeded(array &$row, array $adminInfo): void
|
||
{
|
||
if (!self::canEditOrderRemarkExtra($adminInfo)) {
|
||
unset($row['remark_extra']);
|
||
}
|
||
}
|
||
|
||
/** 定金门槛(元),0 表示不校验 */
|
||
public static function depositMinAmount(): float
|
||
{
|
||
return round((float) Config::get('prescription_order.link_pay_min_amount', 0), 2);
|
||
}
|
||
|
||
/**
|
||
* @param int[] $payOrderIds
|
||
*/
|
||
public static function assertDepositAndLinkPayRules(float $orderAmount, array $payOrderIds): ?string
|
||
{
|
||
$min = self::depositMinAmount();
|
||
if ($min <= 0) {
|
||
return null;
|
||
}
|
||
if ($orderAmount < $min) {
|
||
return '订单金额须大于等于定金门槛 ¥' . $min . '(当前 ¥' . $orderAmount . ')';
|
||
}
|
||
if ($payOrderIds === []) {
|
||
return '未关联支付单,关联支付单总金额须大于等于定金门槛 ¥' . $min;
|
||
}
|
||
|
||
// 豁免权:关联的支付单中有豁免单则跳过金额校验
|
||
$hasExempt = Order::whereIn('id', $payOrderIds)
|
||
->whereNull('delete_time')
|
||
->where('is_exempt', 1)
|
||
->count();
|
||
if ($hasExempt > 0) {
|
||
return null;
|
||
}
|
||
|
||
// 检查关联支付单的总金额
|
||
$rows = Order::whereIn('id', $payOrderIds)->whereNull('delete_time')->column('amount', 'id');
|
||
$totalAmount = 0.0;
|
||
foreach ($payOrderIds as $pid) {
|
||
$pid = (int) $pid;
|
||
if ($pid <= 0) {
|
||
continue;
|
||
}
|
||
$amt = round((float) ($rows[$pid] ?? 0), 2);
|
||
$totalAmount += $amt;
|
||
}
|
||
|
||
if ($totalAmount < $min) {
|
||
return '关联支付单总金额须大于等于定金门槛 ¥' . $min . '(当前总金额 ¥' . $totalAmount . ')';
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
public static function generateOrderNo(): string
|
||
{
|
||
return 'PO' . date('YmdHis') . mt_rand(100000, 999999);
|
||
}
|
||
|
||
private static function normalizeExpressCompany($raw): string
|
||
{
|
||
$ec = strtolower(trim((string) ($raw ?? '')));
|
||
if ($ec === '') {
|
||
return 'auto';
|
||
}
|
||
if (!in_array($ec, ['sf', 'jd', 'jt', 'jtexpress', 'auto'], true)) {
|
||
return 'auto';
|
||
}
|
||
// 统一 jtexpress 为 jt
|
||
if ($ec === 'jtexpress') {
|
||
return 'jt';
|
||
}
|
||
|
||
return $ec;
|
||
}
|
||
|
||
/** 业务订单「处方审核」驳回后,允许医生继续编辑已通过的消费者处方 */
|
||
public static function allowDoctorEditApprovedPrescriptionDueToBusinessReject(int $prescriptionId): bool
|
||
{
|
||
return PrescriptionOrder::where('prescription_id', $prescriptionId)
|
||
->where('prescription_audit_status', 2)
|
||
->whereNull('delete_time')
|
||
->where('fulfillment_status', '<>', 4)
|
||
->count() > 0;
|
||
}
|
||
|
||
/**
|
||
* 医生保存消费者处方后:重置关联业务订单的处方/支付单审核为待审(因处方内容已变)
|
||
*/
|
||
public static function onConsumerPrescriptionSaved(int $prescriptionId): void
|
||
{
|
||
$rows = PrescriptionOrder::where('prescription_id', $prescriptionId)
|
||
->where('prescription_audit_status', 2)
|
||
->whereNull('delete_time')
|
||
->where('fulfillment_status', '<>', 4)
|
||
->select();
|
||
foreach ($rows as $order) {
|
||
$order->prescription_audit_status = 0;
|
||
$order->prescription_audit_remark = '';
|
||
$order->payment_slip_audit_status = 0;
|
||
$order->payment_slip_audit_remark = '';
|
||
self::syncFulfillmentStatus($order);
|
||
$order->save();
|
||
}
|
||
}
|
||
|
||
private static function syncFulfillmentStatus(PrescriptionOrder $order): void
|
||
{
|
||
$fs = (int) $order->fulfillment_status;
|
||
if ($fs === 3 || $fs === 4) {
|
||
return;
|
||
}
|
||
$pa = (int) $order->prescription_audit_status;
|
||
$pay = (int) $order->payment_slip_audit_status;
|
||
if ($pa === 1 && $pay === 1) {
|
||
$order->fulfillment_status = 2;
|
||
} elseif ($pa === 2 || $pay === 2) {
|
||
$order->fulfillment_status = 1;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param mixed $raw
|
||
* @return int[]
|
||
*/
|
||
private static function normalizePayOrderIds($raw): array
|
||
{
|
||
if (!is_array($raw)) {
|
||
return [];
|
||
}
|
||
$ids = array_map('intval', $raw);
|
||
|
||
return array_values(array_unique(array_filter($ids, static fn (int $v): bool => $v > 0)));
|
||
}
|
||
|
||
/**
|
||
* 解除业务订单与支付单的关联(撤回/驳回后支付单可再次被选用)
|
||
*/
|
||
private static function clearPayOrderLinks(PrescriptionOrder $order): void
|
||
{
|
||
PrescriptionOrderPayOrder::where('prescription_order_id', (int) $order->id)->delete();
|
||
$order->linked_pay_order_id = null;
|
||
}
|
||
|
||
/**
|
||
* 支付单不可同时关联多条「有效」业务订单(未删除且未撤回/取消);$exceptPrescriptionOrderId 为当前编辑单 ID 时跳过自检
|
||
*
|
||
* @param int[] $payOrderIds
|
||
*/
|
||
private static function assertPayOrdersExclusive(array $payOrderIds, ?int $exceptPrescriptionOrderId): ?string
|
||
{
|
||
$ids = array_values(array_unique(array_filter(array_map('intval', $payOrderIds), static fn (int $v): bool => $v > 0)));
|
||
if ($ids === []) {
|
||
return null;
|
||
}
|
||
$activePoIds = PrescriptionOrder::whereNull('delete_time')
|
||
->where('fulfillment_status', '<>', 4)
|
||
->column('id');
|
||
if ($activePoIds === []) {
|
||
return null;
|
||
}
|
||
$activeSet = array_fill_keys(array_map('intval', $activePoIds), true);
|
||
$links = PrescriptionOrderPayOrder::whereIn('pay_order_id', $ids)->select();
|
||
foreach ($links as $link) {
|
||
$poId = (int) $link->prescription_order_id;
|
||
if ($exceptPrescriptionOrderId !== null && $poId === $exceptPrescriptionOrderId) {
|
||
continue;
|
||
}
|
||
if (!isset($activeSet[$poId])) {
|
||
continue;
|
||
}
|
||
|
||
return '支付单 #' . (int) $link->pay_order_id . ' 已关联其他有效业务订单,不可重复关联';
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* @param int[] $payOrderIds
|
||
*/
|
||
private static function replacePayOrderLinks(int $prescriptionOrderId, array $payOrderIds): void
|
||
{
|
||
PrescriptionOrderPayOrder::where('prescription_order_id', $prescriptionOrderId)->delete();
|
||
$t = time();
|
||
foreach ($payOrderIds as $pid) {
|
||
$link = new PrescriptionOrderPayOrder();
|
||
$link->prescription_order_id = $prescriptionOrderId;
|
||
$link->pay_order_id = $pid;
|
||
$link->create_time = $t;
|
||
$link->save();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @return int[]
|
||
*/
|
||
private static function linkedPayOrderIdList(int $prescriptionOrderId): array
|
||
{
|
||
return array_map(
|
||
'intval',
|
||
PrescriptionOrderPayOrder::where('prescription_order_id', $prescriptionOrderId)->order('id', 'asc')->column('pay_order_id')
|
||
);
|
||
}
|
||
|
||
/**
|
||
* @return array<int, array<string, mixed>>
|
||
*/
|
||
private static function linkedPayOrdersPayload(array $ids): array
|
||
{
|
||
if ($ids === []) {
|
||
return [];
|
||
}
|
||
$rows = Order::whereIn('id', $ids)->whereNull('delete_time')
|
||
->field(['id', 'order_no', 'order_type', 'amount', 'status', 'create_time', 'creator_id', 'remark', 'is_exempt'])
|
||
->order('id', 'asc')
|
||
->whereIn('status', [2, 4, 5])
|
||
->select()
|
||
->toArray();
|
||
|
||
$creatorIds = array_values(array_unique(array_filter(array_map('intval', array_column($rows, 'creator_id')))));
|
||
$creatorNames = [];
|
||
if ($creatorIds !== []) {
|
||
$creatorNames = \app\common\model\auth\Admin::whereIn('id', $creatorIds)->column('name', 'id');
|
||
}
|
||
$typeMap = [
|
||
1 => '挂号费',
|
||
2 => '问诊费',
|
||
3 => '药品费用',
|
||
4 => '首付费用',
|
||
5 => '尾款费用',
|
||
6 => '其他费用',
|
||
7 => '全部费用',
|
||
8 => '驼奶费用',
|
||
];
|
||
$statusMap = [
|
||
1 => '待支付',
|
||
2 => '已支付',
|
||
3 => '已取消',
|
||
4 => '已退款',
|
||
5 => '待审核',
|
||
];
|
||
foreach ($rows as &$r) {
|
||
$ot = (int) ($r['order_type'] ?? 0);
|
||
$st = (int) ($r['status'] ?? 0);
|
||
$r['order_type_desc'] = $typeMap[$ot] ?? '—';
|
||
$r['status_desc'] = $statusMap[$st] ?? '—';
|
||
$cid = (int) ($r['creator_id'] ?? 0);
|
||
$r['creator_name'] = $cid > 0 ? (string) ($creatorNames[$cid] ?? '') : '';
|
||
}
|
||
unset($r);
|
||
|
||
return $rows;
|
||
}
|
||
|
||
/**
|
||
* @param array<string,mixed> $arr
|
||
*/
|
||
private static function attachLinkedPayOrders(array &$arr): void
|
||
{
|
||
$id = (int) ($arr['id'] ?? 0);
|
||
if ($id <= 0) {
|
||
$arr['pay_order_ids'] = [];
|
||
$arr['linked_pay_orders'] = [];
|
||
$arr['linked_pay_paid_total'] = 0.0;
|
||
$arr['deposit_min_amount'] = self::depositMinAmount();
|
||
|
||
return;
|
||
}
|
||
$ids = self::linkedPayOrderIdList($id);
|
||
$arr['pay_order_ids'] = $ids;
|
||
$arr['linked_pay_orders'] = self::linkedPayOrdersPayload($ids);
|
||
$sum = 0.0;
|
||
foreach ($arr['linked_pay_orders'] as $o) {
|
||
$st = (int) ($o['status'] ?? 0);
|
||
if ($st === 2 || $st === 5) {
|
||
$sum += (float) ($o['amount'] ?? 0);
|
||
}
|
||
}
|
||
$arr['linked_pay_paid_total'] = round($sum, 2);
|
||
$arr['deposit_min_amount'] = self::depositMinAmount();
|
||
}
|
||
|
||
/**
|
||
* @param array<string,mixed> $params
|
||
* @return array<string,mixed>|false
|
||
*/
|
||
public static function create(array $params, int $adminId, array $adminInfo)
|
||
{
|
||
self::$error = '';
|
||
$rxId = (int) $params['prescription_id'];
|
||
$diagId = (int) $params['diagnosis_id'];
|
||
$payOrderIds = self::normalizePayOrderIds($params['pay_order_ids'] ?? []);
|
||
|
||
$rx = PrescriptionLogic::detail($rxId, $adminId, $adminInfo);
|
||
if ($rx === null) {
|
||
$err = PrescriptionLogic::getError();
|
||
|
||
self::$error = $err !== '' ? $err : '处方不存在';
|
||
|
||
return false;
|
||
}
|
||
if ((int) ($rx['diagnosis_id'] ?? 0) !== $diagId) {
|
||
self::$error = '诊单与处方不匹配';
|
||
|
||
return false;
|
||
}
|
||
|
||
if ((int) ($rx['void_status'] ?? 0) === 1) {
|
||
self::$error = '已作废处方不可创建业务订单';
|
||
|
||
return false;
|
||
}
|
||
|
||
if (PrescriptionOrder::where('prescription_id', $rxId)->whereNull('delete_time')->where('fulfillment_status', '<>', 4)->count() > 0) {
|
||
self::$error = '该处方已存在有效业务订单(未撤回前不可重复创建)';
|
||
|
||
return false;
|
||
}
|
||
|
||
$diag = Diagnosis::where('id', $diagId)->whereNull('delete_time')->find();
|
||
if (!$diag) {
|
||
self::$error = '诊单不存在';
|
||
|
||
return false;
|
||
}
|
||
|
||
if ($payOrderIds !== []) {
|
||
$linkErr = OrderLogic::assertPayOrdersLinkable($payOrderIds, $diagId, $adminId, $adminInfo);
|
||
if ($linkErr !== null) {
|
||
self::$error = $linkErr;
|
||
|
||
return false;
|
||
}
|
||
$exErr = self::assertPayOrdersExclusive($payOrderIds, null);
|
||
if ($exErr !== null) {
|
||
self::$error = $exErr;
|
||
|
||
return false;
|
||
}
|
||
}
|
||
|
||
$orderAmtPreview = round((float) ($params['amount'] ?? 0), 2);
|
||
$depErrCreate = self::assertDepositAndLinkPayRules($orderAmtPreview, $payOrderIds);
|
||
if ($depErrCreate !== null) {
|
||
self::$error = $depErrCreate;
|
||
|
||
return false;
|
||
}
|
||
|
||
$internalCost = null;
|
||
if (isset($params['internal_cost']) && $params['internal_cost'] !== '' && $params['internal_cost'] !== null) {
|
||
if (self::canViewInternalCost($adminInfo)) {
|
||
$internalCost = round((float) $params['internal_cost'], 2);
|
||
}
|
||
}
|
||
|
||
$medDays = $params['medication_days'] ?? null;
|
||
$medDays = $medDays === '' || $medDays === null ? null : (int) $medDays;
|
||
|
||
$order = new PrescriptionOrder();
|
||
$order->order_no = self::generateOrderNo();
|
||
$order->prescription_id = $rxId;
|
||
$order->diagnosis_id = $diagId;
|
||
$order->creator_id = $adminId;
|
||
$order->recipient_name = (string) $params['recipient_name'];
|
||
$order->recipient_phone = (string) $params['recipient_phone'];
|
||
|
||
// 处理省市区字段
|
||
if (isset($params['shipping_province'])) {
|
||
$order->shipping_province = (string) $params['shipping_province'];
|
||
}
|
||
if (isset($params['shipping_city'])) {
|
||
$order->shipping_city = (string) $params['shipping_city'];
|
||
}
|
||
if (isset($params['shipping_district'])) {
|
||
$order->shipping_district = (string) $params['shipping_district'];
|
||
}
|
||
|
||
$order->shipping_address = (string) $params['shipping_address'];
|
||
$order->is_follow_up = (int) ($params['is_follow_up'] ?? 0) === 1 ? 1 : 0;
|
||
$order->medication_days = $medDays > 0 ? $medDays : null;
|
||
$order->prev_staff = (string) ($params['prev_staff'] ?? '');
|
||
$order->service_channel = (string) ($params['service_channel'] ?? '');
|
||
$order->service_package = (string) ($params['service_package'] ?? '');
|
||
$order->tracking_number = (string) ($params['tracking_number'] ?? '');
|
||
$order->express_company = self::normalizeExpressCompany($params['express_company'] ?? 'auto');
|
||
$order->fee_type = (int) $params['fee_type'];
|
||
$order->amount = round((float) $params['amount'], 2);
|
||
$order->internal_cost = $internalCost;
|
||
$order->remark_extra = self::canEditOrderRemarkExtra($adminInfo)
|
||
? (string) ($params['remark_extra'] ?? '')
|
||
: '';
|
||
$order->remark_assistant = (string) ($params['remark_assistant'] ?? '');
|
||
$order->linked_pay_order_id = $payOrderIds !== [] ? $payOrderIds[0] : null;
|
||
$order->prescription_audit_status = 0;
|
||
$order->prescription_audit_remark = '';
|
||
$order->payment_slip_audit_status = 0;
|
||
$order->payment_slip_audit_remark = '';
|
||
$order->fulfillment_status = 1;
|
||
|
||
try {
|
||
$order->save();
|
||
} catch (\Throwable $e) {
|
||
self::$error = $e->getMessage();
|
||
|
||
return false;
|
||
}
|
||
|
||
self::writeLog((int) $order->id, $adminId, $adminInfo, 'create', '创建业务订单');
|
||
|
||
if ($payOrderIds !== []) {
|
||
self::replacePayOrderLinks((int) $order->id, $payOrderIds);
|
||
}
|
||
|
||
$out = $order->toArray();
|
||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||
self::attachLinkedPayOrders($out);
|
||
|
||
return $out;
|
||
}
|
||
|
||
public static function detail(int $id, int $adminId, array $adminInfo): ?array
|
||
{
|
||
self::$error = '';
|
||
$row = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||
if (!$row) {
|
||
self::$error = '订单不存在';
|
||
|
||
return null;
|
||
}
|
||
if (!self::canAccessOrder($row, $adminId, $adminInfo)) {
|
||
self::$error = '无权限查看';
|
||
|
||
return null;
|
||
}
|
||
$arr = $row->toArray();
|
||
self::maskInternalCostIfNeeded($arr, $adminInfo);
|
||
self::maskRemarkExtraIfNeeded($arr, $adminInfo);
|
||
self::attachLinkedPayOrders($arr);
|
||
|
||
// 添加创建人姓名、登录账号(业务订单 creator_id;la_admin 无手机列则不发手机号)
|
||
$creatorId = (int) ($arr['creator_id'] ?? 0);
|
||
$arr['creator_name'] = '';
|
||
$arr['creator_account'] = '';
|
||
$arr['creator_mobile'] = '';
|
||
if ($creatorId > 0) {
|
||
// 须包含 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<string, string>
|
||
*/
|
||
private static function channelSourceDictMap(): array
|
||
{
|
||
try {
|
||
/** @var array<string, string> $m */
|
||
$m = Db::name('dict_data')->where('type_value', 'channels')->column('name', 'value');
|
||
|
||
return $m;
|
||
} catch (\Throwable) {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param array<string, mixed> $row doctor_appointment 行
|
||
* @param array<string, string> $channelDict
|
||
*
|
||
* @return array<string, mixed>
|
||
*/
|
||
private static function formatAppointmentBriefFromRow(array $row, string $doctorName, array $channelDict = []): array
|
||
{
|
||
$apptId = (int) ($row['id'] ?? 0);
|
||
$statusMap = [
|
||
1 => '已预约',
|
||
2 => '已取消',
|
||
3 => '已完成',
|
||
4 => '已过号',
|
||
];
|
||
$typeMap = [
|
||
'video' => '视频问诊',
|
||
'text' => '图文问诊',
|
||
'phone' => '电话问诊',
|
||
];
|
||
$periodRaw = (string) ($row['period'] ?? '');
|
||
$period = $periodRaw === 'afternoon' ? '下午' : ($periodRaw === 'morning' ? '上午' : $periodRaw);
|
||
$timePart = (string) ($row['appointment_time'] ?? '');
|
||
if (strlen($timePart) > 5) {
|
||
$timePart = substr($timePart, 0, 5);
|
||
}
|
||
$status = (int) ($row['status'] ?? 0);
|
||
$atype = (string) ($row['appointment_type'] ?? '');
|
||
$chSrc = trim((string) ($row['channel_source'] ?? ''));
|
||
if ($chSrc === '') {
|
||
$legacy = $row['channels'] ?? null;
|
||
if ($legacy !== null && $legacy !== '') {
|
||
$chSrc = is_numeric($legacy)
|
||
? (string) (int) $legacy
|
||
: trim((string) $legacy);
|
||
}
|
||
}
|
||
$chDesc = '';
|
||
if ($chSrc !== '') {
|
||
$chDesc = (string) ($channelDict[$chSrc] ?? '');
|
||
if ($chDesc === '' && is_numeric($chSrc)) {
|
||
$chDesc = (string) ($channelDict[(string) (int) $chSrc] ?? '');
|
||
}
|
||
if ($chDesc === '') {
|
||
$chDesc = $chSrc;
|
||
}
|
||
}
|
||
|
||
$channelDictValue = null;
|
||
if ($chSrc !== '' && is_numeric($chSrc)) {
|
||
$channelDictValue = (int) $chSrc;
|
||
}
|
||
|
||
return [
|
||
'id' => $apptId,
|
||
'appointment_date' => (string) ($row['appointment_date'] ?? ''),
|
||
'period' => $period,
|
||
'appointment_time' => $timePart,
|
||
'appointment_type' => $atype,
|
||
'appointment_type_desc' => $typeMap[$atype] ?? '未知',
|
||
'status' => $status,
|
||
'status_desc' => $statusMap[$status] ?? '未知',
|
||
'doctor_name' => $doctorName,
|
||
'channel_source' => $chSrc,
|
||
'channel_source_desc' => $chDesc,
|
||
/** 字典 type_value=channels 的 value(整型);与业绩看板挂号 channels 收窄、进线统计所用字典为同套 */
|
||
'channel_dict_value' => $channelDictValue,
|
||
'remark' => (string) ($row['remark'] ?? ''),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 挂号预约简要信息(业务订单详情展示)
|
||
*
|
||
* @return array<string, mixed>|null
|
||
*/
|
||
private static function buildAppointmentBrief(int $appointmentId): ?array
|
||
{
|
||
if ($appointmentId <= 0) {
|
||
return null;
|
||
}
|
||
$ap = Appointment::find($appointmentId);
|
||
if ($ap === null) {
|
||
return null;
|
||
}
|
||
$row = $ap->toArray();
|
||
$doctorName = '';
|
||
$did = (int) ($row['doctor_id'] ?? 0);
|
||
if ($did > 0) {
|
||
$doctorName = (string) (Admin::where('id', $did)->value('name') ?? '');
|
||
}
|
||
$channelDict = self::channelSourceDictMap();
|
||
|
||
return self::formatAppointmentBriefFromRow($row, $doctorName, $channelDict);
|
||
}
|
||
|
||
/**
|
||
* 业务订单列表行批量附加 linked_appointment(解析规则与详情一致:优先处方 appointment_id,否则诊单最近一条预约;
|
||
* 业绩侧栏且带 channel_code 时优先展示与渠道收窄同源的已完成挂号,避免列表行属自媒体2Q 但展示关联为自媒体1)
|
||
*
|
||
* @param array<int, array<string, mixed>> $lists
|
||
* @param array<string, mixed> $listsParams PrescriptionOrderLists 请求参数(可选)
|
||
*/
|
||
public static function appendLinkedAppointmentsToListRows(array &$lists, array $listsParams = []): void
|
||
{
|
||
if ($lists === []) {
|
||
return;
|
||
}
|
||
foreach ($lists as &$item) {
|
||
$item['linked_appointment'] = null;
|
||
}
|
||
unset($item);
|
||
|
||
$yejiChannelHighlightByDiag = [];
|
||
if (
|
||
(int) ($listsParams['yeji_order_drawer'] ?? 0) === 1
|
||
&& trim((string) ($listsParams['channel_code'] ?? '')) !== ''
|
||
) {
|
||
$allDiagIdsForYeji = array_values(array_unique(array_filter(
|
||
array_map(static fn ($x): int => (int) ($x['diagnosis_id'] ?? 0), $lists),
|
||
static fn (int $id): bool => $id > 0
|
||
)));
|
||
if ($allDiagIdsForYeji !== []) {
|
||
$yejiChannelHighlightByDiag = YejiStatsLogic::batchResolveYejiChannelDrawerHighlightAppointmentIds(
|
||
$allDiagIdsForYeji,
|
||
$listsParams
|
||
);
|
||
}
|
||
}
|
||
|
||
$rxIds = array_values(array_unique(array_filter(array_map(
|
||
static fn ($x) => (int) ($x['prescription_id'] ?? 0),
|
||
$lists
|
||
))));
|
||
$rxApptByRx = [];
|
||
if ($rxIds !== []) {
|
||
$rxApptByRx = Prescription::whereIn('id', $rxIds)->whereNull('delete_time')->column('appointment_id', 'id');
|
||
}
|
||
|
||
$diagNeedFallback = [];
|
||
foreach ($lists as $item) {
|
||
$rxId = (int) ($item['prescription_id'] ?? 0);
|
||
$dgId = (int) ($item['diagnosis_id'] ?? 0);
|
||
$apFromRx = $rxId > 0 ? (int) ($rxApptByRx[$rxId] ?? 0) : 0;
|
||
if ($apFromRx <= 0 && $dgId > 0) {
|
||
$diagNeedFallback[$dgId] = true;
|
||
}
|
||
}
|
||
$diagIds = array_keys($diagNeedFallback);
|
||
$latestApptByDiag = [];
|
||
if ($diagIds !== []) {
|
||
try {
|
||
$agg = Db::name('doctor_appointment')
|
||
->whereIn('patient_id', $diagIds)
|
||
->fieldRaw('patient_id, MAX(id) as max_id')
|
||
->group('patient_id')
|
||
->select()
|
||
->toArray();
|
||
foreach ($agg as $r) {
|
||
$pid = (int) ($r['patient_id'] ?? 0);
|
||
$mid = (int) ($r['max_id'] ?? 0);
|
||
if ($pid > 0 && $mid > 0) {
|
||
$latestApptByDiag[$pid] = $mid;
|
||
}
|
||
}
|
||
} catch (\Throwable) {
|
||
$latestApptByDiag = [];
|
||
}
|
||
}
|
||
|
||
/** @var array<int, array{0: int, 1: int}> $resolvedPairs index => [ resolvedApptId, rxAppointmentId ] */
|
||
$resolvedPairs = [];
|
||
foreach ($lists as $idx => $item) {
|
||
$rxId = (int) ($item['prescription_id'] ?? 0);
|
||
$dgId = (int) ($item['diagnosis_id'] ?? 0);
|
||
$rxApptId = $rxId > 0 ? (int) ($rxApptByRx[$rxId] ?? 0) : 0;
|
||
$resolved = $rxApptId;
|
||
if ($resolved <= 0 && $dgId > 0) {
|
||
$resolved = (int) ($latestApptByDiag[$dgId] ?? 0);
|
||
}
|
||
$preferredYeji = $dgId > 0 ? (int) ($yejiChannelHighlightByDiag[$dgId] ?? 0) : 0;
|
||
if ($preferredYeji > 0) {
|
||
$resolved = $preferredYeji;
|
||
}
|
||
if ($resolved > 0) {
|
||
$resolvedPairs[$idx] = [$resolved, $rxApptId];
|
||
}
|
||
}
|
||
if ($resolvedPairs === []) {
|
||
return;
|
||
}
|
||
|
||
$allApptIds = array_values(array_unique(array_map(static fn ($p) => $p[0], $resolvedPairs)));
|
||
$appts = Appointment::whereIn('id', $allApptIds)->select()->toArray();
|
||
$apptById = [];
|
||
foreach ($appts as $a) {
|
||
$aid = (int) ($a['id'] ?? 0);
|
||
if ($aid > 0) {
|
||
$apptById[$aid] = $a;
|
||
}
|
||
}
|
||
$doctorIds = array_values(array_unique(array_filter(array_map(
|
||
static fn ($a) => (int) ($a['doctor_id'] ?? 0),
|
||
$appts
|
||
))));
|
||
$doctorNames = [];
|
||
if ($doctorIds !== []) {
|
||
$doctorNames = Admin::whereIn('id', $doctorIds)->column('name', 'id');
|
||
}
|
||
$channelDict = self::channelSourceDictMap();
|
||
|
||
foreach ($resolvedPairs as $idx => $pair) {
|
||
$resolvedId = $pair[0];
|
||
$rxApptId = $pair[1];
|
||
$row = $apptById[$resolvedId] ?? null;
|
||
if ($row === null || !is_array($row)) {
|
||
continue;
|
||
}
|
||
$did = (int) ($row['doctor_id'] ?? 0);
|
||
$dname = $did > 0 ? (string) ($doctorNames[$did] ?? '') : '';
|
||
$brief = self::formatAppointmentBriefFromRow($row, $dname, $channelDict);
|
||
$brief['resolved_from'] = $rxApptId === $resolvedId ? 'prescription' : 'diagnosis';
|
||
$lists[$idx]['linked_appointment'] = $brief;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 物流轨迹(顺丰/京东等,优先快递100;未配置密钥时返回官网链接)
|
||
*
|
||
* @return array<string,mixed>|null
|
||
*/
|
||
public static function logisticsTrace(int $id, string $expressCompanyOverride, int $adminId, array $adminInfo, string $phoneTail = ''): ?array
|
||
{
|
||
self::$error = '';
|
||
$row = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||
if (!$row) {
|
||
self::$error = '订单不存在';
|
||
|
||
return null;
|
||
}
|
||
if (!self::canAccessOrder($row, $adminId, $adminInfo)) {
|
||
self::$error = '无权限查看';
|
||
|
||
return null;
|
||
}
|
||
$num = trim((string) ($row->tracking_number ?? ''));
|
||
if ($num === '') {
|
||
self::$error = '未填写快递单号';
|
||
|
||
return null;
|
||
}
|
||
$ec = trim($expressCompanyOverride) !== '' ? self::normalizeExpressCompany($expressCompanyOverride) : self::normalizeExpressCompany($row->express_company ?? 'auto');
|
||
|
||
// 优先从数据库查询物流追踪信息
|
||
$trackingData = \app\common\service\ExpressTrackingService::getDetailByTrackingNumber($num);
|
||
|
||
if ($trackingData && !empty($trackingData['traces'])) {
|
||
// 从数据库获取到数据,直接返回
|
||
$payload = [
|
||
'carrier' => $trackingData['express_company'],
|
||
'carrier_label' => $trackingData['express_company_name'],
|
||
'kuaidi_com' => $trackingData['express_company'],
|
||
'traces' => array_map(function($trace) {
|
||
return [
|
||
'time' => $trace['trace_time'],
|
||
'ftime' => $trace['trace_time'],
|
||
'context' => $trace['trace_context'],
|
||
'location' => $trace['location'],
|
||
'status' => $trace['status'],
|
||
'statusCode' => $trace['status_code'],
|
||
];
|
||
}, $trackingData['traces']),
|
||
'state' => $trackingData['current_state'],
|
||
'state_text' => $trackingData['current_state_text'],
|
||
'source' => 'database',
|
||
'hint' => '',
|
||
'official_url' => '',
|
||
'last_query_time' => date('Y-m-d H:i:s', $trackingData['last_query_time']),
|
||
'query_count' => $trackingData['query_count'],
|
||
];
|
||
} else {
|
||
// 数据库没有数据,调用快递100 API
|
||
$payload = ExpressTrackService::query($ec, $num, (string) ($row->recipient_phone ?? ''), $phoneTail);
|
||
$payload['source'] = 'api';
|
||
}
|
||
|
||
$payload['official_urls'] = ExpressTrackService::officialUrls($num);
|
||
$payload['tracking_number'] = $num;
|
||
$payload['order_id'] = $id;
|
||
$payload['express_company_used'] = $ec;
|
||
|
||
return $payload;
|
||
}
|
||
|
||
/**
|
||
* @param array<string,mixed> $params
|
||
* @return array<string,mixed>|false
|
||
*/
|
||
public static function edit(array $params, int $adminId, array $adminInfo)
|
||
{
|
||
self::$error = '';
|
||
$id = (int) $params['id'];
|
||
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||
if (!$order) {
|
||
self::$error = '订单不存在';
|
||
|
||
return false;
|
||
}
|
||
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
|
||
self::$error = '无权限编辑';
|
||
|
||
return false;
|
||
}
|
||
if (
|
||
(int) $order->creator_id === $adminId
|
||
&& (int) $order->prescription_audit_status === 1
|
||
&& (int) $order->payment_slip_audit_status === 1
|
||
&& !self::canSeeAllPrescriptionOrders($adminInfo)
|
||
) {
|
||
self::$error = '处方与支付单均已审核通过,创建人不可再编辑';
|
||
|
||
return false;
|
||
}
|
||
$fs = (int) $order->fulfillment_status;
|
||
if ($fs === 3 || $fs === 4) {
|
||
self::$error = '已完成或已取消的订单不可编辑';
|
||
|
||
return false;
|
||
}
|
||
if ($fs === 6) {
|
||
self::$error = '已签收订单不可修改快递单号';
|
||
|
||
return false;
|
||
}
|
||
// 已成功提交甘草 SCM:仅允许更新快递单号与承运商(与甘草侧地址/药方等仍以取消流程为准)
|
||
$gcOrderNo = trim((string) $order->gancao_reciperl_order_no);
|
||
$gcSubmitTime = (int) $order->gancao_submit_time;
|
||
if ($gcOrderNo !== '' || $gcSubmitTime > 0) {
|
||
return self::editGancaoLogisticsOnly($order, $params, $adminId, $adminInfo);
|
||
}
|
||
|
||
$medDays = $params['medication_days'] ?? null;
|
||
$medDays = $medDays === '' || $medDays === null ? null : (int) $medDays;
|
||
|
||
$order->recipient_name = (string) $params['recipient_name'];
|
||
$order->recipient_phone = (string) $params['recipient_phone'];
|
||
|
||
// 处理省市区字段
|
||
if (isset($params['shipping_province'])) {
|
||
$order->shipping_province = (string) $params['shipping_province'];
|
||
}
|
||
if (isset($params['shipping_city'])) {
|
||
$order->shipping_city = (string) $params['shipping_city'];
|
||
}
|
||
if (isset($params['shipping_district'])) {
|
||
$order->shipping_district = (string) $params['shipping_district'];
|
||
}
|
||
|
||
$order->shipping_address = (string) $params['shipping_address'];
|
||
$order->is_follow_up = (int) ($params['is_follow_up'] ?? 0) === 1 ? 1 : 0;
|
||
$order->medication_days = $medDays > 0 ? $medDays : null;
|
||
$order->dose_unit = (string) ($params['dose_unit'] ?? '剂');
|
||
$order->dose_count = isset($params['dose_count']) && (int)$params['dose_count'] > 0 ? (int)$params['dose_count'] : 1;
|
||
$order->prev_staff = (string) ($params['prev_staff'] ?? '');
|
||
$order->service_channel = (string) ($params['service_channel'] ?? '');
|
||
$order->service_package = (string) ($params['service_package'] ?? '');
|
||
$order->tracking_number = (string) ($params['tracking_number'] ?? '');
|
||
if (array_key_exists('express_company', $params)) {
|
||
$order->express_company = self::normalizeExpressCompany($params['express_company']);
|
||
}
|
||
$order->fee_type = (int) $params['fee_type'];
|
||
$order->amount = round((float) $params['amount'], 2);
|
||
if (self::canEditOrderRemarkExtra($adminInfo)) {
|
||
$order->remark_extra = (string) ($params['remark_extra'] ?? '');
|
||
}
|
||
$order->remark_assistant = (string) ($params['remark_assistant'] ?? '');
|
||
|
||
$diagId = (int) $order->diagnosis_id;
|
||
|
||
if (array_key_exists('pay_order_ids', $params)) {
|
||
$payOrderIds = self::normalizePayOrderIds($params['pay_order_ids']);
|
||
|
||
// 过滤掉已删除的支付单ID(拒绝审核时可能软删除了待审核支付单)
|
||
if (!empty($payOrderIds)) {
|
||
$existingIds = Order::whereIn('id', $payOrderIds)
|
||
->whereNull('delete_time')
|
||
->column('id');
|
||
$payOrderIds = array_values(array_intersect($payOrderIds, $existingIds));
|
||
}
|
||
|
||
$linkErr = OrderLogic::assertPayOrdersLinkable($payOrderIds, $diagId, $adminId, $adminInfo, (int) $order->id);
|
||
if ($linkErr !== null) {
|
||
self::$error = $linkErr;
|
||
|
||
return false;
|
||
}
|
||
$exErr = self::assertPayOrdersExclusive($payOrderIds, (int) $order->id);
|
||
if ($exErr !== null) {
|
||
self::$error = $exErr;
|
||
|
||
return false;
|
||
}
|
||
self::replacePayOrderLinks((int) $order->id, $payOrderIds);
|
||
$order->linked_pay_order_id = $payOrderIds !== [] ? $payOrderIds[0] : null;
|
||
} elseif (array_key_exists('linked_pay_order_id', $params)) {
|
||
$lp = $params['linked_pay_order_id'];
|
||
if ($lp === '' || $lp === null || (int) $lp <= 0) {
|
||
self::replacePayOrderLinks((int) $order->id, []);
|
||
$order->linked_pay_order_id = null;
|
||
} else {
|
||
$single = [(int) $lp];
|
||
$linkErr = OrderLogic::assertPayOrdersLinkable($single, $diagId, $adminId, $adminInfo, (int) $order->id);
|
||
if ($linkErr !== null) {
|
||
self::$error = $linkErr;
|
||
|
||
return false;
|
||
}
|
||
$exErr = self::assertPayOrdersExclusive($single, (int) $order->id);
|
||
if ($exErr !== null) {
|
||
self::$error = $exErr;
|
||
|
||
return false;
|
||
}
|
||
self::replacePayOrderLinks((int) $order->id, $single);
|
||
$order->linked_pay_order_id = $single[0];
|
||
}
|
||
}
|
||
|
||
if (array_key_exists('internal_cost', $params) && self::canViewInternalCost($adminInfo)) {
|
||
$raw = $params['internal_cost'];
|
||
|
||
if ($raw === '' || $raw === null) {
|
||
$order->internal_cost = null;
|
||
} else {
|
||
$order->internal_cost = round((float) $raw, 2);
|
||
}
|
||
}
|
||
|
||
$finalPayIds = self::linkedPayOrderIdList((int) $order->id);
|
||
$depErr = self::assertDepositAndLinkPayRules(round((float) $order->amount, 2), $finalPayIds);
|
||
if ($depErr !== null) {
|
||
self::$error = $depErr;
|
||
|
||
return false;
|
||
}
|
||
|
||
// 编辑后重置支付审核状态为待审核(需要重新审核)
|
||
// 但如果当前状态是"已发货"(5),则不重置审核状态
|
||
if ((int) $order->fulfillment_status !== 5) {
|
||
$order->payment_slip_audit_status = 0;
|
||
$order->payment_slip_audit_remark = '';
|
||
}
|
||
self::syncFulfillmentStatus($order);
|
||
|
||
try {
|
||
$order->save();
|
||
} catch (\Throwable $e) {
|
||
self::$error = $e->getMessage();
|
||
|
||
return false;
|
||
}
|
||
|
||
self::writeLog((int) $order->id, $adminId, $adminInfo, 'edit', '编辑了业务订单及相关信息');
|
||
|
||
$out = $order->toArray();
|
||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||
self::attachLinkedPayOrders($out);
|
||
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* 甘草已提交后仅更新物流字段(tracking_number、express_company),忽略金额/地址等其它请求参数。
|
||
*
|
||
* @param array<string,mixed> $params
|
||
* @return array<string,mixed>|false
|
||
*/
|
||
private static function editGancaoLogisticsOnly(
|
||
PrescriptionOrder $order,
|
||
array $params,
|
||
int $adminId,
|
||
array $adminInfo
|
||
) {
|
||
$order->tracking_number = mb_substr(trim((string) ($params['tracking_number'] ?? '')), 0, 80);
|
||
if (array_key_exists('express_company', $params)) {
|
||
$order->express_company = self::normalizeExpressCompany($params['express_company']);
|
||
}
|
||
|
||
try {
|
||
$order->save();
|
||
} catch (\Throwable $e) {
|
||
self::$error = $e->getMessage();
|
||
|
||
return false;
|
||
}
|
||
|
||
self::writeLog(
|
||
(int) $order->id,
|
||
$adminId,
|
||
$adminInfo,
|
||
'edit',
|
||
'甘草订单已提交,仅更新快递信息(单号/承运商)'
|
||
);
|
||
|
||
$out = $order->toArray();
|
||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||
self::attachLinkedPayOrders($out);
|
||
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* 确认发货:更新快递信息并将 fulfillment_status 从 2(履约中)推进到 5(已发货)
|
||
*
|
||
* @return array<string,mixed>|false
|
||
*/
|
||
public static function ship(int $id, string $expressCompany, string $trackingNumber, string $shipMode, int $adminId, array $adminInfo)
|
||
{
|
||
self::$error = '';
|
||
|
||
$trackingNumber = trim($trackingNumber);
|
||
if ($trackingNumber === '') {
|
||
self::$error = '快递单号不能为空';
|
||
|
||
return false;
|
||
}
|
||
|
||
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||
if (!$order) {
|
||
self::$error = '订单不存在';
|
||
|
||
return false;
|
||
}
|
||
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
|
||
self::$error = '无权限操作';
|
||
|
||
return false;
|
||
}
|
||
if ((int) $order->fulfillment_status !== 2 && (int) $order->fulfillment_status !== 5) {
|
||
self::$error = '仅「履约中」或「已发货」状态的订单可更新快递信息';
|
||
|
||
return false;
|
||
}
|
||
|
||
$shipMode = self::normalizeShipMode($shipMode);
|
||
$shipModeText = $shipMode === 'direct' ? '药房直发' : '甘草药方发';
|
||
|
||
$order->express_company = self::normalizeExpressCompany($expressCompany);
|
||
$order->tracking_number = $trackingNumber;
|
||
$order->ship_mode = $shipMode;
|
||
// 仅履约中(2)时才推进到已发货(5),已发货(5)则只更新快递信息保持状态
|
||
$isFirstShip = false;
|
||
if ((int) $order->fulfillment_status === 2) {
|
||
$order->fulfillment_status = 5;
|
||
$isFirstShip = true;
|
||
}
|
||
|
||
try {
|
||
$order->save();
|
||
} catch (\Throwable $e) {
|
||
self::$error = $e->getMessage();
|
||
|
||
return false;
|
||
}
|
||
|
||
if ($isFirstShip) {
|
||
self::writeLog(
|
||
(int) $order->id,
|
||
$adminId,
|
||
$adminInfo,
|
||
'ship',
|
||
'确认发货(' . $shipModeText . '),运单:' . $trackingNumber . '(' . $expressCompany . ')'
|
||
);
|
||
} else {
|
||
self::writeLog(
|
||
(int) $order->id,
|
||
$adminId,
|
||
$adminInfo,
|
||
'fill_tracking',
|
||
'更新发货信息(' . $shipModeText . '),运单:' . $trackingNumber . '(' . $expressCompany . ')'
|
||
);
|
||
}
|
||
|
||
$out = $order->toArray();
|
||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||
self::attachLinkedPayOrders($out);
|
||
|
||
return $out;
|
||
}
|
||
|
||
private static function normalizeShipMode(string $shipMode): string
|
||
{
|
||
$s = strtolower(trim($shipMode));
|
||
if ($s === 'direct') {
|
||
return 'direct';
|
||
}
|
||
|
||
return 'gancao';
|
||
}
|
||
|
||
/**
|
||
* @return array<string,mixed>|false
|
||
*/
|
||
public static function withdraw(int $id, int $adminId, array $adminInfo)
|
||
{
|
||
self::$error = '';
|
||
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||
if (!$order) {
|
||
self::$error = '订单不存在';
|
||
|
||
return false;
|
||
}
|
||
if (!self::canWithdrawOrder($order, $adminId, $adminInfo)) {
|
||
self::$error = '无权限撤回';
|
||
|
||
return false;
|
||
}
|
||
if ((int) $order->fulfillment_status !== 1) {
|
||
self::$error = '仅「待双审通过」状态可撤回;双审通过后请走履约/完成流程';
|
||
|
||
return false;
|
||
}
|
||
|
||
$order->fulfillment_status = 4;
|
||
self::clearPayOrderLinks($order);
|
||
|
||
try {
|
||
$order->save();
|
||
|
||
// 撤回订单后,将关联的处方审核状态改回"待审核"
|
||
$prescriptionId = (int) $order->prescription_id;
|
||
if ($prescriptionId > 0) {
|
||
Prescription::where('id', $prescriptionId)
|
||
->whereNull('delete_time')
|
||
->update([
|
||
'audit_status' => 0, // 0=待审核
|
||
'audit_time' => null,
|
||
'audit_by' => null,
|
||
'audit_by_name' => '',
|
||
'audit_remark' => '',
|
||
'update_time' => time(),
|
||
]);
|
||
}
|
||
} catch (\Throwable $e) {
|
||
self::$error = $e->getMessage();
|
||
|
||
return false;
|
||
}
|
||
|
||
self::writeLog((int) $order->id, $adminId, $adminInfo, 'withdraw', '撤回订单,状态变更为「已取消」');
|
||
|
||
$out = $order->toArray();
|
||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||
self::attachLinkedPayOrders($out);
|
||
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* @return array<string,mixed>|false
|
||
*/
|
||
public static function auditPrescription(int $id, string $action, string $remark, int $adminId, array $adminInfo)
|
||
{
|
||
self::$error = '';
|
||
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||
if (!$order) {
|
||
self::$error = '订单不存在';
|
||
|
||
return false;
|
||
}
|
||
if (!self::canAuditPrescriptionOrder($adminInfo)) {
|
||
self::$error = '无处方审核权限';
|
||
|
||
return false;
|
||
}
|
||
if ((int) $order->prescription_audit_status !== 0) {
|
||
self::$error = '当前无需处方审核';
|
||
|
||
return false;
|
||
}
|
||
$fs = (int) $order->fulfillment_status;
|
||
if ($fs === 3 || $fs === 4) {
|
||
self::$error = '订单已结束';
|
||
|
||
return false;
|
||
}
|
||
|
||
if ($action === 'reject' && trim($remark) === '') {
|
||
self::$error = '驳回时请填写审核意见';
|
||
|
||
return false;
|
||
}
|
||
|
||
if ($action === 'approve') {
|
||
$order->prescription_audit_status = 1;
|
||
$order->prescription_audit_remark = mb_substr(trim($remark), 0, 500);
|
||
} else {
|
||
$order->prescription_audit_status = 2;
|
||
$order->prescription_audit_remark = mb_substr(trim($remark), 0, 500);
|
||
$order->payment_slip_audit_status = 0;
|
||
$order->payment_slip_audit_remark = '';
|
||
}
|
||
self::syncFulfillmentStatus($order);
|
||
if ($action === 'reject') {
|
||
self::clearPayOrderLinks($order);
|
||
}
|
||
|
||
try {
|
||
$order->save();
|
||
} catch (\Throwable $e) {
|
||
self::$error = $e->getMessage();
|
||
|
||
return false;
|
||
}
|
||
|
||
if ($action === 'approve') {
|
||
$syncRemark = trim($remark) !== '' ? trim($remark) : '业务订单处方审核通过';
|
||
PrescriptionLogic::syncApproveConsumerPrescriptionIfPending(
|
||
(int) $order->prescription_id,
|
||
$adminId,
|
||
$adminInfo,
|
||
$syncRemark
|
||
);
|
||
}
|
||
|
||
$actionName = $action === 'approve' ? 'audit_rx_approve' : 'audit_rx_reject';
|
||
$summary = $action === 'approve' ? '处方审核通过' . ($remark ? ',意见:' . $remark : '') : '处方审核驳回,意见:' . $remark;
|
||
self::writeLog((int) $order->id, $adminId, $adminInfo, $actionName, $summary);
|
||
|
||
$out = $order->toArray();
|
||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||
self::attachLinkedPayOrders($out);
|
||
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* @return array<string,mixed>|false
|
||
*/
|
||
public static function auditPaymentSlip(int $id, string $action, string $remark, int $adminId, array $adminInfo)
|
||
{
|
||
self::$error = '';
|
||
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||
if (!$order) {
|
||
self::$error = '订单不存在';
|
||
|
||
return false;
|
||
}
|
||
if (!self::canAuditPaymentSlipOrder($adminInfo)) {
|
||
self::$error = '无支付单审核权限';
|
||
|
||
return false;
|
||
}
|
||
if ((int) $order->prescription_audit_status !== 1) {
|
||
self::$error = '请先通过处方审核';
|
||
|
||
return false;
|
||
}
|
||
if ((int) $order->payment_slip_audit_status !== 0) {
|
||
self::$error = '当前无需支付单审核';
|
||
|
||
return false;
|
||
}
|
||
$fs = (int) $order->fulfillment_status;
|
||
if ($fs === 3 || $fs === 4) {
|
||
self::$error = '订单已结束';
|
||
|
||
return false;
|
||
}
|
||
// 已发货状态(5)允许重新发起支付单审核(新增支付单场景)
|
||
|
||
if ($action === 'reject' && trim($remark) === '') {
|
||
self::$error = '驳回时请填写审核意见';
|
||
|
||
return false;
|
||
}
|
||
|
||
$didAutoComplete = false;
|
||
|
||
if ($action === 'approve') {
|
||
$order->payment_slip_audit_status = 1;
|
||
$order->payment_slip_audit_remark = mb_substr(trim($remark), 0, 500);
|
||
|
||
// 将关联的待审核支付单(status=5)更新为已支付(status=2)
|
||
$payOrderIds = self::linkedPayOrderIdList($id);
|
||
if (!empty($payOrderIds)) {
|
||
Order::whereIn('id', $payOrderIds)
|
||
->where('status', 5) // 只更新待审核状态的
|
||
->update([
|
||
'status' => 2, // 已支付
|
||
'payment_time' => date('Y-m-d H:i:s')
|
||
]);
|
||
}
|
||
|
||
// 已发货/已签收且已申请完单:同意支付审核后直接结案(与手动「完成订单」一致:实付=关联支付单合计)
|
||
if ((int) $order->completion_request === 1 && in_array((int) $order->fulfillment_status, [5, 6], true)) {
|
||
$linkedPayOrderIds = self::linkedPayOrderIdList($id);
|
||
$linkedPayOrders = self::linkedPayOrdersPayload($linkedPayOrderIds);
|
||
$linkedPayPaidTotal = 0.0;
|
||
foreach ($linkedPayOrders as $o) {
|
||
$linkedPayPaidTotal += (float) ($o['amount'] ?? 0);
|
||
}
|
||
$linkedPayPaidTotal = round($linkedPayPaidTotal, 2);
|
||
|
||
$order->fulfillment_status = 3;
|
||
$order->paid = $linkedPayPaidTotal;
|
||
$order->completion_request = 0;
|
||
$order->completion_request_time = 0;
|
||
$order->completion_request_by = 0;
|
||
$order->completion_request_by_name = '';
|
||
$didAutoComplete = true;
|
||
}
|
||
} else {
|
||
$order->payment_slip_audit_status = 2;
|
||
$order->payment_slip_audit_remark = mb_substr(trim($remark), 0, 500);
|
||
|
||
// 拒绝审核时,软删除待审核状态的关联支付单(status=5)
|
||
$payOrderIds = self::linkedPayOrderIdList($id);
|
||
if (!empty($payOrderIds)) {
|
||
Order::whereIn('id', $payOrderIds)
|
||
->where('status', 5) // 只删除待审核状态的
|
||
->update([
|
||
'delete_time' => date('Y-m-d H:i:s')
|
||
]);
|
||
}
|
||
|
||
// 拒绝审核时,清除完单申请
|
||
if ((int) $order->completion_request === 1) {
|
||
$order->completion_request = 0;
|
||
$order->completion_request_time = 0;
|
||
$order->completion_request_by = 0;
|
||
$order->completion_request_by_name = '';
|
||
}
|
||
}
|
||
|
||
// 状态同步和关联清除逻辑:
|
||
// - 已发货(5)/已签收(6):不同步为「待发货」,拒绝时也不整单解绑(与原先仅已发货一致)
|
||
// - 其它状态:同步;拒绝时清除所有支付单关联关系
|
||
$currentStatus = (int) $order->fulfillment_status;
|
||
if (in_array($currentStatus, [5, 6], true)) {
|
||
// 保持履约节点不变
|
||
} else {
|
||
self::syncFulfillmentStatus($order);
|
||
if ($action === 'reject') {
|
||
self::clearPayOrderLinks($order);
|
||
}
|
||
}
|
||
|
||
try {
|
||
$order->save();
|
||
} catch (\Throwable $e) {
|
||
self::$error = $e->getMessage();
|
||
|
||
return false;
|
||
}
|
||
|
||
if ($didAutoComplete) {
|
||
$unassignSummary = self::clearDiagnosisAssistantOnComplete((int) $order->diagnosis_id);
|
||
$paidStr = number_format((float) $order->paid, 2, '.', '');
|
||
self::writeLog((int) $order->id, $adminId, $adminInfo, 'auto_complete',
|
||
'支付审核通过,根据完单申请自动完成订单,实付金额更新为 ¥' . $paidStr . $unassignSummary);
|
||
}
|
||
|
||
$actionName = $action === 'approve' ? 'audit_pay_approve' : 'audit_pay_reject';
|
||
$summary = $action === 'approve' ? '支付单审核通过' . ($remark ? ',意见:' . $remark : '') : '支付单审核驳回,意见:' . $remark;
|
||
self::writeLog((int) $order->id, $adminId, $adminInfo, $actionName, $summary);
|
||
|
||
$out = $order->toArray();
|
||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||
self::attachLinkedPayOrders($out);
|
||
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* 撤回处方审核:将已通过/已驳回的处方审核重置为待审核
|
||
*
|
||
* @return array<string,mixed>|false
|
||
*/
|
||
public static function revokeRxAudit(int $id, int $adminId, array $adminInfo)
|
||
{
|
||
self::$error = '';
|
||
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||
if (!$order) {
|
||
self::$error = '订单不存在';
|
||
|
||
return false;
|
||
}
|
||
if (!self::canAuditPrescriptionOrder($adminInfo)) {
|
||
self::$error = '无处方审核权限';
|
||
|
||
return false;
|
||
}
|
||
$rxStatus = (int) $order->prescription_audit_status;
|
||
if ($rxStatus === 0) {
|
||
self::$error = '当前已是待审核状态,无需撤回';
|
||
|
||
return false;
|
||
}
|
||
$fs = (int) $order->fulfillment_status;
|
||
if ($fs === 3 || $fs === 4) {
|
||
self::$error = '订单已结束,不可撤回审核';
|
||
|
||
return false;
|
||
}
|
||
// 如果处方审核已通过,且支付审核也已通过或已驳回,不允许撤回处方审核
|
||
if ($rxStatus === 1 && (int) $order->payment_slip_audit_status !== 0) {
|
||
self::$error = '支付单审核已进行,请先撤回支付单审核';
|
||
|
||
return false;
|
||
}
|
||
|
||
$order->prescription_audit_status = 0;
|
||
$order->prescription_audit_remark = '';
|
||
// 如果支付审核不是待审核,也一并重置
|
||
if ((int) $order->payment_slip_audit_status !== 0) {
|
||
$order->payment_slip_audit_status = 0;
|
||
$order->payment_slip_audit_remark = '';
|
||
}
|
||
self::syncFulfillmentStatus($order);
|
||
|
||
try {
|
||
$order->save();
|
||
} catch (\Throwable $e) {
|
||
self::$error = $e->getMessage();
|
||
|
||
return false;
|
||
}
|
||
|
||
// 同步撤回处方表的审核状态
|
||
$prescriptionId = (int) $order->prescription_id;
|
||
if ($prescriptionId > 0) {
|
||
try {
|
||
$prescription = Prescription::where('id', $prescriptionId)->whereNull('delete_time')->find();
|
||
if ($prescription) {
|
||
$prescription->audit_status = 0;
|
||
$prescription->audit_remark = '';
|
||
$prescription->audit_time = null;
|
||
$prescription->audit_by = null;
|
||
$prescription->audit_by_name = '';
|
||
$prescription->save();
|
||
}
|
||
} catch (\Throwable $e) {
|
||
// 记录日志但不影响主流程
|
||
Log::error('撤回处方审核时同步处方表失败: ' . $e->getMessage(), [
|
||
'prescription_id' => $prescriptionId,
|
||
'order_id' => $id
|
||
]);
|
||
}
|
||
}
|
||
|
||
self::writeLog((int) $order->id, $adminId, $adminInfo, 'revoke_rx_audit', '撤回处方审核,状态重置为待审核');
|
||
|
||
$out = $order->toArray();
|
||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||
self::attachLinkedPayOrders($out);
|
||
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* 撤回支付单审核:将已通过/已驳回的支付单审核重置为待审核
|
||
*
|
||
* @return array<string,mixed>|false
|
||
*/
|
||
public static function revokePayAudit(int $id, int $adminId, array $adminInfo)
|
||
{
|
||
self::$error = '';
|
||
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||
if (!$order) {
|
||
self::$error = '订单不存在';
|
||
|
||
return false;
|
||
}
|
||
if (!self::canAuditPaymentSlipOrder($adminInfo)) {
|
||
self::$error = '无支付单审核权限';
|
||
|
||
return false;
|
||
}
|
||
if ((int) $order->prescription_audit_status !== 1) {
|
||
self::$error = '处方审核未通过,无法撤回支付单审核';
|
||
|
||
return false;
|
||
}
|
||
$payStatus = (int) $order->payment_slip_audit_status;
|
||
if ($payStatus === 0) {
|
||
self::$error = '当前已是待审核状态,无需撤回';
|
||
|
||
return false;
|
||
}
|
||
$fs = (int) $order->fulfillment_status;
|
||
if ($fs === 3 || $fs === 4) {
|
||
self::$error = '订单已结束,不可撤回审核';
|
||
|
||
return false;
|
||
}
|
||
|
||
$order->payment_slip_audit_status = 0;
|
||
$order->payment_slip_audit_remark = '';
|
||
self::syncFulfillmentStatus($order);
|
||
|
||
// 将关联的已支付支付单(status=2,且payment_method='manual')恢复为待审核(status=5)
|
||
$payOrderIds = self::linkedPayOrderIdList($id);
|
||
if (!empty($payOrderIds)) {
|
||
Order::whereIn('id', $payOrderIds)
|
||
->where('status', 2) // 只恢复已支付状态的
|
||
->where('payment_method', 'manual') // 只恢复手动创建的
|
||
->update([
|
||
'status' => 5, // 待审核
|
||
'payment_time' => null
|
||
]);
|
||
}
|
||
|
||
try {
|
||
$order->save();
|
||
} catch (\Throwable $e) {
|
||
self::$error = $e->getMessage();
|
||
|
||
return false;
|
||
}
|
||
|
||
self::writeLog((int) $order->id, $adminId, $adminInfo, 'revoke_pay_audit', '撤回支付单审核,状态重置为待审核');
|
||
|
||
$out = $order->toArray();
|
||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||
self::attachLinkedPayOrders($out);
|
||
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* @return array<int, array<string, mixed>>
|
||
*/
|
||
public static function getLogs(int $orderId, int $adminId, array $adminInfo): array
|
||
{
|
||
self::$error = '';
|
||
$order = PrescriptionOrder::where('id', $orderId)->whereNull('delete_time')->find();
|
||
if (!$order) {
|
||
self::$error = '订单不存在';
|
||
|
||
return [];
|
||
}
|
||
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
|
||
self::$error = '无权限查看';
|
||
|
||
return [];
|
||
}
|
||
|
||
return PrescriptionOrderLog::where('prescription_order_id', $orderId)
|
||
->order('id', 'desc')
|
||
->select()
|
||
->toArray();
|
||
}
|
||
|
||
/**
|
||
* 为「已发货/已签收」(fulfillment_status=5/6) 的业务订单新增一条关联支付单(zyt_order),
|
||
* 创建后将支付单链接到业务订单,并将处方/支付审核状态重置为待审核以启动再次审核流程。
|
||
*
|
||
* @param array<string,mixed> $params id, order_type, amount, remark
|
||
* @return array<string,mixed>|false
|
||
*/
|
||
public static function addPayOrder(array $params, int $adminId, array $adminInfo)
|
||
{
|
||
self::$error = '';
|
||
$id = (int) $params['id'];
|
||
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||
if (!$order) {
|
||
self::$error = '订单不存在';
|
||
return false;
|
||
}
|
||
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
|
||
self::$error = '无权限操作';
|
||
return false;
|
||
}
|
||
if (!in_array((int) $order->fulfillment_status, [5, 6], true)) {
|
||
self::$error = '仅「已发货」或「已签收」状态的订单可新增支付单';
|
||
return false;
|
||
}
|
||
|
||
$orderType = (int) ($params['order_type'] ?? 3);
|
||
$amount = round((float) ($params['pay_amount'] ?? 0), 2);
|
||
$remark = mb_substr(trim((string) ($params['pay_remark'] ?? '')), 0, 200);
|
||
$completionRequest = (int) ($params['completion_request'] ?? 0);
|
||
|
||
if ($amount < 0) {
|
||
self::$error = '支付单金额不能为负数';
|
||
return false;
|
||
}
|
||
|
||
// 创建 zyt_order 并标记为待审核(需要审核后才算已支付)
|
||
$payOrder = new Order();
|
||
$payOrder->order_no = OrderLogic::generateOrderNo();
|
||
$payOrder->patient_id = (int) $order->diagnosis_id;
|
||
$payOrder->creator_id = $adminId;
|
||
$payOrder->order_type = $orderType;
|
||
$payOrder->amount = $amount;
|
||
$payOrder->status = 5; // 待审核
|
||
$payOrder->payment_method = 'manual';
|
||
$payOrder->payment_time = null; // 审核通过后再设置支付时间
|
||
$payOrder->remark = $remark;
|
||
|
||
try {
|
||
$payOrder->save();
|
||
} catch (\Throwable $e) {
|
||
self::$error = $e->getMessage();
|
||
return false;
|
||
}
|
||
|
||
// 将新支付单追加到业务订单关联列表
|
||
$existingIds = self::linkedPayOrderIdList($id);
|
||
$newIds = array_unique(array_merge($existingIds, [(int) $payOrder->id]));
|
||
self::replacePayOrderLinks($id, $newIds);
|
||
$order->linked_pay_order_id = $newIds[0];
|
||
|
||
// 重置支付单审核为待审核,以启动新一轮审核(处方审核保持通过状态不变)
|
||
$order->payment_slip_audit_status = 0;
|
||
$order->payment_slip_audit_remark = '';
|
||
|
||
// 处理完单申请
|
||
if ($completionRequest === 1) {
|
||
$order->completion_request = 1;
|
||
$order->completion_request_time = time();
|
||
$order->completion_request_by = $adminId;
|
||
$order->completion_request_by_name = (string) ($adminInfo['name'] ?? '');
|
||
}
|
||
|
||
try {
|
||
$order->save();
|
||
} catch (\Throwable $e) {
|
||
self::$error = $e->getMessage();
|
||
return false;
|
||
}
|
||
|
||
$logMsg = '新增支付单 #' . $payOrder->id . '(¥' . $amount . '),类型:' . $orderType . ',等待支付审核';
|
||
if ($completionRequest === 1) {
|
||
$logMsg .= ',并申请完成订单';
|
||
}
|
||
self::writeLog($id, $adminId, $adminInfo, 'add_pay_order', $logMsg);
|
||
|
||
$out = $order->toArray();
|
||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||
self::attachLinkedPayOrders($out);
|
||
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* 为「已发货/已签收」(fulfillment_status=5/6) 的业务订单关联已有支付单,
|
||
* 关联后将支付审核状态重置为待审核以启动再次审核流程。
|
||
*
|
||
* @param array<string,mixed> $params id, pay_order_id
|
||
* @return array<string,mixed>|false
|
||
*/
|
||
public static function linkPayOrder(array $params, int $adminId, array $adminInfo)
|
||
{
|
||
self::$error = '';
|
||
$id = (int) $params['id'];
|
||
$completionRequest = (int) ($params['completion_request'] ?? 0);
|
||
|
||
// 支持单个或多个支付单ID
|
||
$payOrderIds = [];
|
||
if (isset($params['pay_order_ids']) && is_array($params['pay_order_ids'])) {
|
||
// 多个支付单(数组)
|
||
$payOrderIds = array_values(array_unique(array_filter(array_map('intval', $params['pay_order_ids']))));
|
||
} elseif (isset($params['pay_order_id'])) {
|
||
// 单个支付单(兼容旧接口)
|
||
$payOrderIds = [(int) $params['pay_order_id']];
|
||
}
|
||
|
||
if (empty($payOrderIds)) {
|
||
self::$error = '请选择要关联的支付单';
|
||
return false;
|
||
}
|
||
|
||
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||
if (!$order) {
|
||
self::$error = '订单不存在';
|
||
return false;
|
||
}
|
||
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
|
||
self::$error = '无权限操作';
|
||
return false;
|
||
}
|
||
if (!in_array((int) $order->fulfillment_status, [5, 6], true)) {
|
||
self::$error = '仅「已发货」或「已签收」状态的订单可关联支付单';
|
||
return false;
|
||
}
|
||
|
||
$diagId = (int) $order->diagnosis_id;
|
||
|
||
// 验证所有支付单是否可关联
|
||
$linkErr = OrderLogic::assertPayOrdersLinkable($payOrderIds, $diagId, $adminId, $adminInfo);
|
||
if ($linkErr !== null) {
|
||
self::$error = $linkErr;
|
||
return false;
|
||
}
|
||
|
||
// 验证所有支付单是否已被其他业务订单占用
|
||
$exErr = self::assertPayOrdersExclusive($payOrderIds, $id);
|
||
if ($exErr !== null) {
|
||
self::$error = $exErr;
|
||
return false;
|
||
}
|
||
|
||
// 当前单已关联的支付单不允许重复提交
|
||
$existingIds = self::linkedPayOrderIdList($id);
|
||
$duplicated = array_values(array_intersect($payOrderIds, $existingIds));
|
||
if ($duplicated !== []) {
|
||
self::$error = '支付单 #' . implode('、#', $duplicated) . ' 已绑定当前业务订单,请勿重复关联';
|
||
return false;
|
||
}
|
||
|
||
// 将支付单追加到业务订单关联列表
|
||
$newIds = array_unique(array_merge($existingIds, $payOrderIds));
|
||
self::replacePayOrderLinks($id, $newIds);
|
||
$order->linked_pay_order_id = $newIds[0];
|
||
|
||
// 重置支付单审核为待审核,以启动新一轮审核(处方审核保持通过状态不变)
|
||
$order->payment_slip_audit_status = 0;
|
||
$order->payment_slip_audit_remark = '';
|
||
|
||
// 处理完单申请
|
||
if ($completionRequest === 1) {
|
||
$order->completion_request = 1;
|
||
$order->completion_request_time = time();
|
||
$order->completion_request_by = $adminId;
|
||
$order->completion_request_by_name = (string) ($adminInfo['name'] ?? '');
|
||
}
|
||
|
||
try {
|
||
$order->save();
|
||
} catch (\Throwable $e) {
|
||
self::$error = $e->getMessage();
|
||
return false;
|
||
}
|
||
|
||
// 计算总金额
|
||
$totalAmount = 0;
|
||
$payOrders = Order::whereIn('id', $payOrderIds)->select();
|
||
foreach ($payOrders as $po) {
|
||
$totalAmount += round((float) $po->amount, 2);
|
||
}
|
||
|
||
$logMsg = '关联支付单 ' . count($payOrderIds) . ' 笔(共¥' . $totalAmount . '),等待支付审核';
|
||
if ($completionRequest === 1) {
|
||
$logMsg .= ',并申请完成订单';
|
||
}
|
||
self::writeLog($id, $adminId, $adminInfo, 'link_pay_order', $logMsg);
|
||
|
||
$out = $order->toArray();
|
||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||
self::attachLinkedPayOrders($out);
|
||
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* 已发货/已签收订单:不新增/关联支付单,仅提交完单申请并将支付审核置为待审核。
|
||
*
|
||
* @param array<string,mixed> $params id
|
||
* @return array<string,mixed>|false
|
||
*/
|
||
public static function requestCompletion(array $params, int $adminId, array $adminInfo)
|
||
{
|
||
self::$error = '';
|
||
$id = (int) $params['id'];
|
||
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||
if (!$order) {
|
||
self::$error = '订单不存在';
|
||
return false;
|
||
}
|
||
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
|
||
self::$error = '无权限操作';
|
||
return false;
|
||
}
|
||
if (!in_array((int) $order->fulfillment_status, [5, 6], true)) {
|
||
self::$error = '仅「已发货」或「已签收」状态的订单可申请完单';
|
||
return false;
|
||
}
|
||
|
||
$order->completion_request = 1;
|
||
$order->completion_request_time = time();
|
||
$order->completion_request_by = $adminId;
|
||
$order->completion_request_by_name = (string) ($adminInfo['name'] ?? '');
|
||
$order->payment_slip_audit_status = 0;
|
||
$order->payment_slip_audit_remark = '';
|
||
|
||
try {
|
||
$order->save();
|
||
} catch (\Throwable $e) {
|
||
self::$error = $e->getMessage();
|
||
return false;
|
||
}
|
||
|
||
self::writeLog($id, $adminId, $adminInfo, 'completion_request', '提交完单申请(未新增/关联支付单),等待支付审核');
|
||
|
||
$out = $order->toArray();
|
||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||
self::attachLinkedPayOrders($out);
|
||
|
||
return $out;
|
||
}
|
||
|
||
/** 完成订单时可选的履约终态:3=已完成,7-12 业务结案类状态(退款 10 走独立 refund 接口) */
|
||
private const COMPLETE_FULFILLMENT_TARGETS = [3, 7, 8, 9, 11, 12];
|
||
|
||
/** 完成时是否按「实付=关联支付单金额」重算并清空诊单医助(与 clearDiagnosisAssistantOnComplete 的终态判断一致) */
|
||
private const COMPLETE_FULFILLMENT_TERMINAL = [3, 8, 9, 10, 11, 12];
|
||
|
||
/**
|
||
* 将「已发货/已签收」且支付审核已通过的订单结案:可选「已完成」或 7-12 业务状态。
|
||
*
|
||
* @return array<string,mixed>|false
|
||
*/
|
||
public static function complete(int $id, int $adminId, array $adminInfo, int $targetFulfillmentStatus = 3)
|
||
{
|
||
self::$error = '';
|
||
if (!in_array($targetFulfillmentStatus, self::COMPLETE_FULFILLMENT_TARGETS, true)) {
|
||
self::$error = '无效的完成状态';
|
||
return false;
|
||
}
|
||
|
||
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||
if (!$order) {
|
||
self::$error = '订单不存在';
|
||
return false;
|
||
}
|
||
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
|
||
self::$error = '无权限操作';
|
||
return false;
|
||
}
|
||
$curFs = (int) $order->fulfillment_status;
|
||
if ($curFs !== 5 && $curFs !== 6) {
|
||
self::$error = '仅「已发货」或「已签收」状态可完成订单';
|
||
return false;
|
||
}
|
||
if ((int) $order->payment_slip_audit_status !== 1) {
|
||
self::$error = '支付单审核尚未通过,不可完成订单';
|
||
return false;
|
||
}
|
||
|
||
$label = self::fulfillmentStatusLabel($targetFulfillmentStatus);
|
||
|
||
$linkedPayOrderIds = self::linkedPayOrderIdList($id);
|
||
$linkedPayOrders = self::linkedPayOrdersPayload($linkedPayOrderIds);
|
||
$linkedPayPaidTotal = 0.0;
|
||
foreach ($linkedPayOrders as $o) {
|
||
$st = (int) ($o['status'] ?? 0);
|
||
if ($st === 2 || $st === 5) {
|
||
$linkedPayPaidTotal += (float) ($o['amount'] ?? 0);
|
||
}
|
||
}
|
||
$linkedPayPaidTotal = round($linkedPayPaidTotal, 2);
|
||
|
||
if ($targetFulfillmentStatus === 3) {
|
||
$order->fulfillment_status = 3;
|
||
$order->paid = $linkedPayPaidTotal;
|
||
} else {
|
||
$order->fulfillment_status = $targetFulfillmentStatus;
|
||
}
|
||
|
||
try {
|
||
$order->save();
|
||
} catch (\Throwable $e) {
|
||
self::$error = $e->getMessage();
|
||
return false;
|
||
}
|
||
|
||
$unassignSummary = '';
|
||
if (in_array($targetFulfillmentStatus, self::COMPLETE_FULFILLMENT_TERMINAL, true)) {
|
||
$unassignSummary = self::clearDiagnosisAssistantOnComplete((int) $order->diagnosis_id);
|
||
}
|
||
|
||
if ($targetFulfillmentStatus === 3) {
|
||
$logLine = '订单完成,状态变更为「' . $label . '」,实付金额更新为 ¥' . $linkedPayPaidTotal . $unassignSummary;
|
||
} else {
|
||
$logLine = '订单处理完成,状态变更为「' . $label . '」' . $unassignSummary;
|
||
}
|
||
self::writeLog(
|
||
$id,
|
||
$adminId,
|
||
$adminInfo,
|
||
'complete',
|
||
$logLine
|
||
);
|
||
|
||
$out = $order->toArray();
|
||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||
self::attachLinkedPayOrders($out);
|
||
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* 关联收款单中可退款的记录(已支付/待审核)
|
||
*
|
||
* @return array<int, array<string, mixed>>
|
||
*/
|
||
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<string,mixed>|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<int, array<string, mixed>> $diagById
|
||
* @return array<int, string> diagnosis_id => 医助姓名
|
||
*/
|
||
private static function batchFirstVisitAssistantNameByDiagnosis(array $diagIds, array $diagById): array
|
||
{
|
||
if ($diagIds === []) {
|
||
return [];
|
||
}
|
||
|
||
$firstRows = PrescriptionOrder::whereIn('diagnosis_id', $diagIds)
|
||
->whereNull('delete_time')
|
||
->where('is_follow_up', 0)
|
||
->field(['id', 'diagnosis_id', 'prescription_id', 'creator_id', 'create_time'])
|
||
->order('create_time', 'asc')
|
||
->order('id', 'asc')
|
||
->select()
|
||
->toArray();
|
||
|
||
$firstByDiag = [];
|
||
$rxIds = [];
|
||
foreach ($firstRows as $row) {
|
||
$did = (int) ($row['diagnosis_id'] ?? 0);
|
||
if ($did <= 0 || isset($firstByDiag[$did])) {
|
||
continue;
|
||
}
|
||
$firstByDiag[$did] = $row;
|
||
$rxId = (int) ($row['prescription_id'] ?? 0);
|
||
if ($rxId > 0) {
|
||
$rxIds[$rxId] = true;
|
||
}
|
||
}
|
||
|
||
$rxAssistantById = [];
|
||
if ($rxIds !== []) {
|
||
$rxAssistantById = Prescription::whereIn('id', array_keys($rxIds))
|
||
->whereNull('delete_time')
|
||
->column('assistant_id', 'id');
|
||
}
|
||
|
||
$adminIds = [];
|
||
foreach ($firstByDiag as $did => $row) {
|
||
$diag = $diagById[$did] ?? [];
|
||
$rxId = (int) ($row['prescription_id'] ?? 0);
|
||
$rxAst = $rxId > 0 ? (int) ($rxAssistantById[$rxId] ?? 0) : 0;
|
||
$aid = self::resolveAssistantAdminIdForPrescriptionOrderRow(
|
||
$rxAst,
|
||
(int) ($diag['assistant_id'] ?? 0)
|
||
);
|
||
if ($aid <= 0) {
|
||
$aid = (int) ($row['creator_id'] ?? 0);
|
||
}
|
||
if ($aid > 0) {
|
||
$adminIds[$aid] = true;
|
||
}
|
||
}
|
||
|
||
$nameByAdminId = [];
|
||
if ($adminIds !== []) {
|
||
$admins = Admin::whereIn('id', array_keys($adminIds))
|
||
->field(['id', 'name'])
|
||
->select()
|
||
->toArray();
|
||
foreach ($admins as $a) {
|
||
$nameByAdminId[(int) $a['id']] = trim((string) ($a['name'] ?? ''));
|
||
}
|
||
}
|
||
|
||
$out = [];
|
||
foreach ($firstByDiag as $did => $row) {
|
||
$diag = $diagById[$did] ?? [];
|
||
$rxId = (int) ($row['prescription_id'] ?? 0);
|
||
$rxAst = $rxId > 0 ? (int) ($rxAssistantById[$rxId] ?? 0) : 0;
|
||
$aid = self::resolveAssistantAdminIdForPrescriptionOrderRow(
|
||
$rxAst,
|
||
(int) ($diag['assistant_id'] ?? 0)
|
||
);
|
||
if ($aid <= 0) {
|
||
$aid = (int) ($row['creator_id'] ?? 0);
|
||
}
|
||
$out[$did] = $aid > 0 ? (string) ($nameByAdminId[$aid] ?? '') : '';
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* 导出用:管理员在 admin_dept 中的部门路径(多部门「;」、路径内「 / 」;业务订单导出取 creator_id)
|
||
*/
|
||
public static function formatAssistantDeptPathForExport(int $assistantAdminId): string
|
||
{
|
||
if ($assistantAdminId <= 0) {
|
||
return '';
|
||
}
|
||
$deptIds = Db::name('admin_dept')->where('admin_id', $assistantAdminId)->column('dept_id');
|
||
$pathSegments = [];
|
||
foreach ($deptIds as $did) {
|
||
$p = self::buildDeptPath((int) $did);
|
||
if ($p !== '') {
|
||
$pathSegments[] = $p;
|
||
}
|
||
}
|
||
$pathSegments = array_values(array_unique($pathSegments));
|
||
|
||
return $pathSegments === [] ? '' : implode(';', $pathSegments);
|
||
}
|
||
|
||
/**
|
||
* 导出用:服务套餐(与列表/详情 formatServicePackage 同口径,按 dict_data type_value=server_order 解析)
|
||
* 入参可能是逗号分隔字符串或数组;空串返回空,避免 Excel 显示「—」占位。
|
||
*
|
||
* @param mixed $value zyt_tcm_prescription_order.service_package
|
||
* @param array<string, string> $packageNameByValue dict_data server_order value=>name 字典缓存
|
||
*/
|
||
public static function formatServicePackageForExport($value, array $packageNameByValue): string
|
||
{
|
||
if ($value === null || $value === '' || $value === false) {
|
||
return '';
|
||
}
|
||
$items = [];
|
||
if (\is_array($value)) {
|
||
$items = $value;
|
||
} else {
|
||
$items = explode(',', (string) $value);
|
||
}
|
||
$names = [];
|
||
foreach ($items as $raw) {
|
||
$key = trim((string) $raw);
|
||
if ($key === '') {
|
||
continue;
|
||
}
|
||
$names[] = (string) ($packageNameByValue[$key] ?? $packageNameByValue[(string) (int) $key] ?? $key);
|
||
}
|
||
|
||
return $names === [] ? '' : implode('、', $names);
|
||
}
|
||
|
||
/**
|
||
* 导出用:药品形态(丸剂 / 饮片+代煎|自煎等)
|
||
*
|
||
* @param array<string, mixed> $rx
|
||
*/
|
||
public static function formatMedicationFormForExport(array $rx): string
|
||
{
|
||
$type = trim((string) ($rx['prescription_type'] ?? ''));
|
||
if ($type === '') {
|
||
return '';
|
||
}
|
||
if ($type === '饮片') {
|
||
$nd = (int) ($rx['need_decoction'] ?? 0);
|
||
|
||
return $type . ($nd === 1 ? '(代煎)' : '(自煎)');
|
||
}
|
||
|
||
return $type;
|
||
}
|
||
|
||
/**
|
||
* 导出列:挂号表渠道来源展示(与 AppointmentLists channel_source_desc 同字典口径)
|
||
*
|
||
* @param array<string, mixed> $apRow doctor_appointment 一行
|
||
* @param array<string, string> $channelNameByValue DictData channels value=>name
|
||
*/
|
||
private static function formatGuahaoChannelSourceForExport(array $apRow, array $channelNameByValue): string
|
||
{
|
||
$srcKey = trim((string) ($apRow['channel_source'] ?? ''));
|
||
if ($srcKey === '' && isset($apRow['channels']) && $apRow['channels'] !== '' && $apRow['channels'] !== null) {
|
||
$srcKey = trim((string) $apRow['channels']);
|
||
}
|
||
$label = '';
|
||
if ($srcKey !== '') {
|
||
$label = (string) ($channelNameByValue[$srcKey] ?? $channelNameByValue[(string) (int) $srcKey] ?? $srcKey);
|
||
}
|
||
$detail = trim((string) ($apRow['channel_source_detail'] ?? ''));
|
||
if ($label === '' && $detail === '') {
|
||
return '';
|
||
}
|
||
if ($detail !== '') {
|
||
return $label !== '' ? "{$label}({$detail})" : $detail;
|
||
}
|
||
|
||
return $label;
|
||
}
|
||
|
||
/**
|
||
* 挂号表渠道相关列是否存在(未加 channel_source 的老库仅查 channels 或整列留空)
|
||
*
|
||
* @return array{channel_source: bool, channel_source_detail: bool, channels: bool}
|
||
*/
|
||
private static function doctorAppointmentChannelColumnsForExport(): array
|
||
{
|
||
$out = [
|
||
'channel_source' => false,
|
||
'channel_source_detail' => false,
|
||
'channels' => false,
|
||
];
|
||
try {
|
||
$cols = Db::name('doctor_appointment')->getTableFields();
|
||
if (!is_array($cols)) {
|
||
return $out;
|
||
}
|
||
foreach (array_keys($out) as $k) {
|
||
$out[$k] = in_array($k, $cols, true);
|
||
}
|
||
} catch (\Throwable) {
|
||
// keep false
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* 业务订单列表导出:写入与 PrescriptionOrderLists::setExcelFields 对应的字段(export=2 时由列表类调用)
|
||
* 「挂号渠道来源」取值与详情/列表解析一致:处方 appointment_id → 否则诊单下 MAX(挂号.id);业绩抽屉带渠道时与列表同源高亮。
|
||
*
|
||
* @param array<int, array<string, mixed>> $lists
|
||
* @param array<string, mixed> $listsParams PrescriptionOrderLists 请求参数(含 yeji_order_drawer、channel_code)
|
||
*/
|
||
public static function appendPrescriptionOrderListExportRows(array &$lists, array $adminInfo, array $listsParams = []): void
|
||
{
|
||
if ($lists === []) {
|
||
return;
|
||
}
|
||
$diagIds = [];
|
||
$rxIds = [];
|
||
$poIds = [];
|
||
foreach ($lists as $row) {
|
||
$poIds[] = (int) ($row['id'] ?? 0);
|
||
$d = (int) ($row['diagnosis_id'] ?? 0);
|
||
if ($d > 0) {
|
||
$diagIds[$d] = true;
|
||
}
|
||
$r = (int) ($row['prescription_id'] ?? 0);
|
||
if ($r > 0) {
|
||
$rxIds[$r] = true;
|
||
}
|
||
}
|
||
$poIds = array_values(array_filter(array_unique($poIds), static fn (int $id): bool => $id > 0));
|
||
$diagIdList = array_keys($diagIds);
|
||
$rxIdList = array_keys($rxIds);
|
||
|
||
$diagById = [];
|
||
if ($diagIdList !== []) {
|
||
$diagRows = Diagnosis::whereIn('id', $diagIdList)->whereNull('delete_time')
|
||
->field(['id', 'patient_name', 'gender', 'age', 'phone', 'assistant_id'])
|
||
->select()
|
||
->toArray();
|
||
foreach ($diagRows as $dr) {
|
||
$diagById[(int) $dr['id']] = $dr;
|
||
}
|
||
}
|
||
|
||
$chCols = self::doctorAppointmentChannelColumnsForExport();
|
||
$needGuahaoChannel = $chCols['channel_source'] || $chCols['channels'] || $chCols['channel_source_detail'];
|
||
$channelNameByValue = [];
|
||
$apptById = [];
|
||
/** @var array<int, int> $resolvedApptIdByPoId 订单 id → 用于导出渠道的那条挂号 id */
|
||
$resolvedApptIdByPoId = [];
|
||
|
||
$rxById = [];
|
||
if ($rxIdList !== []) {
|
||
$rxRows = Prescription::whereIn('id', $rxIdList)->whereNull('delete_time')
|
||
->field(['id', 'prescription_type', 'need_decoction', 'dose_unit', 'assistant_id', 'appointment_id', '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<int, int> $signTsByPoId order_id => Unix 秒 */
|
||
$signTsByPoId = [];
|
||
if ($poIds !== []) {
|
||
$signItems = [];
|
||
foreach ($lists as $row) {
|
||
$oid = (int) ($row['id'] ?? 0);
|
||
if ($oid <= 0) {
|
||
continue;
|
||
}
|
||
$signItems[] = [
|
||
'order_id' => $oid,
|
||
'tracking_number' => trim((string) ($row['tracking_number'] ?? '')),
|
||
];
|
||
}
|
||
try {
|
||
$signTsByPoId = ExpressTrackingService::batchResolveSignUnixFromDbForOrders($signItems);
|
||
} catch (\Throwable $e) {
|
||
Log::warning('appendPrescriptionOrderListExportRows batch sign_time failed: ' . $e->getMessage());
|
||
$signTsByPoId = [];
|
||
}
|
||
}
|
||
|
||
$firstVisitAssistantByDiag = self::batchFirstVisitAssistantNameByDiagnosis($diagIdList, $diagById);
|
||
$assistantDeptCache = [];
|
||
|
||
foreach ($lists as &$item) {
|
||
$poId = (int) ($item['id'] ?? 0);
|
||
$fs = (int) ($item['fulfillment_status'] ?? 0);
|
||
$item['export_fulfillment_status_text'] = self::fulfillmentStatusLabel($fs);
|
||
$ct = (int) ($item['create_time'] ?? 0);
|
||
$item['export_order_time'] = $ct > 0 ? date('Y-m-d H:i:s', $ct) : '';
|
||
|
||
$diagId = (int) ($item['diagnosis_id'] ?? 0);
|
||
$dg = $diagById[$diagId] ?? null;
|
||
$rxId = (int) ($item['prescription_id'] ?? 0);
|
||
$rx = $rxById[$rxId] ?? [];
|
||
$item['export_patient_name'] = \is_array($dg) ? (string) ($dg['patient_name'] ?? '') : '';
|
||
if (\is_array($dg)) {
|
||
$g = (int) ($dg['gender'] ?? 0);
|
||
$item['export_patient_gender'] = $g === 1 ? '男' : '女';
|
||
$item['export_patient_age'] = (string) ($dg['age'] ?? '');
|
||
$item['export_patient_phone'] = (string) ($dg['phone'] ?? '');
|
||
} else {
|
||
$item['export_patient_gender'] = '';
|
||
$item['export_patient_age'] = '';
|
||
$item['export_patient_phone'] = '';
|
||
}
|
||
|
||
// 导出「医助名字 / 医助部门」:与详情 order_creator 一致,按业务订单 creator_id(非诊单二诊 assistant_id)
|
||
$creatorId = (int) ($item['creator_id'] ?? 0);
|
||
$item['assistant_name'] = (string) ($item['creator_name'] ?? '');
|
||
if ($creatorId > 0) {
|
||
if (!isset($assistantDeptCache[$creatorId])) {
|
||
$assistantDeptCache[$creatorId] = self::formatAssistantDeptPathForExport($creatorId);
|
||
}
|
||
$item['export_assistant_dept'] = $assistantDeptCache[$creatorId];
|
||
} else {
|
||
$item['export_assistant_dept'] = '';
|
||
}
|
||
|
||
$item['export_medication_form'] = self::formatMedicationFormForExport(\is_array($rx) ? $rx : []);
|
||
|
||
// 药方名称:主方/辅方均反查处方库取名称(与处方笺一致),带标签;甘草/无匹配留空
|
||
[$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, '.', '')
|
||
: '';
|
||
$item['export_agency_collect'] = number_format(round($amt - $paid, 2), 2, '.', '');
|
||
|
||
$item['export_tracking_number'] = (string) ($item['tracking_number'] ?? '');
|
||
$signTs = $poId > 0 ? (int) ($signTsByPoId[$poId] ?? 0) : 0;
|
||
$item['export_sign_time'] = $signTs > 0 ? date('Y-m-d', $signTs) : '';
|
||
$isGc = trim((string) ($item['gancao_reciperl_order_no'] ?? '')) !== '';
|
||
$item['export_supply_mode'] = $isGc ? '甘草' : '自营';
|
||
// 「处方成本」列:与列表/详情一致,取订单 internal_cost(含「测试价格」预报价写入;甘草/自营均导出)
|
||
if (self::canViewInternalCost($adminInfo)) {
|
||
$rawCost = $item['internal_cost'] ?? null;
|
||
$item['export_gancao_prescription_cost'] = $rawCost !== null && $rawCost !== ''
|
||
? number_format(round((float) $rawCost, 2), 2, '.', '')
|
||
: '';
|
||
} else {
|
||
$item['export_gancao_prescription_cost'] = '';
|
||
}
|
||
|
||
$isFu = (int) ($item['is_follow_up'] ?? 0) === 1;
|
||
$item['export_first_visit_assistant'] = $isFu
|
||
? (string) ($firstVisitAssistantByDiag[(int) ($item['diagnosis_id'] ?? 0)] ?? '')
|
||
: '';
|
||
|
||
$hit = (int) ($item['po_assign_snapshot_hit'] ?? 0);
|
||
$rel = (int) ($item['po_assign_is_release'] ?? 0);
|
||
$er = (int) ($item['po_assign_to_er_center'] ?? 0);
|
||
if ($hit !== 1) {
|
||
$item['export_center_label'] = '—';
|
||
} elseif ($rel === 1) {
|
||
$item['export_center_label'] = '已释放';
|
||
} elseif ($er === 1) {
|
||
$item['export_center_label'] = '二中心';
|
||
} else {
|
||
$item['export_center_label'] = '一中心';
|
||
}
|
||
|
||
$paSt = (int) ($item['prescription_audit_status'] ?? 0);
|
||
$aud = $auditEffectiveByPo[$poId] ?? null;
|
||
if (($paSt === 1 || $paSt === 2) && \is_array($aud)) {
|
||
$at = (int) ($aud['create_time'] ?? 0);
|
||
$item['export_rx_audit_time'] = $at > 0 ? date('Y-m-d H:i:s', $at) : '';
|
||
$item['export_rx_auditor'] = (string) ($aud['admin_name'] ?? '');
|
||
} else {
|
||
$item['export_rx_audit_time'] = '';
|
||
$item['export_rx_auditor'] = '';
|
||
}
|
||
}
|
||
unset($item);
|
||
}
|
||
|
||
/**
|
||
* 导出药方名称反查:批量加载相关开方医师处方库(creator_id 命中 + 公开模板),
|
||
* 建立 [creator_id][formula_type][herbKey] => name 与 公开 [formula_type][herbKey] => name 两级索引。
|
||
*
|
||
* @param array<int, array> $rxById 处方 id => 处方行(需含 herbs / creator_id)
|
||
*
|
||
* @return array{0: array<int, array<string, array<string, string>>>, 1: array<string, array<string, string>>}
|
||
*/
|
||
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<int, array<string, array<string, string>>> $libByDoctor
|
||
* @param array<string, array<string, string>> $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<string,mixed> $item
|
||
* @param array<int, int|string> $assistantByDiag diagnosis_id => assistant_id
|
||
*/
|
||
public static function canUploadGancaoRecipel(array $item, int $adminId, array $adminInfo, array $assistantByDiag = []): bool
|
||
{
|
||
if (!GancaoScmRecipelService::isConfigured()) {
|
||
return false;
|
||
}
|
||
$pa = (int) ($item['prescription_audit_status'] ?? 0);
|
||
$fs = (int) ($item['fulfillment_status'] ?? 0);
|
||
if ($pa !== 1) {
|
||
return false;
|
||
}
|
||
if (in_array($fs, [3, 4, 8, 9, 10, 11, 12], true)) {
|
||
return false;
|
||
}
|
||
if (trim((string) ($item['gancao_reciperl_order_no'] ?? '')) !== '') {
|
||
return false;
|
||
}
|
||
if (self::canSeeAllPrescriptionOrders($adminInfo)) {
|
||
return true;
|
||
}
|
||
if ((int) ($item['creator_id'] ?? 0) === $adminId) {
|
||
return true;
|
||
}
|
||
if (
|
||
self::canViewOrdersForOwnPrescription($adminInfo)
|
||
&& self::isPrescriptionOrderPrescriber((int) ($item['prescription_id'] ?? 0), $adminId)
|
||
) {
|
||
return true;
|
||
}
|
||
$did = (int) ($item['diagnosis_id'] ?? 0);
|
||
$aid = (int) ($assistantByDiag[$did] ?? 0);
|
||
|
||
return $aid === $adminId && $adminId > 0;
|
||
}
|
||
|
||
/**
|
||
* 将当前业务订单关联处方提交至甘草药管家(CTM_PREVIEW → CTM_SUBMIT_RECIPEL)。
|
||
*
|
||
* @return array<string,mixed>|false 成功返回甘草 result 主要字段
|
||
*/
|
||
public static function submitGancaoRecipel(int $id, int $adminId, array $adminInfo)
|
||
{
|
||
|
||
self::$error = '';
|
||
if (!GancaoScmRecipelService::isConfigured()) {
|
||
$why = GancaoScmRecipelService::whyNotConfigured();
|
||
self::$error = $why !== '' ? $why : '甘草处方上传未启用或未配置完整,请检查 .env(GANCAO_SCM_*)须写在任意 [分区] 之前,或使用 [GANCAO_SCM] 分区';
|
||
|
||
return false;
|
||
}
|
||
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||
if (!$order) {
|
||
self::$error = '订单不存在';
|
||
|
||
return false;
|
||
}
|
||
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
|
||
self::$error = '无权限操作';
|
||
|
||
return false;
|
||
}
|
||
$item = $order->toArray();
|
||
$assistantByDiag = Diagnosis::where('id', (int) $order->diagnosis_id)->whereNull('delete_time')->column('assistant_id', 'id');
|
||
|
||
if (!self::canUploadGancaoRecipel($item, $adminId, $adminInfo, $assistantByDiag)) {
|
||
self::$error = '须处方审核通过后方可上传甘草;已完成/已取消订单不可上传;同一订单仅可上传一次';
|
||
|
||
return false;
|
||
}
|
||
$rxId = (int) $order->prescription_id;
|
||
|
||
if ($rxId <= 0) {
|
||
self::$error = '订单未关联处方';
|
||
|
||
return false;
|
||
}
|
||
$rx = PrescriptionLogic::detail($rxId, $adminId, $adminInfo);
|
||
if ($rx === null) {
|
||
self::$error = PrescriptionLogic::getError() !== '' ? PrescriptionLogic::getError() : '处方不存在';
|
||
|
||
return false;
|
||
}
|
||
$c = Config::get('gancao_scm', []);
|
||
$nameMap = is_array($c['herb_id_map'] ?? null) ? $c['herb_id_map'] : [];
|
||
$herbs = is_array($rx['herbs'] ?? null) ? $rx['herbs'] : [];
|
||
$docGidMap = GancaoScmRecipelService::doctorMedicineGidMapForHerbs($herbs);
|
||
|
||
[, $missing] = GancaoScmRecipelService::buildMList($herbs, $nameMap, $docGidMap);
|
||
if ($missing !== []) {
|
||
self::$error = '以下药材无法匹配甘草药ID(请核对医师药品库 zyt_doctor_medicine 中同名字段的 gid,或在处方 herbs 中写 gc_id / config 的 herb_id_map):' . implode('、', $missing);
|
||
|
||
return false;
|
||
}
|
||
|
||
$token = GancaoScmRecipelService::getToken(false);
|
||
if ($token === null || $token === '') {
|
||
$why = GancaoScmRecipelService::getLastGetTokenError();
|
||
self::$error = $why !== '' ? $why : '获取甘草开放平台 token 失败,请检查 GANCAO_SCM_GATEWAY_*(网关 OpenAPI)与 GANCAO_SCM_BIZ_*(业务账号,常与网关不同)、网关地址及服务器时间';
|
||
|
||
return false;
|
||
}
|
||
|
||
$orderArr = $order->toArray();
|
||
|
||
// dump($rx);
|
||
// dump($orderArr);
|
||
$previewPayload = GancaoScmRecipelService::buildPreviewPayload($rx, $orderArr, $token);
|
||
|
||
$prevRet = GancaoScmRecipelService::ctmPreview($previewPayload);
|
||
|
||
if ((int) ($prevRet['state'] ?? 0) !== 1) {
|
||
self::$error = '甘草预检查通信失败:' . (string) ($prevRet['msg'] ?? '');
|
||
|
||
return false;
|
||
}
|
||
$prevBody = $prevRet['body'] ?? [];
|
||
|
||
if (!GancaoScmRecipelService::isApiSuccess($prevBody)) {
|
||
self::$error = '甘草预检查未通过:' . GancaoScmRecipelService::apiStatusMessage($prevBody);
|
||
|
||
return false;
|
||
}
|
||
$result = $prevBody['result'] ?? [];
|
||
|
||
if (is_array($result)) {
|
||
$rules = $result['rule_check'] ?? [];
|
||
if (is_array($rules)) {
|
||
foreach ($rules as $ru) {
|
||
if (!is_array($ru)) {
|
||
continue;
|
||
}
|
||
$t = (int) ($ru['type'] ?? 0);
|
||
if ($t >= 2) {
|
||
self::$error = '甘草预检查规则拦截:' . (string) ($ru['msg'] ?? '') . '(code:' . (string) ($ru['code'] ?? '') . ')';
|
||
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
$mListOut = $result['m_list'] ?? [];
|
||
if (is_array($mListOut)) {
|
||
foreach ($mListOut as $rowM) {
|
||
if (is_array($rowM) && isset($rowM['is_available']) && (int) $rowM['is_available'] === 0) {
|
||
$tn = (string) ($rowM['title'] ?? '');
|
||
self::$error = '甘草药库缺药,请调整处方:' . ($tn !== '' ? $tn : '未知药材');
|
||
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
$addrParts = GancaoScmRecipelService::splitCnAddress((string) $order->shipping_address);
|
||
|
||
$appNo = (string) $order->order_no;
|
||
|
||
$submitPayload = GancaoScmRecipelService::buildSubmitPayload($rx, $orderArr, $token, $addrParts, $appNo);
|
||
|
||
$subRet = GancaoScmRecipelService::ctmSubmit($submitPayload);
|
||
|
||
if ((int) ($subRet['state'] ?? 0) !== 1) {
|
||
$errMsg = (string) ($subRet['msg'] ?? '');
|
||
$response = isset($subRet['response']) ? mb_substr((string) $subRet['response'], 0, 500) : '';
|
||
self::$error = '甘草下单通信失败:' . $errMsg . ($response !== '' ? ';响应:' . $response : '');
|
||
Log::error('Gancao CTM_SUBMIT failed', [
|
||
'subRet' => $subRet,
|
||
'payload' => json_encode(GancaoScmRecipelService::maskSensitiveData($submitPayload), JSON_UNESCAPED_UNICODE)
|
||
]);
|
||
|
||
return false;
|
||
}
|
||
$subBody = $subRet['body'] ?? [];
|
||
|
||
if (!GancaoScmRecipelService::isApiSuccess($subBody)) {
|
||
self::$error = '甘草下单失败:' . GancaoScmRecipelService::apiStatusMessage($subBody);
|
||
Log::error('Gancao CTM_SUBMIT api error', [
|
||
'body' => $subBody,
|
||
'payload' => json_encode(GancaoScmRecipelService::maskSensitiveData($submitPayload), JSON_UNESCAPED_UNICODE)
|
||
]);
|
||
|
||
return false;
|
||
}
|
||
$gcNo = (string) (($subBody['result'] ?? [])['recipel_order_no'] ?? '');
|
||
if ($gcNo === '') {
|
||
self::$error = '甘草返回缺少 recipel_order_no';
|
||
|
||
return false;
|
||
}
|
||
$order->gancao_reciperl_order_no = mb_substr($gcNo, 0, 32);
|
||
$order->gancao_submit_time = time();
|
||
try {
|
||
$order->save();
|
||
} catch (\Throwable $e) {
|
||
self::$error = '本地保存甘草单号失败:' . $e->getMessage();
|
||
|
||
return false;
|
||
}
|
||
|
||
self::writeLog($id, $adminId, $adminInfo, 'gancao_submit', '上传甘草药方成功 ' . $gcNo);
|
||
|
||
$fee = $subBody['result']['fee'] ?? [];
|
||
$appNo = $submitPayload['app_order_no'] ?? ('PO' . (string) $order->id);
|
||
|
||
return [
|
||
'recipel_order_no' => $gcNo,
|
||
'app_order_no' => $appNo,
|
||
'fee' => is_array($fee) ? $fee : [],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 甘草预下单测试(仅 CTM_PREVIEW,不提交订单)
|
||
* 用于在编辑订单时测试价格和配置
|
||
*
|
||
* @param array $params 包含 id 和可选的 dose_count
|
||
* @return array<string,mixed>|false 成功返回预览结果(包含价格信息)
|
||
*/
|
||
public static function previewGancaoRecipel(int $id, int $adminId, array $adminInfo, array $params = [])
|
||
{
|
||
self::$error = '';
|
||
if (!GancaoScmRecipelService::isConfigured()) {
|
||
$why = GancaoScmRecipelService::whyNotConfigured();
|
||
self::$error = $why !== '' ? $why : '甘草处方上传未启用或未配置完整';
|
||
return false;
|
||
}
|
||
|
||
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||
if (!$order) {
|
||
self::$error = '订单不存在';
|
||
return false;
|
||
}
|
||
|
||
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
|
||
self::$error = '无权限操作';
|
||
return false;
|
||
}
|
||
|
||
$rxId = (int) $order->prescription_id;
|
||
if ($rxId <= 0) {
|
||
self::$error = '订单未关联处方';
|
||
return false;
|
||
}
|
||
|
||
$rx = PrescriptionLogic::detail($rxId, $adminId, $adminInfo);
|
||
if ($rx === null) {
|
||
self::$error = PrescriptionLogic::getError() !== '' ? PrescriptionLogic::getError() : '处方不存在';
|
||
return false;
|
||
}
|
||
|
||
$c = Config::get('gancao_scm', []);
|
||
$nameMap = is_array($c['herb_id_map'] ?? null) ? $c['herb_id_map'] : [];
|
||
$herbs = is_array($rx['herbs'] ?? null) ? $rx['herbs'] : [];
|
||
$docGidMap = GancaoScmRecipelService::doctorMedicineGidMapForHerbs($herbs);
|
||
|
||
[, $missing] = GancaoScmRecipelService::buildMList($herbs, $nameMap, $docGidMap);
|
||
if ($missing !== []) {
|
||
self::$error = '以下药材无法匹配甘草药ID:' . implode('、', $missing);
|
||
return false;
|
||
}
|
||
|
||
$token = GancaoScmRecipelService::getToken(false);
|
||
if ($token === null || $token === '') {
|
||
$why = GancaoScmRecipelService::getLastGetTokenError();
|
||
self::$error = $why !== '' ? $why : '获取甘草开放平台 token 失败';
|
||
return false;
|
||
}
|
||
|
||
$orderArr = $order->toArray();
|
||
|
||
// 如果传递了 dose_count 参数,使用传递的值覆盖订单中的值
|
||
if (isset($params['dose_count']) && (int)$params['dose_count'] > 0) {
|
||
$orderArr['dose_count'] = (int)$params['dose_count'];
|
||
}
|
||
|
||
// 如果传递了 medication_days 参数,使用传递的值覆盖订单中的值
|
||
if (isset($params['medication_days']) && (int)$params['medication_days'] > 0) {
|
||
$orderArr['medication_days'] = (int)$params['medication_days'];
|
||
}
|
||
|
||
$previewPayload = GancaoScmRecipelService::buildPreviewPayload($rx, $orderArr, $token);
|
||
$prevRet = GancaoScmRecipelService::ctmPreview($previewPayload);
|
||
|
||
if ((int) ($prevRet['state'] ?? 0) !== 1) {
|
||
self::$error = '甘草预检查通信失败:' . (string) ($prevRet['msg'] ?? '');
|
||
return false;
|
||
}
|
||
|
||
$prevBody = $prevRet['body'] ?? [];
|
||
if (!GancaoScmRecipelService::isApiSuccess($prevBody)) {
|
||
self::$error = '甘草预检查未通过:' . GancaoScmRecipelService::apiStatusMessage($prevBody);
|
||
return false;
|
||
}
|
||
|
||
$result = $prevBody['result'] ?? [];
|
||
|
||
// 检查规则拦截
|
||
if (is_array($result)) {
|
||
$rules = $result['rule_check'] ?? [];
|
||
if (is_array($rules)) {
|
||
foreach ($rules as $ru) {
|
||
if (!is_array($ru)) continue;
|
||
$t = (int) ($ru['type'] ?? 0);
|
||
if ($t >= 2) {
|
||
self::$error = '甘草预检查规则拦截:' . (string) ($ru['msg'] ?? '');
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 检查药材可用性
|
||
$mListOut = $result['m_list'] ?? [];
|
||
if (is_array($mListOut)) {
|
||
foreach ($mListOut as $rowM) {
|
||
if (is_array($rowM) && isset($rowM['is_available']) && (int) $rowM['is_available'] === 0) {
|
||
$tn = (string) ($rowM['title'] ?? '');
|
||
self::$error = '甘草药库缺药:' . ($tn !== '' ? $tn : '未知药材');
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 返回预览结果
|
||
$fee = $result['fee'] ?? [];
|
||
return [
|
||
'success' => true,
|
||
'fee' => is_array($fee) ? $fee : [],
|
||
'result' => $result,
|
||
'order_no' => (string) $order->order_no,
|
||
'dose_count' => (int) ($orderArr['dose_count'] ?? $rx['dose_count'] ?? 0),
|
||
/** 本业务订单金额(与甘草预报价合计对比占比用) */
|
||
'amount' => round((float) ($order->amount ?? 0), 2),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 设置发货类型(甘草 / 洛阳),与确认发货选项一致
|
||
*
|
||
* @return array<string,mixed>|false
|
||
*/
|
||
public static function setShipMode(int $id, string $shipMode, int $adminId, array $adminInfo)
|
||
{
|
||
self::$error = '';
|
||
|
||
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||
if (!$order) {
|
||
self::$error = '订单不存在';
|
||
|
||
return false;
|
||
}
|
||
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
|
||
self::$error = '无权限操作';
|
||
|
||
return false;
|
||
}
|
||
$fs = (int) $order->fulfillment_status;
|
||
if ($fs === 3 || $fs === 4) {
|
||
self::$error = '已完成或已取消的订单不可修改发货类型';
|
||
|
||
return false;
|
||
}
|
||
|
||
$shipMode = self::normalizeShipMode($shipMode);
|
||
if ($shipMode === 'direct' && trim((string) $order->gancao_reciperl_order_no) !== '') {
|
||
self::$error = '已上传甘草药方,发货类型须为甘草药房';
|
||
|
||
return false;
|
||
}
|
||
|
||
$oldMode = self::normalizeShipMode((string) ($order->ship_mode ?? 'gancao'));
|
||
if ($oldMode === $shipMode) {
|
||
$out = $order->toArray();
|
||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||
self::attachLinkedPayOrders($out);
|
||
|
||
return $out;
|
||
}
|
||
|
||
$order->ship_mode = $shipMode;
|
||
|
||
try {
|
||
$order->save();
|
||
} catch (\Throwable $e) {
|
||
self::$error = $e->getMessage();
|
||
|
||
return false;
|
||
}
|
||
|
||
$oldText = $oldMode === 'direct' ? '洛阳药房' : '甘草药房';
|
||
$newText = $shipMode === 'direct' ? '洛阳药房' : '甘草药房';
|
||
self::writeLog($id, $adminId, $adminInfo, 'set_ship_mode', '发货类型:' . $oldText . ' → ' . $newText);
|
||
|
||
$out = $order->toArray();
|
||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||
self::attachLinkedPayOrders($out);
|
||
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* 修改订单金额
|
||
*/
|
||
public static function updateAmount(array $params, int $adminId, array $adminInfo): bool
|
||
{
|
||
self::$error = '';
|
||
$id = (int) $params['id'];
|
||
$newAmount = round((float) $params['amount'], 2);
|
||
|
||
// 查询订单
|
||
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||
if (!$order) {
|
||
self::setError('订单不存在');
|
||
return false;
|
||
}
|
||
|
||
// 数据权限检查
|
||
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
|
||
self::setError('无权限操作此订单');
|
||
return false;
|
||
}
|
||
|
||
// 状态检查:已完成(3)和已取消(4)不允许修改
|
||
if (in_array((int) $order->fulfillment_status, [3, 4], true)) {
|
||
self::setError('已完成或已取消的订单不允许修改金额');
|
||
return false;
|
||
}
|
||
|
||
$oldAmount = round((float) $order->amount, 2);
|
||
$oldAmountCents = (int) round($oldAmount * 100);
|
||
$newAmountCents = (int) round($newAmount * 100);
|
||
|
||
// 金额未变化
|
||
if ($newAmountCents === $oldAmountCents) {
|
||
self::setError('金额未发生变化');
|
||
return false;
|
||
}
|
||
|
||
// 更新金额
|
||
$order->amount = $newAmount;
|
||
try {
|
||
$order->save();
|
||
} catch (\Throwable $e) {
|
||
self::setError($e->getMessage());
|
||
return false;
|
||
}
|
||
|
||
// 记录日志
|
||
$summary = sprintf('将订单金额从 ¥%.2f 修改为 ¥%.2f', $oldAmount, $newAmount);
|
||
self::writeLog($id, $adminId, $adminInfo, 'update_amount', $summary);
|
||
|
||
return true;
|
||
}
|
||
|
||
private static function writeLog(int $orderId, int $adminId, array $adminInfo, string $action, string $summary): void
|
||
{
|
||
$adminName = $adminInfo['name'] ?? '';
|
||
if ($adminName === '' && $adminId > 0) {
|
||
$adminName = (string) \app\common\model\auth\Admin::where('id', $adminId)->value('name');
|
||
}
|
||
|
||
$log = new PrescriptionOrderLog();
|
||
$log->prescription_order_id = $orderId;
|
||
$log->admin_id = $adminId;
|
||
$log->admin_name = mb_substr($adminName, 0, 64);
|
||
$log->action = mb_substr($action, 0, 32);
|
||
$log->summary = mb_substr($summary, 0, 500);
|
||
$log->create_time = time();
|
||
|
||
try {
|
||
$log->save();
|
||
} catch (\Throwable $e) {
|
||
// 忽略日志写入错误
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处方患者姓名/手机号修正日志(写入 zyt_tcm_prescription_order_log,与 patchPrescriptionPatient 同源)
|
||
*
|
||
* @param int $prescriptionOrderId 无关联订单时可传 0(若库表禁止则可能静默失败)
|
||
*/
|
||
public static function appendRxPatientPatchLog(int $prescriptionOrderId, int $adminId, array $adminInfo, string $summary): void
|
||
{
|
||
self::writeLog($prescriptionOrderId, $adminId, $adminInfo, 'patch_rx_patient', $summary);
|
||
}
|
||
}
|