where('parent_admin_id', $adminId) ->where('status', 'enable') ->column('id'); $all = []; foreach ($childIds as $cid) { $cid = intval($cid); if ($cid <= 0) { continue; } $all[] = $cid; foreach (self::getDescendantAdminIds($cid) as $descId) { $all[] = $descId; } } return $all; } /** * 非超管可见管理员 ID;超管返回空数组表示不限制 * * @return int[] */ public static function getVisibleAdminIdsForOperator(int $operatorAdminId, bool $isSuperAdmin): array { if ($isSuperAdmin || $operatorAdminId <= 0) { return []; } $ids = self::getDescendantAdminIds($operatorAdminId); $ids[] = $operatorAdminId; return array_values(array_unique($ids)); } /** * @return array{used_rate:string,remaining_rate:string} */ public static function getShareRemainder(int $parentAdminId, ?int $excludeAdminId = null): array { $used = '0.00'; if ($parentAdminId <= 0) { return ['used_rate' => $used, 'remaining_rate' => '100.00']; } $query = Db::name('admin') ->where('parent_admin_id', $parentAdminId) ->where('status', 'enable'); if ($excludeAdminId !== null && $excludeAdminId > 0) { $query->where('id', '<>', $excludeAdminId); } $rows = $query->column('commission_share_rate'); foreach ($rows as $rate) { if ($rate === null || $rate === '') { continue; } $used = bcadd($used, bcadd(strval($rate), '0', 2), 2); } $remaining = bcsub('100.00', $used, 2); if (bccomp($remaining, '0', 2) < 0) { $remaining = '0.00'; } return ['used_rate' => $used, 'remaining_rate' => $remaining]; } /** * 同渠道下顶级代理(无上级)已占用的渠道分红比例 * * @return array{used_rate:string,remaining_rate:string} */ public static function getChannelRootShareRemainder(int $channelId, ?int $excludeAdminId = null): array { $used = '0.00'; if ($channelId <= 0) { return ['used_rate' => $used, 'remaining_rate' => '100.00']; } $query = Db::name('admin') ->where('channel_id', $channelId) ->where('status', 'enable') ->whereRaw('(parent_admin_id IS NULL OR parent_admin_id = 0)'); if ($excludeAdminId !== null && $excludeAdminId > 0) { $query->where('id', '<>', $excludeAdminId); } $rows = $query->column('commission_share_rate'); foreach ($rows as $rate) { if ($rate === null || $rate === '') { continue; } $used = bcadd($used, bcadd(strval($rate), '0', 2), 2); } $remaining = bcsub('100.00', $used, 2); if (bccomp($remaining, '0', 2) < 0) { $remaining = '0.00'; } return ['used_rate' => $used, 'remaining_rate' => $remaining]; } public static function validateChannelRootCommissionShareRate(int $channelId, mixed $rateRaw, ?int $excludeAdminId = null): ?string { if ($channelId <= 0) { return (string) __('Channel is required for top-level agent commission share'); } if ($rateRaw === null || $rateRaw === '') { return (string) __('Top-level agent commission share rate is required'); } $rate = bcadd(strval($rateRaw), '0', 2); if (bccomp($rate, '0', 2) <= 0 || bccomp($rate, '100', 2) > 0) { return (string) __('Commission share rate must be between 0 and 100'); } $remainder = self::getChannelRootShareRemainder($channelId, $excludeAdminId); if (bccomp(bcadd($remainder['used_rate'], $rate, 2), '100.00', 2) > 0) { return (string) __('Sum of channel top-level commission share rates cannot exceed 100%'); } return null; } public static function validateCommissionShareRate(?int $parentAdminId, mixed $rateRaw, ?int $excludeAdminId = null): ?string { if ($parentAdminId === null || $parentAdminId <= 0) { return null; } if ($rateRaw === null || $rateRaw === '') { return (string) __('Sub-agent commission share rate is required'); } $rate = bcadd(strval($rateRaw), '0', 2); if (bccomp($rate, '0', 2) < 0 || bccomp($rate, '100', 2) > 0) { return (string) __('Commission share rate must be between 0 and 100'); } $remainder = self::getShareRemainder($parentAdminId, $excludeAdminId); if (bccomp(bcadd($remainder['used_rate'], $rate, 2), '100.00', 2) > 0) { return (string) __('Sum of sibling commission share rates cannot exceed 100%'); } return null; } /** * 代理费前佣金占渠道本期总佣金的比例(%) */ public static function calcCommissionSharePercent(string $gross, string $totalCommission): string { if (bccomp($totalCommission, '0', 2) <= 0 || bccomp($gross, '0', 2) <= 0) { return '0.00'; } return bcdiv(bcmul($gross, '100', 4), $totalCommission, 2); } /** * 按百分比计算手续费金额:手续费 = 费前佣金 × (费率% / 100) */ public static function calcHandlingFeeAmount(string $gross, string $ratePercent): string { $rate = bcadd($ratePercent, '0', 2); if (bccomp($gross, '0', 2) <= 0 || bccomp($rate, '0', 2) <= 0) { return '0.00'; } if (bccomp($rate, '100', 2) > 0) { $rate = '100.00'; } return bcmul($gross, bcdiv($rate, '100', 4), 2); } /** * 将渠道本期总佣金按管理员树分配,返回各管理员实得金额(费前) * * @param array|null $handlingFeeRateByAdmin admin_id => 手续费比例(%) * @return array */ public static function distributeChannelCommission( int $channelId, string $totalCommission, string $calcBaseAmount, string $defaultHandlingFeeRate = '0.00', ?array $handlingFeeRateByAdmin = null ): array { $nodes = self::collectHierarchicalNodes($channelId, $totalCommission); if ($nodes === []) { return []; } $defaultRate = self::normalizeHandlingFeeRatePercent($defaultHandlingFeeRate); $merged = []; foreach ($nodes as $node) { $adminId = intval($node['admin_id'] ?? 0); if ($adminId <= 0) { continue; } $gross = strval($node['commission_amount'] ?? '0.00'); if (bccomp($gross, '0', 2) <= 0) { continue; } $settlementBase = strval($node['settlement_base_amount'] ?? '0.00'); $feeRate = $defaultRate; if ($handlingFeeRateByAdmin !== null && isset($handlingFeeRateByAdmin[$adminId])) { $feeRate = self::normalizeHandlingFeeRatePercent(strval($handlingFeeRateByAdmin[$adminId])); } $feeAmount = self::calcHandlingFeeAmount($gross, $feeRate); $net = bcsub($gross, $feeAmount, 2); if (bccomp($net, '0', 2) < 0) { $net = '0.00'; } $effectiveRate = bccomp($settlementBase, '0', 2) <= 0 ? '0.0000' : bcdiv($gross, $settlementBase, 6); $merged[$adminId] = [ 'admin_id' => $adminId, 'commission_amount' => $gross, 'commission_rate' => $effectiveRate, 'calc_base_amount' => $settlementBase, 'commission_share_percent' => self::calcCommissionSharePercent($gross, $totalCommission), 'handling_fee_rate' => $feeRate, 'handling_fee' => $feeAmount, 'net_commission_amount' => $net, ]; } return array_values($merged); } /** * 层级分配预览(先序遍历),含结算基数与手续费 * * @param array|null $handlingFeeRateByAdmin admin_id => 手续费比例(%) * @return array> */ public static function buildSplitPreview( int $channelId, string $commissionTotal, string $calcBaseAmount, string $defaultHandlingFeeRate = '0.00', ?array $handlingFeeRateByAdmin = null ): array { unset($calcBaseAmount); $nodes = self::collectHierarchicalNodes($channelId, $commissionTotal); if ($nodes === []) { return []; } $defaultRate = self::normalizeHandlingFeeRatePercent($defaultHandlingFeeRate); $out = []; foreach ($nodes as $node) { $adminId = intval($node['admin_id'] ?? 0); if ($adminId <= 0) { continue; } $gross = strval($node['commission_amount'] ?? '0.00'); $feeRate = $defaultRate; if ($handlingFeeRateByAdmin !== null && isset($handlingFeeRateByAdmin[$adminId])) { $feeRate = self::normalizeHandlingFeeRatePercent(strval($handlingFeeRateByAdmin[$adminId])); } $feeAmount = self::calcHandlingFeeAmount($gross, $feeRate); $net = bcsub($gross, $feeAmount, 2); if (bccomp($net, '0', 2) < 0) { $net = '0.00'; } $out[] = [ 'admin_id' => $adminId, 'admin_username' => strval($node['admin_username'] ?? ('#' . $adminId)), 'parent_admin_id' => intval($node['parent_admin_id'] ?? 0), 'level' => intval($node['level'] ?? 0), 'settlement_base_amount' => strval($node['settlement_base_amount'] ?? '0.00'), 'share_rate' => strval($node['share_rate'] ?? '0.00'), 'commission_amount' => $gross, 'commission_share_percent' => self::calcCommissionSharePercent($gross, $commissionTotal), 'handling_fee_rate' => $feeRate, 'handling_fee' => $feeAmount, 'net_commission_amount' => $net, ]; } return $out; } private static function normalizeHandlingFeeRatePercent(string $rateRaw): string { $rate = bcadd($rateRaw, '0', 2); if (bccomp($rate, '0', 2) < 0) { return '0.00'; } if (bccomp($rate, '100', 2) > 0) { return '100.00'; } return $rate; } /** * @return array */ private static function collectHierarchicalNodes(int $channelId, string $totalCommission): array { if ($channelId <= 0 || bccomp($totalCommission, '0', 2) <= 0) { return []; } $rootRows = Db::name('admin') ->where('channel_id', $channelId) ->where('status', 'enable') ->whereRaw('(parent_admin_id IS NULL OR parent_admin_id = 0)') ->order('id', 'asc') ->field(['id', 'commission_share_rate', 'username']) ->select() ->toArray(); if ($rootRows === []) { return []; } $useRateSplit = true; foreach ($rootRows as $rootRow) { $rate = bcadd(strval($rootRow['commission_share_rate'] ?? '0'), '0', 2); if (bccomp($rate, '0', 2) <= 0) { $useRateSplit = false; break; } } $nodes = []; if ($useRateSplit) { foreach ($rootRows as $rootRow) { $rootId = intval($rootRow['id'] ?? 0); if ($rootId <= 0) { continue; } $rate = bcadd(strval($rootRow['commission_share_rate'] ?? '0'), '0', 2); $rootAmount = bcmul($totalCommission, bcdiv($rate, '100', 4), 2); if (bccomp($rootAmount, '0', 2) <= 0) { continue; } self::appendNodeTree($rootId, $rootAmount, 0, 0, $rate, $nodes); } } else { $rootCount = count($rootRows); $perRoot = bcdiv($totalCommission, strval($rootCount), 2); $assigned = '0.00'; foreach ($rootRows as $index => $rootRow) { $rootId = intval($rootRow['id'] ?? 0); if ($rootId <= 0) { continue; } $isLast = $index === $rootCount - 1; $rootAmount = $isLast ? bcsub($totalCommission, $assigned, 2) : $perRoot; if (!$isLast) { $assigned = bcadd($assigned, $rootAmount, 2); } if (bccomp($rootAmount, '0', 2) <= 0) { continue; } self::appendNodeTree($rootId, $rootAmount, 0, 0, '100.00', $nodes); } } return $nodes; } /** * @param array $nodes */ private static function appendNodeTree( int $adminId, string $incomingAmount, int $level, int $parentAdminId, string $shareRateFromParent, array &$nodes ): void { if ($adminId <= 0 || bccomp($incomingAmount, '0', 2) <= 0) { return; } $adminRow = Db::name('admin') ->where('id', $adminId) ->field(['id', 'username', 'commission_share_rate']) ->find(); if (!is_array($adminRow)) { return; } $children = Db::name('admin') ->where('parent_admin_id', $adminId) ->where('status', 'enable') ->order('id', 'asc') ->field(['id', 'commission_share_rate']) ->select() ->toArray(); $givenToChildren = '0.00'; $childPlans = []; foreach ($children as $child) { $childId = intval($child['id'] ?? 0); if ($childId <= 0) { continue; } $rate = bcadd(strval($child['commission_share_rate'] ?? '0'), '0', 2); if (bccomp($rate, '0', 2) <= 0) { continue; } $childAmount = bcmul($incomingAmount, bcdiv($rate, '100', 4), 2); if (bccomp($childAmount, '0', 2) <= 0) { continue; } $givenToChildren = bcadd($givenToChildren, $childAmount, 2); $childPlans[] = ['id' => $childId, 'amount' => $childAmount, 'rate' => $rate]; } $selfKeep = bcsub($incomingAmount, $givenToChildren, 2); $nodes[] = [ 'admin_id' => $adminId, 'admin_username' => strval($adminRow['username'] ?? ('#' . $adminId)), 'parent_admin_id' => $parentAdminId, 'level' => $level, 'settlement_base_amount' => $incomingAmount, 'share_rate' => $shareRateFromParent, 'commission_amount' => bccomp($selfKeep, '0', 2) > 0 ? $selfKeep : '0.00', ]; foreach ($childPlans as $plan) { self::appendNodeTree(intval($plan['id']), strval($plan['amount']), $level + 1, $adminId, strval($plan['rate']), $nodes); } } /** * @return array admin_id => amount */ private static function distributeFromAdmin(int $adminId, string $amount, string $calcBaseAmount): array { unset($calcBaseAmount); if ($adminId <= 0 || bccomp($amount, '0', 2) <= 0) { return []; } $children = Db::name('admin') ->where('parent_admin_id', $adminId) ->where('status', 'enable') ->order('id', 'asc') ->field(['id', 'commission_share_rate']) ->select() ->toArray(); $givenToChildren = '0.00'; $result = []; foreach ($children as $child) { $childId = intval($child['id'] ?? 0); if ($childId <= 0) { continue; } $rate = bcadd(strval($child['commission_share_rate'] ?? '0'), '0', 2); if (bccomp($rate, '0', 2) <= 0) { continue; } $childAmount = bcmul($amount, bcdiv($rate, '100', 4), 2); if (bccomp($childAmount, '0', 2) <= 0) { continue; } $givenToChildren = bcadd($givenToChildren, $childAmount, 2); $childParts = self::distributeFromAdmin($childId, $childAmount, '0.00'); foreach ($childParts as $aid => $part) { if (!isset($result[$aid])) { $result[$aid] = '0.00'; } $result[$aid] = bcadd($result[$aid], $part, 2); } } $selfKeep = bcsub($amount, $givenToChildren, 2); if (bccomp($selfKeep, '0', 2) > 0) { if (!isset($result[$adminId])) { $result[$adminId] = '0.00'; } $result[$adminId] = bcadd($result[$adminId], $selfKeep, 2); } return $result; } }