From 28bd9f1a0996852d9e919bd02db15641da2b4c07 Mon Sep 17 00:00:00 2001 From: zhenhui <1276357500@qq.com> Date: Fri, 3 Apr 2026 17:49:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B8=B8=E6=88=8F-=E6=B8=A0=E9=81=93=E7=AE=A1?= =?UTF-8?q?=E7=90=86-=E4=BC=98=E5=8C=96=E6=A0=B7=E5=BC=8F=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=E9=AA=8C=E8=AF=81=EF=BC=8C=E6=96=B0=E5=A2=9E=E5=85=B3?= =?UTF-8?q?=E8=81=94=E5=88=A0=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/controller/game/Channel.php | 306 +++++++++++++++++- app/admin/lang/zh-cn.php | 1 + app/common/service/GameChannelUserCount.php | 47 +++ web/src/lang/backend/en/game/channel.ts | 3 + web/src/lang/backend/zh-cn/game/channel.ts | 3 + web/src/views/backend/game/channel/index.vue | 47 ++- .../views/backend/game/config/popupForm.vue | 197 +++++++++-- 7 files changed, 578 insertions(+), 26 deletions(-) create mode 100644 app/common/service/GameChannelUserCount.php diff --git a/app/admin/controller/game/Channel.php b/app/admin/controller/game/Channel.php index 5cec95c..5b1f418 100644 --- a/app/admin/controller/game/Channel.php +++ b/app/admin/controller/game/Channel.php @@ -36,7 +36,7 @@ class Channel extends Backend * adminTree 为辅助接口,默认权限节点名 game/channel/admintree 往往未在后台录入; * 与列表权限 game/channel/index 对齐,避免子管理员已勾「渠道管理」仍 401。 */ - protected array $noNeedPermission = ['adminTree']; + protected array $noNeedPermission = ['adminTree', 'deleteRelatedCounts']; protected function initController(WebmanRequest $request): ?Response { @@ -44,6 +44,25 @@ class Channel extends Backend return null; } + /** + * 列表;附带 delete_related_counts=1 时返回删除前关联数据统计(走与 index 相同的路由入口,避免单独 URL 在部分环境下 404) + * + * @throws Throwable + */ + public function index(WebmanRequest $request): Response + { + $response = $this->initializeBackend($request); + if ($response !== null) { + return $response; + } + $delPreview = $request->get('delete_related_counts'); + if ($delPreview === '1' || $delPreview === 1 || $delPreview === true) { + return $this->deleteRelatedCountsResponse($request); + } + + return $this->_index(); + } + /** * 渠道-管理员树(父级=渠道,子级=管理员,仅可选择子级) */ @@ -200,6 +219,20 @@ class Channel extends Backend return $this->error($e->getMessage()); } if ($result !== false) { + $newChannelId = $this->resolveNewChannelIdAfterInsert($data); + if (!$this->isPositiveChannelId($newChannelId)) { + $code = $data['code'] ?? null; + if (is_string($code) && trim($code) !== '') { + $newChannelId = Db::name('game_channel')->where('code', trim($code))->order('id', 'desc')->value('id'); + } + } + if ($this->isPositiveChannelId($newChannelId)) { + try { + $this->copyGameConfigFromChannelZero($newChannelId); + } catch (Throwable $e) { + return $this->error(__('Game channel copy default config failed') . ': ' . $e->getMessage()); + } + } return $this->success(__('Added successfully')); } return $this->error(__('No rows were added')); @@ -316,13 +349,282 @@ class Channel extends Backend ->order($order) ->paginate($limit); + $list = $this->buildChannelListWithRealtimeUserCounts($res->items()); + return $this->success('', [ - 'list' => $res->items(), + 'list' => $list, 'total' => $res->total(), 'remark' => get_route_remark(), ]); } + /** + * 列表 user_count 按 game_user.game_channel_id 实时 COUNT,与库字段无关(用户增删改时会回写 game_channel.user_count) + * + * @param iterable $items + * @return list> + */ + private function buildChannelListWithRealtimeUserCounts(iterable $items): array + { + $rows = []; + foreach ($items as $item) { + $rows[] = is_array($item) ? $item : $item->toArray(); + } + if ($rows === []) { + return []; + } + $ids = []; + foreach ($rows as $r) { + if (isset($r['id'])) { + $ids[] = $r['id']; + } + } + if ($ids === []) { + return $rows; + } + $agg = Db::name('game_user') + ->where('game_channel_id', 'in', $ids) + ->field('game_channel_id, count(*) as cnt') + ->group('game_channel_id') + ->select() + ->toArray(); + $countMap = []; + foreach ($agg as $a) { + $countMap[$a['game_channel_id']] = (int) $a['cnt']; + } + foreach ($rows as &$r) { + $cid = $r['id'] ?? null; + $r['user_count'] = ($cid !== null && $cid !== '') ? ($countMap[$cid] ?? 0) : 0; + } + unset($r); + + return $rows; + } + + /** + * 删除前统计:与当前选中渠道相关的游戏配置条数、游戏用户条数(须具备 game/channel/del) + * + * @throws Throwable + */ + public function deleteRelatedCounts(WebmanRequest $request): Response + { + $response = $this->initializeBackend($request); + if ($response !== null) { + return $response; + } + + return $this->deleteRelatedCountsResponse($request); + } + + /** + * @throws Throwable + */ + private function deleteRelatedCountsResponse(WebmanRequest $request): Response + { + if (!$this->auth->check('game/channel/del')) { + return $this->error(__('You have no permission')); + } + + $channelIds = $this->getAuthorizedChannelIdsForIncomingIds($request); + if ($channelIds === []) { + return $this->success('', [ + 'game_config_count' => 0, + 'game_user_count' => 0, + ]); + } + + // 实时统计:game_config.channel_id、game_user.game_channel_id(与渠道 id 一致) + $configCount = Db::name('game_config')->where('channel_id', 'in', $channelIds)->count(); + $userCount = Db::name('game_user')->where('game_channel_id', 'in', $channelIds)->count(); + + return $this->success('', [ + 'game_config_count' => $configCount, + 'game_user_count' => $userCount, + ]); + } + + /** + * 删除渠道:若存在关联的游戏配置或用户,须带 confirm_cascade=1;同时级联删除关联数据 + * + * @throws Throwable + */ + protected function _del(): Response + { + $where = []; + $dataLimitAdminIds = $this->getDataLimitAdminIds(); + if ($dataLimitAdminIds) { + $where[] = [$this->dataLimitField, 'in', $dataLimitAdminIds]; + } + + $ids = $this->request ? ($this->request->post('ids') ?? $this->request->get('ids') ?? []) : []; + if (!is_array($ids)) { + $ids = $ids !== null && $ids !== '' ? [$ids] : []; + } + if ($ids === []) { + return $this->error(__('Parameter error')); + } + + $pk = $this->model->getPk(); + $where[] = [$pk, 'in', $ids]; + + $data = $this->model->where($where)->select(); + if (count($data) === 0) { + return $this->error(__('No rows were deleted')); + } + + $channelIds = []; + foreach ($data as $v) { + $channelIds[] = $v[$pk]; + } + + // 删除确认用实时条数:game_config.channel_id、game_user.game_channel_id + $configCount = Db::name('game_config')->where('channel_id', 'in', $channelIds)->count(); + $userCount = Db::name('game_user')->where('game_channel_id', 'in', $channelIds)->count(); + + $confirmCascade = $this->request->get('confirm_cascade'); + if ($confirmCascade === null || $confirmCascade === '') { + $confirmCascade = $this->request->post('confirm_cascade'); + } + $confirmed = $confirmCascade === 1 || $confirmCascade === '1' || $confirmCascade === true; + + if (($configCount > 0 || $userCount > 0) && !$confirmed) { + return $this->error(__('Game channel delete need confirm related'), [ + 'need_confirm' => true, + 'game_config_count' => $configCount, + 'game_user_count' => $userCount, + ]); + } + + $count = 0; + $this->model->startTrans(); + try { + Db::name('game_config')->where('channel_id', 'in', $channelIds)->delete(); + Db::name('game_user')->where('game_channel_id', 'in', $channelIds)->delete(); + foreach ($data as $v) { + $count += $v->delete(); + } + $this->model->commit(); + } catch (Throwable $e) { + $this->model->rollback(); + return $this->error($e->getMessage()); + } + + if ($count) { + return $this->success(__('Deleted successfully')); + } + + return $this->error(__('No rows were deleted')); + } + + /** + * @param list $ids + * @return list + */ + private function getAuthorizedChannelIdsForIncomingIds(WebmanRequest $request): array + { + $where = []; + $dataLimitAdminIds = $this->getDataLimitAdminIds(); + if ($dataLimitAdminIds) { + $where[] = [$this->dataLimitField, 'in', $dataLimitAdminIds]; + } + + $ids = $request->post('ids') ?? $request->get('ids') ?? []; + if (!is_array($ids)) { + $ids = $ids !== null && $ids !== '' ? [$ids] : []; + } + if ($ids === []) { + return []; + } + + $pk = $this->model->getPk(); + $where[] = [$pk, 'in', $ids]; + + return $this->model->where($where)->column($pk); + } + + /** + * ThinkORM 在连接池/部分驱动下,insert 后 getKey() 可能未及时带上自增 id,这里多路兜底 + * + * @param array $postedChannelData 已过滤后的入库数据(含 code 等) + */ + private function resolveNewChannelIdAfterInsert(array $postedChannelData): int|string|null + { + $pk = $this->model->getPk(); + $id = $this->model->getKey(); + if ($this->isPositiveChannelId($id)) { + return $id; + } + $rowData = $this->model->getData(); + if (is_array($rowData)) { + if (isset($rowData[$pk]) && $this->isPositiveChannelId($rowData[$pk])) { + return $rowData[$pk]; + } + if (isset($rowData['id']) && $this->isPositiveChannelId($rowData['id'])) { + return $rowData['id']; + } + } + $lastInsId = $this->model->db()->getLastInsID(); + if ($this->isPositiveChannelId($lastInsId)) { + return $lastInsId; + } + $lastInsId2 = Db::name('game_channel')->getLastInsID(); + if ($this->isPositiveChannelId($lastInsId2)) { + return $lastInsId2; + } + $code = $postedChannelData['code'] ?? null; + if (is_string($code) && trim($code) !== '') { + $found = Db::name('game_channel')->where('code', trim($code))->order('id', 'desc')->value('id'); + if ($this->isPositiveChannelId($found)) { + return $found; + } + } + + return null; + } + + private function isPositiveChannelId(mixed $id): bool + { + if ($id === null || $id === '') { + return false; + } + if (is_numeric($id)) { + return $id > 0; + } + + return false; + } + + /** + * 新建渠道后:将 channel_id=0 的全局默认游戏配置复制一份,channel_id 指向新渠道主键 + * + * @param int|string $newChannelId 新建 game_channel.id + */ + private function copyGameConfigFromChannelZero(int|string $newChannelId): void + { + $exists = Db::name('game_config')->where('channel_id', $newChannelId)->count(); + if ($exists > 0) { + return; + } + // 全局模板:channel_id 为 0 或字符串 '0'(与业务约定一致) + $rows = Db::name('game_config') + ->whereIn('channel_id', [0, '0']) + ->select() + ->toArray(); + if ($rows === []) { + return; + } + $now = time(); + foreach ($rows as $row) { + foreach (['ID', 'id', 'Id'] as $pkField) { + unset($row[$pkField]); + } + $row['channel_id'] = $newChannelId; + $row['create_time'] = $now; + $row['update_time'] = $now; + Db::name('game_config')->strict(false)->insert($row); + } + } + /** * 若需重写查看、编辑、删除等方法,请复制 @see \app\admin\library\traits\Backend 中对应的方法至此进行重写 */ diff --git a/app/admin/lang/zh-cn.php b/app/admin/lang/zh-cn.php index 1a2c4f2..466d9a7 100644 --- a/app/admin/lang/zh-cn.php +++ b/app/admin/lang/zh-cn.php @@ -37,6 +37,7 @@ return [ 'Topic format error' => '上传存储子目录格式错误!', 'Driver %s not supported' => '不支持的驱动:%s', 'Unknown' => '未知', + 'Global default' => '全局默认(channel_id=0)', // 权限类语言包-s 'Super administrator' => '超级管理员', 'No permission' => '无权限', diff --git a/app/common/service/GameChannelUserCount.php b/app/common/service/GameChannelUserCount.php new file mode 100644 index 0000000..98504fc --- /dev/null +++ b/app/common/service/GameChannelUserCount.php @@ -0,0 +1,47 @@ +where('game_channel_id', $channelId)->count(); + Db::name('game_channel')->where('id', $channelId)->update(['user_count' => $count]); + } + + /** + * @param list $channelIds + */ + public static function syncChannels(array $channelIds): void + { + $seen = []; + foreach ($channelIds as $cid) { + if ($cid === null || $cid === '') { + continue; + } + $k = (string) $cid; + if (isset($seen[$k])) { + continue; + } + $seen[$k] = true; + self::syncFromGameUser($cid); + } + } +} diff --git a/web/src/lang/backend/en/game/channel.ts b/web/src/lang/backend/en/game/channel.ts index 35bea2f..10a946c 100644 --- a/web/src/lang/backend/en/game/channel.ts +++ b/web/src/lang/backend/en/game/channel.ts @@ -1,4 +1,7 @@ export default { + delete_confirm_title: 'Delete channel', + delete_confirm_related: + 'This will also delete {countConfig} game config row(s) and {countUser} game user row(s) under this channel. This cannot be undone. Continue?', id: 'id', code: 'code', name: 'name', diff --git a/web/src/lang/backend/zh-cn/game/channel.ts b/web/src/lang/backend/zh-cn/game/channel.ts index f185326..840a8b3 100644 --- a/web/src/lang/backend/zh-cn/game/channel.ts +++ b/web/src/lang/backend/zh-cn/game/channel.ts @@ -1,4 +1,7 @@ export default { + delete_confirm_title: '删除渠道', + delete_confirm_related: + '将同时删除该渠道下 {countConfig} 条游戏配置、{countUser} 条游戏用户数据,此操作不可恢复。确定删除所选渠道吗?', id: 'ID', code: '渠道标识', name: '渠道名', diff --git a/web/src/views/backend/game/channel/index.vue b/web/src/views/backend/game/channel/index.vue index eeee7c7..2ea0286 100644 --- a/web/src/views/backend/game/channel/index.vue +++ b/web/src/views/backend/game/channel/index.vue @@ -22,12 +22,15 @@