Files
webman-buildadmin/app/common/service/AdminCommissionDistributionService.php
zhenhui 7ab3db121c 1.结算记录中新增结算信息
2.优化渠道页面样式
2026-05-30 14:58:17 +08:00

483 lines
18 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace app\common\service;
use support\think\Db;
/**
* 代理管理员树形分红:子代理从上级分红中按 commission_share_rate 抽取,上级保留剩余部分
*/
class AdminCommissionDistributionService
{
/**
* @return int[]
*/
public static function getDescendantAdminIds(int $adminId): array
{
if ($adminId <= 0) {
return [];
}
$childIds = Db::name('admin')
->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<int, string>|null $handlingFeeRateByAdmin admin_id => 手续费比例(%)
* @return array<int, array{admin_id:int,commission_amount:string,commission_rate:string,calc_base_amount:string,handling_fee_rate:string,handling_fee:string,net_commission_amount:string}>
*/
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 [];
}
$shareRateByAdmin = [];
foreach ($nodes as $node) {
$nodeAdminId = intval($node['admin_id'] ?? 0);
if ($nodeAdminId <= 0) {
continue;
}
$shareRateByAdmin[$nodeAdminId] = strval($node['share_rate'] ?? '0.00');
}
$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,
'share_rate' => $shareRateByAdmin[$adminId] ?? '0.00',
'commission_share_percent' => self::calcCommissionSharePercent($gross, $totalCommission),
'handling_fee_rate' => $feeRate,
'handling_fee' => $feeAmount,
'net_commission_amount' => $net,
];
}
return array_values($merged);
}
/**
* 层级分配预览(先序遍历),含结算基数与手续费
*
* @param array<int, string>|null $handlingFeeRateByAdmin admin_id => 手续费比例(%)
* @return array<int, array<string, mixed>>
*/
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<int, array{admin_id:int,admin_username:string,parent_admin_id:int,level:int,settlement_base_amount:string,share_rate:string,commission_amount:string}>
*/
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<int, array{admin_id:int,admin_username:string,parent_admin_id:int,level:int,settlement_base_amount:string,share_rate:string,commission_amount:string}> $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<int, string> 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;
}
}