1.优化充值跳转链接的问题
2.优化后台渠道管理页面的显示样式
This commit is contained in:
@@ -22,7 +22,6 @@ class Channel extends Backend
|
||||
'manualSettlePreview',
|
||||
'channelAdminShareList',
|
||||
'saveChannelAdminShare',
|
||||
'batchSettlePending',
|
||||
'settleStats',
|
||||
'dividendRecordList',
|
||||
'directBetRecordList',
|
||||
@@ -44,13 +43,9 @@ class Channel extends Backend
|
||||
|
||||
protected bool $modelSceneValidate = true;
|
||||
|
||||
/** @var array<int, int> 当前管理员绑定的渠道(写操作范围) */
|
||||
private array $ownChannelIds = [];
|
||||
|
||||
protected function initController(WebmanRequest $request): ?Response
|
||||
{
|
||||
$this->model = new \app\common\model\Channel();
|
||||
$this->ownChannelIds = $this->resolveOwnChannelIds();
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -257,7 +252,7 @@ class Channel extends Backend
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除:仅允许删除本人绑定渠道(查看所有渠道不扩大写权限)
|
||||
* 删除:须在可写渠道范围内
|
||||
*/
|
||||
protected function _del(): Response
|
||||
{
|
||||
@@ -685,8 +680,12 @@ class Channel extends Backend
|
||||
}
|
||||
|
||||
$remark = trim((string) $request->post('remark', ''));
|
||||
$handlingFeeByAdmin = $this->parseCommissionSplitHandlingFees($request->post('commission_split'));
|
||||
if ($handlingFeeByAdmin === false) {
|
||||
return $this->error(__('Settlement handling fee rate must be between 0 and 100'));
|
||||
}
|
||||
|
||||
$res = ChannelSettlementService::settleBySuperAdmin((int) $row['id'], intval($this->auth->id), $remark, false);
|
||||
$res = ChannelSettlementService::settleBySuperAdmin((int) $row['id'], intval($this->auth->id), $remark, false, $handlingFeeByAdmin);
|
||||
if (($res['ok'] ?? false) !== true) {
|
||||
return $this->error((string) ($res['msg'] ?? __('Settlement failed')));
|
||||
}
|
||||
@@ -694,7 +693,7 @@ class Channel extends Backend
|
||||
}
|
||||
|
||||
/**
|
||||
* 超管批量结算全部待结算渠道(可作为“提前结算”入口)
|
||||
* 批量结算待结算渠道(需 channel/batchSettlePending;范围=当前账号可写渠道)
|
||||
*/
|
||||
public function batchSettlePending(WebmanRequest $request): Response
|
||||
{
|
||||
@@ -702,11 +701,15 @@ class Channel extends Backend
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
if (!$this->auth->isSuperAdmin()) {
|
||||
if (!$this->canBatchSettle()) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
$scope = AdminChannelScopeService::writableChannelIds($this->auth);
|
||||
if ($scope !== null && $scope === []) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
// 批量按钮语义:手动触发“待结算渠道”结算,不受结算周期到点限制。
|
||||
$res = ChannelSettlementService::settleAllDueChannels(intval($this->auth->id), false);
|
||||
$res = ChannelSettlementService::settleAllDueChannels(intval($this->auth->id), false, $scope);
|
||||
return $this->success(__('Batch settlement completed'), $res);
|
||||
}
|
||||
|
||||
@@ -1003,13 +1006,15 @@ class Channel extends Backend
|
||||
|
||||
private function assertChannelWritable(int $channelId): bool
|
||||
{
|
||||
if ($channelId <= 0) {
|
||||
if ($channelId <= 0 || $this->auth === null) {
|
||||
return false;
|
||||
}
|
||||
if ($this->auth->isSuperAdmin()) {
|
||||
$scope = AdminChannelScopeService::writableChannelIds($this->auth);
|
||||
if ($scope === null) {
|
||||
return Db::name('channel')->where('id', $channelId)->value('id') !== null;
|
||||
}
|
||||
return in_array($channelId, $this->ownChannelIds, true);
|
||||
|
||||
return in_array($channelId, $scope, true);
|
||||
}
|
||||
|
||||
private function assertChannelAccessible(int $channelId): bool
|
||||
@@ -1032,6 +1037,18 @@ class Channel extends Backend
|
||||
return $this->auth->check('channel/manualSettle');
|
||||
}
|
||||
|
||||
private function canBatchSettle(): bool
|
||||
{
|
||||
if ($this->auth === null) {
|
||||
return false;
|
||||
}
|
||||
if ($this->auth->isSuperAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->auth->check('channel/batchSettlePending');
|
||||
}
|
||||
|
||||
private function buildChannelPlayRecordQuery(int $channelId, bool $settledOnly)
|
||||
{
|
||||
$query = Db::name('game_play_record')->alias('pr')
|
||||
@@ -1412,34 +1429,6 @@ class Channel extends Backend
|
||||
return $chosen;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int>
|
||||
*/
|
||||
/**
|
||||
* 写操作可作用的渠道(角色组绑定渠道 + 账号 channel_id,不含全平台只读)
|
||||
*
|
||||
* @return array<int, int>
|
||||
*/
|
||||
private function resolveOwnChannelIds(): array
|
||||
{
|
||||
if ($this->auth === null) {
|
||||
return [];
|
||||
}
|
||||
$ids = AdminChannelScopeService::resolveBoundGroupChannelIds($this->auth);
|
||||
if ($ids !== []) {
|
||||
return $ids;
|
||||
}
|
||||
$admin = Db::name('admin')
|
||||
->field(['id', 'channel_id'])
|
||||
->where('id', $this->auth->id)
|
||||
->find();
|
||||
if ($admin && !empty($admin['channel_id'])) {
|
||||
$ids[] = (int) $admin['channel_id'];
|
||||
}
|
||||
|
||||
return array_values(array_unique($ids));
|
||||
}
|
||||
|
||||
/**
|
||||
* 佣金归属管理员:取该渠道下 admin.channel_id 匹配的首个管理员(按 id 升序)。
|
||||
*/
|
||||
@@ -1656,6 +1645,16 @@ class Channel extends Backend
|
||||
}
|
||||
|
||||
$mode = isset($data['agent_mode']) ? (string) $data['agent_mode'] : '';
|
||||
if (isset($data['settlement_handling_fee']) && $data['settlement_handling_fee'] !== '' && $data['settlement_handling_fee'] !== null) {
|
||||
$fee = (float) $data['settlement_handling_fee'];
|
||||
if ($fee < 0 || $fee > 100) {
|
||||
return (string) __('Settlement handling fee rate must be between 0 and 100');
|
||||
}
|
||||
$data['settlement_handling_fee'] = number_format($fee, 2, '.', '');
|
||||
} else {
|
||||
$data['settlement_handling_fee'] = '0.00';
|
||||
}
|
||||
|
||||
if ($mode === 'turnover') {
|
||||
if (isset($data['turnover_share_rate']) && $data['turnover_share_rate'] !== '' && $data['turnover_share_rate'] !== null) {
|
||||
$num = (float) $data['turnover_share_rate'];
|
||||
@@ -1714,6 +1713,33 @@ class Channel extends Backend
|
||||
return $negative ? ('-' . $v) : $v;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>|null|false admin_id => 手续费比例(%);false=比例非法
|
||||
*/
|
||||
private function parseCommissionSplitHandlingFees(mixed $splitRaw): array|null|false
|
||||
{
|
||||
if (!is_array($splitRaw) || $splitRaw === []) {
|
||||
return null;
|
||||
}
|
||||
$map = [];
|
||||
foreach ($splitRaw as $item) {
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
$adminId = intval($item['admin_id'] ?? 0);
|
||||
if ($adminId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$rateRaw = $item['handling_fee_rate'] ?? ($item['handling_fee'] ?? '0');
|
||||
$rate = bcadd(strval($rateRaw), '0', 2);
|
||||
if (bccomp($rate, '0', 2) < 0 || bccomp($rate, '100', 2) > 0) {
|
||||
return false;
|
||||
}
|
||||
$map[$adminId] = $rate;
|
||||
}
|
||||
return $map === [] ? null : $map;
|
||||
}
|
||||
|
||||
private function validateLadderRulesField(array &$data): ?string
|
||||
{
|
||||
$rulesRaw = $data['affiliate_ladder_rules'] ?? null;
|
||||
|
||||
@@ -477,7 +477,7 @@ class Backend extends Api
|
||||
}
|
||||
|
||||
/**
|
||||
* 全平台只读范围:角色组均未绑定渠道,或拥有查看所有渠道权限,或超管
|
||||
* 全平台只读范围:超管,或未绑定渠道且拥有渠道模块基础权限
|
||||
*/
|
||||
protected function hasGlobalReadScope(): bool
|
||||
{
|
||||
|
||||
@@ -123,6 +123,8 @@ return [
|
||||
'Weekly settlement must select Monday to Sunday' => 'Weekly settlement must select Monday to Sunday',
|
||||
'Monthly settlement day must be between 1 and 31' => 'Monthly settlement day must be between 1 and 31',
|
||||
'Turnover commission rate must be between 0 and 100' => 'Turnover commission rate must be between 0 and 100',
|
||||
'Settlement handling fee cannot be negative' => 'Settlement handling fee cannot be negative',
|
||||
'Settlement handling fee rate must be between 0 and 100' => 'Settlement handling fee rate must be between 0 and 100',
|
||||
'Affiliate share/fee rates are required' => 'Affiliate share/fee rates are required',
|
||||
'Affiliate share/fee rates must be between 0 and 1' => 'Affiliate share/fee rates must be between 0 and 1',
|
||||
'Affiliate ladder rules are required' => 'Affiliate ladder rules are required',
|
||||
|
||||
@@ -123,6 +123,8 @@ return [
|
||||
'Weekly settlement must select Monday to Sunday' => '周结必须选择周一到周日',
|
||||
'Monthly settlement day must be between 1 and 31' => '月结日期必须在1到31之间',
|
||||
'Turnover commission rate must be between 0 and 100' => '返水分红比例必须在0到100之间',
|
||||
'Settlement handling fee cannot be negative' => '代理结算手续费不能为负数',
|
||||
'Settlement handling fee rate must be between 0 and 100' => '代理结算手续费比例必须在0到100之间',
|
||||
'Affiliate share/fee rates are required' => '联营占成比例不能为空',
|
||||
'Affiliate share/fee rates must be between 0 and 1' => '联营占成比例必须在0到1之间',
|
||||
'Affiliate ladder rules are required' => '联营阶梯规则不能为空',
|
||||
|
||||
@@ -9,13 +9,14 @@ use support\think\Db;
|
||||
|
||||
/**
|
||||
* 后台管理员渠道数据范围:
|
||||
* - 账号 channel_id 或角色组 channel_id 任一绑定 → 仅可读对应渠道(优先于「查看所有渠道」)
|
||||
* - 均未绑定且拥有 viewAllChannels → 全平台只读
|
||||
* - 账号或角色组绑定 channel_id → 仅该批渠道(读+写)
|
||||
* - 均未绑定且拥有渠道模块基础权限(channel/index、edit 等)→ 全部渠道(读+写)
|
||||
* - 均未绑定且无渠道模块权限 → 不可见
|
||||
*/
|
||||
class AdminChannelScopeService
|
||||
{
|
||||
/**
|
||||
* 是否具备全平台只读范围(超管 / 未绑定任何渠道且拥有查看所有渠道)
|
||||
* 是否具备全平台只读范围(其它菜单按 admin 收窄时跳过):超管 / 未绑定渠道且拥有渠道模块权限
|
||||
*/
|
||||
public static function hasGlobalReadScope(Auth $auth): bool
|
||||
{
|
||||
@@ -29,7 +30,7 @@ class AdminChannelScopeService
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::hasViewAllChannelsPermission($auth);
|
||||
return self::hasAnyChannelMenuPermission($auth);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,6 +39,24 @@ class AdminChannelScopeService
|
||||
* @return array<int, int>|null
|
||||
*/
|
||||
public static function readableChannelIds(Auth $auth): ?array
|
||||
{
|
||||
return self::resolveScopedChannelIds($auth);
|
||||
}
|
||||
|
||||
/**
|
||||
* 可写渠道 ID 列表;null 表示不限制(全部渠道)
|
||||
*
|
||||
* @return array<int, int>|null
|
||||
*/
|
||||
public static function writableChannelIds(Auth $auth): ?array
|
||||
{
|
||||
return self::resolveScopedChannelIds($auth);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int>|null
|
||||
*/
|
||||
private static function resolveScopedChannelIds(Auth $auth): ?array
|
||||
{
|
||||
if (!$auth->isLogin()) {
|
||||
return [0];
|
||||
@@ -51,13 +70,55 @@ class AdminChannelScopeService
|
||||
return $ids;
|
||||
}
|
||||
|
||||
if (self::hasViewAllChannelsPermission($auth)) {
|
||||
if (self::hasAnyChannelMenuPermission($auth)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否拥有渠道管理模块相关权限(菜单或任一 channel/* 按钮)
|
||||
*/
|
||||
public static function hasAnyChannelMenuPermission(Auth $auth): bool
|
||||
{
|
||||
if (!$auth->isLogin()) {
|
||||
return false;
|
||||
}
|
||||
if ($auth->isSuperAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$checkNodes = [
|
||||
'channel',
|
||||
'channel/index',
|
||||
'channel/add',
|
||||
'channel/edit',
|
||||
'channel/del',
|
||||
'channel/manualSettle',
|
||||
'channel/batchSettlePending',
|
||||
'channel/settleStats',
|
||||
];
|
||||
foreach ($checkNodes as $node) {
|
||||
if ($auth->check($node)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$ruleList = $auth->getRuleList();
|
||||
foreach ($ruleList as $name) {
|
||||
if (!is_string($name) || $name === '') {
|
||||
continue;
|
||||
}
|
||||
$lower = strtolower($name);
|
||||
if ($lower === 'channel' || str_starts_with($lower, 'channel/')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员实际绑定的渠道(角色组 channel_id + 账号 admin.channel_id,去重)
|
||||
*
|
||||
@@ -124,30 +185,6 @@ class AdminChannelScopeService
|
||||
return array_values(array_unique($ids));
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否拥有「查看所有渠道」按钮权限
|
||||
*/
|
||||
public static function hasViewAllChannelsPermission(Auth $auth): bool
|
||||
{
|
||||
if (!$auth->isLogin()) {
|
||||
return false;
|
||||
}
|
||||
$nodes = ['channel/viewAllChannels', 'channel/viewallchannels'];
|
||||
foreach ($nodes as $node) {
|
||||
if ($auth->check($node)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
$ruleList = $auth->getRuleList();
|
||||
foreach ($nodes as $node) {
|
||||
if (in_array(strtolower($node), $ruleList, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表按 channel_id 过滤时使用的 ID;null=不过滤
|
||||
*
|
||||
|
||||
@@ -152,11 +152,154 @@ class AdminCommissionDistributionService
|
||||
}
|
||||
|
||||
/**
|
||||
* 将渠道本期总佣金按管理员树分配,返回各管理员实得金额
|
||||
*
|
||||
* @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
|
||||
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 [];
|
||||
}
|
||||
$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<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 [];
|
||||
@@ -166,7 +309,7 @@ class AdminCommissionDistributionService
|
||||
->where('status', 'enable')
|
||||
->whereRaw('(parent_admin_id IS NULL OR parent_admin_id = 0)')
|
||||
->order('id', 'asc')
|
||||
->field(['id', 'commission_share_rate'])
|
||||
->field(['id', 'commission_share_rate', 'username'])
|
||||
->select()
|
||||
->toArray();
|
||||
if ($rootRows === []) {
|
||||
@@ -180,7 +323,7 @@ class AdminCommissionDistributionService
|
||||
break;
|
||||
}
|
||||
}
|
||||
$merged = [];
|
||||
$nodes = [];
|
||||
if ($useRateSplit) {
|
||||
foreach ($rootRows as $rootRow) {
|
||||
$rootId = intval($rootRow['id'] ?? 0);
|
||||
@@ -192,13 +335,7 @@ class AdminCommissionDistributionService
|
||||
if (bccomp($rootAmount, '0', 2) <= 0) {
|
||||
continue;
|
||||
}
|
||||
$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);
|
||||
}
|
||||
self::appendNodeTree($rootId, $rootAmount, 0, 0, $rate, $nodes);
|
||||
}
|
||||
} else {
|
||||
$rootCount = count($rootRows);
|
||||
@@ -214,31 +351,74 @@ class AdminCommissionDistributionService
|
||||
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);
|
||||
if (bccomp($rootAmount, '0', 2) <= 0) {
|
||||
continue;
|
||||
}
|
||||
self::appendNodeTree($rootId, $rootAmount, 0, 0, '100.00', $nodes);
|
||||
}
|
||||
}
|
||||
$out = [];
|
||||
foreach ($merged as $adminId => $amount) {
|
||||
if (bccomp($amount, '0', 2) <= 0) {
|
||||
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;
|
||||
}
|
||||
$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,
|
||||
];
|
||||
$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 $out;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,6 +426,7 @@ class AdminCommissionDistributionService
|
||||
*/
|
||||
private static function distributeFromAdmin(int $adminId, string $amount, string $calcBaseAmount): array
|
||||
{
|
||||
unset($calcBaseAmount);
|
||||
if ($adminId <= 0 || bccomp($amount, '0', 2) <= 0) {
|
||||
return [];
|
||||
}
|
||||
@@ -272,7 +453,7 @@ class AdminCommissionDistributionService
|
||||
continue;
|
||||
}
|
||||
$givenToChildren = bcadd($givenToChildren, $childAmount, 2);
|
||||
$childParts = self::distributeFromAdmin($childId, $childAmount, $calcBaseAmount);
|
||||
$childParts = self::distributeFromAdmin($childId, $childAmount, '0.00');
|
||||
foreach ($childParts as $aid => $part) {
|
||||
if (!isset($result[$aid])) {
|
||||
$result[$aid] = '0.00';
|
||||
@@ -289,32 +470,4 @@ class AdminCommissionDistributionService
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,16 @@ use Throwable;
|
||||
|
||||
class ChannelSettlementService
|
||||
{
|
||||
public static function settleBySuperAdmin(int $channelId, int $operatorAdminId, string $remark = '', bool $auto = false): array
|
||||
{
|
||||
/**
|
||||
* @param array<int, string>|null $handlingFeeByAdmin admin_id => handling fee
|
||||
*/
|
||||
public static function settleBySuperAdmin(
|
||||
int $channelId,
|
||||
int $operatorAdminId,
|
||||
string $remark = '',
|
||||
bool $auto = false,
|
||||
?array $handlingFeeByAdmin = null
|
||||
): array {
|
||||
$channel = Db::name('channel')->where('id', $channelId)->find();
|
||||
if (!is_array($channel)) {
|
||||
return ['ok' => false, 'msg' => __('Channel not found')];
|
||||
@@ -19,6 +27,7 @@ class ChannelSettlementService
|
||||
if (is_string($payload)) {
|
||||
return ['ok' => false, 'msg' => $payload];
|
||||
}
|
||||
$defaultFee = bcadd(strval($channel['settlement_handling_fee'] ?? '0'), '0', 2);
|
||||
$settlementNo = self::generateAgentSettlementNo($auto ? 'A' : 'M', $channelId, intval($payload['period_end_ts']));
|
||||
if (Db::name('agent_settlement_period')->where('settlement_no', $settlementNo)->value('id')) {
|
||||
return ['ok' => false, 'msg' => __('Settlement number conflict, please retry')];
|
||||
@@ -26,7 +35,9 @@ class ChannelSettlementService
|
||||
$distributions = AdminCommissionDistributionService::distributeChannelCommission(
|
||||
$channelId,
|
||||
strval($payload['commission_amount']),
|
||||
strval($payload['calc_base_amount'])
|
||||
strval($payload['calc_base_amount']),
|
||||
$defaultFee,
|
||||
$handlingFeeByAdmin
|
||||
);
|
||||
if ($distributions === []) {
|
||||
return ['ok' => false, 'msg' => __('No channel root agent configured for commission distribution')];
|
||||
@@ -58,20 +69,21 @@ class ChannelSettlementService
|
||||
}
|
||||
foreach ($rows as $row) {
|
||||
$adminId = intval($row['admin_id'] ?? 0);
|
||||
$amount = strval($row['commission_amount'] ?? '0.00');
|
||||
$netAmount = strval($row['net_commission_amount'] ?? '0.00');
|
||||
if ($adminId <= 0) {
|
||||
continue;
|
||||
}
|
||||
unset($row['net_commission_amount']);
|
||||
$row['status'] = 1;
|
||||
$row['settled_at'] = $now;
|
||||
$row['remark'] = strval($row['remark'] ?? '') . ' | 超管结算直接发放';
|
||||
$row['update_time'] = $now;
|
||||
$commissionRecordId = intval(Db::name('agent_commission_record')->insertGetId($row));
|
||||
if (bccomp($amount, '0.00', 2) > 0) {
|
||||
if (bccomp($netAmount, '0.00', 2) > 0) {
|
||||
AdminWalletService::creditCommission(
|
||||
$adminId,
|
||||
$channelId,
|
||||
$amount,
|
||||
$netAmount,
|
||||
'agent_commission_record',
|
||||
$commissionRecordId,
|
||||
$remark !== '' ? $remark : '超管结算自动发放分红',
|
||||
@@ -97,9 +109,16 @@ class ChannelSettlementService
|
||||
return ['ok' => false, 'msg' => __('This flow pays commissions automatically after super admin settlement; channel admin does not need to settle again')];
|
||||
}
|
||||
|
||||
public static function settleAllDueChannels(int $operatorAdminId, bool $respectCycle = true): array
|
||||
public static function settleAllDueChannels(int $operatorAdminId, bool $respectCycle = true, ?array $channelIds = null): array
|
||||
{
|
||||
$channels = Db::name('channel')->where('status', 1)->select()->toArray();
|
||||
$query = Db::name('channel')->where('status', 1);
|
||||
if ($channelIds !== null) {
|
||||
if ($channelIds === []) {
|
||||
return ['ok_count' => 0, 'failed' => []];
|
||||
}
|
||||
$query->whereIn('id', $channelIds);
|
||||
}
|
||||
$channels = $query->select()->toArray();
|
||||
$ok = 0;
|
||||
$failed = [];
|
||||
$now = time();
|
||||
@@ -191,10 +210,12 @@ class ChannelSettlementService
|
||||
'calc_base_amount' => $commission['calc_base_amount'],
|
||||
'commission_amount' => $commission['commission_amount'],
|
||||
'agent_mode' => $mode,
|
||||
'settlement_handling_fee' => bcadd(strval($row['settlement_handling_fee'] ?? '0'), '0', 2),
|
||||
'commission_split' => AdminCommissionDistributionService::buildSplitPreview(
|
||||
$channelId,
|
||||
$commission['commission_amount'],
|
||||
$commission['calc_base_amount']
|
||||
$commission['calc_base_amount'],
|
||||
bcadd(strval($row['settlement_handling_fee'] ?? '0'), '0', 2)
|
||||
),
|
||||
];
|
||||
}
|
||||
@@ -332,7 +353,8 @@ class ChannelSettlementService
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{admin_id:int,commission_amount:string,commission_rate:string,calc_base_amount:string}> $distributions
|
||||
* @param array<int, array{admin_id:int,commission_amount:string,commission_rate:string,calc_base_amount:string,handling_fee:string,net_commission_amount:string}> $distributions
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private static function buildCommissionRowsFromDistribution(array $distributions, int $channelId, int $periodId, string $remark, int $now): array
|
||||
{
|
||||
@@ -350,6 +372,8 @@ class ChannelSettlementService
|
||||
'commission_rate' => strval($dist['commission_rate'] ?? '0.0000'),
|
||||
'calc_base_amount' => strval($dist['calc_base_amount'] ?? '0.00'),
|
||||
'commission_amount' => $amount,
|
||||
'handling_fee' => strval($dist['handling_fee'] ?? '0.00'),
|
||||
'net_commission_amount' => strval($dist['net_commission_amount'] ?? '0.00'),
|
||||
'status' => 0,
|
||||
'settled_at' => null,
|
||||
'remark' => $remark . ' | 树形分红实发',
|
||||
|
||||
Reference in New Issue
Block a user