diff --git a/app/admin/controller/auth/Admin.php b/app/admin/controller/auth/Admin.php
index 8ab0762..d6a494c 100644
--- a/app/admin/controller/auth/Admin.php
+++ b/app/admin/controller/auth/Admin.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace app\admin\controller\auth;
+use ba\Random;
use Throwable;
use support\think\Db;
use support\validation\Validator;
@@ -21,7 +22,7 @@ class Admin extends Backend
protected array|string $quickSearchField = ['username', 'nickname'];
- protected string|int|bool $dataLimit = 'allAuthAndOthers';
+ protected string|int|bool $dataLimit = 'parent';
protected string $dataLimitField = 'id';
@@ -61,9 +62,18 @@ class Admin extends Backend
$res = $query
->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' => $res->items(),
+ 'list' => $items,
'total' => $res->total(),
'remark' => get_route_remark(),
]);
@@ -149,6 +159,10 @@ class Admin extends Backend
if (!$data) {
return $this->error(__('Parameter %s can not be empty', ['']));
}
+ $data = $this->normalizeSingleGroup($data);
+ if (!$this->hasSingleGroup($data['group_arr'] ?? null)) {
+ return $this->error('请选择且仅选择一个角色组');
+ }
if ($this->modelValidate) {
try {
@@ -172,6 +186,25 @@ class Admin extends Backend
$passwd = $data['password'] ?? '';
$data = $this->excludeFields($data);
+ $creatorChannelId = $this->getCreatorChannelId();
+ if (!$this->auth->isSuperAdmin()) {
+ if ($creatorChannelId === null || $creatorChannelId === '') {
+ return $this->error(__('You have no permission'));
+ }
+ $data['channel_id'] = $creatorChannelId;
+ $data['parent_admin_id'] = $this->auth->id;
+ }
+ $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']);
@@ -230,6 +263,37 @@ class Admin extends Backend
if (!$data) {
return $this->error(__('Parameter %s can not be empty', ['']));
}
+ $data = $this->normalizeSingleGroup($data);
+ if (!$this->hasSingleGroup($data['group_arr'] ?? null)) {
+ 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)) {
+ $postedGroups = [];
+ }
+ $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) {
+ return $this->error(__('You cannot modify your own management group!'));
+ }
+ }
if ($this->modelValidate) {
try {
@@ -276,15 +340,29 @@ class Admin extends Backend
if ($authRes !== null) return $authRes;
}
- Db::name('admin_group_access')
- ->where('uid', $id)
- ->delete();
-
$data = $this->excludeFields($data);
+ unset($data['invite_code']);
+ $creatorChannelId = $this->getCreatorChannelId();
+ if (!$this->auth->isSuperAdmin() && $creatorChannelId !== null && $creatorChannelId !== '') {
+ $data['channel_id'] = $creatorChannelId;
+ }
+ $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 {
$result = $row->save($data);
+ Db::name('admin_group_access')
+ ->where('uid', $id)
+ ->delete();
if ($groupAccess) {
Db::name('admin_group_access')->insertAll($groupAccess);
}
@@ -357,7 +435,7 @@ class Admin extends Backend
if ($this->auth->isSuperAdmin()) {
return null;
}
- $authGroups = $this->auth->getAllAuthGroups('allAuthAndOthers');
+ $authGroups = $this->getManageableGroupIds();
foreach ($groups as $group) {
if (!in_array($group, $authGroups)) {
return $this->error(__('You have no permission to add an administrator to this group!'));
@@ -365,4 +443,132 @@ class Admin extends Backend
}
return null;
}
+
+ private function getManageableGroupIds(): array
+ {
+ if ($this->auth->isSuperAdmin()) {
+ return Db::name('admin_group')->where('status', 1)->column('id');
+ }
+ $own = Db::name('admin_group_access')->where('uid', $this->auth->id)->column('group_id');
+ $children = $this->auth->getAdminChildGroups();
+ return array_values(array_unique(array_merge($own, $children)));
+ }
+
+ private function getCreatorChannelId(): mixed
+ {
+ $currentAdmin = Db::name('admin')
+ ->field(['id', 'channel_id'])
+ ->where('id', $this->auth->id)
+ ->find();
+ if ($currentAdmin && !empty($currentAdmin['channel_id'])) {
+ return $currentAdmin['channel_id'];
+ }
+ $channelId = Db::name('channel')
+ ->where('top_admin_id', $this->auth->id)
+ ->value('id');
+ return $channelId ?: null;
+ }
+
+ private function generateUniqueInviteCode(): string
+ {
+ $tries = 0;
+ do {
+ $tries++;
+ $code = strtoupper(substr(Random::uuid(), 0, 8));
+ $exists = Db::name('admin')->where('invite_code', $code)->find();
+ } while ($exists && $tries < 20);
+ 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)) {
+ return $data;
+ }
+ $group = $data['group_arr'];
+ if (is_array($group)) {
+ $data['group_arr'] = array_values(array_filter($group, static function ($item) {
+ return $item !== null && $item !== '';
+ }));
+ return $data;
+ }
+ if ($group === null || $group === '') {
+ $data['group_arr'] = [];
+ return $data;
+ }
+ $data['group_arr'] = [$group];
+ return $data;
+ }
+
+ private function hasSingleGroup(mixed $groups): bool
+ {
+ return is_array($groups) && count($groups) === 1;
+ }
}
diff --git a/app/admin/controller/game/User.php b/app/admin/controller/game/User.php
index 736fe11..7c94700 100644
--- a/app/admin/controller/game/User.php
+++ b/app/admin/controller/game/User.php
@@ -21,7 +21,7 @@ class User extends Backend
protected array|string $preExcludeFields = ['id', 'uuid', 'create_time', 'update_time'];
- protected array $withJoinTable = ['gameChannel', 'admin'];
+ protected array $withJoinTable = ['channel', 'admin'];
protected string|array $quickSearchField = ['id', 'username', 'phone'];
@@ -53,7 +53,7 @@ class User extends Backend
$data['password'] = hash_password($password);
$username = $data['username'] ?? '';
- $channelId = $data['channel_id'] ?? ($data['game_channel_id'] ?? null);
+ $channelId = $data['channel_id'] ?? null;
if (!is_string($username) || trim($username) === '' || $channelId === null || $channelId === '') {
return $this->error(__('Parameter %s can not be empty', ['username/channel_id']));
}
@@ -131,10 +131,8 @@ class User extends Backend
$nextChannelId = null;
if (array_key_exists('channel_id', $data)) {
$nextChannelId = $data['channel_id'];
- } elseif (array_key_exists('game_channel_id', $data)) {
- $nextChannelId = $data['game_channel_id'];
} else {
- $nextChannelId = $row['channel_id'] ?? $row['game_channel_id'] ?? null;
+ $nextChannelId = $row['channel_id'] ?? null;
}
if (is_string($nextUsername) && trim($nextUsername) !== '' && $nextChannelId !== null && $nextChannelId !== '') {
@@ -194,7 +192,7 @@ class User extends Backend
$res = $this->model
->withJoin($this->withJoinTable, $this->withJoinType)
->with($this->withJoinTable)
- ->visible(['gameChannel' => ['name'], 'admin' => ['username']])
+ ->visible(['channel' => ['name'], 'admin' => ['username']])
->alias($alias)
->where($where)
->order($order)
diff --git a/app/common/model/GameUser.php b/app/common/model/GameUser.php
index 6345d64..40f363a 100644
--- a/app/common/model/GameUser.php
+++ b/app/common/model/GameUser.php
@@ -27,9 +27,14 @@ class GameUser extends Model
return is_null($value) ? null : (float)$value;
}
+ public function channel(): \think\model\relation\BelongsTo
+ {
+ return $this->belongsTo(\app\common\model\Channel::class, 'channel_id', 'id');
+ }
+
public function gameChannel(): \think\model\relation\BelongsTo
{
- return $this->belongsTo(\app\common\model\GameChannel::class, 'game_channel_id', 'id');
+ return $this->channel();
}
public function admin(): \think\model\relation\BelongsTo
diff --git a/web/src/lang/backend/en/auth/admin.ts b/web/src/lang/backend/en/auth/admin.ts
index 285962a..d2e08a1 100644
--- a/web/src/lang/backend/en/auth/admin.ts
+++ b/web/src/lang/backend/en/auth/admin.ts
@@ -5,6 +5,13 @@ export default {
avatar: 'Avatar',
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',
'Please leave blank if not modified': 'Please leave blank if you do not modify.',
diff --git a/web/src/lang/backend/en/game/user.ts b/web/src/lang/backend/en/game/user.ts
index 7a211d5..0321200 100644
--- a/web/src/lang/backend/en/game/user.ts
+++ b/web/src/lang/backend/en/game/user.ts
@@ -9,8 +9,8 @@ export default {
status: 'status',
'status 0': 'status 0',
'status 1': 'status 1',
- game_channel_id: 'game_channel_id',
- gamechannel__name: 'name',
+ channel_id: 'channel_id',
+ channel__name: 'name',
admin_id: 'admin_id',
admin__username: 'username',
create_time: 'create_time',
diff --git a/web/src/lang/backend/zh-cn/auth/admin.ts b/web/src/lang/backend/zh-cn/auth/admin.ts
index e5c671f..52d261c 100644
--- a/web/src/lang/backend/zh-cn/auth/admin.ts
+++ b/web/src/lang/backend/zh-cn/auth/admin.ts
@@ -5,6 +5,13 @@ export default {
avatar: '头像',
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: '密码',
'Please leave blank if not modified': '不修改请留空',
diff --git a/web/src/lang/backend/zh-cn/game/user.ts b/web/src/lang/backend/zh-cn/game/user.ts
index 5d876ea..7ae6471 100644
--- a/web/src/lang/backend/zh-cn/game/user.ts
+++ b/web/src/lang/backend/zh-cn/game/user.ts
@@ -9,8 +9,8 @@ export default {
status: '状态',
'status 0': '禁用',
'status 1': '启用',
- game_channel_id: '所属渠道',
- gamechannel__name: '渠道名',
+ channel_id: '所属渠道',
+ channel__name: '渠道名',
admin_id: '所属管理员',
admin__username: '用户名',
create_time: '创建时间',
diff --git a/web/src/views/backend/auth/admin/index.vue b/web/src/views/backend/auth/admin/index.vue
index 295f068..4cf90e5 100644
--- a/web/src/views/backend/auth/admin/index.vue
+++ b/web/src/views/backend/auth/admin/index.vue
@@ -40,6 +40,13 @@ 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/'),
{
@@ -48,7 +55,23 @@ const baTable = new baTableClass(
{ label: t('Id'), prop: 'id', align: 'center', operator: '=', operatorPlaceholder: t('Id'), width: 70 },
{ label: t('auth.admin.username'), prop: 'username', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
{ label: t('auth.admin.nickname'), prop: 'nickname', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
- { label: t('auth.admin.group'), prop: 'group_name_arr', align: 'center', operator: false, render: 'tags' },
+ {
+ label: t('auth.admin.group'),
+ prop: 'group_name_arr',
+ align: 'center',
+ minWidth: 150,
+ operator: false,
+ 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 cf3e43c..46c52e6 100644
--- a/web/src/views/backend/auth/admin/popupForm.vue
+++ b/web/src/views/backend/auth/admin/popupForm.vue
@@ -43,18 +43,43 @@
/>
+
+
+
+
+ - {{ t('auth.admin.commission_rate_desc_1') }}
+ - {{ t('auth.admin.commission_rate_desc_2') }}
+ - {{ t('auth.admin.commission_rate_desc_3') }}
+
+
+
+