config = new Config(); } /** * 计算订单分账明细(优先手动覆盖) * @return array [['role_type','role_id','receiver_id','receiver_account','receiver_type','receiver_name','amount','description']] */ public function calculate($orderId) { $manualItems = (new SplitManualService())->getCalculateItems($orderId); if (!empty($manualItems)) { return $manualItems; } return $this->calculateAuto($orderId); } /** * 按佣金/规则自动计算分账明细 */ public function calculateAuto($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; } }