1.优化充值跳转链接的问题

2.优化后台渠道管理页面的显示样式
This commit is contained in:
2026-05-30 14:37:46 +08:00
parent 15b9313c07
commit 1cdd597879
19 changed files with 1096 additions and 180 deletions

View File

@@ -477,7 +477,7 @@ class Backend extends Api
}
/**
* 全平台只读范围:角色组均未绑定渠道,或拥有查看所有渠道权限,或超管
* 全平台只读范围:超管,或未绑定渠道且拥有渠道模块基础权限
*/
protected function hasGlobalReadScope(): bool
{

View File

@@ -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',

View File

@@ -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' => '联营阶梯规则不能为空',

View File

@@ -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 过滤时使用的 IDnull=不过滤
*

View File

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

View File

@@ -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 . ' | 树形分红实发',