文件
xionggeshop/addons/shopro/library/split/SplitCalculator.php
可乐加冰 0b6a7d675a ```
feat(split): 优化分账计算诊断功能

- 添加用户信息格式化方法,显示带脱敏手机号的用户简要信息
- 实现详细的汇付接收方缺失诊断逻辑,提供具体的错误原因
- 新增分账明细为空时的详细失败消息格式化功能
- 增强后台订单分账失败原因展示,支持完整和摘要两种显示方式
- 添加规则接收方诊断功能,检测分账规则配置问题
- 优化错误信息的可读性和排查效率
```
2026-10-09 10:09:26 +08:00

482 行
17 KiB
PHP

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<?php
namespace addons\shopro\library\split;
use addons\shopro\model\Order;
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
{
protected $config;
public function __construct()
{
$this->config = new Config();
}
/**
* 计算订单分账明细
* @return array [['role_type','role_id','receiver_id','receiver_account','receiver_type','receiver_name','amount','description']]
*/
public function calculate($orderId)
{
$order = Order::get($orderId);
if (!$order || !in_array($order['pay_type'], ['wechat', 'huifu_wechat'])) {
return [];
}
$payFee = floatval($order['pay_fee']);
if ($payFee <= 0) {
return [];
}
$source = $this->config->getAmountSource();
$items = [];
if (in_array($source, ['commission', 'mixed'])) {
$items = $this->calculateFromCommission($orderId);
}
if (empty($items) && in_array($source, ['rule', 'mixed'])) {
$items = $this->calculateFromRules($payFee);
}
$allocated = array_sum(array_column($items, 'amount'));
$maxSplit = $payFee * $this->config->getMaxRate() / 100;
if ($allocated > $maxSplit && $allocated > 0) {
$ratio = $maxSplit / $allocated;
foreach ($items as &$item) {
$item['amount'] = round($item['amount'] * $ratio, 2);
}
unset($item);
}
return $items;
}
/**
* 诊断为何未走佣金分账(供后台展示)
* @return array{amount_source:string,final_source:string,issues:array,commission_total:float,rule_total:float}
*/
public function diagnose($orderId)
{
$order = Order::get($orderId);
$amountSource = $this->config->getAmountSource();
$issues = [];
$commissionTotal = 0.0;
if (!$order) {
return [
'amount_source' => $amountSource,
'final_source' => 'none',
'issues' => ['订单不存在'],
'commission_total'=> 0,
'rule_total' => 0,
];
}
$payFee = floatval($order['pay_fee']);
$commissionRewards = CommissionReward::where('order_id', $orderId)->where('status', '>=', 0)->select();
if ($this->isEmptyResult($commissionRewards)) {
$issues[] = '无分销佣金记录';
}
foreach ($this->toResultList($commissionRewards) as $reward) {
$userId = intval($reward['agent_id']);
$amount = floatval($reward['commission']);
if ($userId <= 0) {
$issues[] = '佣金记录 agent_id 无效';
continue;
}
if ($amount <= 0) {
continue;
}
$receiver = $this->getReceiver('promoter', $userId);
if (!$receiver) {
$issues[] = sprintf(
'推广员 %s 佣金¥%s:%s',
$this->formatUserBrief($userId),
number_format($amount, 2, '.', ''),
$this->describeHuifuReceiverGap('promoter', $userId)
);
continue;
}
$commissionTotal += $amount;
}
$regionalRewards = $this->getRegionalRewardsByOrderId($orderId);
foreach ($this->toResultList($regionalRewards) as $reward) {
$uid = intval($reward['agent_user_id']);
$amount = floatval($reward['commission']);
if ($uid <= 0 || $amount <= 0) {
continue;
}
if (!$this->getReceiver('regional_agent', $uid)) {
$issues[] = sprintf(
'区域公司 %s 佣金¥%s:%s',
$this->formatUserBrief($uid),
number_format($amount, 2, '.', ''),
$this->describeHuifuReceiverGap('regional_agent', $uid)
);
continue;
}
$commissionTotal += $amount;
}
$commissionItems = $this->calculateFromCommission($orderId);
$ruleItems = $this->calculateFromRules($payFee);
$ruleTotal = array_sum(array_column($ruleItems, 'amount'));
$finalSource = 'none';
if (!empty($commissionItems) && in_array($amountSource, ['commission', 'mixed'])) {
$finalSource = 'commission';
} elseif (!empty($ruleItems) && ($amountSource === 'rule' || ($amountSource === 'mixed' && empty($commissionItems)))) {
$finalSource = 'rule';
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 [
'amount_source' => $amountSource,
'final_source' => $finalSource,
'issues' => $issues,
'commission_total' => round($commissionTotal, 2),
'rule_total' => round($ruleTotal, 2),
];
}
/**
* 分账明细为空时的可读失败说明(写入 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)) {
return count($result) === 0;
}
if (is_object($result) && method_exists($result, 'isEmpty')) {
return $result->isEmpty();
}
return empty($result);
}
protected function toResultList($result)
{
if (is_array($result)) {
return $result;
}
if (is_object($result) && method_exists($result, 'all')) {
return $result->all();
}
return $result ? (array) $result : [];
}
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 calculateFromCommission($orderId)
{
$items = [];
$commissionRewards = CommissionReward::where('order_id', $orderId)->where('status', '>=', 0)->select();
foreach ($commissionRewards as $reward) {
// agent_id 即分销商 user_id(shopro_commission_agent 主键)
$userId = intval($reward['agent_id']);
if ($userId <= 0) {
continue;
}
$receiver = $this->getReceiver('promoter', $userId);
if (!$receiver) {
continue;
}
$amount = floatval($reward['commission']);
if ($amount <= 0) {
continue;
}
$items[] = $this->buildItem('promoter', $userId, $receiver, $amount, '推广员差价');
}
$regionalRewards = $this->getRegionalRewardsByOrderId($orderId);
foreach ($regionalRewards as $reward) {
$receiver = $this->getReceiver('regional_agent', $reward['agent_user_id']);
if (!$receiver) {
continue;
}
$amount = floatval($reward['commission']);
if ($amount <= 0) {
continue;
}
$items[] = $this->buildItem('regional_agent', $reward['agent_user_id'], $receiver, $amount, '区域公司差价');
}
return $items;
}
protected function calculateFromRules($payFee)
{
$items = [];
$rules = Rule::where('status', 'normal')->order('priority desc')->select();
foreach ($rules as $rule) {
if (in_array($rule['role_type'], ['platform'])) {
continue;
}
$receiver = $this->getReceiver($rule['role_type'], 0, true);
if (!$receiver) {
continue;
}
if ($rule['rule_type'] === 'rate') {
$amount = round($payFee * floatval($rule['rule_value']) / 100, 2);
} else {
$amount = round(floatval($rule['rule_value']), 2);
}
if ($amount <= 0) {
continue;
}
$items[] = $this->buildItem($rule['role_type'], 0, $receiver, $amount, '规则分账');
}
return $items;
}
protected function buildItem($roleType, $roleId, $receiver, $amount, $desc)
{
return [
'role_type' => $roleType,
'role_id' => $roleId,
'receiver_id' => $receiver['id'] ?? 0,
'receiver_account' => $receiver['receiver_account'],
'receiver_type' => $receiver['receiver_type'],
'receiver_name' => $receiver['receiver_name'] ?? '',
'amount' => round($amount, 2),
'description' => $desc,
];
}
protected function getReceiver($roleType, $roleId, $ruleMode = false)
{
$channel = $this->config->isHuifuMode() ? 'huifu' : 'wechat';
$query = Receiver::where('role_type', $roleType)->where('status', 'normal');
if ($roleId) {
$query->where('role_id', $roleId);
}
$receiver = $query->where(function ($q) use ($channel) {
$q->where('channel', $channel)->whereOr('channel', null)->whereOr('channel', '');
})->find();
if ($receiver) {
return $receiver;
}
if ($this->config->isHuifuMode()) {
if ($roleType === 'platform') {
$account = $this->config->getPlatformHuifuId();
if ($account) {
return [
'id' => 0,
'receiver_account' => $account,
'receiver_type' => 'HUIFU_ID',
'receiver_name' => $this->config->getPlatformReceiverName(),
];
}
}
return null;
}
if (!$ruleMode && in_array($roleType, ['promoter', 'regional_agent'])) {
$oauth = UserOauth::where('user_id', $roleId)->where('provider', 'Wechat')->where('platform', 'wxMiniProgram')->find();
if ($oauth && $oauth['openid']) {
return Receiver::create([
'role_type' => $roleType,
'role_id' => $roleId,
'channel' => 'wechat',
'receiver_type' => 'PERSONAL_OPENID',
'receiver_account' => $oauth['openid'],
'receiver_name' => '用户' . $roleId,
'relation_type' => 'USER',
'wx_status' => 0,
'status' => 'normal',
'createtime' => time(),
'updatetime' => time(),
]);
}
}
if ($roleType === 'platform') {
$account = $this->config->getPlatformReceiverAccount();
if ($account) {
return [
'id' => 0,
'receiver_account' => $account,
'receiver_type' => 'MERCHANT_ID',
'receiver_name' => $this->config->getPlatformReceiverName(),
];
}
}
return null;
}
}