diff --git a/app/admin/controller/Channel.php b/app/admin/controller/Channel.php index ee1743a..c64d73c 100644 --- a/app/admin/controller/Channel.php +++ b/app/admin/controller/Channel.php @@ -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 $adminIds + * @return array + */ + 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 + */ + 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 $shareRows + * @return array + */ + 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 $shareRows + * @return array + */ + 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; diff --git a/app/admin/controller/auth/Admin.php b/app/admin/controller/auth/Admin.php index e418290..77591e9 100644 --- a/app/admin/controller/auth/Admin.php +++ b/app/admin/controller/auth/Admin.php @@ -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)) { diff --git a/app/admin/controller/auth/Group.php b/app/admin/controller/auth/Group.php index f0f1adb..11cf600 100644 --- a/app/admin/controller/auth/Group.php +++ b/app/admin/controller/auth/Group.php @@ -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)。 * diff --git a/app/common/model/ChannelAdminShare.php b/app/common/model/ChannelAdminShare.php new file mode 100644 index 0000000..b19266d --- /dev/null +++ b/app/common/model/ChannelAdminShare.php @@ -0,0 +1,20 @@ + 'integer', + 'update_time' => 'integer', + 'share_rate' => 'string', + 'status' => 'integer', + ]; +} + diff --git a/web/src/lang/backend/en/auth/admin.ts b/web/src/lang/backend/en/auth/admin.ts index d2e08a1..ce648ed 100644 --- a/web/src/lang/backend/en/auth/admin.ts +++ b/web/src/lang/backend/en/auth/admin.ts @@ -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', diff --git a/web/src/lang/backend/en/auth/group.ts b/web/src/lang/backend/en/auth/group.ts index 3166b51..1ade0ea 100644 --- a/web/src/lang/backend/en/auth/group.ts +++ b/web/src/lang/backend/en/auth/group.ts @@ -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', diff --git a/web/src/lang/backend/en/channel.ts b/web/src/lang/backend/en/channel.ts index 56715c7..51b2994 100644 --- a/web/src/lang/backend/en/channel.ts +++ b/web/src/lang/backend/en/channel.ts @@ -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', diff --git a/web/src/lang/backend/zh-cn/auth/admin.ts b/web/src/lang/backend/zh-cn/auth/admin.ts index 52d261c..3c3404b 100644 --- a/web/src/lang/backend/zh-cn/auth/admin.ts +++ b/web/src/lang/backend/zh-cn/auth/admin.ts @@ -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: '密码', diff --git a/web/src/lang/backend/zh-cn/auth/group.ts b/web/src/lang/backend/zh-cn/auth/group.ts index 5a2c16c..721303e 100644 --- a/web/src/lang/backend/zh-cn/auth/group.ts +++ b/web/src/lang/backend/zh-cn/auth/group.ts @@ -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': '上级分组不能是分组本身', diff --git a/web/src/lang/backend/zh-cn/channel.ts b/web/src/lang/backend/zh-cn/channel.ts index 518003c..0e91ef5 100644 --- a/web/src/lang/backend/zh-cn/channel.ts +++ b/web/src/lang/backend/zh-cn/channel.ts @@ -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、渠道标识、渠道名', diff --git a/web/src/views/backend/auth/admin/index.vue b/web/src/views/backend/auth/admin/index.vue index b2e7ad5..8b6c30c 100644 --- a/web/src/views/backend/auth/admin/index.vue +++ b/web/src/views/backend/auth/admin/index.vue @@ -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') }, diff --git a/web/src/views/backend/auth/admin/popupForm.vue b/web/src/views/backend/auth/admin/popupForm.vue index 46c52e6..b7405b7 100644 --- a/web/src/views/backend/auth/admin/popupForm.vue +++ b/web/src/views/backend/auth/admin/popupForm.vue @@ -56,23 +56,6 @@ placeholder: t('Click select'), }" /> - - - - { - 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> = 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); } diff --git a/web/src/views/backend/auth/group/index.vue b/web/src/views/backend/auth/group/index.vue index 6f4bf9c..8939e4f 100644 --- a/web/src/views/backend/auth/group/index.vue +++ b/web/src/views/backend/auth/group/index.vue @@ -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() diff --git a/web/src/views/backend/auth/group/popupForm.vue b/web/src/views/backend/auth/group/popupForm.vue index 99772b2..15d1e7a 100644 --- a/web/src/views/backend/auth/group/popupForm.vue +++ b/web/src/views/backend/auth/group/popupForm.vue @@ -85,23 +85,6 @@ :placeholder="t('Please input field', { field: t('auth.group.Group name') })" > - - - - { const channelPreviewName = computed(() => strFromRow('channel_name')) -const shouldDisableCommissionRate = () => { - return false -} - /** * 子角色组:选择上级分组后,只拉取展示用渠道名;channel_id 由后端按父级保存,不在此写入提交字段。 */ @@ -253,25 +232,6 @@ watch( const rules: Partial> = 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; diff --git a/web/src/views/backend/channel/index.vue b/web/src/views/backend/channel/index.vue index 01fa681..44f9dfa 100644 --- a/web/src/views/backend/channel/index.vue +++ b/web/src/views/backend/channel/index.vue @@ -44,6 +44,15 @@ + + + + + + + + + @@ -56,12 +65,59 @@ + + + +
+ + {{ t('channel.share_config_tip') }} + + + + + + + + + + + + + + +
+ +
- +