Files
webman-buildadmin/app/common/service/AdminCommissionDistributionService.php
2026-05-29 11:06:24 +08:00

239 lines
8.6 KiB
PHP
Raw 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];
}
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;
}
/**
* 将渠道本期总佣金按管理员树分配,返回各管理员实得金额
*
* @return array<int, array{admin_id:int,commission_amount:string,commission_rate:string,calc_base_amount:string}>
*/
public static function distributeChannelCommission(int $channelId, string $totalCommission, string $calcBaseAmount): array
{
if ($channelId <= 0 || bccomp($totalCommission, '0', 2) <= 0) {
return [];
}
$roots = Db::name('admin')
->where('channel_id', $channelId)
->where('status', 'enable')
->whereRaw('(parent_admin_id IS NULL OR parent_admin_id = 0)')
->order('id', 'asc')
->column('id');
if ($roots === []) {
return [];
}
$rootCount = count($roots);
$perRoot = bcdiv($totalCommission, strval($rootCount), 2);
$assigned = '0.00';
$merged = [];
foreach ($roots as $index => $rootId) {
$rootId = intval($rootId);
if ($rootId <= 0) {
continue;
}
$isLast = $index === $rootCount - 1;
$rootAmount = $isLast ? bcsub($totalCommission, $assigned, 2) : $perRoot;
if (!$isLast) {
$assigned = bcadd($assigned, $rootAmount, 2);
}
$parts = self::distributeFromAdmin($rootId, $rootAmount, $calcBaseAmount);
foreach ($parts as $adminId => $amount) {
if (!isset($merged[$adminId])) {
$merged[$adminId] = '0.00';
}
$merged[$adminId] = bcadd($merged[$adminId], $amount, 2);
}
}
$out = [];
foreach ($merged as $adminId => $amount) {
if (bccomp($amount, '0', 2) <= 0) {
continue;
}
$effectiveRate = bccomp($calcBaseAmount, '0', 2) <= 0
? '0.0000'
: bcdiv($amount, $calcBaseAmount, 6);
$out[] = [
'admin_id' => intval($adminId),
'commission_amount' => $amount,
'commission_rate' => $effectiveRate,
'calc_base_amount' => $calcBaseAmount,
];
}
return $out;
}
/**
* @return array<int, string> admin_id => amount
*/
private static function distributeFromAdmin(int $adminId, string $amount, string $calcBaseAmount): array
{
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, $calcBaseAmount);
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;
}
/**
* @return array<int, array{admin_id:int,admin_username:string,share_rate:string,commission_amount:string}>
*/
public static function buildSplitPreview(int $channelId, string $commissionTotal, string $calcBaseAmount): array
{
$rows = self::distributeChannelCommission($channelId, $commissionTotal, $calcBaseAmount);
if ($rows === []) {
return [];
}
$adminIds = array_map(static fn(array $r): int => intval($r['admin_id']), $rows);
$adminNames = Db::name('admin')->where('id', 'in', $adminIds)->column('username', 'id');
$parentMap = Db::name('admin')->where('id', 'in', $adminIds)->column('parent_admin_id', 'id');
$shareRates = Db::name('admin')->where('id', 'in', $adminIds)->column('commission_share_rate', 'id');
$out = [];
foreach ($rows as $row) {
$aid = intval($row['admin_id']);
$parentId = intval($parentMap[$aid] ?? 0);
$shareRate = $parentId > 0 ? bcadd(strval($shareRates[$aid] ?? '0'), '0', 2) : '100.00';
$out[] = [
'admin_id' => $aid,
'admin_username' => strval($adminNames[$aid] ?? ('#' . $aid)),
'share_rate' => $shareRate,
'commission_amount' => strval($row['commission_amount']),
];
}
return $out;
}
}