diff --git a/app/admin/controller/game/User.php b/app/admin/controller/game/User.php index 7c94700..cf3585a 100644 --- a/app/admin/controller/game/User.php +++ b/app/admin/controller/game/User.php @@ -4,6 +4,7 @@ namespace app\admin\controller\game; use Throwable; use app\common\controller\Backend; +use support\think\Db; use support\Response; use Webman\Http\Request as WebmanRequest; @@ -23,7 +24,7 @@ class User extends Backend protected array $withJoinTable = ['channel', 'admin']; - protected string|array $quickSearchField = ['id', 'username', 'phone']; + protected string|array $quickSearchField = ['id', 'username', 'phone', 'email', 'register_invite_code']; protected function initController(WebmanRequest $request): ?Response { @@ -205,6 +206,135 @@ class User extends Backend ]); } + /** + * 角色组 → 管理员树(仅当前账号可管理的角色组及其下管理员;用于游戏用户归属) + * 同一管理员若属于多个组,只挂在 id 最小的所属组下,避免树中重复 value + */ + public function adminScopeTree(WebmanRequest $request): Response + { + $response = $this->initializeBackend($request); + if ($response !== null) { + return $response; + } + + $groupIds = $this->getManageableAdminGroupIds(); + if ($groupIds === []) { + return $this->success('', ['list' => []]); + } + + $groups = Db::name('admin_group') + ->where('id', 'in', $groupIds) + ->where('status', 1) + ->field(['id', 'pid', 'name']) + ->order('id', 'asc') + ->select() + ->toArray(); + + $accessRows = Db::name('admin_group_access')->alias('aga') + ->join('admin a', 'aga.uid = a.id') + ->where('aga.group_id', 'in', $groupIds) + ->field(['a.id', 'a.username', 'a.channel_id', 'a.invite_code', 'aga.group_id']) + ->select() + ->toArray(); + + $adminPrimary = []; + foreach ($accessRows as $row) { + $uid = intval((string)$row['id']); + $gid = intval((string)$row['group_id']); + if (!isset($adminPrimary[$uid]) || $gid < $adminPrimary[$uid]['gid']) { + $adminPrimary[$uid] = [ + 'gid' => $gid, + 'user' => $row, + ]; + } + } + + $adminsByGroup = []; + foreach ($adminPrimary as $item) { + $row = $item['user']; + $gid = $item['gid']; + $invite = $row['invite_code'] ?? ''; + $invite = is_string($invite) ? $invite : ''; + $channelId = $row['channel_id'] ?? null; + $adminsByGroup[$gid][] = [ + 'value' => (string)$row['id'], + 'label' => (string)$row['username'], + 'is_leaf' => true, + 'channel_id' => $channelId === null || $channelId === '' ? null : intval((string)$channelId), + 'invite_code' => $invite, + ]; + } + + $groupMap = []; + foreach ($groups as $g) { + $groupMap[intval((string)$g['id'])] = $g; + } + + $childGroupIdsByPid = []; + foreach ($groups as $g) { + $id = intval((string)$g['id']); + $pid = intval((string)($g['pid'] ?? 0)); + $childGroupIdsByPid[$pid][] = $id; + } + + $buildNode = null; + $buildNode = function (int $groupId) use (&$buildNode, $groupMap, $childGroupIdsByPid, $adminsByGroup): array { + if (!isset($groupMap[$groupId])) { + return []; + } + $g = $groupMap[$groupId]; + $children = []; + foreach ($childGroupIdsByPid[$groupId] ?? [] as $childId) { + $children[] = $buildNode($childId); + } + foreach ($adminsByGroup[$groupId] ?? [] as $leaf) { + $children[] = $leaf; + } + + return [ + 'value' => 'group_' . $groupId, + 'label' => (string)$g['name'], + 'disabled' => true, + 'children' => $children, + ]; + }; + + $groupIdSet = array_fill_keys(array_keys($groupMap), true); + $roots = []; + foreach ($groups as $g) { + $id = intval((string)$g['id']); + $pid = intval((string)($g['pid'] ?? 0)); + if ($pid === 0 || !isset($groupIdSet[$pid])) { + $roots[] = $id; + } + } + $roots = array_values(array_unique($roots)); + sort($roots); + + $tree = []; + foreach ($roots as $rid) { + $tree[] = $buildNode($rid); + } + + return $this->success('', [ + 'list' => $tree, + ]); + } + + /** + * @return int[] + */ + private function getManageableAdminGroupIds(): 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'); + $own = array_map('intval', $own); + $children = array_map('intval', $this->auth->getAdminChildGroups()); + return array_values(array_unique(array_merge($own, $children))); + } + /** * 若需重写查看、编辑、删除等方法,请复制 @see \app\admin\library\traits\Backend 中对应的方法至此进行重写 */ diff --git a/app/common/model/GameUser.php b/app/common/model/GameUser.php index 40f363a..52be576 100644 --- a/app/common/model/GameUser.php +++ b/app/common/model/GameUser.php @@ -15,28 +15,22 @@ class GameUser extends Model // 自动写入时间戳字段 protected $autoWriteTimestamp = true; - // 字段类型转换 + // 字段类型转换(金额 decimal(18,4) 用字符串避免浮点误差) protected $type = [ 'create_time' => 'integer', 'update_time' => 'integer', + 'coin' => 'string', + 'total_deposit_coin' => 'string', + 'total_valid_bet_coin' => 'string', + 'risk_flags' => 'integer', + 'current_streak' => 'integer', ]; - - public function getcoinAttr($value): ?float - { - 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->channel(); - } - public function admin(): \think\model\relation\BelongsTo { return $this->belongsTo(\app\admin\model\Admin::class, 'admin_id', 'id'); diff --git a/web/src/lang/backend/en/game/user.ts b/web/src/lang/backend/en/game/user.ts index 0321200..6a8a0a7 100644 --- a/web/src/lang/backend/en/game/user.ts +++ b/web/src/lang/backend/en/game/user.ts @@ -1,19 +1,45 @@ export default { - id: 'id', - username: 'username', - password: 'password', - uuid: 'uuid', - phone: 'phone', - remark: 'remark', - coin: 'coin', - status: 'status', - 'status 0': 'status 0', - 'status 1': 'status 1', - channel_id: 'channel_id', - channel__name: 'name', - admin_id: 'admin_id', - admin__username: 'username', - create_time: 'create_time', - update_time: 'update_time', - 'quick Search Fields': 'id,username,phone', + id: 'ID', + username: 'Username', + password: 'Password', + uuid: 'UUID', + phone: 'Phone', + email: 'Email', + email_placeholder: 'Optional (register with phone or email)', + head_image: 'Avatar', + remark: 'Remark', + coin: 'Coin balance', + coin_placeholder: 'decimal(18,4)', + total_deposit_coin: 'Total deposit (coin)', + total_valid_bet_coin: 'Total valid bet (coin)', + risk_flags: 'Risk', + risk_none: 'None', + risk_no_login: 'No login', + risk_no_bet: 'No bet', + risk_no_withdraw: 'No withdraw', + current_streak: 'Win streak', + last_bet_period_no: 'Last bet period', + last_bet_period_no_placeholder: 'DB fallback for streak', + register_invite_code: 'Invite code (snapshot)', + register_invite_code_placeholder: 'Invite code at registration', + status: 'Status', + 'status 0': 'Disabled', + 'status 1': 'Enabled', + section_admin_attribution: 'Administrator', + admin_affiliation: 'Assigned admin', + admin_affiliation_placeholder: 'Role group tree — only admins in your scope', + register_invite_code_auto_placeholder: 'Filled from selected admin invite code', + channel_id: 'Channel', + channel__name: 'Channel', + admin_id: 'Admin', + admin__username: 'Admin', + create_time: 'Created', + update_time: 'Updated', + 'quick Search Fields': 'ID, username, phone, email, invite code', + section_basic: 'Account', + section_register: 'Registration', + section_finance: 'Balance & turnover', + section_risk: 'Risk control', + section_streak: 'Streak (fallback)', + section_other: 'Other', } diff --git a/web/src/lang/backend/zh-cn/game/user.ts b/web/src/lang/backend/zh-cn/game/user.ts index 7ae6471..70f299d 100644 --- a/web/src/lang/backend/zh-cn/game/user.ts +++ b/web/src/lang/backend/zh-cn/game/user.ts @@ -4,16 +4,42 @@ export default { password: '密码', uuid: '用户唯一标识', phone: '手机号', + email: '邮箱', + email_placeholder: '可选,与手机号二选一注册时填写', + head_image: '头像', remark: '备注', - coin: '平台币', + coin: '游戏币余额', + coin_placeholder: 'decimal(18,4),禁止业务用浮点存库', + total_deposit_coin: '累计充值(币)', + total_valid_bet_coin: '累计有效投注(币)', + risk_flags: '风控', + risk_none: '无限制', + risk_no_login: '禁止登录', + risk_no_bet: '禁止下注', + risk_no_withdraw: '禁止提现', + current_streak: '当前连胜', + last_bet_period_no: '最近下注期号', + last_bet_period_no_placeholder: '连胜兜底同步用,可与 Redis 对照', + register_invite_code: '注册邀请码快照', + register_invite_code_placeholder: '注册时绑定渠道/代理邀请码', status: '状态', 'status 0': '禁用', 'status 1': '启用', + section_admin_attribution: '管理员归属', + admin_affiliation: '归属管理员', + admin_affiliation_placeholder: '按角色组展开,仅展示您可管理范围内的管理员', + register_invite_code_auto_placeholder: '随所选管理员邀请码自动带出', channel_id: '所属渠道', channel__name: '渠道名', - admin_id: '所属管理员', - admin__username: '用户名', + admin_id: '归属管理员', + admin__username: '管理员', create_time: '创建时间', update_time: '修改时间', - 'quick Search Fields': 'ID、用户名、手机号', + 'quick Search Fields': 'ID、用户名、手机号、邮箱、邀请码', + section_basic: '账号信息', + section_register: '注册与邀请', + section_finance: '资金与流水', + section_risk: '风控', + section_streak: '连胜(兜底)', + section_other: '其他', } diff --git a/web/src/views/backend/game/user/index.vue b/web/src/views/backend/game/user/index.vue index 4a844f1..20b674a 100644 --- a/web/src/views/backend/game/user/index.vue +++ b/web/src/views/backend/game/user/index.vue @@ -2,19 +2,13 @@