diff --git a/app/admin/controller/auth/Group.php b/app/admin/controller/auth/Group.php index 26cb0ec..4bd22ca 100644 --- a/app/admin/controller/auth/Group.php +++ b/app/admin/controller/auth/Group.php @@ -34,6 +34,7 @@ class Group extends Backend protected bool $assembleTree = true; protected array $adminGroups = []; + protected array $manageableGroupIds = []; protected function initController(Request $request): ?Response { @@ -48,6 +49,7 @@ class Group extends Backend $this->assembleTree = $isTree && !$this->initValue; $this->adminGroups = Db::name('admin_group_access')->where('uid', $this->auth->id)->column('group_id'); + $this->manageableGroupIds = $this->getManageableGroupIds(); return null; } @@ -79,6 +81,23 @@ class Group extends Backend } $data = $this->excludeFields($data); + $pid = $data['pid'] ?? 0; + $pidInt = intval((string)$pid); + if (!$this->auth->isSuperAdmin() && $pidInt !== 0 && !in_array($pidInt, $this->manageableGroupIds, true)) { + return $this->error(__('You have no permission')); + } + $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; @@ -141,6 +160,23 @@ class Group extends Backend } $data = $this->excludeFields($data); + $pid = $data['pid'] ?? $row['pid'] ?? 0; + $pidInt = intval((string)$pid); + if (!$this->auth->isSuperAdmin() && $pidInt !== 0 && !in_array($pidInt, $this->manageableGroupIds, true)) { + return $this->error(__('You have no permission')); + } + $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; @@ -308,11 +344,12 @@ class Group extends Backend } if (!$this->auth->isSuperAdmin()) { - $authGroups = $this->auth->getAllAuthGroups($this->authMethod, $where); - if (!$absoluteAuth) { - $authGroups = array_merge($this->adminGroups, $authGroups); + // 仅本人所在角色组 + 其下级子组(getManageableGroupIds);不包含上级/同级。无父节点在结果集时,assembleChild 将该节点作为树根展示,符合「只看我这条线」 + $authGroups = $this->manageableGroupIds; + if ($absoluteAuth) { + $authGroups = array_values(array_diff($authGroups, $this->adminGroups)); } - $where[] = ['id', 'in', $authGroups]; + $where[] = ['id', 'in', $authGroups ?: [0]]; } $data = $this->model->where($where)->select()->toArray(); @@ -337,10 +374,48 @@ class Group extends Backend private function checkAuth($groupId): ?Response { - $authGroups = $this->auth->getAllAuthGroups($this->authMethod, []); - if (!$this->auth->isSuperAdmin() && !in_array($groupId, $authGroups)) { + $authGroups = $this->manageableGroupIds; + if (!$this->auth->isSuperAdmin() && !in_array(intval((string)$groupId), $authGroups, true)) { return $this->error(__($this->authMethod == 'allAuth' ? 'You need to have all permissions of this group to operate this group~' : 'You need to have all the permissions of the group and have additional permissions before you can operate the group~')); } return null; } + + private function getManageableGroupIds(): array + { + if ($this->auth->isSuperAdmin()) { + return AdminGroup::where('status', 1)->column('id'); + } + $own = array_map('intval', $this->adminGroups); + $children = array_map('intval', $this->auth->getAdminChildGroups()); + 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; + } + } diff --git a/app/admin/lang/en.php b/app/admin/lang/en.php index 087e8a8..c60ea01 100644 --- a/app/admin/lang/en.php +++ b/app/admin/lang/en.php @@ -94,5 +94,6 @@ return [ 'No rows were restore' => 'No rows were restored', '%d records and files have been deleted' => '%d records and files have been deleted', 'Please input correct username' => 'Please enter the correct username', + 'Please enter a valid commission rate for non-top role group' => 'Non-top role groups require a commission rate between 0 and 100 (%)', 'Group Name Arr' => 'Group Name Arr', ]; \ No newline at end of file diff --git a/app/admin/lang/zh-cn.php b/app/admin/lang/zh-cn.php index 1445196..ca6d866 100644 --- a/app/admin/lang/zh-cn.php +++ b/app/admin/lang/zh-cn.php @@ -113,5 +113,6 @@ return [ 'No rows were restore' => '未恢复任何行', '%d records and files have been deleted' => '已删除%d条记录和文件', 'Please input correct username' => '请输入正确的用户名', + 'Please enter a valid commission rate for non-top role group' => '非顶级角色组须填写 0~100 的分红比例(%)', 'Group Name Arr' => '分组名称数组', ]; \ No newline at end of file diff --git a/web/src/lang/backend/en/auth/group.ts b/web/src/lang/backend/en/auth/group.ts index 57f8a04..e85b712 100644 --- a/web/src/lang/backend/en/auth/group.ts +++ b/web/src/lang/backend/en/auth/group.ts @@ -1,6 +1,11 @@ export default { GroupName: 'Group Name', 'Group name': 'Group Name', + 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/zh-cn/auth/group.ts b/web/src/lang/backend/zh-cn/auth/group.ts index 18f6a82..f1190ca 100644 --- a/web/src/lang/backend/zh-cn/auth/group.ts +++ b/web/src/lang/backend/zh-cn/auth/group.ts @@ -1,6 +1,11 @@ export default { GroupName: '组名', 'Group name': '组别名称', + 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/views/backend/auth/group/index.vue b/web/src/views/backend/auth/group/index.vue index 42d574b..77df8c1 100644 --- a/web/src/views/backend/auth/group/index.vue +++ b/web/src/views/backend/auth/group/index.vue @@ -54,7 +54,13 @@ const baTable: baTableClass = new baTableClass( dblClickNotEditColumn: [undefined], column: [ { type: 'selection', align: 'center' }, - { label: t('auth.group.Group name'), prop: 'name', align: 'left', width: '200' }, + { + label: t('auth.group.Group name'), + prop: 'name', + align: 'left', + minWidth: '180', + }, + { 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'), @@ -165,6 +171,13 @@ 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 95ebb66..e2b3c5c 100644 --- a/web/src/views/backend/auth/group/popupForm.vue +++ b/web/src/views/backend/auth/group/popupForm.vue @@ -49,6 +49,23 @@ :placeholder="t('Please input field', { field: t('auth.group.Group name') })" > + + + + { + return false +} 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, @@ -153,6 +194,16 @@ defineExpose({