1.优化分红方式

This commit is contained in:
2026-05-29 11:06:24 +08:00
parent eba80b1bf4
commit f3677eb0e3
15 changed files with 982 additions and 487 deletions

View File

@@ -5,24 +5,32 @@ declare(strict_types=1);
namespace app\admin\controller\auth;
use ba\Random;
use ba\Tree;
use Throwable;
use support\think\Db;
use support\validation\Validator;
use support\validation\ValidationException;
use app\common\controller\Backend;
use app\common\service\AdminCommissionDistributionService;
use app\admin\model\Admin as AdminModel;
use support\Response;
use Webman\Http\Request;
class Admin extends Backend
{
/**
* 分红比例余量查询(表单提示用)
*/
protected array $noNeedPermission = ['commissionShareRemainder'];
protected ?object $model = null;
protected array|string $preExcludeFields = ['create_time', 'update_time', 'password', 'salt', 'login_failure', 'last_login_time', 'last_login_ip'];
protected array|string $quickSearchField = ['username', 'nickname'];
protected string|int|bool $dataLimit = 'parent';
/** 使用 parent_admin_id 树过滤,不用角色组 dataLimit */
protected string|int|bool $dataLimit = false;
protected string $dataLimitField = 'id';
@@ -35,41 +43,94 @@ class Admin extends Backend
public function index(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) return $response;
if ($response !== null) {
return $response;
}
if ($request->get('select') ?? $request->post('select')) {
$selectRes = $this->select($request);
if ($selectRes !== null) return $selectRes;
if ($selectRes !== null) {
return $selectRes;
}
}
list($where, $alias, $limit, $order) = $this->queryBuilder();
$adminAlias = $alias[strtolower($this->model->getTable())] ?? 'admin';
$query = $this->model
->withoutField('login_failure,password,salt')
->withJoin($this->withJoinTable, $this->withJoinType)
->alias($alias)
->where($where);
// 仅返回“顶级角色组(pid=0)”下的管理员(用于远程下拉等场景)
$visibleIds = AdminCommissionDistributionService::getVisibleAdminIdsForOperator(
intval($this->auth->id),
$this->auth->isSuperAdmin()
);
if ($visibleIds !== []) {
$query->where($adminAlias . '.id', 'in', $visibleIds);
}
$topGroup = $request->get('top_group') ?? $request->post('top_group');
if ($topGroup === '1' || $topGroup === 1 || $topGroup === true) {
$query = $query
->join('admin_group_access aga', $alias['admin'] . '.id = aga.uid')
->join('admin_group_access aga', $adminAlias . '.id = aga.uid')
->join('admin_group ag', 'aga.group_id = ag.id')
->where('ag.pid', 0)
->distinct(true);
}
$res = $query
->order($order)
->paginate($limit);
$items = $res->items();
$rows = $query->order($order)->select()->toArray();
$rows = $this->enrichAdminRows($rows);
foreach ($rows as $k => $row) {
$parentId = intval($row['parent_admin_id'] ?? 0);
$rows[$k]['pid'] = $parentId > 0 ? $parentId : 0;
}
$tree = Tree::instance()->assembleChild($rows, 'pid', 'id');
return $this->success('', [
'list' => $items,
'total' => $res->total(),
'list' => $tree,
'total' => count($rows),
'remark' => get_route_remark(),
]);
}
/**
* 查询同上级下剩余可分配分红比例(表单提示)
*/
public function commissionShareRemainder(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
$parentAdminId = intval($request->get('parent_admin_id', 0));
$excludeId = intval($request->get('exclude_id', 0));
if ($parentAdminId <= 0) {
return $this->success('', [
'used_rate' => '0.00',
'remaining_rate' => '100.00',
'parent_has_no_share' => false,
]);
}
if (!$this->canManageAdminId($parentAdminId)) {
return $this->error(__('You have no permission'));
}
$stats = AdminCommissionDistributionService::getShareRemainder(
$parentAdminId,
$excludeId > 0 ? $excludeId : null
);
return $this->success('', [
'used_rate' => $stats['used_rate'],
'remaining_rate' => $stats['remaining_rate'],
'parent_has_no_share' => bccomp($stats['remaining_rate'], '0', 2) <= 0,
]);
}
/**
* 远程下拉(重写:支持 top_group=1 仅返回顶级组管理员)
*/
@@ -88,7 +149,9 @@ class Admin extends Backend
$quickSearchArr = is_array($this->quickSearchField) ? $this->quickSearchField : explode(',', (string) $this->quickSearchField);
foreach ($quickSearchArr as $f) {
$f = trim((string) $f);
if ($f === '') continue;
if ($f === '') {
continue;
}
$f = str_contains($f, '.') ? substr($f, strrpos($f, '.') + 1) : $f;
if ($f !== '' && !in_array($f, $fields, true)) {
$fields[] = $f;
@@ -99,15 +162,15 @@ class Admin extends Backend
$modelTable = strtolower($this->model->getTable());
$mainAlias = ($alias[$modelTable] ?? $modelTable) . '.';
// 联表时避免字段歧义:主表字段统一 select 为 "admin.xxx as xxx"
$selectFields = [];
foreach ($fields as $f) {
$f = trim((string) $f);
if ($f === '') continue;
if ($f === '') {
continue;
}
$selectFields[] = $mainAlias . $f . ' as ' . $f;
}
// 联表时避免排序字段歧义:无前缀的字段默认加主表前缀
$qualifiedOrder = [];
if (is_array($order)) {
foreach ($order as $k => $v) {
@@ -121,6 +184,14 @@ class Admin extends Backend
->alias($alias)
->where($where);
$visibleIds = AdminCommissionDistributionService::getVisibleAdminIdsForOperator(
intval($this->auth->id),
$this->auth->isSuperAdmin()
);
if ($visibleIds !== []) {
$query->where($mainAlias . 'id', 'in', $visibleIds);
}
$topGroup = $this->request ? ($this->request->get('top_group') ?? $this->request->post('top_group')) : null;
if ($topGroup === '1' || $topGroup === 1 || $topGroup === true) {
$query = $query
@@ -143,7 +214,9 @@ class Admin extends Backend
public function add(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) return $response;
if ($response !== null) {
return $response;
}
if ($request->method() === 'POST') {
$data = $request->post();
@@ -179,6 +252,7 @@ class Admin extends Backend
$data = $this->excludeFields($data);
$creatorChannelId = $this->getCreatorChannelId();
$groupChannelId = $this->resolveChannelIdFromPrimaryGroup($data['group_arr'] ?? []);
if (!$this->auth->isSuperAdmin()) {
if ($creatorChannelId === null || $creatorChannelId === '') {
return $this->error(__('You have no permission'));
@@ -192,13 +266,24 @@ class Admin extends Backend
$data['channel_id'] = $creatorChannelId;
$data['parent_admin_id'] = $this->auth->id;
} else {
$data['channel_id'] = ($groupChannelId === null || $groupChannelId === '') ? null : $groupChannelId;
$postedChannel = $data['channel_id'] ?? null;
if ($postedChannel === null || $postedChannel === '') {
$data['channel_id'] = ($groupChannelId === null || $groupChannelId === '') ? null : $groupChannelId;
}
}
$parentErr = $this->normalizeParentAndShareFields($data, null);
if ($parentErr !== null) {
return $this->error($parentErr);
}
$data['invite_code'] = $this->generateUniqueInviteCode();
$result = false;
if (!empty($data['group_arr'])) {
$authRes = $this->checkGroupAuth($data['group_arr']);
if ($authRes !== null) return $authRes;
if ($authRes !== null) {
return $authRes;
}
}
$this->model->startTrans();
try {
@@ -234,7 +319,9 @@ class Admin extends Backend
public function edit(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) return $response;
if ($response !== null) {
return $response;
}
$pk = $this->model->getPk();
$id = $request->get($pk) ?? $request->post($pk);
@@ -243,8 +330,7 @@ class Admin extends Backend
return $this->error(__('Record not found'));
}
$dataLimitAdminIds = $this->getDataLimitAdminIds();
if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) {
if (!$this->canManageAdminId(intval($row['id']))) {
return $this->error(__('You have no permission'));
}
@@ -255,7 +341,7 @@ class Admin extends Backend
}
$isSelfEdit = (int) $this->auth->id === (int) $id;
if ($isSelfEdit) {
unset($data['group_arr'], $data['group_name_arr']);
unset($data['group_arr'], $data['group_name_arr'], $data['parent_admin_id'], $data['commission_share_rate']);
}
$editGroupArr = null;
@@ -313,7 +399,9 @@ class Admin extends Backend
];
}
$authRes = $this->checkGroupAuth($checkGroups);
if ($authRes !== null) return $authRes;
if ($authRes !== null) {
return $authRes;
}
}
$data = $this->excludeFields($data);
@@ -336,6 +424,17 @@ class Admin extends Backend
$data['channel_id'] = ($groupChannelId === null || $groupChannelId === '') ? null : $groupChannelId;
}
}
if (!$isSelfEdit) {
if (!$this->auth->isSuperAdmin()) {
unset($data['parent_admin_id']);
}
$parentErr = $this->normalizeParentAndShareFields($data, intval($id));
if ($parentErr !== null) {
return $this->error($parentErr);
}
}
$result = false;
$this->model->startTrans();
try {
@@ -361,37 +460,54 @@ class Admin extends Backend
unset($row['salt'], $row['login_failure']);
$row['password'] = '';
$rowData = $row->toArray();
$enriched = $this->enrichAdminRows([$rowData]);
$rowData = $enriched[0] ?? $rowData;
$parentId = intval($rowData['parent_admin_id'] ?? 0);
if ($parentId > 0) {
$remainder = AdminCommissionDistributionService::getShareRemainder($parentId, intval($id));
$rowData['share_remainder'] = $remainder;
}
return $this->success('', [
'row' => $row
'row' => $rowData,
]);
}
public function del(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) return $response;
$where = [];
$dataLimitAdminIds = $this->getDataLimitAdminIds();
if ($dataLimitAdminIds) {
$where[] = [$this->dataLimitField, 'in', $dataLimitAdminIds];
if ($response !== null) {
return $response;
}
$ids = $request->get('ids') ?? $request->post('ids') ?? [];
$ids = is_array($ids) ? $ids : [];
$where[] = [$this->model->getPk(), 'in', $ids];
$data = $this->model->where($where)->select();
if ($ids === []) {
return $this->error(__('Parameter error'));
}
$data = $this->model->where($this->model->getPk(), 'in', $ids)->select();
$count = 0;
$this->model->startTrans();
try {
foreach ($data as $v) {
if ($v->id != $this->auth->id) {
$count += $v->delete();
Db::name('admin_group_access')
->where('uid', $v['id'])
->delete();
if ($v->id == $this->auth->id) {
continue;
}
if (!$this->canManageAdminId(intval($v->id))) {
continue;
}
$childCount = Db::name('admin')->where('parent_admin_id', $v->id)->count();
if ($childCount > 0) {
$this->model->rollback();
return $this->error(__('Cannot delete administrator with sub-agents'));
}
$count += $v->delete();
Db::name('admin_group_access')
->where('uid', $v['id'])
->delete();
}
$this->model->commit();
} catch (Throwable $e) {
@@ -404,9 +520,6 @@ class Admin extends Backend
return $this->error(__('No rows were deleted'));
}
/**
* 远程下拉Admin 无自定义,走父类默认列表)
*/
public function select(Request $request): Response
{
return parent::select($request);
@@ -465,6 +578,141 @@ class Admin extends Backend
return Db::name('admin_group')->where('id', $gid)->value('channel_id');
}
private function canManageAdminId(int $adminId): bool
{
if ($this->auth->isSuperAdmin()) {
return true;
}
if ($adminId === intval($this->auth->id)) {
return true;
}
$visible = AdminCommissionDistributionService::getVisibleAdminIdsForOperator(
intval($this->auth->id),
false
);
return in_array($adminId, $visible, true);
}
/**
* @param array<string, mixed> $data
*/
private function normalizeParentAndShareFields(array &$data, ?int $editAdminId): ?string
{
$parentId = isset($data['parent_admin_id']) && $data['parent_admin_id'] !== '' && $data['parent_admin_id'] !== null
? intval($data['parent_admin_id'])
: 0;
if ($parentId <= 0 && $editAdminId !== null && $editAdminId > 0) {
$existingParent = Db::name('admin')->where('id', $editAdminId)->value('parent_admin_id');
$parentId = intval($existingParent ?? 0);
}
if ($parentId <= 0) {
$data['parent_admin_id'] = null;
$data['commission_share_rate'] = null;
return null;
}
if ($editAdminId !== null && $parentId === $editAdminId) {
return (string) __('Cannot set yourself as parent administrator');
}
$parent = Db::name('admin')->where('id', $parentId)->find();
if (!is_array($parent)) {
return (string) __('Invalid parent administrator');
}
if (!$this->canManageAdminId($parentId) && !$this->auth->isSuperAdmin()) {
return (string) __('You have no permission');
}
$channelId = $data['channel_id'] ?? null;
if ($channelId !== null && $channelId !== '' && (string) ($parent['channel_id'] ?? '') !== (string) $channelId) {
return (string) __('Parent administrator must belong to the same channel');
}
if (($channelId === null || $channelId === '') && !empty($parent['channel_id'])) {
$data['channel_id'] = $parent['channel_id'];
}
$shareErr = AdminCommissionDistributionService::validateCommissionShareRate(
$parentId,
$data['commission_share_rate'] ?? null,
$editAdminId
);
if ($shareErr !== null) {
return $shareErr;
}
$data['commission_share_rate'] = bcadd(strval($data['commission_share_rate'] ?? '0'), '0', 2);
$data['parent_admin_id'] = $parentId;
return null;
}
/**
* @param array<int, array<string, mixed>> $rows
* @return array<int, array<string, mixed>>
*/
private function enrichAdminRows(array $rows): array
{
if ($rows === []) {
return [];
}
$adminIds = [];
$channelIds = [];
$parentIds = [];
foreach ($rows as $row) {
$aid = intval($row['id'] ?? 0);
if ($aid > 0) {
$adminIds[] = $aid;
}
$cid = $row['channel_id'] ?? null;
if ($cid !== null && $cid !== '') {
$channelIds[] = intval($cid);
}
$pid = intval($row['parent_admin_id'] ?? 0);
if ($pid > 0) {
$parentIds[] = $pid;
}
}
$channelNames = $channelIds !== []
? Db::name('channel')->where('id', 'in', array_unique($channelIds))->column('name', 'id')
: [];
$parentNames = $parentIds !== []
? Db::name('admin')->where('id', 'in', array_unique($parentIds))->column('username', 'id')
: [];
$groupMap = [];
if ($adminIds !== []) {
$accessRows = Db::name('admin_group_access')->where('uid', 'in', $adminIds)->select()->toArray();
$groupIds = array_unique(array_map(static fn(array $r): int => intval($r['group_id'] ?? 0), $accessRows));
$groupNames = $groupIds !== []
? Db::name('admin_group')->where('id', 'in', $groupIds)->column('name', 'id')
: [];
foreach ($accessRows as $access) {
$uid = intval($access['uid'] ?? 0);
$gid = intval($access['group_id'] ?? 0);
if ($uid <= 0 || $gid <= 0) {
continue;
}
if (!isset($groupMap[$uid])) {
$groupMap[$uid] = [];
}
$name = $groupNames[$gid] ?? '';
if ($name !== '') {
$groupMap[$uid][] = $name;
}
}
}
foreach ($rows as $k => $row) {
$cid = $row['channel_id'] ?? null;
$rows[$k]['channel_name'] = ($cid !== null && $cid !== '') ? strval($channelNames[intval($cid)] ?? '') : '';
$pid = intval($row['parent_admin_id'] ?? 0);
$rows[$k]['parent_admin_username'] = $pid > 0 ? strval($parentNames[$pid] ?? '') : '';
$aid = intval($row['id'] ?? 0);
$rows[$k]['group_name_arr'] = $groupMap[$aid] ?? [];
}
return $rows;
}
private function generateUniqueInviteCode(): string
{
$tries = 0;

View File

@@ -6,6 +6,16 @@ return [
'Channel not found' => 'Channel not found',
'Settlement number conflict, please retry' => 'Settlement number conflict, please retry',
'No available admin share ratios under this channel; cannot settle' => 'No available admin share ratios under this channel; cannot settle',
'No channel root agent configured for commission distribution' => 'No top-level agent is configured for this channel; cannot settle with tree commission',
'Sub-agent commission share rate is required' => 'Commission share rate from parent is required for sub-agents',
'Commission share rate must be between 0 and 100' => 'Commission share rate must be between 0 and 100',
'Sum of sibling commission share rates cannot exceed 100%' => 'Sum of sibling commission share rates cannot exceed 100%',
'Parent administrator is required for sub-agent' => 'Parent administrator is required for sub-agent',
'Invalid parent administrator' => 'Invalid parent administrator',
'Parent administrator must belong to the same channel' => 'Parent administrator must belong to the same channel',
'Cannot set yourself as parent administrator' => 'Cannot set yourself as parent administrator',
'Top-level agent does not need a commission share rate' => 'Top-level agent does not need a commission share rate',
'Cannot delete administrator with sub-agents' => 'Cannot delete an administrator who has sub-agents',
'This flow pays commissions automatically after super admin settlement; channel admin does not need to settle again' => 'This flow pays commissions automatically after super admin settlement; channel admin does not need to settle again',
'Invalid channel data' => 'Invalid channel data',
'Invalid settlement period (start time is not earlier than now)' => 'Invalid settlement period (start time is not earlier than now)',

View File

@@ -6,6 +6,16 @@ return [
'Channel not found' => '渠道不存在',
'Settlement number conflict, please retry' => '结算单号冲突,请重试',
'No available admin share ratios under this channel; cannot settle' => '渠道下无可用管理员分配比例,无法结算',
'No channel root agent configured for commission distribution' => '渠道未配置顶级代理管理员,无法按树形分红结算',
'Sub-agent commission share rate is required' => '子代理须填写从上级分红中抽取的比例',
'Commission share rate must be between 0 and 100' => '分红比例须在0到100之间',
'Sum of sibling commission share rates cannot exceed 100%' => '同上级下各子代理分红比例合计不能超过100%',
'Parent administrator is required for sub-agent' => '子代理须绑定上级管理员',
'Invalid parent administrator' => '上级管理员无效',
'Parent administrator must belong to the same channel' => '上级管理员须与当前渠道一致',
'Cannot set yourself as parent administrator' => '不能将自己设为上级管理员',
'Top-level agent does not need a commission share rate' => '顶级代理无需填写分红比例',
'Cannot delete administrator with sub-agents' => '存在下级子代理的管理员不可删除',
'This flow pays commissions automatically after super admin settlement; channel admin does not need to settle again' => '当前流程为超管结算后自动发放,渠道管理员无需二次结算',
'Invalid channel data' => '渠道数据异常',
'Invalid settlement period (start time is not earlier than now)' => '结算区间无效(开始时间不早于当前)',

View 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;
}
}

View File

@@ -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;
}
}