```
feat(split): 新增分账订单线下结案功能 - 添加 SplitCloseService 类用于处理订单结案逻辑 - 实现订单结案状态检查和标记功能 - 在汇付分账重试逻辑中加入结案状态验证,防止已结案订单继续重试 - 更新分账手动服务,添加结案操作类型 - 扩展钱包结算类,增加结案相关诊断方法 - 修改测试脚本,支持批量标记订单为已结案状态 - 添加数据库升级脚本,增加结案相关字段和菜单权限 ```
这个提交包含在:
@@ -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 {
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
.status-tag.s2 { background: #f0f9eb; color: #67c23a; }
|
||||
.status-tag.s3 { background: #fdf6ec; color: #e6a23c; }
|
||||
.status-tag.s4 { background: #fef0f0; color: #f56c6c; }
|
||||
.status-tag.s5 { background: #f4f4f5; color: #606266; }
|
||||
.money-num {
|
||||
display: block;
|
||||
text-align: right;
|
||||
@@ -279,6 +280,7 @@
|
||||
<el-option label="成功" value="2"></el-option>
|
||||
<el-option label="部分成功" value="3"></el-option>
|
||||
<el-option label="失败" value="4"></el-option>
|
||||
<el-option label="已结案" value="5"></el-option>
|
||||
</el-select>
|
||||
<el-select v-model="filters.channel" placeholder="通道" size="small" style="width:100px;" clearable>
|
||||
<el-option label="汇付" value="huifu"></el-option>
|
||||
@@ -322,8 +324,13 @@
|
||||
<span class="money-num highlight">¥{{ formatMoney(s.row.split_amount) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="分账来源" class-name="col-source" width="130" align="center">
|
||||
<el-table-column label="分账来源" class-name="col-source" width="148" align="center">
|
||||
<template slot-scope="s">
|
||||
<div v-if="s.row.settlement_source_text" style="font-size:11px;color:#606266;margin-bottom:4px;">
|
||||
结算:{{ s.row.settlement_source_text }}
|
||||
<span v-if="parseFloat(s.row.wallet_settlement_sum) > 0">(钱包 ¥{{ formatMoney(s.row.wallet_settlement_sum) }})</span>
|
||||
</div>
|
||||
<div v-if="s.row.retry_blocked && s.row.status == 4" style="font-size:11px;color:#f56c6c;margin-bottom:4px;">不可重试</div>
|
||||
<div class="source-cell" v-if="s.row.split_source === 'manual' || s.row.has_manual_override || s.row.calc_source">
|
||||
<span class="source-tag manual" v-if="s.row.split_source === 'manual' || s.row.has_manual_override">手动</span>
|
||||
<span v-else-if="s.row.calc_source" :class="'source-tag ' + (s.row.calc_source === 'commission' ? 'commission' : 'rule')">
|
||||
@@ -388,7 +395,8 @@
|
||||
<template slot-scope="s">
|
||||
<el-button type="text" size="small" v-if="s.row.can_edit_manual" @click="openManualDialog(s.row)">指定接收方</el-button>
|
||||
<el-button type="text" size="small" v-if="s.row.can_view_manual_history" @click="openHistoryDrawer(s.row)">变更历史</el-button>
|
||||
<el-button type="text" size="small" v-if="s.row.status == 4" @click="retry(s.row)">重试</el-button>
|
||||
<el-button type="text" size="small" v-if="s.row.can_retry_split && !s.row.is_closed" @click="retry(s.row)">重试</el-button>
|
||||
<el-button type="text" size="small" v-if="s.row.can_mark_closed" @click="markClosed(s.row)">标记结案</el-button>
|
||||
<el-button type="text" size="small" @click="query(s.row.id)">同步</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -405,7 +413,7 @@
|
||||
@size-change="onSizeChange">
|
||||
</el-pagination>
|
||||
</div>
|
||||
<p class="op-hint">失败汇付单可「指定接收方」改人改金额后重试;分账成功或失败后均可点「变更历史」查看每次改人记录。「同步」只更新状态,不会重复扣款。</p>
|
||||
<p class="op-hint">失败汇付单可「指定接收方」改人改金额后重试;财务已线下处理的异常单请「标记结案」以停止自动重试。分账成功或失败后均可点「变更历史」查看记录。「同步」只更新状态,不会重复扣款。</p>
|
||||
</div>
|
||||
|
||||
<!-- 指定接收方弹窗 -->
|
||||
|
||||
在新工单中引用
屏蔽一个用户