feat(split): 新增分账订单线下结案功能

- 添加 SplitCloseService 类用于处理订单结案逻辑
- 实现订单结案状态检查和标记功能
- 在汇付分账重试逻辑中加入结案状态验证,防止已结案订单继续重试
- 更新分账手动服务,添加结案操作类型
- 扩展钱包结算类,增加结案相关诊断方法
- 修改测试脚本,支持批量标记订单为已结案状态
- 添加数据库升级脚本,增加结案相关字段和菜单权限
```
这个提交包含在:
可乐加冰
2026-10-24 21:18:48 +08:00
父节点 557d3e69ba
当前提交 6a654070ed
共有 16 个文件被更改,包括 1314 次插入 和 96 次删除

查看文件

@@ -5,8 +5,10 @@ namespace app\admin\controller\shopro\split;
use app\admin\controller\shopro\Base;
use addons\shopro\library\split\Config as SplitConfig;
use addons\shopro\library\split\SplitCalculator;
use addons\shopro\library\split\SplitCloseService;
use addons\shopro\library\split\SplitExecutor;
use addons\shopro\library\split\SplitManualService;
use addons\shopro\library\split\SplitWalletSettlement;
use addons\shopro\library\split\ProfitSharingService;
use app\admin\model\shopro\order\Order as ShopOrder;
use app\admin\model\shopro\user\User as ShopUser;
@@ -22,6 +24,7 @@ class Order extends Base
2 => '成功',
3 => '部分成功',
4 => '失败',
5 => '已结案',
];
public function _initialize()
@@ -136,6 +139,7 @@ class Order extends Base
{
$rows = [];
$manualService = new SplitManualService();
$closeService = new SplitCloseService();
$orderIds = [];
foreach ($list as $item) {
$data = is_array($item) ? $item : $item->toArray();
@@ -146,6 +150,7 @@ class Order extends Base
}
$overrideOrderIds = array_flip($manualService->orderIdsWithOverride($orderIds));
$logOrderIds = array_flip($manualService->orderIdsWithLog($orderIds));
$closedLogOrderIds = array_flip($closeService->orderIdsWithCloseLog($orderIds));
foreach ($list as $item) {
$data = is_array($item) ? $item : $item->toArray();
@@ -209,10 +214,42 @@ class Order extends Base
$hasManualLog = isset($logOrderIds[$orderId]);
$data['has_manual_override'] = $hasOverride;
$data['has_manual_log'] = $hasManualLog;
$data['can_edit_manual'] = ($status === 4 && $channel === 'huifu');
$settleDiag = SplitWalletSettlement::diagnoseOrderSettlement($orderId);
$data['settlement_source'] = $settleDiag['settlement_source'];
$data['settlement_source_text'] = $settleDiag['settlement_source_text'];
$data['wallet_settlement_sum'] = $settleDiag['wallet_sum'];
$data['retry_blocked'] = $settleDiag['retry_blocked'];
$data['retry_block_reason'] = $settleDiag['retry_block_reason'];
$data['can_edit_manual'] = ($status === 4 && $channel === 'huifu')
&& !$settleDiag['retry_blocked'];
$data['can_retry_split'] = ($status === 4 && $channel === 'huifu')
&& !$settleDiag['retry_blocked'];
// 变更历史:成功/失败后均可查看,不限于 status=4
$data['can_view_manual_history'] = ($channel === 'huifu')
&& ($hasOverride || $hasManualLog || $orderSplitSource === 'manual');
&& ($hasOverride || $hasManualLog || $orderSplitSource === 'manual' || isset($closedLogOrderIds[$orderId]));
$isClosed = ($status === SplitCloseService::STATUS_CLOSED) || isset($closedLogOrderIds[$orderId]);
$data['is_closed'] = $isClosed;
$data['close_reason'] = trim((string) ($data['close_reason'] ?? ''));
$data['closetime_text'] = !empty($data['closetime'])
? date('Y-m-d H:i:s', $data['closetime'])
: '';
$data['can_mark_closed'] = ($channel === 'huifu')
&& in_array($status, [2, 3, 4], true)
&& !$isClosed;
if ($settleDiag['retry_blocked'] && $status === 4 && $data['fail_reason_full'] === '') {
$data['fail_reason_full'] = $settleDiag['retry_block_reason'];
$data['fail_reason'] = mb_strlen($settleDiag['retry_block_reason']) > 72
? (mb_substr($settleDiag['retry_block_reason'], 0, 72) . '…')
: $settleDiag['retry_block_reason'];
}
if ($status === SplitCloseService::STATUS_CLOSED && $data['close_reason'] !== '') {
$data['fail_reason_full'] = '线下结案:' . $data['close_reason'];
$data['fail_reason'] = mb_strlen($data['close_reason']) > 72
? (mb_substr($data['close_reason'], 0, 72) . '…')
: $data['close_reason'];
}
unset($data['order'], $data['receivers']);
$rows[] = $data;
@@ -630,6 +667,37 @@ class Order extends Base
$this->success('操作成功', null, $result);
}
/**
* 标记已线下处理(停止重试,status=5)
*/
public function markClosed()
{
$splitOrderId = intval($this->request->post('split_order_id', 0));
$reason = trim((string) $this->request->post('reason', ''));
$row = $this->model->get($splitOrderId);
if (!$row) {
$this->error('分账记录不存在');
}
if (($row['channel'] ?? '') !== 'huifu') {
$this->error('仅汇付分账记录可标记结案');
}
try {
$result = (new SplitCloseService())->closeByOrderId(
intval($row['order_id']),
$reason,
intval($this->auth->id),
(string) ($this->auth->nickname ?? $this->auth->username ?? '')
);
} catch (\Throwable $e) {
$this->error($e->getMessage());
}
if (!empty($result['already'])) {
$this->success('该订单此前已标记结案');
}
$this->success('已标记线下结案,将不再自动重试');
}
public function retry($ids = null)
{
$id = $ids ?: $this->request->post('id');
@@ -637,6 +705,14 @@ class Order extends Base
if (!$row) {
$this->error('分账记录不存在,可能已被上次重试替换。请刷新列表后,对最新一条失败记录重试');
}
if (intval($row['status']) === SplitCloseService::STATUS_CLOSED
|| (new SplitCloseService())->isClosed((int) $row['order_id'])) {
$this->error('该订单已标记线下结案,不可重试');
}
$block = SplitWalletSettlement::getHuifuRetryBlockReason((int) $row['order_id']);
if ($block !== null) {
$this->error($block);
}
$orderId = (int) $row['order_id'];
$channel = $row['channel'] ?? 'wechat';
try {