feat(split): 优化手工分账服务日志记录和历史展示功能

- 添加CommissionReward和RegionalReward模型引用,用于获取原有分账数据
- 实现snapshotPlannedAutoItems方法,首次改人时保存原系统计划快照
- 新增itemsEqual方法,比较分账项目是否发生变化,避免重复记录日志
- 重构历史记录展示界面,添加change_summary摘要显示变更内容
- 优化前端格式化显示,统一账户显示格式为"未进件"或实际账户
- 修复重试操作时的日志记录问题,改进备注显示逻辑
```
这个提交包含在:
可乐加冰
2026-10-23 00:31:47 +08:00
父节点 d2f22d7ecf
当前提交 f1e2be8dc4
共有 3 个文件被更改,包括 180 次插入 和 19 次删除

查看文件

@@ -3,6 +3,9 @@
namespace addons\shopro\library\split; namespace addons\shopro\library\split;
use addons\shopro\model\Order; use addons\shopro\model\Order;
use addons\shopro\model\User;
use addons\shopro\model\commission\Reward as CommissionReward;
use addons\shopro\model\regional_agent\Reward as RegionalReward;
use addons\shopro\model\split\Receiver; use addons\shopro\model\split\Receiver;
use addons\shopro\model\split\SplitManual; use addons\shopro\model\split\SplitManual;
use addons\shopro\model\split\SplitManualLog; use addons\shopro\model\split\SplitManualLog;
@@ -92,14 +95,14 @@ class SplitManualService
$originalItems = []; $originalItems = [];
if (!$existing) { if (!$existing) {
$calculator = new SplitCalculator(); $originalItems = $this->snapshotPlannedAutoItems($orderId);
$originalItems = $calculator->calculateAuto($orderId);
$originalItems = $this->enrichItemsForStorage($originalItems);
} else { } else {
$originalItems = $existing['original_items'] ?? []; $originalItems = $existing['original_items'] ?? [];
} }
$storageItems = $this->enrichItemsForStorage($normalized); $storageItems = $this->enrichItemsForStorage($normalized);
$itemsChanged = !$existing || !$this->itemsEqual($beforeItems, $storageItems);
$logBefore = $existing ? $beforeItems : $originalItems;
Db::startTrans(); Db::startTrans();
try { try {
@@ -119,7 +122,9 @@ class SplitManualService
]); ]);
} }
$this->appendLog($orderId, $action, $beforeItems, $storageItems, $adminId, $adminName, $remark); if ($itemsChanged) {
$this->appendLog($orderId, $action, $logBefore, $storageItems, $adminId, $adminName, $remark);
}
Db::commit(); Db::commit();
return $manual; return $manual;
} catch (\Exception $e) { } catch (\Exception $e) {
@@ -174,11 +179,11 @@ class SplitManualService
'order_id' => $orderId, 'order_id' => $orderId,
'order_sn' => $orderSn, 'order_sn' => $orderSn,
'action' => 'retry', 'action' => 'retry',
'before_items' => $manual['items'] ?? [], 'before_items' => [],
'after_items' => $manual['items'] ?? [], 'after_items' => $manual['items'] ?? [],
'admin_id' => intval($adminId), 'admin_id' => intval($adminId),
'admin_name' => $adminName, 'admin_name' => $adminName,
'remark' => $remark, 'remark' => $remark ?: '保存并重试',
'retry_result' => 'pending', 'retry_result' => 'pending',
'createtime' => time(), 'createtime' => time(),
]); ]);
@@ -236,6 +241,11 @@ class SplitManualService
$data['createtime_text'] = !empty($data['createtime']) $data['createtime_text'] = !empty($data['createtime'])
? date('Y-m-d H:i:s', $data['createtime']) ? date('Y-m-d H:i:s', $data['createtime'])
: '-'; : '-';
$before = is_array($data['before_items'] ?? null) ? $data['before_items'] : [];
$after = is_array($data['after_items'] ?? null) ? $data['after_items'] : [];
$data['before_summary'] = $this->formatItemsBrief($before);
$data['after_summary'] = $this->formatItemsBrief($after);
$data['change_summary'] = $this->buildChangeSummary($data['action'] ?? '', $before, $after);
$list[] = $data; $list[] = $data;
} }
@@ -578,4 +588,147 @@ class SplitManualService
]; ];
return $map[$result] ?? $result; return $map[$result] ?? $result;
} }
/**
* 首次改人时的「原系统计划」快照(含未进件推广员)
*/
protected function snapshotPlannedAutoItems($orderId)
{
$calculator = new SplitCalculator();
$auto = $calculator->calculateAuto($orderId);
if (!empty($auto)) {
return $this->enrichItemsForStorage($auto);
}
$items = [];
$commissionRewards = CommissionReward::where('order_id', $orderId)->where('status', '>=', 0)->select();
foreach ($commissionRewards as $reward) {
$userId = intval($reward['agent_id']);
$amount = round(floatval($reward['commission']), 2);
if ($userId <= 0 || $amount <= 0) {
continue;
}
$items[] = $this->buildPlannedItem('promoter', $userId, $amount, '原推广员佣金');
}
foreach ($this->getRegionalRewardsByOrderId($orderId) as $reward) {
$userId = intval($reward['agent_user_id']);
$amount = round(floatval($reward['commission']), 2);
if ($userId <= 0 || $amount <= 0) {
continue;
}
$items[] = $this->buildPlannedItem('regional_agent', $userId, $amount, '原区域公司佣金');
}
return $items;
}
protected function getRegionalRewardsByOrderId($orderId)
{
try {
return RegionalReward::where('order_id', $orderId)->where('status', '>=', 0)->select();
} catch (\Exception $e) {
return RegionalReward::alias('rr')
->join('shopro_regional_agent_order ro', 'ro.id = rr.regional_order_id')
->where('ro.order_id', $orderId)
->where('rr.status', '>=', 0)
->field('rr.*')
->select();
}
}
protected function buildPlannedItem($roleType, $userId, $amount, $desc)
{
$receiver = Receiver::where('role_type', $roleType)
->where('role_id', $userId)
->where('status', 'normal')
->find();
$user = User::get($userId);
$name = $receiver ? trim((string) ($receiver['receiver_name'] ?? '')) : '';
if ($name === '' && $user) {
$name = trim((string) ($user['nickname'] ?? '')) ?: ('用户#' . $userId);
}
return [
'receiver_id' => $receiver ? intval($receiver['id']) : 0,
'role_type' => $roleType,
'role_id' => $userId,
'receiver_account' => $receiver ? trim((string) ($receiver['receiver_account'] ?? '')) : '',
'receiver_name' => $name,
'amount' => $amount,
'description' => $desc,
];
}
protected function buildChangeSummary($action, array $before, array $after)
{
if ($action === 'retry') {
$afterText = $this->formatItemsBrief($after);
return $afterText ? ('按以下接收方重新发起分账:' . $afterText) : '重新发起分账重试';
}
if ($action === 'clear') {
$beforeText = $this->formatItemsBrief($before);
return $beforeText
? ('清除手动指定,恢复自动计算(原手动指定:' . $beforeText . ')')
: '清除手动指定,恢复自动计算';
}
$beforeText = $this->formatItemsBrief($before);
$afterText = $this->formatItemsBrief($after);
if ($beforeText === '' && $afterText !== '') {
return '指定分账接收方为:' . $afterText;
}
if ($beforeText === $afterText) {
return '分账接收方未变化:' . $afterText;
}
return '分账接收方:' . ($beforeText ?: '(无有效自动接收方)') . ' → ' . $afterText;
}
protected function formatItemsBrief(array $items)
{
if (empty($items)) {
return '';
}
$parts = [];
foreach ($items as $item) {
$parts[] = $this->formatOneItemBrief($item);
}
return implode(';', $parts);
}
protected function formatOneItemBrief(array $item)
{
$roleMap = [
'platform' => '平台',
'promoter' => '推广员',
'regional_agent' => '区域公司',
];
$role = $roleMap[$item['role_type'] ?? ''] ?? ($item['role_type'] ?? '接收方');
$name = trim((string) ($item['receiver_name'] ?? ''));
if ($name === '' && !empty($item['role_id'])) {
$name = '用户#' . intval($item['role_id']);
}
$account = trim((string) ($item['receiver_account'] ?? ''));
$accountPart = $account !== '' ? $account : '未进件';
$amt = number_format(floatval($item['amount'] ?? 0), 2, '.', '');
return $role . ' ' . $name . '(' . $accountPart . ')¥' . $amt;
}
protected function itemsEqual(array $before, array $after)
{
$norm = function (array $items) {
$rows = [];
foreach ($items as $item) {
$rows[] = [
intval($item['receiver_id'] ?? 0),
number_format(floatval($item['amount'] ?? 0), 2, '.', ''),
trim((string) ($item['receiver_account'] ?? '')),
];
}
usort($rows, function ($a, $b) {
return ($a[0] <=> $b[0]) ?: strcmp($a[1], $b[1]);
});
return $rows;
};
return $norm($before) === $norm($after);
}
} }

查看文件

@@ -244,7 +244,17 @@
} }
.history-drawer .history-time { font-size: 12px; color: #909399; } .history-drawer .history-time { font-size: 12px; color: #909399; }
.history-drawer .history-meta { font-size: 12px; color: #606266; margin-bottom: 6px; } .history-drawer .history-meta { font-size: 12px; color: #606266; margin-bottom: 6px; }
.history-drawer .items-diff { font-size: 12px; line-height: 1.65; color: #606266; } .history-drawer .change-summary {
font-size: 13px;
line-height: 1.7;
color: #303133;
padding: 8px 10px;
background: #f5f7fa;
border-radius: 6px;
margin-bottom: 6px;
}
.history-drawer .change-summary .arrow-hl { color: #7438D5; font-weight: 600; margin: 0 4px; }
.history-drawer .items-diff { font-size: 12px; line-height: 1.65; color: #909399; margin-top: 4px; }
.history-drawer .items-diff .arrow { color: #909399; margin: 0 4px; } .history-drawer .items-diff .arrow { color: #909399; margin: 0 4px; }
.history-drawer .retry-result { margin-top: 6px; font-size: 12px; } .history-drawer .retry-result { margin-top: 6px; font-size: 12px; }
.history-drawer .retry-result.success { color: #67c23a; } .history-drawer .retry-result.success { color: #67c23a; }
@@ -469,17 +479,14 @@
<span class="history-time">{{ h.createtime_text }}</span> <span class="history-time">{{ h.createtime_text }}</span>
</div> </div>
<div class="history-meta" v-if="h.admin_name">操作人:{{ h.admin_name }}</div> <div class="history-meta" v-if="h.admin_name">操作人:{{ h.admin_name }}</div>
<div class="history-meta" v-if="h.remark">备注:{{ h.remark }}</div> <div class="history-meta" v-if="h.remark && h.action !== 'retry'">备注:{{ h.remark }}</div>
<div class="items-diff"> <div class="change-summary" v-if="h.change_summary">{{ h.change_summary }}</div>
<div v-if="h.before_items && h.before_items.length"> <div class="items-diff" v-if="h.action === 'create' || h.action === 'update' || h.action === 'clear'">
<div v-for="(it, i) in h.before_items" :key="'b'+i">{{ formatManualItemLine(it) }}</div> <span v-if="h.before_summary">{{ h.before_summary }}</span>
</div> <span v-else style="color:#c0c4cc;">(变更前无有效接收方)</span>
<span v-else style="color:#c0c4cc;">(空)</span> <span class="arrow-hl">→</span>
<span class="arrow">→</span> <span v-if="h.after_summary">{{ h.after_summary }}</span>
<div v-if="h.after_items && h.after_items.length"> <span v-else style="color:#c0c4cc;">(已清除)</span>
<div v-for="(it, i) in h.after_items" :key="'a'+i">{{ formatManualItemLine(it) }}</div>
</div>
<span v-else style="color:#c0c4cc;">(空)</span>
</div> </div>
<div v-if="h.retry_result" :class="'retry-result ' + h.retry_result"> <div v-if="h.retry_result" :class="'retry-result ' + h.retry_result">
重试结果:{{ h.retry_result_text }} 重试结果:{{ h.retry_result_text }}

查看文件

@@ -110,7 +110,8 @@ define(['jquery', 'bootstrap', 'backend', 'form'], function ($, undefined, Backe
var account = it.receiver_account || ''; var account = it.receiver_account || '';
var amt = this.formatMoney(it.amount); var amt = this.formatMoney(it.amount);
var role = this.roleTypeLabel(it.role_type); var role = this.roleTypeLabel(it.role_type);
return role + ' ' + name + (account ? ' (' + account + ')' : '') + ' ¥' + amt; var acctLabel = account ? account : '未进件';
return role + ' ' + name + '(' + acctLabel + ')¥' + amt;
}, },
scrollCellWheel(e) { scrollCellWheel(e) {
var el = e.currentTarget; var el = e.currentTarget;