优化分红方式
This commit is contained in:
@@ -16,7 +16,7 @@ class Channel extends Backend
|
||||
/**
|
||||
* 预览接口与手动结算共用「手动结算」按钮权限(避免额外菜单节点)
|
||||
*/
|
||||
protected array $noNeedPermission = ['manualSettlePreview'];
|
||||
protected array $noNeedPermission = ['manualSettlePreview', 'channelAdminShareList', 'saveChannelAdminShare'];
|
||||
|
||||
/**
|
||||
* Channel模型对象
|
||||
@@ -322,6 +322,273 @@ class Channel extends Backend
|
||||
return $this->success('', $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渠道管理员分配比例列表(用于结算二次分配配置)
|
||||
*/
|
||||
public function channelAdminShareList(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
if (!$this->auth->check('channel/edit')) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
$id = (int) ($request->get('id', 0));
|
||||
if ($id <= 0) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$row = $this->model->find($id);
|
||||
if (!$row) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
if (!$this->auth->isSuperAdmin() && !in_array((int) $row['id'], $this->currentChannelIds, true)) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
|
||||
$adminRows = Db::name('admin')
|
||||
->field(['id', 'username', 'status'])
|
||||
->where('channel_id', (int) $row['id'])
|
||||
->order('id', 'asc')
|
||||
->select()
|
||||
->toArray();
|
||||
$shareRows = Db::name('channel_admin_share')
|
||||
->where('channel_id', (int) $row['id'])
|
||||
->column(['share_rate', 'status'], 'admin_id');
|
||||
|
||||
$adminIds = [];
|
||||
foreach ($adminRows as $adminRow) {
|
||||
$aid = (int) ($adminRow['id'] ?? 0);
|
||||
if ($aid > 0) {
|
||||
$adminIds[] = $aid;
|
||||
}
|
||||
}
|
||||
$adminIds = array_values(array_unique($adminIds));
|
||||
$roleMetaMap = $this->resolveAdminRoleMetaForChannel((int) $row['id'], $adminIds);
|
||||
|
||||
$list = [];
|
||||
foreach ($adminRows as $adminRow) {
|
||||
$aid = (int) ($adminRow['id'] ?? 0);
|
||||
if ($aid <= 0) {
|
||||
continue;
|
||||
}
|
||||
$saved = $shareRows[$aid] ?? null;
|
||||
$roleMeta = $roleMetaMap[$aid] ?? null;
|
||||
$list[] = [
|
||||
'admin_id' => $aid,
|
||||
'username' => (string) ($adminRow['username'] ?? ''),
|
||||
'role_group_name' => is_array($roleMeta) ? (string) ($roleMeta['role_group_name'] ?? '') : '',
|
||||
'role_level' => is_array($roleMeta) ? (int) ($roleMeta['role_level'] ?? 9999) : 9999,
|
||||
'admin_status' => (string) ($adminRow['status'] ?? ''),
|
||||
'share_rate' => $saved['share_rate'] ?? null,
|
||||
'status' => isset($saved['status']) ? (int) $saved['status'] : 1,
|
||||
];
|
||||
}
|
||||
usort($list, static function (array $a, array $b): int {
|
||||
$levelA = (int) ($a['role_level'] ?? 9999);
|
||||
$levelB = (int) ($b['role_level'] ?? 9999);
|
||||
if ($levelA !== $levelB) {
|
||||
return $levelA <=> $levelB;
|
||||
}
|
||||
$idA = (int) ($a['admin_id'] ?? 0);
|
||||
$idB = (int) ($b['admin_id'] ?? 0);
|
||||
return $idA <=> $idB;
|
||||
});
|
||||
|
||||
return $this->success('', [
|
||||
'channel_id' => (int) $row['id'],
|
||||
'channel_name' => (string) ($row['name'] ?? ''),
|
||||
'list' => $list,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, int> $adminIds
|
||||
* @return array<int, array{role_group_name:string,role_level:int}>
|
||||
*/
|
||||
private function resolveAdminRoleMetaForChannel(int $channelId, array $adminIds): array
|
||||
{
|
||||
if ($channelId <= 0 || $adminIds === []) {
|
||||
return [];
|
||||
}
|
||||
$groupRows = Db::name('admin_group')
|
||||
->field(['id', 'pid', 'name'])
|
||||
->where('channel_id', $channelId)
|
||||
->order('id', 'asc')
|
||||
->select()
|
||||
->toArray();
|
||||
if ($groupRows === []) {
|
||||
return [];
|
||||
}
|
||||
$groupMap = [];
|
||||
foreach ($groupRows as $groupRow) {
|
||||
$gid = (int) ($groupRow['id'] ?? 0);
|
||||
if ($gid <= 0) {
|
||||
continue;
|
||||
}
|
||||
$groupMap[$gid] = [
|
||||
'pid' => (int) ($groupRow['pid'] ?? 0),
|
||||
'name' => trim((string) ($groupRow['name'] ?? '')),
|
||||
];
|
||||
}
|
||||
if ($groupMap === []) {
|
||||
return [];
|
||||
}
|
||||
$groupDepthById = [];
|
||||
$calcDepth = function (int $groupId) use (&$calcDepth, &$groupDepthById, $groupMap): int {
|
||||
if (isset($groupDepthById[$groupId])) {
|
||||
return $groupDepthById[$groupId];
|
||||
}
|
||||
$current = $groupMap[$groupId] ?? null;
|
||||
if (!$current) {
|
||||
$groupDepthById[$groupId] = 9999;
|
||||
return 9999;
|
||||
}
|
||||
$pid = (int) ($current['pid'] ?? 0);
|
||||
if ($pid <= 0 || !isset($groupMap[$pid])) {
|
||||
$groupDepthById[$groupId] = 1;
|
||||
return 1;
|
||||
}
|
||||
$depth = $calcDepth($pid) + 1;
|
||||
$groupDepthById[$groupId] = $depth;
|
||||
return $depth;
|
||||
};
|
||||
|
||||
$accessRows = Db::name('admin_group_access')
|
||||
->field(['uid', 'group_id'])
|
||||
->where('uid', 'in', $adminIds)
|
||||
->order('uid', 'asc')
|
||||
->order('group_id', 'asc')
|
||||
->select()
|
||||
->toArray();
|
||||
$metaMap = [];
|
||||
foreach ($accessRows as $accessRow) {
|
||||
$uid = (int) ($accessRow['uid'] ?? 0);
|
||||
$groupId = (int) ($accessRow['group_id'] ?? 0);
|
||||
if ($uid <= 0 || $groupId <= 0 || !isset($groupMap[$groupId])) {
|
||||
continue;
|
||||
}
|
||||
$roleName = trim((string) ($groupMap[$groupId]['name'] ?? ''));
|
||||
if ($roleName === '') {
|
||||
continue;
|
||||
}
|
||||
$depth = $calcDepth($groupId);
|
||||
if (!isset($metaMap[$uid])) {
|
||||
$metaMap[$uid] = [
|
||||
'role_group_name' => $roleName,
|
||||
'role_level' => $depth,
|
||||
'group_id' => $groupId,
|
||||
];
|
||||
continue;
|
||||
}
|
||||
$currentDepth = (int) ($metaMap[$uid]['role_level'] ?? 9999);
|
||||
$currentGroupId = (int) ($metaMap[$uid]['group_id'] ?? 0);
|
||||
if ($depth < $currentDepth || ($depth === $currentDepth && $groupId < $currentGroupId)) {
|
||||
$metaMap[$uid]['role_group_name'] = $roleName;
|
||||
$metaMap[$uid]['role_level'] = $depth;
|
||||
$metaMap[$uid]['group_id'] = $groupId;
|
||||
}
|
||||
}
|
||||
$out = [];
|
||||
foreach ($metaMap as $uid => $meta) {
|
||||
$out[$uid] = [
|
||||
'role_group_name' => (string) ($meta['role_group_name'] ?? ''),
|
||||
'role_level' => (int) ($meta['role_level'] ?? 9999),
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存渠道管理员分配比例(启用项总和必须=100)
|
||||
*/
|
||||
public function saveChannelAdminShare(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
if (!$this->auth->check('channel/edit')) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
if ($request->method() !== 'POST') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$id = (int) ($request->post('id', 0));
|
||||
if ($id <= 0) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$row = $this->model->find($id);
|
||||
if (!$row) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
if (!$this->auth->isSuperAdmin() && !in_array((int) $row['id'], $this->currentChannelIds, true)) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
$rowsRaw = $request->post('list', []);
|
||||
if (!is_array($rowsRaw) || $rowsRaw === []) {
|
||||
return $this->error('请至少配置一条分配记录');
|
||||
}
|
||||
|
||||
$adminIds = Db::name('admin')
|
||||
->where('channel_id', (int) $row['id'])
|
||||
->column('id');
|
||||
$adminIdSet = [];
|
||||
foreach ($adminIds as $adminId) {
|
||||
$adminIdSet[(int) $adminId] = true;
|
||||
}
|
||||
if ($adminIdSet === []) {
|
||||
return $this->error('该渠道下暂无管理员,无法配置分配比例');
|
||||
}
|
||||
|
||||
$enabledSum = '0.0000';
|
||||
$insertRows = [];
|
||||
foreach ($rowsRaw as $line) {
|
||||
if (!is_array($line)) {
|
||||
continue;
|
||||
}
|
||||
$adminId = (int) ($line['admin_id'] ?? 0);
|
||||
if ($adminId <= 0 || !isset($adminIdSet[$adminId])) {
|
||||
continue;
|
||||
}
|
||||
$status = ((int) ($line['status'] ?? 1)) === 1 ? 1 : 0;
|
||||
$shareRaw = $line['share_rate'] ?? null;
|
||||
$shareRate = self::normalizeAmountScale($shareRaw === null ? '0' : (string) $shareRaw, 4);
|
||||
if (bccomp($shareRate, '0', 4) < 0 || bccomp($shareRate, '100', 4) > 0) {
|
||||
return $this->error('分配比例必须在0到100之间');
|
||||
}
|
||||
if ($status === 1) {
|
||||
$enabledSum = bcadd($enabledSum, $shareRate, 4);
|
||||
}
|
||||
$insertRows[] = [
|
||||
'channel_id' => (int) $row['id'],
|
||||
'admin_id' => $adminId,
|
||||
'share_rate' => $shareRate,
|
||||
'status' => $status,
|
||||
'create_time' => time(),
|
||||
'update_time' => time(),
|
||||
];
|
||||
}
|
||||
if ($insertRows === []) {
|
||||
return $this->error('请至少配置一条有效分配记录');
|
||||
}
|
||||
if (bccomp($enabledSum, '100.0000', 4) !== 0) {
|
||||
return $this->error('启用的分配比例总和必须等于100');
|
||||
}
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
Db::name('channel_admin_share')->where('channel_id', (int) $row['id'])->delete();
|
||||
Db::name('channel_admin_share')->insertAll($insertRows);
|
||||
Db::commit();
|
||||
} catch (Throwable $e) {
|
||||
Db::rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
return $this->success('分配比例保存成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动结算(渠道维度):仅接收备注;周期与金额全部由服务端按注单汇总计算,并写入结算周期与佣金记录
|
||||
*/
|
||||
@@ -356,9 +623,9 @@ class Channel extends Backend
|
||||
return $this->error('结算单号已存在,请稍后重试');
|
||||
}
|
||||
|
||||
$adminId = $this->resolveCommissionAdminIdForChannel((int) $row['id']);
|
||||
if ($adminId === null || $adminId <= 0) {
|
||||
return $this->error('渠道下无归属管理员账号(请为管理员设置所属渠道),无法生成佣金记录');
|
||||
$shareRows = $this->resolveCommissionSharesForChannel((int) $row['id']);
|
||||
if ($shareRows === []) {
|
||||
return $this->error('渠道下无可用管理员分配比例,无法生成佣金记录');
|
||||
}
|
||||
|
||||
$now = time();
|
||||
@@ -377,19 +644,19 @@ class Channel extends Backend
|
||||
'update_time' => $now,
|
||||
]);
|
||||
|
||||
Db::name('agent_commission_record')->insert([
|
||||
'settlement_period_id' => $periodId,
|
||||
'channel_id' => (int) $row['id'],
|
||||
'admin_id' => (int) $adminId,
|
||||
'commission_rate' => $payload['commission_rate'],
|
||||
'calc_base_amount' => $payload['calc_base_amount'],
|
||||
'commission_amount' => $payload['commission_amount'],
|
||||
'status' => 0,
|
||||
'settled_at' => null,
|
||||
'remark' => trim($remark) !== '' ? $remark : ('手动结算佣金-CH' . $row['id']),
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
$commissionRows = $this->buildCommissionRowsForSplit(
|
||||
$shareRows,
|
||||
(int) $row['id'],
|
||||
$periodId,
|
||||
(string) $payload['calc_base_amount'],
|
||||
(string) $payload['commission_amount'],
|
||||
trim($remark) !== '' ? $remark : ('手动结算佣金-CH' . $row['id']),
|
||||
$now
|
||||
);
|
||||
if ($commissionRows === []) {
|
||||
throw new \RuntimeException('分配比例拆分失败,未生成佣金记录');
|
||||
}
|
||||
Db::name('agent_commission_record')->insertAll($commissionRows);
|
||||
|
||||
Db::name('channel')->where('id', $row['id'])->update([
|
||||
'update_time' => $now,
|
||||
@@ -439,6 +706,9 @@ class Channel extends Backend
|
||||
|
||||
$settlementNo = $this->generateAgentSettlementNo('M', $channelId, $endTs);
|
||||
|
||||
$splitRows = $this->resolveCommissionSharesForChannel($channelId);
|
||||
$splitPreview = $this->buildCommissionSplitPreview($splitRows, $commission['commission_amount']);
|
||||
|
||||
return [
|
||||
'settlement_no' => $settlementNo,
|
||||
'period_start_ts' => $periodStartTs,
|
||||
@@ -452,6 +722,7 @@ class Channel extends Backend
|
||||
'calc_base_amount' => $commission['calc_base_amount'],
|
||||
'commission_amount' => $commission['commission_amount'],
|
||||
'agent_mode' => $mode,
|
||||
'commission_split' => $splitPreview,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -670,6 +941,140 @@ class Channel extends Backend
|
||||
return (int) $aid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{admin_id:int, share_rate:string}>
|
||||
*/
|
||||
private function resolveCommissionSharesForChannel(int $channelId): array
|
||||
{
|
||||
if ($channelId <= 0) {
|
||||
return [];
|
||||
}
|
||||
$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 !== []) {
|
||||
$sum = '0.0000';
|
||||
$out = [];
|
||||
foreach ($rows as $row) {
|
||||
$adminId = (int) ($row['admin_id'] ?? 0);
|
||||
if ($adminId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$shareRate = self::normalizeAmountScale((string) ($row['share_rate'] ?? '0'), 4);
|
||||
if (bccomp($shareRate, '0', 4) <= 0) {
|
||||
continue;
|
||||
}
|
||||
$sum = bcadd($sum, $shareRate, 4);
|
||||
$out[] = [
|
||||
'admin_id' => $adminId,
|
||||
'share_rate' => $shareRate,
|
||||
];
|
||||
}
|
||||
if ($out !== [] && bccomp($sum, '100.0000', 4) === 0) {
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
|
||||
$fallbackAdminId = $this->resolveCommissionAdminIdForChannel($channelId);
|
||||
if ($fallbackAdminId === null || $fallbackAdminId <= 0) {
|
||||
return [];
|
||||
}
|
||||
return [[
|
||||
'admin_id' => (int) $fallbackAdminId,
|
||||
'share_rate' => '100.0000',
|
||||
]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{admin_id:int, share_rate:string}> $shareRows
|
||||
* @return array<int, array{settlement_period_id:int, channel_id:int, admin_id:int, commission_rate:string, calc_base_amount:string, commission_amount:string, status:int, settled_at:null, remark:string, create_time:int, update_time:int}>
|
||||
*/
|
||||
private function buildCommissionRowsForSplit(
|
||||
array $shareRows,
|
||||
int $channelId,
|
||||
int $periodId,
|
||||
string $calcBaseAmount,
|
||||
string $commissionTotal,
|
||||
string $remark,
|
||||
int $now
|
||||
): array {
|
||||
if ($shareRows === []) {
|
||||
return [];
|
||||
}
|
||||
$sum = '0.0000';
|
||||
$rows = [];
|
||||
$lastIndex = count($shareRows) - 1;
|
||||
foreach ($shareRows as $index => $shareRow) {
|
||||
$shareRate = self::normalizeAmountScale((string) ($shareRow['share_rate'] ?? '0'), 4);
|
||||
$shareDec = bcdiv($shareRate, '100', 8);
|
||||
$amount = $index === $lastIndex
|
||||
? bcsub($commissionTotal, $sum, 4)
|
||||
: bcmul($commissionTotal, $shareDec, 4);
|
||||
if ($index !== $lastIndex) {
|
||||
$sum = bcadd($sum, $amount, 4);
|
||||
}
|
||||
$effectiveRate = bccomp($calcBaseAmount, '0', 4) <= 0 ? '0.000000' : bcdiv($amount, $calcBaseAmount, 6);
|
||||
$rows[] = [
|
||||
'settlement_period_id' => $periodId,
|
||||
'channel_id' => $channelId,
|
||||
'admin_id' => (int) ($shareRow['admin_id'] ?? 0),
|
||||
'commission_rate' => $effectiveRate,
|
||||
'calc_base_amount' => $calcBaseAmount,
|
||||
'commission_amount' => $amount,
|
||||
'status' => 0,
|
||||
'settled_at' => null,
|
||||
'remark' => $remark . ' | 分配比例=' . $shareRate . '%',
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
];
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{admin_id:int, share_rate:string}> $shareRows
|
||||
* @return array<int, array{admin_id:int, admin_username:string, share_rate:string, commission_amount:string}>
|
||||
*/
|
||||
private function buildCommissionSplitPreview(array $shareRows, string $commissionTotal): array
|
||||
{
|
||||
if ($shareRows === []) {
|
||||
return [];
|
||||
}
|
||||
$adminIds = [];
|
||||
foreach ($shareRows as $shareRow) {
|
||||
$adminIds[] = (int) ($shareRow['admin_id'] ?? 0);
|
||||
}
|
||||
$adminNames = Db::name('admin')->where('id', 'in', $adminIds)->column('username', 'id');
|
||||
|
||||
$sum = '0.0000';
|
||||
$out = [];
|
||||
$lastIndex = count($shareRows) - 1;
|
||||
foreach ($shareRows as $index => $shareRow) {
|
||||
$shareRate = self::normalizeAmountScale((string) ($shareRow['share_rate'] ?? '0'), 4);
|
||||
$shareDec = bcdiv($shareRate, '100', 8);
|
||||
$amount = $index === $lastIndex
|
||||
? bcsub($commissionTotal, $sum, 4)
|
||||
: bcmul($commissionTotal, $shareDec, 4);
|
||||
if ($index !== $lastIndex) {
|
||||
$sum = bcadd($sum, $amount, 4);
|
||||
}
|
||||
$adminId = (int) ($shareRow['admin_id'] ?? 0);
|
||||
$out[] = [
|
||||
'admin_id' => $adminId,
|
||||
'admin_username' => (string) ($adminNames[$adminId] ?? ('#' . $adminId)),
|
||||
'share_rate' => $shareRate,
|
||||
'commission_amount' => $amount,
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
private function normalizeAgentModeFields(array $data): array
|
||||
{
|
||||
$mode = $data['agent_mode'] ?? null;
|
||||
@@ -763,6 +1168,35 @@ class Channel extends Backend
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function normalizeAmountScale(string $amount, int $scale): string
|
||||
{
|
||||
$raw = trim(str_replace(',', '.', $amount));
|
||||
if ($raw === '') {
|
||||
return '0';
|
||||
}
|
||||
$negative = false;
|
||||
if (str_starts_with($raw, '-')) {
|
||||
$negative = true;
|
||||
$raw = ltrim(substr($raw, 1));
|
||||
}
|
||||
if (!str_contains($raw, '.')) {
|
||||
$v = ltrim($raw, '0');
|
||||
$v = $v === '' ? '0' : $v;
|
||||
return $negative ? ('-' . $v) : $v;
|
||||
}
|
||||
[$intPart, $fracPart] = explode('.', $raw, 2);
|
||||
$intPart = ltrim($intPart, '0');
|
||||
$intPart = $intPart === '' ? '0' : $intPart;
|
||||
$fracPart = preg_replace('/\D+/', '', $fracPart) ?? '';
|
||||
if (strlen($fracPart) > $scale) {
|
||||
$fracPart = substr($fracPart, 0, $scale);
|
||||
} else {
|
||||
$fracPart = str_pad($fracPart, $scale, '0');
|
||||
}
|
||||
$v = $intPart . '.' . $fracPart;
|
||||
return $negative ? ('-' . $v) : $v;
|
||||
}
|
||||
|
||||
private function validateLadderRulesField(array &$data): ?string
|
||||
{
|
||||
$rulesRaw = $data['affiliate_ladder_rules'] ?? null;
|
||||
|
||||
@@ -63,15 +63,6 @@ class Admin extends Backend
|
||||
->order($order)
|
||||
->paginate($limit);
|
||||
$items = $res->items();
|
||||
$topGroupUids = $this->getTopGroupUserMap(array_column($items, 'id'));
|
||||
foreach ($items as &$item) {
|
||||
$id = $item['id'] ?? null;
|
||||
if ($id === 1 || isset($topGroupUids[$id])) {
|
||||
$item['commission_rate'] = null;
|
||||
}
|
||||
}
|
||||
unset($item);
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $items,
|
||||
'total' => $res->total(),
|
||||
@@ -204,16 +195,6 @@ class Admin extends Backend
|
||||
$data['channel_id'] = ($groupChannelId === null || $groupChannelId === '') ? null : $groupChannelId;
|
||||
}
|
||||
$data['invite_code'] = $this->generateUniqueInviteCode();
|
||||
$requireCommissionRate = $this->requireCommissionRate($data['group_arr'] ?? []);
|
||||
if ($requireCommissionRate) {
|
||||
if (!$this->isValidCommissionRate($data['commission_rate'] ?? null)) {
|
||||
return $this->error(__('Please enter a valid commission rate for non-top role group'));
|
||||
}
|
||||
$commissionRes = $this->validateAdminCommissionByGroups($data['group_arr'] ?? [], floatval((string)$data['commission_rate']));
|
||||
if ($commissionRes !== null) return $commissionRes;
|
||||
} else {
|
||||
$data['commission_rate'] = null;
|
||||
}
|
||||
$result = false;
|
||||
if (!empty($data['group_arr'])) {
|
||||
$authRes = $this->checkGroupAuth($data['group_arr']);
|
||||
@@ -277,18 +258,12 @@ class Admin extends Backend
|
||||
return $this->error('请选择且仅选择一个角色组');
|
||||
}
|
||||
|
||||
// 未提交分红比例时,若角色组未变更则沿用数据库原值(避免表单单项 number 校验把空串判错)
|
||||
$postedGroups = array_map('intval', $data['group_arr'] ?? []);
|
||||
$rowGroups = array_map('intval', $row->group_arr ?? []);
|
||||
sort($postedGroups);
|
||||
sort($rowGroups);
|
||||
$sameGroups = $postedGroups === $rowGroups;
|
||||
$postedCommission = $data['commission_rate'] ?? null;
|
||||
if (($postedCommission === null || $postedCommission === '') && $sameGroups && $this->isValidCommissionRate($row['commission_rate'] ?? null)) {
|
||||
$data['commission_rate'] = $row['commission_rate'];
|
||||
}
|
||||
|
||||
// 当前管理员编辑自身时,不允许修改角色组和分红比
|
||||
// 当前管理员编辑自身时,不允许修改角色组
|
||||
if ((int)$this->auth->id === (int)$id) {
|
||||
$postedGroups = $data['group_arr'] ?? [];
|
||||
if (!is_array($postedGroups)) {
|
||||
@@ -297,9 +272,7 @@ class Admin extends Backend
|
||||
$originGroups = $row->group_arr ?? [];
|
||||
sort($postedGroups);
|
||||
sort($originGroups);
|
||||
$postedRate = $data['commission_rate'] ?? null;
|
||||
$originRate = $row['commission_rate'] ?? null;
|
||||
if ($postedGroups !== $originGroups || (string)$postedRate !== (string)$originRate) {
|
||||
if ($postedGroups !== $originGroups) {
|
||||
return $this->error(__('You cannot modify your own management group!'));
|
||||
}
|
||||
}
|
||||
@@ -367,16 +340,6 @@ class Admin extends Backend
|
||||
} else {
|
||||
$data['channel_id'] = ($groupChannelId === null || $groupChannelId === '') ? null : $groupChannelId;
|
||||
}
|
||||
$requireCommissionRate = $this->requireCommissionRate($data['group_arr'] ?? []);
|
||||
if ($requireCommissionRate) {
|
||||
if (!$this->isValidCommissionRate($data['commission_rate'] ?? null)) {
|
||||
return $this->error(__('Please enter a valid commission rate for non-top role group'));
|
||||
}
|
||||
$commissionRes = $this->validateAdminCommissionByGroups($data['group_arr'] ?? [], floatval((string)$data['commission_rate']), intval((string)$id));
|
||||
if ($commissionRes !== null) return $commissionRes;
|
||||
} else {
|
||||
$data['commission_rate'] = null;
|
||||
}
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
@@ -515,73 +478,6 @@ class Admin extends Backend
|
||||
return $code;
|
||||
}
|
||||
|
||||
private function requireCommissionRate(array $groupIds): bool
|
||||
{
|
||||
if (!$groupIds) {
|
||||
return false;
|
||||
}
|
||||
$count = Db::name('admin_group')
|
||||
->where('id', 'in', $groupIds)
|
||||
->where('pid', '<>', 0)
|
||||
->count();
|
||||
return $count > 0;
|
||||
}
|
||||
|
||||
private function isValidCommissionRate(mixed $value): bool
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return false;
|
||||
}
|
||||
$rate = trim((string)$value);
|
||||
if (!preg_match('/^(100(\.00?)?|[0-9]{1,2}(\.[0-9]{1,2})?)$/', $rate)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function validateAdminCommissionByGroups(array $groupIds, float $currentRate, ?int $excludeAdminId = null): ?Response
|
||||
{
|
||||
if (!$groupIds) {
|
||||
return null;
|
||||
}
|
||||
$groups = Db::name('admin_group')
|
||||
->where('id', 'in', $groupIds)
|
||||
->where('pid', '<>', 0)
|
||||
->column('name', 'id');
|
||||
foreach ($groups as $groupId => $groupName) {
|
||||
$query = Db::name('admin_group_access')->alias('aga')
|
||||
->join('admin a', 'aga.uid = a.id')
|
||||
->where('aga.group_id', intval((string)$groupId));
|
||||
if ($excludeAdminId !== null) {
|
||||
$query = $query->where('a.id', '<>', $excludeAdminId);
|
||||
}
|
||||
$sum = (float)$query->sum('a.commission_rate');
|
||||
$remaining = 100 - $sum;
|
||||
if ($currentRate > $remaining + 0.000001) {
|
||||
$exceed = $currentRate - $remaining;
|
||||
return $this->error(sprintf('角色组[%s]分红比例总和不能超过100%%,当前剩余 %.2f%%,本次超出 %.2f%%', $groupName, max(0, $remaining), $exceed));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getTopGroupUserMap(array $userIds): array
|
||||
{
|
||||
if (!$userIds) {
|
||||
return [];
|
||||
}
|
||||
$uids = Db::name('admin_group_access')->alias('aga')
|
||||
->join('admin_group ag', 'aga.group_id = ag.id')
|
||||
->where('aga.uid', 'in', $userIds)
|
||||
->where('ag.pid', 0)
|
||||
->column('aga.uid');
|
||||
$map = [];
|
||||
foreach ($uids as $uid) {
|
||||
$map[$uid] = true;
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
private function normalizeSingleGroup(array $data): array
|
||||
{
|
||||
if (!array_key_exists('group_arr', $data)) {
|
||||
|
||||
@@ -90,18 +90,6 @@ class Group extends Backend
|
||||
if ($inheritRes !== null) {
|
||||
return $inheritRes;
|
||||
}
|
||||
$shouldHandleCommissionRate = true;
|
||||
if ($shouldHandleCommissionRate) {
|
||||
if (!$this->isValidCommissionRate($data['commission_rate'] ?? null)) {
|
||||
return $this->error(__('Please enter the correct field', ['commission_rate']));
|
||||
}
|
||||
if ($pidInt !== 0) {
|
||||
$commissionRes = $this->validateSiblingCommissionRate($pidInt, floatval((string)$data['commission_rate']));
|
||||
if ($commissionRes !== null) return $commissionRes;
|
||||
}
|
||||
} else {
|
||||
$data['commission_rate'] = 0;
|
||||
}
|
||||
$rulesRes = $this->handleRules($data);
|
||||
if ($rulesRes instanceof Response) return $rulesRes;
|
||||
|
||||
@@ -173,18 +161,6 @@ class Group extends Backend
|
||||
if ($inheritRes !== null) {
|
||||
return $inheritRes;
|
||||
}
|
||||
$shouldHandleCommissionRate = true;
|
||||
if ($shouldHandleCommissionRate) {
|
||||
if (!$this->isValidCommissionRate($data['commission_rate'] ?? null)) {
|
||||
return $this->error(__('Please enter the correct field', ['commission_rate']));
|
||||
}
|
||||
if ($pidInt !== 0) {
|
||||
$commissionRes = $this->validateSiblingCommissionRate($pidInt, floatval((string)$data['commission_rate']), intval((string)$row['id']));
|
||||
if ($commissionRes !== null) return $commissionRes;
|
||||
}
|
||||
} else {
|
||||
$data['commission_rate'] = 0;
|
||||
}
|
||||
$rulesRes = $this->handleRules($data);
|
||||
if ($rulesRes instanceof Response) return $rulesRes;
|
||||
|
||||
@@ -443,33 +419,6 @@ class Group extends Backend
|
||||
return array_values(array_unique(array_merge($own, $children)));
|
||||
}
|
||||
|
||||
private function isValidCommissionRate(mixed $value): bool
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return false;
|
||||
}
|
||||
$rate = trim((string)$value);
|
||||
if (!preg_match('/^(100(\.00?)?|[0-9]{1,2}(\.[0-9]{1,2})?)$/', $rate)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function validateSiblingCommissionRate(int $pid, float $currentRate, ?int $excludeId = null): ?Response
|
||||
{
|
||||
$query = Db::name('admin_group')->where('pid', $pid);
|
||||
if ($excludeId !== null) {
|
||||
$query = $query->where('id', '<>', $excludeId);
|
||||
}
|
||||
$sum = (float)$query->sum('commission_rate');
|
||||
$remaining = 100 - $sum;
|
||||
if ($currentRate > $remaining + 0.000001) {
|
||||
$exceed = $currentRate - $remaining;
|
||||
return $this->error(sprintf('同一父级角色组分红比例总和不能超过100%%,当前父级剩余 %.2f%%,本次超出 %.2f%%', max(0, $remaining), $exceed));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 顶级角色组可选渠道;子级继承父级 channel_id(不信任客户端提交的子级 channel_id)。
|
||||
*
|
||||
|
||||
20
app/common/model/ChannelAdminShare.php
Normal file
20
app/common/model/ChannelAdminShare.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\think\Model;
|
||||
|
||||
class ChannelAdminShare extends Model
|
||||
{
|
||||
protected $name = 'channel_admin_share';
|
||||
|
||||
protected $autoWriteTimestamp = true;
|
||||
|
||||
protected $type = [
|
||||
'create_time' => 'integer',
|
||||
'update_time' => 'integer',
|
||||
'share_rate' => 'string',
|
||||
'status' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -6,11 +6,6 @@ export default {
|
||||
email: 'Email',
|
||||
mobile: 'Mobile Number',
|
||||
invite_code: 'Invite code',
|
||||
commission_rate: 'Commission rate(%)',
|
||||
commission_rate_desc_title: 'Admin commission notes',
|
||||
commission_rate_desc_1: 'Admin commission means this admin allocation ratio inside assigned group.',
|
||||
commission_rate_desc_2: 'Current admin commission = current group commission × current admin commission rate.',
|
||||
commission_rate_desc_3: 'Within same group, total admin commission rate cannot exceed 100%; exceed and remaining are returned on validation.',
|
||||
'Please select exactly one group': 'Please select exactly one group',
|
||||
'Last login': 'Last login',
|
||||
Password: 'Password',
|
||||
|
||||
@@ -8,11 +8,6 @@ export default {
|
||||
channel_inherit_hint:
|
||||
'Sub groups do not pick a channel separately: saving uses the parent group channel; changing parent syncs automatically.',
|
||||
system_group_no_channel: 'System (no channel)',
|
||||
commission_rate: 'Commission rate (%)',
|
||||
commission_rate_desc_title: 'Group commission notes',
|
||||
commission_rate_desc_1: 'The total group commission rate under the same parent cannot exceed 100%.',
|
||||
commission_rate_desc_2: 'Current group commission = channel commission × (1 - parent group commission rate) × current group commission rate.',
|
||||
commission_rate_desc_3: 'If exceeded, the system returns both exceeded value and remaining quota under current parent.',
|
||||
jurisdiction: 'Permissions',
|
||||
'Parent group': 'Superior group',
|
||||
'The parent group cannot be the group itself': 'The parent group cannot be the group itself',
|
||||
|
||||
@@ -71,8 +71,16 @@ export default {
|
||||
manual_settle_calc_base: 'Settlement base',
|
||||
manual_settle_commission_amount: 'Commission amount',
|
||||
manual_settle_remark: 'Remark',
|
||||
share_config: 'Share config',
|
||||
share_config_title: 'Channel admin share config',
|
||||
share_config_tip: 'Only enabled rows participate in settlement split, and enabled share total must equal 100%.',
|
||||
share_rate_percent: 'Share rate(%)',
|
||||
share_total_enabled: 'Enabled total',
|
||||
share_total_must_100: 'Enabled share total must equal 100%',
|
||||
admin_id_placeholder: 'Select an admin (within your permission scope)',
|
||||
admin__username: 'Person in charge',
|
||||
admin_group_names: 'Role group',
|
||||
admin_group_paths: 'Role hierarchy',
|
||||
create_time: 'create_time',
|
||||
update_time: 'update_time',
|
||||
'quick Search Fields': 'id,code,name',
|
||||
|
||||
@@ -6,11 +6,6 @@ export default {
|
||||
email: '电子邮箱',
|
||||
mobile: '手机号',
|
||||
invite_code: '邀请码',
|
||||
commission_rate: '分红比(%)',
|
||||
commission_rate_desc_title: '管理员分红说明',
|
||||
commission_rate_desc_1: '管理员分红用于该管理员在所属角色组内的分配比例。',
|
||||
commission_rate_desc_2: '当前管理员分红=当前角色分红×当前管理员分红比例。',
|
||||
commission_rate_desc_3: '同一角色组内,管理员分红比例总和不能超过100%;超额会提示超出值与剩余额度。',
|
||||
'Please select exactly one group': '请选择且仅选择一个角色组',
|
||||
'Last login': '最后登录',
|
||||
Password: '密码',
|
||||
|
||||
@@ -7,11 +7,6 @@ export default {
|
||||
channel_auto_bind: '将自动绑定为当前账号所属渠道',
|
||||
channel_inherit_hint: '子级不单独选渠道:保存时将使用上级分组对应渠道,变更上级时会自动同步。',
|
||||
system_group_no_channel: '系统级(未绑定渠道)',
|
||||
commission_rate: '分红比例(%)',
|
||||
commission_rate_desc_title: '角色组分红说明',
|
||||
commission_rate_desc_1: '同一父级下角色组分红比例总和不能超过100%。',
|
||||
commission_rate_desc_2: '当前角色分红=渠道设置获取分红×(1-上级角色分红比例)×当前角色分红比例。',
|
||||
commission_rate_desc_3: '提交超额时,系统会提示超出值与当前父级剩余额度。',
|
||||
jurisdiction: '权限',
|
||||
'Parent group': '上级分组',
|
||||
'The parent group cannot be the group itself': '上级分组不能是分组本身',
|
||||
|
||||
@@ -71,8 +71,16 @@ export default {
|
||||
manual_settle_calc_base: '结算基数',
|
||||
manual_settle_commission_amount: '佣金金额',
|
||||
manual_settle_remark: '备注',
|
||||
share_config: '分配比例',
|
||||
share_config_title: '渠道管理员分配比例',
|
||||
share_config_tip: '仅启用项参与结算拆分,且启用项占比总和必须等于100%。',
|
||||
share_rate_percent: '分配比例(%)',
|
||||
share_total_enabled: '启用项合计',
|
||||
share_total_must_100: '启用项分配比例总和必须等于100%',
|
||||
admin_id_placeholder: '请选择管理员(仅当前权限范围内)',
|
||||
admin__username: '负责人',
|
||||
admin_group_names: '角色组',
|
||||
admin_group_paths: '角色组层级',
|
||||
create_time: '创建时间',
|
||||
update_time: '修改时间',
|
||||
'quick Search Fields': 'ID、渠道标识、渠道名',
|
||||
|
||||
@@ -40,13 +40,6 @@ optButtons[1].display = (row) => {
|
||||
return row.id != adminInfo.id
|
||||
}
|
||||
|
||||
const formatRatePercent = (_row: any, _column: any, cellValue: number | string | null) => {
|
||||
if (cellValue === null || cellValue === undefined || cellValue === '') return '--'
|
||||
const num = Number(cellValue)
|
||||
if (Number.isNaN(num)) return '--'
|
||||
return `${num.toFixed(2)}%`
|
||||
}
|
||||
|
||||
const baTable = new baTableClass(
|
||||
new baTableApi('/admin/auth.Admin/'),
|
||||
{
|
||||
@@ -64,14 +57,6 @@ const baTable = new baTableClass(
|
||||
render: 'tags',
|
||||
},
|
||||
{ label: t('auth.admin.invite_code'), prop: 'invite_code', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
|
||||
{
|
||||
label: t('auth.admin.commission_rate'),
|
||||
prop: 'commission_rate',
|
||||
align: 'center',
|
||||
minWidth: 90,
|
||||
operator: 'RANGE',
|
||||
formatter: formatRatePercent,
|
||||
},
|
||||
{ label: t('auth.admin.avatar'), prop: 'avatar', align: 'center', render: 'image', operator: false },
|
||||
{ label: t('auth.admin.email'), prop: 'email', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
|
||||
{ label: t('auth.admin.mobile'), prop: 'mobile', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
|
||||
|
||||
@@ -56,23 +56,6 @@
|
||||
placeholder: t('Click select'),
|
||||
}"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('auth.admin.commission_rate')"
|
||||
v-model="baTable.form.items!.commission_rate"
|
||||
type="number"
|
||||
prop="commission_rate"
|
||||
:input-attr="{ step: 0.01, precision: 2, min: 0, max: 100, disabled: shouldDisableCommissionRate() }"
|
||||
:placeholder="t('Please input field', { field: t('auth.admin.commission_rate') })"
|
||||
/>
|
||||
<el-alert class="commission-rate-alert" :title="t('auth.admin.commission_rate_desc_title')" type="info" :closable="false" show-icon>
|
||||
<template #default>
|
||||
<ul class="commission-rate-desc-list">
|
||||
<li>{{ t('auth.admin.commission_rate_desc_1') }}</li>
|
||||
<li>{{ t('auth.admin.commission_rate_desc_2') }}</li>
|
||||
<li>{{ t('auth.admin.commission_rate_desc_3') }}</li>
|
||||
</ul>
|
||||
</template>
|
||||
</el-alert>
|
||||
<FormItem
|
||||
v-if="baTable.form.operate == 'Edit'"
|
||||
:label="t('auth.admin.invite_code')"
|
||||
@@ -156,32 +139,6 @@ const baTable = inject('baTable') as baTableClass
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
/** 解析管理员分红比例:与后端 isValidCommissionRate 一致地拒绝非法字符,避免 Number('30,00') 等为 NaN 导致误报 */
|
||||
function parseAdminCommissionRateInput(raw: unknown): { kind: 'empty' } | { kind: 'invalid' } | { kind: 'ok'; value: number } {
|
||||
if (raw === null || raw === undefined || raw === '') {
|
||||
return { kind: 'empty' }
|
||||
}
|
||||
if (typeof raw === 'number') {
|
||||
if (!Number.isFinite(raw)) {
|
||||
return { kind: 'invalid' }
|
||||
}
|
||||
return { kind: 'ok', value: raw }
|
||||
}
|
||||
const s = String(raw).trim()
|
||||
if (s === '') {
|
||||
return { kind: 'empty' }
|
||||
}
|
||||
const normalized = s.replace(',', '.')
|
||||
const n = parseFloat(normalized)
|
||||
if (!Number.isFinite(n)) {
|
||||
return { kind: 'invalid' }
|
||||
}
|
||||
return { kind: 'ok', value: n }
|
||||
}
|
||||
|
||||
const shouldDisableCommissionRate = () => {
|
||||
return adminInfo.id == baTable.form.items!.id
|
||||
}
|
||||
const singleGroupValue = computed({
|
||||
get: () => {
|
||||
const group = baTable.form.items?.group_arr
|
||||
@@ -241,26 +198,6 @@ const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
commission_rate: [
|
||||
{
|
||||
validator: (_rule: unknown, val: unknown, callback: (e?: Error) => void) => {
|
||||
const parsed = parseAdminCommissionRateInput(val)
|
||||
if (parsed.kind === 'empty') {
|
||||
return callback()
|
||||
}
|
||||
if (parsed.kind === 'invalid') {
|
||||
return callback(new Error(t('Please enter the correct field', { field: t('auth.admin.commission_rate') })))
|
||||
}
|
||||
const n = parsed.value
|
||||
const rounded = Math.round(n * 100) / 100
|
||||
if (rounded < -0.000001 || rounded > 100.000001) {
|
||||
return callback(new Error(t('Please enter the correct field', { field: t('auth.admin.commission_rate') })))
|
||||
}
|
||||
return callback()
|
||||
},
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
watch(
|
||||
@@ -285,14 +222,6 @@ watch(
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
}
|
||||
.commission-rate-alert {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.commission-rate-desc-list {
|
||||
margin: 6px 0 0;
|
||||
padding-left: 18px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.avatar-uploader:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
@@ -66,7 +66,6 @@ const baTable: baTableClass = new baTableClass(
|
||||
align: 'center',
|
||||
minWidth: '140',
|
||||
},
|
||||
{ label: t('auth.group.commission_rate'), prop: 'commission_rate', align: 'center', formatter: formatRatePercent },
|
||||
{ label: t('auth.group.jurisdiction'), prop: 'rules', align: 'center' },
|
||||
{
|
||||
label: t('State'),
|
||||
@@ -199,13 +198,6 @@ const menuRuleTreeUpdate = () => {
|
||||
|
||||
provide('baTable', baTable)
|
||||
|
||||
function formatRatePercent(row: anyObj, _column: any, cellValue: number | string | null) {
|
||||
if (cellValue === null || cellValue === undefined || cellValue === '') {
|
||||
return '0%'
|
||||
}
|
||||
return `${cellValue}%`
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
baTable.table.ref = tableRef.value
|
||||
baTable.mount()
|
||||
|
||||
@@ -85,23 +85,6 @@
|
||||
:placeholder="t('Please input field', { field: t('auth.group.Group name') })"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<FormItem
|
||||
:label="t('auth.group.commission_rate')"
|
||||
v-model="baTable.form.items!.commission_rate"
|
||||
type="number"
|
||||
prop="commission_rate"
|
||||
:input-attr="{ step: 0.01, precision: 2, min: 0, max: 100, disabled: shouldDisableCommissionRate() }"
|
||||
:placeholder="t('Please input field', { field: t('auth.group.commission_rate') })"
|
||||
/>
|
||||
<el-alert class="commission-rate-alert" :title="t('auth.group.commission_rate_desc_title')" type="info" :closable="false" show-icon>
|
||||
<template #default>
|
||||
<ul class="commission-rate-desc-list">
|
||||
<li>{{ t('auth.group.commission_rate_desc_1') }}</li>
|
||||
<li>{{ t('auth.group.commission_rate_desc_2') }}</li>
|
||||
<li>{{ t('auth.group.commission_rate_desc_3') }}</li>
|
||||
</ul>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-form-item prop="auth" :label="t('auth.group.jurisdiction')">
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
@@ -174,10 +157,6 @@ const strFromRow = (key: string): string => {
|
||||
|
||||
const channelPreviewName = computed(() => strFromRow('channel_name'))
|
||||
|
||||
const shouldDisableCommissionRate = () => {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 子角色组:选择上级分组后,只拉取展示用渠道名;channel_id 由后端按父级保存,不在此写入提交字段。
|
||||
*/
|
||||
@@ -253,25 +232,6 @@ watch(
|
||||
|
||||
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
||||
name: [buildValidatorData({ name: 'required', title: t('auth.group.Group name') })],
|
||||
commission_rate: [
|
||||
{
|
||||
required: true,
|
||||
validator: (_rule: any, val: number | string, callback: Function) => {
|
||||
if (shouldDisableCommissionRate()) {
|
||||
return callback()
|
||||
}
|
||||
const strVal = String(val ?? '').trim()
|
||||
if (!strVal) {
|
||||
return callback(new Error(t('Please input field', { field: t('auth.group.commission_rate') })))
|
||||
}
|
||||
if (!/^(100(\.00?)?|[0-9]{1,2}(\.[0-9]{1,2})?)$/.test(strVal)) {
|
||||
return callback(new Error(t('auth.admin.Commission rate must be between 0 and 100 with up to 2 decimals')))
|
||||
}
|
||||
return callback()
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
auth: [
|
||||
{
|
||||
required: true,
|
||||
@@ -329,16 +289,6 @@ defineExpose({
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.commission-rate-alert {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.commission-rate-desc-list {
|
||||
margin: 6px 0 0;
|
||||
padding-left: 18px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
:deep(.penultimate-node) {
|
||||
.el-tree-node__children {
|
||||
padding-left: 60px;
|
||||
|
||||
@@ -44,6 +44,15 @@
|
||||
<el-form-item :label="t('channel.manual_settle_commission_amount')">
|
||||
<el-input v-model="manualSettle.form.commission_amount" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('channel.share_config')">
|
||||
<el-table :data="manualSettle.form.commission_split" border size="small" class="w100">
|
||||
<el-table-column prop="admin_username" :label="t('channel.admin__username')" min-width="100" />
|
||||
<el-table-column prop="share_rate" :label="t('channel.share_rate_percent')" min-width="90">
|
||||
<template #default="scope">{{ scope.row.share_rate }}%</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="commission_amount" :label="t('channel.manual_settle_commission_amount')" min-width="110" />
|
||||
</el-table>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('channel.manual_settle_remark')">
|
||||
<el-input v-model="manualSettle.form.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
@@ -56,12 +65,59 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog class="ba-operate-dialog" :close-on-click-modal="false" :model-value="shareDialog.visible" @close="closeShareDialog">
|
||||
<template #header>
|
||||
<div class="title">{{ t('channel.share_config_title') }}</div>
|
||||
</template>
|
||||
<div v-loading="shareDialog.loading" class="manual-settle-dialog-body">
|
||||
<el-alert type="info" :closable="false" show-icon class="mb-12">
|
||||
{{ t('channel.share_config_tip') }}
|
||||
</el-alert>
|
||||
<el-table :data="shareDialog.list" border size="small">
|
||||
<el-table-column :label="t('channel.admin_group_names')" min-width="260">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.role_group_name" class="share-group-single">{{ scope.row.role_group_name }}</span>
|
||||
<span v-else class="share-group-empty">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="username" :label="t('channel.admin__username')" min-width="120" />
|
||||
<el-table-column :label="t('channel.status')" width="120">
|
||||
<template #default="scope">
|
||||
<el-switch v-model="scope.row.status" :active-value="1" :inactive-value="0" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('channel.share_rate_percent')" min-width="180">
|
||||
<template #default="scope">
|
||||
<el-input-number
|
||||
v-model="scope.row.share_rate"
|
||||
:disabled="scope.row.status !== 1"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="0.01"
|
||||
:precision="2"
|
||||
class="w100"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="share-total-row">
|
||||
<span>{{ t('channel.share_total_enabled') }}: </span>
|
||||
<el-tag :type="shareEnabledTotal === '100.00' ? 'success' : 'danger'">{{ shareEnabledTotal }}%</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="closeShareDialog">{{ t('Cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="shareDialog.saving" @click="saveShareDialog">{{ t('Save') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, provide, reactive, useTemplateRef } from 'vue'
|
||||
import { computed, onMounted, provide, reactive, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import PopupForm from './popupForm.vue'
|
||||
import { baTableApi } from '/@/api/common'
|
||||
import { auth } from '/@/utils/common'
|
||||
@@ -78,6 +134,20 @@ defineOptions({
|
||||
const { t } = useI18n()
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
let optButtons: OptButton[] = [
|
||||
{
|
||||
render: 'tipButton',
|
||||
name: 'shareConfig',
|
||||
title: 'channel.share_config',
|
||||
text: '',
|
||||
type: 'primary',
|
||||
icon: 'el-icon-Setting',
|
||||
class: 'table-row-share-config',
|
||||
disabledTip: false,
|
||||
display: () => auth('edit'),
|
||||
click: (row: TableRow) => {
|
||||
void openShareDialog(row)
|
||||
},
|
||||
},
|
||||
{
|
||||
render: 'tipButton',
|
||||
name: 'manualSettle',
|
||||
@@ -94,12 +164,6 @@ let optButtons: OptButton[] = [
|
||||
},
|
||||
]
|
||||
optButtons = optButtons.concat(defaultOptButtons(['edit', 'delete']))
|
||||
const formatRatePercent = (_row: any, _column: any, cellValue: number | string | null) => {
|
||||
if (cellValue === null || cellValue === undefined || cellValue === '') return '-'
|
||||
const num = Number(cellValue)
|
||||
if (Number.isNaN(num)) return '-'
|
||||
return `${num.toFixed(2)}%`
|
||||
}
|
||||
const formatAmountInt = (_row: any, _column: any, cellValue: number | string | null) => {
|
||||
if (cellValue === null || cellValue === undefined || cellValue === '') return '-'
|
||||
const num = Number(cellValue)
|
||||
@@ -131,10 +195,103 @@ const manualSettle = reactive({
|
||||
commission_rate: '',
|
||||
calc_base_amount: '',
|
||||
commission_amount: '',
|
||||
commission_split: [] as Array<{ admin_id: number; admin_username: string; share_rate: string; commission_amount: string }>,
|
||||
remark: '',
|
||||
},
|
||||
})
|
||||
|
||||
const shareDialog = reactive({
|
||||
visible: false,
|
||||
loading: false,
|
||||
saving: false,
|
||||
channelId: 0,
|
||||
list: [] as Array<{ admin_id: number; username: string; role_group_name: string; role_level: number; status: number; share_rate: number | null }>,
|
||||
})
|
||||
|
||||
const shareEnabledTotal = computed(() => {
|
||||
let sum = 0
|
||||
for (const row of shareDialog.list) {
|
||||
if (row.status === 1) {
|
||||
const n = Number(row.share_rate ?? 0)
|
||||
if (Number.isFinite(n)) {
|
||||
sum += n
|
||||
}
|
||||
}
|
||||
}
|
||||
return sum.toFixed(2)
|
||||
})
|
||||
|
||||
const closeShareDialog = () => {
|
||||
shareDialog.visible = false
|
||||
shareDialog.channelId = 0
|
||||
shareDialog.list = []
|
||||
}
|
||||
|
||||
const openShareDialog = async (row: TableRow) => {
|
||||
shareDialog.channelId = Number(row.id || 0)
|
||||
shareDialog.visible = true
|
||||
shareDialog.loading = true
|
||||
try {
|
||||
const res = await createAxios(
|
||||
{
|
||||
url: '/admin/channel/channelAdminShareList',
|
||||
method: 'get',
|
||||
params: { id: row.id },
|
||||
},
|
||||
{ showErrorMessage: true }
|
||||
)
|
||||
if (res.code !== 1 || !res.data) {
|
||||
closeShareDialog()
|
||||
return
|
||||
}
|
||||
const list = Array.isArray(res.data.list) ? res.data.list : []
|
||||
shareDialog.list = list.map((item: anyObj) => {
|
||||
const rate = item.share_rate
|
||||
return {
|
||||
admin_id: Number(item.admin_id || 0),
|
||||
username: String(item.username || ''),
|
||||
role_group_name: String(item.role_group_name || ''),
|
||||
role_level: Number(item.role_level ?? 9999),
|
||||
status: Number(item.status ?? 1) === 1 ? 1 : 0,
|
||||
share_rate: rate === null || rate === undefined || rate === '' ? null : Number(rate),
|
||||
}
|
||||
})
|
||||
} finally {
|
||||
shareDialog.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
const saveShareDialog = async () => {
|
||||
if (!shareDialog.channelId) {
|
||||
return
|
||||
}
|
||||
if (shareEnabledTotal.value !== '100.00') {
|
||||
ElMessage.error(t('channel.share_total_must_100'))
|
||||
return
|
||||
}
|
||||
shareDialog.saving = true
|
||||
try {
|
||||
await createAxios(
|
||||
{
|
||||
url: '/admin/channel/saveChannelAdminShare',
|
||||
method: 'post',
|
||||
data: {
|
||||
id: shareDialog.channelId,
|
||||
list: shareDialog.list.map((row) => ({
|
||||
admin_id: row.admin_id,
|
||||
status: row.status,
|
||||
share_rate: Number(row.share_rate || 0).toFixed(2),
|
||||
})),
|
||||
},
|
||||
},
|
||||
{ showSuccessMessage: true }
|
||||
)
|
||||
closeShareDialog()
|
||||
} finally {
|
||||
shareDialog.saving = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetManualSettleForm = () => {
|
||||
manualSettle.form.settlement_no = ''
|
||||
manualSettle.form.period_start_at = ''
|
||||
@@ -145,6 +302,7 @@ const resetManualSettleForm = () => {
|
||||
manualSettle.form.commission_rate = ''
|
||||
manualSettle.form.calc_base_amount = ''
|
||||
manualSettle.form.commission_amount = ''
|
||||
manualSettle.form.commission_split = []
|
||||
manualSettle.form.remark = ''
|
||||
}
|
||||
|
||||
@@ -181,6 +339,7 @@ const openManualSettleDialog = async (row: TableRow) => {
|
||||
manualSettle.form.commission_rate = d.commission_rate ?? ''
|
||||
manualSettle.form.calc_base_amount = d.calc_base_amount ?? ''
|
||||
manualSettle.form.commission_amount = d.commission_amount ?? ''
|
||||
manualSettle.form.commission_split = Array.isArray(d.commission_split) ? d.commission_split : []
|
||||
manualSettle.form.remark = `${t('channel.manual_settle')}-CH${row.id}`
|
||||
} catch {
|
||||
manualSettle.visible = false
|
||||
@@ -251,33 +410,6 @@ const baTable = new baTableClass(
|
||||
affiliate: t('channel.agent_mode affiliate'),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('channel.turnover_share_rate'),
|
||||
prop: 'turnover_share_rate',
|
||||
align: 'center',
|
||||
minWidth: 110,
|
||||
sortable: false,
|
||||
operator: 'RANGE',
|
||||
formatter: formatRatePercent,
|
||||
},
|
||||
{
|
||||
label: t('channel.affiliate_share_rate'),
|
||||
prop: 'affiliate_share_rate',
|
||||
align: 'center',
|
||||
minWidth: 110,
|
||||
sortable: false,
|
||||
operator: 'RANGE',
|
||||
formatter: formatRatePercent,
|
||||
},
|
||||
{
|
||||
label: t('channel.affiliate_fee_rate'),
|
||||
prop: 'affiliate_fee_rate',
|
||||
align: 'center',
|
||||
minWidth: 140,
|
||||
sortable: false,
|
||||
operator: 'RANGE',
|
||||
formatter: formatRatePercent,
|
||||
},
|
||||
{
|
||||
label: t('channel.carryover_balance'),
|
||||
prop: 'carryover_balance',
|
||||
@@ -444,4 +576,25 @@ onMounted(() => {
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
<style scoped lang="scss">
|
||||
.mb-12 {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.share-total-row {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.share-group-single {
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.share-group-empty {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user