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') })"
>
+
+
+
+
+ - {{ t('auth.group.commission_rate_desc_1') }}
+ - {{ t('auth.group.commission_rate_desc_2') }}
+ - {{ t('auth.group.commission_rate_desc_3') }}
+
+
+
{
+ 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({