This commit is contained in:
Your Name
2026-04-30 17:36:47 +08:00
parent 4c21ee5938
commit 7e557b1ea2
33 changed files with 8718 additions and 94 deletions
@@ -289,6 +289,45 @@ class OrderController extends BaseAdminController
return $this->success($msg, $result);
}
/**
* @notes 拆分订单:生成多笔子单(待支付/待审核/已支付继承支付信息),金额合计须等于原单;原单软删除;业务订单关联自动迁移
* @return \think\response\Json
*/
public function split()
{
$params = (new OrderValidate())->post()->goCheck('split');
$orderId = (int) $params['id'];
$amounts = $params['amounts'] ?? [];
if (! is_array($amounts)) {
return $this->fail('子单金额格式错误');
}
$orderTypes = $params['order_types'] ?? [];
if (! is_array($orderTypes)) {
return $this->fail('子单订单类型格式错误');
}
$order = \app\common\model\Order::find($orderId);
if (! $order) {
return $this->fail('订单不存在');
}
if (! $this->canEditOrder($order)) {
return $this->fail('无权限拆分此订单');
}
$result = OrderLogic::split($orderId, $amounts, $orderTypes);
if ($result === false) {
return $this->fail(OrderLogic::getError());
}
$summary = '拆分为子单 id=' . implode(',', $result['new_order_ids'] ?? []);
$this->logOrderAction($orderId, 'split', $summary);
foreach ($result['new_order_ids'] ?? [] as $nid) {
$this->logOrderAction((int) $nid, 'split_child', '来源原单 id=' . $orderId . ' ' . (string) ($order->order_no ?? ''));
}
return $this->success('拆分成功', $result);
}
/**
* @notes 编辑订单(关联患者、订单类型)
* 权限:超管或指定角色组可修改任意订单;其他用户只能修改自己创建的订单
@@ -79,11 +79,13 @@ class PrescriptionOrderController extends BaseAdminController
{
$params = (new PrescriptionOrderValidate())->get()->goCheck('logisticsTrace');
$expressOverride = trim((string) $this->request->get('express_company', ''));
$phoneTail = trim((string) ($params['phone_tail'] ?? ''));
$data = PrescriptionOrderLogic::logisticsTrace(
(int) $params['id'],
$expressOverride,
$this->adminId,
$this->adminInfo
$this->adminInfo,
$phoneTail
);
if ($data === null) {
return $this->fail(PrescriptionOrderLogic::getError());
@@ -16,6 +16,7 @@ use app\common\model\auth\AdminDept;
use app\common\model\tcm\Prescription;
use app\common\model\tcm\PrescriptionOrder;
use app\common\model\tcm\PrescriptionOrderPayOrder;
use app\common\model\ExpressTracking;
use app\common\service\gancao\GancaoScmRecipelService;
use think\facade\Config;
use think\db\Query;
@@ -66,16 +67,111 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
));
}
/**
* 重点看板统计:去掉审核/履约状态筛选,保留其它筛选(时间、医生、医助、患者、供货方式等)
*
* @return array<int, array{0: string, 1: string, 2: mixed}>
*/
private function searchWhereWithoutStatusFilters(): array
{
return array_values(array_filter(
$this->searchWhere,
static function ($w): bool {
if (!\is_array($w) || \count($w) < 2) {
return true;
}
$field = (string) ($w[0] ?? '');
if (!in_array($field, ['prescription_audit_status', 'payment_slip_audit_status', 'fulfillment_status'], true)) {
return true;
}
$op = strtolower((string) ($w[1] ?? ''));
return $op !== '=';
}
));
}
private function applyPermissionAndExtraFilters($query): void
{
$this->applyDoctorAssistantFilters($query);
$this->applyPatientKeywordFilter($query);
$this->applyExpressCompanyFilter($query);
$this->applyExpressKeywordFilter($query);
$this->applySupplyModeFilter($query);
if (!PrescriptionOrderLogic::canSeeAllPrescriptionOrders($this->adminInfo)) {
$query->where('creator_id', $this->adminId);
}
$this->applyDataScopeForPrescriptionOrder($query);
}
/**
* 按快递公司筛选(点击快捷标签:顺丰 / 京东)
*
* 说明:主表 express_company 常为 auto/空;快递100 回写承运商在 zyt_express_tracking,需一并匹配。
*/
private function applyExpressCompanyFilter($query): void
{
$company = strtolower(trim((string) ($this->params['express_company'] ?? '')));
if (!in_array($company, ['sf', 'jd'], true)) {
return;
}
$poTbl = (new PrescriptionOrder())->getTable();
$etTbl = (new ExpressTracking())->getTable();
if ($company === 'sf') {
$po = "(LOWER(TRIM(IFNULL(`{$poTbl}`.`express_company`,''))) IN ('sf','shunfeng')"
. " OR `{$poTbl}`.`express_company` LIKE '%顺丰%'"
. " OR (TRIM(IFNULL(`{$poTbl}`.`tracking_number`,'')) <> ''"
. " AND (`{$poTbl}`.`tracking_number` LIKE 'SF%' OR `{$poTbl}`.`tracking_number` LIKE 'sf%')))";
$et = "EXISTS (SELECT 1 FROM `{$etTbl}` et WHERE et.`delete_time` IS NULL"
. " AND et.`order_id` = `{$poTbl}`.`id` AND et.`order_type` = 'prescription'"
. " AND (LOWER(TRIM(IFNULL(et.`express_company`,''))) IN ('sf','shunfeng')"
. " OR et.`express_company_name` LIKE '%顺丰%'"
. " OR (TRIM(IFNULL(et.`tracking_number`,'')) <> ''"
. " AND (et.`tracking_number` LIKE 'SF%' OR et.`tracking_number` LIKE 'sf%'))))";
$query->whereRaw("(($po) OR ($et))");
return;
}
$po = "(LOWER(TRIM(IFNULL(`{$poTbl}`.`express_company`,''))) IN ('jd','jingdong')"
. " OR `{$poTbl}`.`express_company` LIKE '%京东%'"
. " OR (TRIM(IFNULL(`{$poTbl}`.`tracking_number`,'')) <> ''"
. " AND (`{$poTbl}`.`tracking_number` LIKE 'JD%' OR `{$poTbl}`.`tracking_number` LIKE 'jd%'"
. " OR `{$poTbl}`.`tracking_number` LIKE 'JDVB%' OR `{$poTbl}`.`tracking_number` LIKE 'jdvb%')))";
$et = "EXISTS (SELECT 1 FROM `{$etTbl}` et WHERE et.`delete_time` IS NULL"
. " AND et.`order_id` = `{$poTbl}`.`id` AND et.`order_type` = 'prescription'"
. " AND (LOWER(TRIM(IFNULL(et.`express_company`,''))) IN ('jd','jingdong')"
. " OR et.`express_company_name` LIKE '%京东%'"
. " OR (TRIM(IFNULL(et.`tracking_number`,'')) <> ''"
. " AND (et.`tracking_number` LIKE 'JD%' OR et.`tracking_number` LIKE 'jd%'"
. " OR et.`tracking_number` LIKE 'JDVB%' OR et.`tracking_number` LIKE 'jdvb%'))))";
$query->whereRaw("(($po) OR ($et))");
}
/**
* 供货方式:甘草(已上传甘草药方单号)/ 自营(无甘草单号,走药房直发等)
*
* 入参 supply_modegancao | self,空表示不限
*/
private function applySupplyModeFilter($query): void
{
$mode = strtolower(trim((string) ($this->params['supply_mode'] ?? '')));
if ($mode === '') {
return;
}
if ($mode === 'gancao') {
$query->whereRaw("TRIM(IFNULL(`gancao_reciperl_order_no`,'')) <> ''");
return;
}
if ($mode === 'self') {
$query->whereRaw("TRIM(IFNULL(`gancao_reciperl_order_no`,'')) = ''");
return;
}
}
/**
* 数据隔离:创建者 ∈ 可见 admin,或 关联诊单医助 ∈ 可见 admin
*/
@@ -302,6 +398,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
public function extend(): array
{
$s = $this->computeListStatsForExtend();
$focus = $this->computeFocusCountsForExtend();
return [
'deposit_min_amount' => PrescriptionOrderLogic::depositMinAmount(),
@@ -320,6 +417,32 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
'stats_time_end' => $s['label_end'],
/** 统计口径:all=支付审核白名单全量;assistant=医助仅诊单协助业绩;restricted=与列表同域 */
'stats_scope' => $s['scope'],
'focus_pending_rx_count' => $focus['pending_rx'],
'focus_pending_pay_count' => $focus['pending_pay'],
'focus_pending_ship_count' => $focus['pending_ship'],
'focus_risk_count' => $focus['risk'],
];
}
/**
* 重点看板计数(基于“当前筛选(去除审核/履约状态)”后的全量命中)
*
* @return array{pending_rx:int,pending_pay:int,pending_ship:int,risk:int}
*/
private function computeFocusCountsForExtend(): array
{
$query = PrescriptionOrder::where($this->searchWhereWithoutStatusFilters())->whereNull('delete_time');
$this->applyPermissionAndExtraFilters($query);
return [
// 处方审核待处理
'pending_rx' => (int) (clone $query)->where('prescription_audit_status', 0)->count(),
// 支付审核待处理(前提:处方已通过)
'pending_pay' => (int) (clone $query)->where('prescription_audit_status', 1)->where('payment_slip_audit_status', 0)->count(),
// 履约待发货
'pending_ship' => (int) (clone $query)->where('fulfillment_status', 2)->count(),
// 重点异常:支付审核驳回(与前端「重点异常」快捷筛选口径一致)
'risk' => (int) (clone $query)->where('payment_slip_audit_status', 2)->count(),
];
}
@@ -512,4 +635,30 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
. "AND (dg.`patient_name` LIKE '{$likeSql}' OR dg.`phone` LIKE '{$likeSql}')"
);
}
/**
* 按快递单号 / 快递公司 模糊检索
*/
private function applyExpressKeywordFilter($query): void
{
$kw = trim((string) ($this->params['express_keyword'] ?? ''));
if ($kw === '') {
return;
}
$likeInner = str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $kw);
$like = '%' . $likeInner . '%';
$likeSql = str_replace("'", "''", $like);
$poTbl = (new PrescriptionOrder())->getTable();
$etTbl = (new ExpressTracking())->getTable();
$query->whereRaw(
"(IFNULL(`{$poTbl}`.`tracking_number`,'') LIKE '{$likeSql}'"
. " OR IFNULL(`{$poTbl}`.`express_company`,'') LIKE '{$likeSql}'"
. " OR EXISTS (SELECT 1 FROM `{$etTbl}` et WHERE et.`delete_time` IS NULL"
. " AND et.`order_id` = `{$poTbl}`.`id` AND et.`order_type` = 'prescription'"
. " AND (IFNULL(et.`tracking_number`,'') LIKE '{$likeSql}'"
. " OR IFNULL(et.`express_company`,'') LIKE '{$likeSql}'"
. " OR IFNULL(et.`express_company_name`,'') LIKE '{$likeSql}'))"
. ")"
);
}
}
@@ -24,6 +24,8 @@ class OrderActionLogLogic
'create' => '创建订单',
'create_wechat_work' => '创建订单(企微对外收款)',
'assign_assistant' => '变更创建人(指派医助)',
'split' => '拆分订单',
'split_child' => '拆分生成子单',
];
public static function record(
+180 -2
View File
@@ -450,6 +450,182 @@ class OrderLogic
}
}
/**
* 拆分订单:待支付(1)、付呗待审核(5)、已支付(2);已关联业务订单的自动迁关联至各子单
*
* @param array<int, float|int|string> $amounts 子单金额(元),2~20 笔,分合计须与原单一致
* @param array<int, int|string> $orderTypes 与子单一一对应的订单类型 1~7
* @return array{new_order_ids: int[], orders: array<int, array<string, mixed>>}|false
*/
public static function split(int $orderId, array $amounts, array $orderTypes): array|false
{
$order = Order::whereNull('delete_time')->find($orderId);
if (! $order) {
self::setError('订单不存在');
return false;
}
$st = (int) $order->status;
if ($st === 5 && (string) ($order->payment_method ?? '') !== 'fubei') {
self::setError('待审核订单仅支持付呗渠道拆分');
return false;
}
if (! in_array($st, [1, 5, 2], true)) {
self::setError('仅「待支付」「待审核(付呗)」或「已支付」的订单可拆分');
return false;
}
$rawList = array_values($amounts);
$count = count($rawList);
if ($count < 2 || $count > 20) {
self::setError('拆分管数须在 220 之间');
return false;
}
$centsList = [];
foreach ($rawList as $a) {
$v = round((float) $a, 2);
if ($v <= 0) {
self::setError('每笔子单金额须大于 0');
return false;
}
$centsList[] = (int) round($v * 100);
}
$origCents = (int) round(round((float) $order->amount, 2) * 100);
if (array_sum($centsList) !== $origCents) {
self::setError(
'子单金额之和必须等于原订单金额(¥' . number_format($origCents / 100, 2, '.', '') . ''
);
return false;
}
$normAmounts = array_map(static fn (int $c) => round($c / 100, 2), $centsList);
$typeList = array_values($orderTypes);
if (count($typeList) !== $count) {
self::setError('订单类型笔数须与拆分数一致');
return false;
}
$allowedOt = [1, 2, 3, 4, 5, 6, 7, 8];
foreach ($typeList as $ot) {
if (! in_array((int) $ot, $allowedOt, true)) {
self::setError('订单类型无效,须为 18');
return false;
}
}
Db::startTrans();
try {
$origRemark = trim((string) ($order->remark ?? ''));
$splitTag = '[拆自原单#' . $orderId . '-' . (string) ($order->order_no ?? '') . ']';
$newOrders = [];
foreach ($normAmounts as $i => $amt) {
$n = new Order();
$n->order_no = self::generateOrderNo();
$n->patient_id = $order->patient_id;
$n->creator_id = $order->creator_id;
$n->order_type = (int) $typeList[$i];
$n->amount = $amt;
if ($st === 5) {
$n->payment_method = 'fubei';
$n->status = 5;
} elseif ($st === 2) {
$n->status = 2;
$n->payment_method = (string) ($order->payment_method ?? 'manual');
if ((string) ($order->payment_time ?? '') !== '') {
$n->payment_time = (string) $order->payment_time;
}
if ((string) ($order->trade_no ?? '') !== '') {
$n->trade_no = (string) $order->trade_no;
}
} else {
$n->status = 1;
}
$combined = $origRemark !== '' ? $origRemark . ' ' . $splitTag : $splitTag;
$n->remark = mb_substr($combined, 0, 500);
if ($order->payee_userid !== null && (string) $order->payee_userid !== '') {
$n->payee_userid = (string) $order->payee_userid;
}
if ($order->payer_external_userid !== null && (string) $order->payer_external_userid !== '') {
$n->payer_external_userid = (string) $order->payer_external_userid;
}
$n->save();
$newOrders[] = $n;
}
$newIds = [];
foreach ($newOrders as $o) {
$newIds[] = (int) $o->id;
}
// 处方业务订单 ↔ 支付单:原单关联迁移到全部子单(各业务订单对每笔子单各一条关联,合计金额不变)
$linkRows = PrescriptionOrderPayOrder::where('pay_order_id', $orderId)->select();
foreach ($linkRows as $link) {
$poId = (int) $link->prescription_order_id;
if ($poId <= 0) {
continue;
}
PrescriptionOrderPayOrder::where('prescription_order_id', $poId)
->where('pay_order_id', $orderId)
->delete();
foreach ($newIds as $nid) {
$dup = PrescriptionOrderPayOrder::where('prescription_order_id', $poId)
->where('pay_order_id', $nid)
->count();
if ($dup > 0) {
continue;
}
$nl = new PrescriptionOrderPayOrder();
$nl->prescription_order_id = $poId;
$nl->pay_order_id = $nid;
$nl->create_time = time();
$nl->save();
}
}
$order->remark = mb_substr(
($origRemark !== '' ? $origRemark . ' | ' : '') . '已拆分,子单ID:' . implode(',', $newIds),
0,
500
);
$order->save();
$order->delete();
Db::commit();
$brief = [];
foreach ($newOrders as $o) {
$brief[] = [
'id' => (int) $o->id,
'order_no' => (string) $o->order_no,
'amount' => (float) $o->amount,
'order_type' => (int) $o->order_type,
];
}
return [
'new_order_ids' => $newIds,
'orders' => $brief,
];
} catch (\Throwable $e) {
Db::rollback();
Log::error('Order split failed: ' . $e->getMessage(), ['order_id' => $orderId]);
self::setError('拆分失败:' . $e->getMessage());
return false;
}
}
/**
* @notes 今日收益(按角色权限:超管/主管看全部,普通员工看自己)
* @param int $adminId 当前管理员ID
@@ -723,16 +899,18 @@ class OrderLogic
4 => '首付费用',
5 => '尾款费用',
6 => '其他费用',
7 => '全部费用',
8 => '驼奶费用',
];
/**
* @notes 订单统计(按订单类型或退款)
* @param array $params order_type(-1全部已支付类型合计,0退款,1挂号费,2问诊费,3药品费用,4首付,5尾款,6其他), days(0今天,7,30)
* @param array $params order_type(-1全部已支付类型合计,0退款,18 各费用类型含驼奶费用), days(0今天,7,30)
* @return array
*/
public static function orderStats(array $params = [], int $adminId = 0, ?array $adminInfo = null)
{
$allPaidOrderTypes = [1, 2, 3, 4, 5, 6];
$allPaidOrderTypes = [1, 2, 3, 4, 5, 6, 7, 8];
$orderType = array_key_exists('order_type', $params) ? (int) $params['order_type'] : 1;
if ($orderType !== -1 && $orderType !== 0 && !array_key_exists($orderType, self::$orderTypeNames)) {
$orderType = 1;
@@ -422,6 +422,7 @@ class PrescriptionOrderLogic
5 => '尾款费用',
6 => '其他费用',
7 => '全部费用',
8 => '驼奶费用',
];
$statusMap = [
1 => '待支付',
@@ -882,7 +883,7 @@ class PrescriptionOrderLogic
*
* @return array<string,mixed>|null
*/
public static function logisticsTrace(int $id, string $expressCompanyOverride, int $adminId, array $adminInfo): ?array
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();
@@ -933,7 +934,7 @@ class PrescriptionOrderLogic
];
} else {
// 数据库没有数据,调用快递100 API
$payload = ExpressTrackService::query($ec, $num, (string) ($row->recipient_phone ?? ''));
$payload = ExpressTrackService::query($ec, $num, (string) ($row->recipient_phone ?? ''), $phoneTail);
$payload['source'] = 'api';
}
@@ -17,7 +17,7 @@ class OrderValidate extends BaseValidate
'id' => 'require|integer',
'patient_id' => 'require|integer',
'patient_id_optional' => 'integer',
'order_type' => 'require|in:1,2,3,4,5,6,7',
'order_type' => 'require|in:1,2,3,4,5,6,7,8',
'amount' => 'require|float',
'status' => 'require|in:1,2,3,4',
'payment_method' => 'in:alipay,wechat,wechat_work,fubei,manual',
@@ -26,6 +26,8 @@ class OrderValidate extends BaseValidate
'is_supplement' => 'in:0,1',
'order_ids' => 'require|array',
'assistant_id' => 'require|integer|gt:0',
'amounts' => 'require|array',
'order_types' => 'require|array',
];
protected $message = [
@@ -47,5 +49,6 @@ class OrderValidate extends BaseValidate
'refund' => ['id'],
'delete' => ['id'],
'assign_assistant' => ['order_ids', 'assistant_id'],
'split' => ['id', 'amounts', 'order_types'],
];
}
@@ -23,9 +23,9 @@ class PrescriptionOrderValidate extends BaseValidate
'tracking_number' => 'max:80',
'express_company' => 'max:20',
'ship_mode' => 'in:gancao,direct',
'fee_type' => 'require|in:1,2,3,4,5,6,7',
'fee_type' => 'require|in:1,2,3,4,5,6,7,8',
'amount' => 'require|float',
'order_type' => 'require|in:1,2,3,4,5,6,7',
'order_type' => 'require|in:1,2,3,4,5,6,7,8',
'pay_amount' => 'require|float|gt:0',
'pay_remark' => 'max:200',
'remark_extra' => 'max:500',
@@ -34,6 +34,7 @@ class PrescriptionOrderValidate extends BaseValidate
'fulfillment_status' => 'require|integer|in:3,7,8,9,10,11,12',
'patient_name' => 'require|max:50',
'phone' => 'require|max:20',
'phone_tail' => 'max:20|regex:/^\\d*$/',
];
protected $message = [
@@ -47,6 +48,7 @@ class PrescriptionOrderValidate extends BaseValidate
'action.require' => '请选择审核操作',
'patient_name.require' => '请输入患者姓名',
'phone.require' => '请输入手机号',
'phone_tail.regex' => '手机后四位仅支持数字',
];
protected $scene = [
@@ -61,7 +63,7 @@ class PrescriptionOrderValidate extends BaseValidate
'is_follow_up', 'prev_staff', 'service_channel', 'service_package',
'tracking_number', 'express_company', 'fee_type', 'amount', 'remark_extra', 'pay_order_ids',
],
'logisticsTrace' => ['id'],
'logisticsTrace' => ['id', 'phone_tail'],
'auditPrescription' => ['id', 'action', 'remark'],
'auditPayment' => ['id', 'action', 'remark'],
'withdraw' => ['id'],