feat(split): 优化分账计算诊断功能

- 添加用户信息格式化方法,显示带脱敏手机号的用户简要信息
- 实现详细的汇付接收方缺失诊断逻辑,提供具体的错误原因
- 新增分账明细为空时的详细失败消息格式化功能
- 增强后台订单分账失败原因展示,支持完整和摘要两种显示方式
- 添加规则接收方诊断功能,检测分账规则配置问题
- 优化错误信息的可读性和排查效率
```
这个提交包含在:
可乐加冰
2026-10-09 10:09:26 +08:00
父节点 fec4d58e41
当前提交 0b6a7d675a
共有 3 个文件被更改,包括 217 次插入 和 8 次删除

查看文件

@@ -7,7 +7,9 @@ use addons\shopro\model\split\Receiver;
use addons\shopro\model\split\Rule;
use addons\shopro\model\commission\Reward as CommissionReward;
use addons\shopro\model\regional_agent\Reward as RegionalReward;
use addons\shopro\model\User;
use addons\shopro\model\UserOauth;
use think\Db;
class SplitCalculator
{
@@ -98,9 +100,10 @@ class SplitCalculator
$receiver = $this->getReceiver('promoter', $userId);
if (!$receiver) {
$issues[] = sprintf(
'推广员#%d 佣金¥%s:未配置汇付接收方(role_id 须等于该 user_id)',
$userId,
number_format($amount, 2, '.', '')
'推广员 %s 佣金¥%s:%s',
$this->formatUserBrief($userId),
number_format($amount, 2, '.', ''),
$this->describeHuifuReceiverGap('promoter', $userId)
);
continue;
}
@@ -115,7 +118,12 @@ class SplitCalculator
continue;
}
if (!$this->getReceiver('regional_agent', $uid)) {
$issues[] = sprintf('区域代理#%d 佣金¥%s 无汇付接收方', $uid, number_format($amount, 2, '.', ''));
$issues[] = sprintf(
'区域公司 %s 佣金¥%s:%s',
$this->formatUserBrief($uid),
number_format($amount, 2, '.', ''),
$this->describeHuifuReceiverGap('regional_agent', $uid)
);
continue;
}
$commissionTotal += $amount;
@@ -133,6 +141,8 @@ class SplitCalculator
if ($amountSource === 'mixed' && !empty($issues)) {
$issues[] = '已回退分账规则(如 5%),与佣金金额不一致';
}
} elseif (empty($ruleItems) && empty($commissionItems) && in_array($amountSource, ['rule', 'mixed'])) {
$issues = array_merge($issues, $this->diagnoseRuleReceivers($payFee));
}
return [
@@ -144,6 +154,153 @@ class SplitCalculator
];
}
/**
* 分账明细为空时的可读失败说明(写入 wx_result / 后台原因)
*/
public function formatEmptySplitFailureMessage($orderId)
{
$diag = $this->diagnose($orderId);
$issues = array_filter($diag['issues'] ?? []);
if ($issues) {
return '分账明细为空:' . implode(';', $issues);
}
return '分账明细为空:请检查分账金额来源、汇付接收方(huifu_id)与佣金/规则是否大于0';
}
protected function diagnoseRuleReceivers($payFee)
{
$issues = [];
if (!$this->config->isHuifuMode()) {
return $issues;
}
$rules = Rule::where('status', 'normal')->order('priority desc')->select();
if ($this->isEmptyResult($rules)) {
$issues[] = '无生效分账规则,且佣金侧未生成有效接收方';
return $issues;
}
foreach ($this->toResultList($rules) as $rule) {
if (in_array($rule['role_type'], ['platform'], true)) {
continue;
}
$receiver = $this->getReceiver($rule['role_type'], 0, true);
if ($receiver && trim((string) ($receiver['receiver_account'] ?? '')) !== '') {
continue;
}
$roleLabel = $rule['role_type'] === 'regional_agent' ? '区域公司' : '推广员';
$ruleVal = $rule['rule_type'] === 'rate'
? (floatval($rule['rule_value']) . '%')
: ('¥' . number_format(floatval($rule['rule_value']), 2, '.', ''));
$issues[] = sprintf(
'规则兜底「%s %s」无汇付 huifu_id:请在分账接收方配置或完成对应角色进件',
$roleLabel,
$ruleVal
);
}
if (!$issues) {
$issues[] = '规则兜底金额为 0 或超过分账上限,未生成有效明细';
}
return $issues;
}
protected function formatUserBrief($userId)
{
$userId = intval($userId);
if ($userId <= 0) {
return '用户#?';
}
$user = User::where('id', $userId)->field('id,nickname,mobile')->find();
if (!$user) {
return '用户#' . $userId;
}
$nickname = trim((string) ($user['nickname'] ?? ''));
$mobile = trim((string) ($user['mobile'] ?? ''));
if ($nickname !== '' && $mobile !== '') {
return $nickname . ' #' . $userId . ' ' . $this->maskMobile($mobile);
}
if ($nickname !== '') {
return $nickname . ' #' . $userId;
}
if ($mobile !== '') {
return '用户#' . $userId . ' ' . $this->maskMobile($mobile);
}
return '用户#' . $userId;
}
protected function maskMobile($mobile)
{
$mobile = trim((string) $mobile);
if (strlen($mobile) >= 11) {
return substr($mobile, 0, 3) . '****' . substr($mobile, -4);
}
return $mobile;
}
protected function describeHuifuReceiverGap($roleType, $userId)
{
if (!$this->config->isHuifuMode()) {
return '未配置分账接收方';
}
$userId = intval($userId);
$receiver = Receiver::where('role_type', $roleType)
->where('role_id', $userId)
->where('status', 'normal')
->where(function ($q) {
$q->where('channel', 'huifu')->whereOr('channel', null)->whereOr('channel', '');
})
->find();
if ($receiver && trim((string) ($receiver['receiver_account'] ?? '')) !== '') {
return '接收方已配置,请检查 huifu_id 是否在汇付控台绑定';
}
if (!class_exists('\\addons\\shopro_huifu\\model\\Onboard')) {
return '未配置汇付分账接收方(huifu_id),请完成进件或后台添加接收方';
}
$roleRefId = $this->resolveOnboardRoleRefId($roleType, $userId);
$onboard = \addons\shopro_huifu\model\Onboard::findByRole($userId, $roleType, $roleRefId);
if (!$onboard) {
return '未汇付进件,请用户在小程序完成进件绑定或后台代提交';
}
$status = (string) ($onboard['onboard_status'] ?? '');
$huifuId = trim((string) ($onboard['huifu_id'] ?? ''));
if ($status === \addons\shopro_huifu\model\Onboard::STATUS_APPROVED) {
if ($huifuId === '') {
return '进件已通过但未回写 huifu_id,请同步进件状态并配置分账接收方';
}
return '进件已通过(huifu_id=' . $huifuId . '),但未写入分账接收方 role_id=' . $userId;
}
return '汇付进件状态:' . $this->onboardStatusLabel($status) . ',暂不能分账';
}
protected function resolveOnboardRoleRefId($roleType, $userId)
{
if ($roleType === 'regional_agent') {
try {
$agent = Db::name('shopro_regional_agent')->where('user_id', $userId)->order('id', 'desc')->find();
if ($agent) {
return intval($agent['id']);
}
} catch (\Exception $e) {
// ignore
}
}
return $userId;
}
protected function onboardStatusLabel($status)
{
$map = [
'draft' => '草稿待提交',
'submitting' => '提交中',
'basic_ok' => '基础进件成功',
'busi_pending' => '业务开通审核中',
'approved' => '已通过',
'rejected' => '已驳回',
'failed' => '进件失败',
];
return $map[$status] ?? ($status !== '' ? $status : '未知');
}
protected function isEmptyResult($result)
{
if (is_array($result)) {

查看文件

@@ -42,7 +42,7 @@ class HuifuSplitService
if ($rewardCount + $regionalCount > 0) {
\think\Log::warning('[shopro_huifu] split skipped order_id=' . $orderId . ' reason=receiver_missing_or_no_huifu_id');
}
$msg = '分账明细为空:请检查分账金额来源、汇付接收方(huifu_id)与佣金/规则是否大于0';
$msg = $calculator->formatEmptySplitFailureMessage($orderId);
$this->markFailed($orderId, 'HFPS' . date('YmdHis') . $orderId, $order, [], $msg);
return false;
}

查看文件

@@ -146,10 +146,11 @@ class Order extends Base
$data['receiver_details'] = $receiverDetails;
$data['receiver_preview'] = $receiverPreview;
$data['receiver_summary'] = $summaryLines ? implode(';', $summaryLines) : '—';
$data['fail_reason'] = $this->formatFailReason($data['wx_result'] ?? '', $status);
$data['fail_reason_full'] = $this->formatFailReasonFull($data['wx_result'] ?? '', $status);
$diag = (new SplitCalculator())->diagnose($orderId);
$data['fail_reason_full'] = $this->formatFailReasonFull($data['wx_result'] ?? '', $status);
$data['fail_reason_full'] = $this->enrichFailReasonWithDiagnose($data['fail_reason_full'], $status, $diag);
$data['fail_reason'] = $this->summarizeFailReason($data['fail_reason_full'], $diag, $status);
$data['split_source'] = $diag['final_source'];
$data['split_source_text'] = $this->formatSplitSourceText($diag);
$data['split_diagnose'] = $diag['issues'];
@@ -304,6 +305,57 @@ class Order extends Base
return $text;
}
protected function enrichFailReasonWithDiagnose($full, $status, array $diag)
{
if ($status !== 4) {
return $full;
}
$issues = array_values(array_filter($diag['issues'] ?? []));
if (!$issues) {
return $full;
}
$raw = is_string($full) ? $full : '';
$isEmptySplit = $raw === ''
|| strpos($raw, '分账明细为空') !== false
|| strpos($raw, '分账未生成') !== false
|| ($diag['final_source'] ?? '') === 'none';
if (!$isEmptySplit) {
return $full;
}
$block = implode("\n", $issues);
if ($raw === '') {
return '分账未生成:' . implode(';', $issues);
}
if (strpos($raw, implode(';', array_slice($issues, 0, 1))) !== false) {
return $raw;
}
return $raw . "\n" . $block;
}
protected function summarizeFailReason($full, array $diag, $status)
{
if ($status !== 4 || $full === '') {
return $this->formatFailReasonFromFull($full, $status);
}
$issues = array_values(array_filter($diag['issues'] ?? []));
if ($issues) {
$brief = $issues[0];
if (count($issues) > 1) {
$brief .= ' 等' . count($issues) . '项';
}
return mb_strlen($brief) > 72 ? (mb_substr($brief, 0, 72) . '…') : $brief;
}
return $this->formatFailReasonFromFull($full, $status);
}
protected function formatFailReasonFromFull($full, $status)
{
if ($status !== 4 || $full === '') {
return '';
}
return mb_strlen($full) > 72 ? (mb_substr($full, 0, 72) . '…') : $full;
}
protected function formatFailReason($wxResult, $status)
{
if ($status !== 4 || $wxResult === '' || $wxResult === null) {