1.优化分红方式
This commit is contained in:
238
app/common/service/AdminCommissionDistributionService.php
Normal file
238
app/common/service/AdminCommissionDistributionService.php
Normal file
@@ -0,0 +1,238 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -23,9 +23,13 @@ class ChannelSettlementService
|
||||
if (Db::name('agent_settlement_period')->where('settlement_no', $settlementNo)->value('id')) {
|
||||
return ['ok' => false, 'msg' => __('Settlement number conflict, please retry')];
|
||||
}
|
||||
$shareRows = self::resolveCommissionSharesForChannel($channelId);
|
||||
if ($shareRows === []) {
|
||||
return ['ok' => false, 'msg' => __('No available admin share ratios under this channel; cannot settle')];
|
||||
$distributions = AdminCommissionDistributionService::distributeChannelCommission(
|
||||
$channelId,
|
||||
strval($payload['commission_amount']),
|
||||
strval($payload['calc_base_amount'])
|
||||
);
|
||||
if ($distributions === []) {
|
||||
return ['ok' => false, 'msg' => __('No channel root agent configured for commission distribution')];
|
||||
}
|
||||
$now = time();
|
||||
Db::startTrans();
|
||||
@@ -42,12 +46,10 @@ class ChannelSettlementService
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
]));
|
||||
$rows = self::buildCommissionRowsForSplit(
|
||||
$shareRows,
|
||||
$rows = self::buildCommissionRowsFromDistribution(
|
||||
$distributions,
|
||||
$channelId,
|
||||
$periodId,
|
||||
strval($payload['calc_base_amount']),
|
||||
strval($payload['commission_amount']),
|
||||
$remark !== '' ? $remark : '渠道待分红记录',
|
||||
$now
|
||||
);
|
||||
@@ -189,7 +191,11 @@ class ChannelSettlementService
|
||||
'calc_base_amount' => $commission['calc_base_amount'],
|
||||
'commission_amount' => $commission['commission_amount'],
|
||||
'agent_mode' => $mode,
|
||||
'commission_split' => self::buildCommissionSplitPreview(self::resolveCommissionSharesForChannel($channelId), $commission['commission_amount']),
|
||||
'commission_split' => AdminCommissionDistributionService::buildSplitPreview(
|
||||
$channelId,
|
||||
$commission['commission_amount'],
|
||||
$commission['calc_base_amount']
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -325,93 +331,33 @@ class ChannelSettlementService
|
||||
return $base . strtoupper(substr(bin2hex(random_bytes(4)), 0, 2));
|
||||
}
|
||||
|
||||
private static function resolveCommissionSharesForChannel(int $channelId): array
|
||||
/**
|
||||
* @param array<int, array{admin_id:int,commission_amount:string,commission_rate:string,calc_base_amount:string}> $distributions
|
||||
*/
|
||||
private static function buildCommissionRowsFromDistribution(array $distributions, int $channelId, int $periodId, string $remark, int $now): array
|
||||
{
|
||||
$rows = Db::name('channel_admin_share')->alias('cas')
|
||||
->join('admin a', 'cas.admin_id = a.id')
|
||||
->field(['cas.admin_id', 'cas.share_rate'])
|
||||
->where('cas.channel_id', $channelId)
|
||||
->where('cas.status', 1)
|
||||
->where('a.status', 'enable')
|
||||
->order('cas.admin_id', 'asc')
|
||||
->select()
|
||||
->toArray();
|
||||
if ($rows === []) {
|
||||
return [];
|
||||
}
|
||||
$sum = '0.00';
|
||||
$out = [];
|
||||
foreach ($rows as $row) {
|
||||
$adminId = intval($row['admin_id'] ?? 0);
|
||||
$shareRate = bcadd(strval($row['share_rate'] ?? '0'), '0', 2);
|
||||
if ($adminId <= 0 || bccomp($shareRate, '0', 2) <= 0) {
|
||||
$rows = [];
|
||||
foreach ($distributions as $dist) {
|
||||
$adminId = intval($dist['admin_id'] ?? 0);
|
||||
if ($adminId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$sum = bcadd($sum, $shareRate, 2);
|
||||
$out[] = ['admin_id' => $adminId, 'share_rate' => $shareRate];
|
||||
}
|
||||
if ($out === [] || bccomp($sum, '100.00', 2) !== 0) {
|
||||
return [];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
private static function buildCommissionRowsForSplit(array $shareRows, int $channelId, int $periodId, string $calcBaseAmount, string $commissionTotal, string $remark, int $now): array
|
||||
{
|
||||
$sum = '0.00';
|
||||
$rows = [];
|
||||
$lastIndex = count($shareRows) - 1;
|
||||
foreach ($shareRows as $index => $shareRow) {
|
||||
$shareRate = bcadd(strval($shareRow['share_rate'] ?? '0.00'), '0', 2);
|
||||
$shareDec = bcdiv($shareRate, '100', 4);
|
||||
$amount = $index === $lastIndex ? bcsub($commissionTotal, $sum, 2) : bcmul($commissionTotal, $shareDec, 2);
|
||||
if ($index !== $lastIndex) {
|
||||
$sum = bcadd($sum, $amount, 2);
|
||||
}
|
||||
$effectiveRate = bccomp($calcBaseAmount, '0', 2) <= 0 ? '0.0000' : bcdiv($amount, $calcBaseAmount, 6);
|
||||
$amount = strval($dist['commission_amount'] ?? '0.00');
|
||||
$rows[] = [
|
||||
'settlement_period_id' => $periodId,
|
||||
'channel_id' => $channelId,
|
||||
'admin_id' => intval($shareRow['admin_id'] ?? 0),
|
||||
'commission_rate' => $effectiveRate,
|
||||
'calc_base_amount' => $calcBaseAmount,
|
||||
'admin_id' => $adminId,
|
||||
'commission_rate' => strval($dist['commission_rate'] ?? '0.0000'),
|
||||
'calc_base_amount' => strval($dist['calc_base_amount'] ?? '0.00'),
|
||||
'commission_amount' => $amount,
|
||||
'status' => 0,
|
||||
'settled_at' => null,
|
||||
'remark' => $remark . ' | 分配比例=' . $shareRate . '%',
|
||||
'remark' => $remark . ' | 树形分红实发',
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
];
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
private static function buildCommissionSplitPreview(array $shareRows, string $commissionTotal): array
|
||||
{
|
||||
if ($shareRows === []) {
|
||||
return [];
|
||||
}
|
||||
$adminIds = array_map(static fn(array $row): int => intval($row['admin_id'] ?? 0), $shareRows);
|
||||
$adminNames = Db::name('admin')->where('id', 'in', $adminIds)->column('username', 'id');
|
||||
$sum = '0.00';
|
||||
$out = [];
|
||||
$lastIndex = count($shareRows) - 1;
|
||||
foreach ($shareRows as $index => $shareRow) {
|
||||
$shareRate = bcadd(strval($shareRow['share_rate'] ?? '0.00'), '0', 2);
|
||||
$shareDec = bcdiv($shareRate, '100', 4);
|
||||
$amount = $index === $lastIndex ? bcsub($commissionTotal, $sum, 2) : bcmul($commissionTotal, $shareDec, 2);
|
||||
if ($index !== $lastIndex) {
|
||||
$sum = bcadd($sum, $amount, 2);
|
||||
}
|
||||
$aid = intval($shareRow['admin_id'] ?? 0);
|
||||
$out[] = [
|
||||
'admin_id' => $aid,
|
||||
'admin_username' => strval($adminNames[$aid] ?? ('#' . $aid)),
|
||||
'share_rate' => $shareRate,
|
||||
'commission_amount' => $amount,
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user