Files
webman-buildadmin/app/admin/controller/game/Channel.php

631 lines
22 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
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;
/**
* 渠道管理
*/
class Channel extends Backend
{
/**
* GameChannel模型对象
* @var object|null
* @phpstan-var \app\common\model\GameChannel|null
*/
protected ?object $model = null;
protected array|string $preExcludeFields = ['id', 'user_count', 'profit_amount', 'create_time', 'update_time'];
protected array $withJoinTable = ['adminGroup', 'admin'];
protected string|array $quickSearchField = ['id', 'code', 'name'];
/**
* 非超级管理员仅能操作 game_channel.admin_id 为当前账号的渠道;超管不限制
* @see \app\common\controller\Backend::getDataLimitAdminIds()
*/
protected bool|string|int $dataLimit = true;
/**
* adminTree 为辅助接口,默认权限节点名 game/channel/admintree 往往未在后台录入;
* 与列表权限 game/channel/index 对齐,避免子管理员已勾「渠道管理」仍 401。
*/
protected array $noNeedPermission = ['adminTree', 'deleteRelatedCounts'];
protected function initController(WebmanRequest $request): ?Response
{
$this->model = new \app\common\model\GameChannel();
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();
}
/**
* 渠道-管理员树(父级=渠道,子级=管理员,仅可选择子级)
*/
public function adminTree(WebmanRequest $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) return $response;
if (!$this->auth->check('game/channel/index')) {
return $this->error(__('You have no permission'));
}
$channelQuery = Db::name('game_channel')
->field(['id', 'name', 'admin_group_id'])
->order('id', 'asc');
if (!$this->auth->isSuperAdmin()) {
$channelQuery->where('admin_id', $this->auth->id);
}
$channels = $channelQuery->select()->toArray();
$groupChildrenCache = [];
$getGroupChildren = function ($groupId) use (&$getGroupChildren, &$groupChildrenCache) {
if ($groupId === null || $groupId === '') return [];
if (array_key_exists($groupId, $groupChildrenCache)) return $groupChildrenCache[$groupId];
$children = Db::name('admin_group')
->where('pid', $groupId)
->where('status', 1)
->column('id');
$all = [];
foreach ($children as $cid) {
$all[] = $cid;
foreach ($getGroupChildren($cid) as $cc) {
$all[] = $cc;
}
}
$groupChildrenCache[$groupId] = $all;
return $all;
};
$tree = [];
foreach ($channels as $ch) {
$groupId = $ch['admin_group_id'] ?? null;
$groupIds = [];
if ($groupId !== null && $groupId !== '') {
$groupIds[] = $groupId;
foreach ($getGroupChildren($groupId) as $gid) {
$groupIds[] = $gid;
}
}
$adminIds = [];
if ($groupIds) {
$adminIds = Db::name('admin_group_access')
->where('group_id', 'in', array_unique($groupIds))
->column('uid');
}
$adminIds = array_values(array_unique($adminIds));
$admins = [];
if ($adminIds) {
$admins = Db::name('admin')
->field(['id', 'username'])
->where('id', 'in', $adminIds)
->order('id', 'asc')
->select()
->toArray();
}
$children = [];
foreach ($admins as $a) {
$children[] = [
'value' => (string) $a['id'],
'label' => $a['username'],
'channel_id' => $ch['id'],
'is_leaf' => true,
];
}
$tree[] = [
'value' => 'channel_' . $ch['id'],
'label' => $ch['name'],
'disabled' => true,
'children' => $children,
];
}
return $this->success('', [
'list' => $tree,
]);
}
/**
* 添加重写管理员只选顶级组admin_group_id 后端自动写入)
* @throws Throwable
*/
protected function _add(): Response
{
if ($this->request && $this->request->method() === 'POST') {
$data = $this->request->post();
if (!$data) {
return $this->error(__('Parameter %s can not be empty', ['']));
}
$data = $this->applyInputFilter($data);
$data = $this->excludeFields($data);
if (!$this->auth->isSuperAdmin()) {
$data['admin_id'] = $this->auth->id;
}
$adminId = $data['admin_id'] ?? null;
if ($adminId === null || $adminId === '') {
return $this->error(__('Parameter %s can not be empty', ['admin_id']));
}
// 不允许前端填写,统一后端根据管理员所属“顶级角色组(pid=0)”自动回填
if (array_key_exists('admin_group_id', $data)) {
unset($data['admin_group_id']);
}
$topGroupId = Db::name('admin_group_access')
->alias('aga')
->join('admin_group ag', 'aga.group_id = ag.id')
->where('aga.uid', $adminId)
->where('ag.pid', 0)
->value('ag.id');
if ($topGroupId === null || $topGroupId === '') {
return $this->error(__('Record not found'));
}
$data['admin_group_id'] = $topGroupId;
if ($this->dataLimit && $this->dataLimitFieldAutoFill) {
$data[$this->dataLimitField] = $this->auth->id;
}
$result = false;
$this->model->startTrans();
try {
if ($this->modelValidate) {
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
if (class_exists($validate)) {
$validate = new $validate();
if ($this->modelSceneValidate) {
$validate->scene('add');
}
$validate->check($data);
}
}
$result = $this->model->save($data);
$this->model->commit();
} catch (Throwable $e) {
$this->model->rollback();
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'));
}
return $this->error(__('Parameter error'));
}
/**
* 编辑重写管理员只选顶级组admin_group_id 后端自动写入)
* @throws Throwable
*/
protected function _edit(): Response
{
$pk = $this->model->getPk();
$id = $this->request ? ($this->request->post($pk) ?? $this->request->get($pk)) : null;
$row = $this->model->find($id);
if (!$row) {
return $this->error(__('Record not found'));
}
$dataLimitAdminIds = $this->getDataLimitAdminIds();
if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) {
return $this->error(__('You have no permission'));
}
if ($this->request && $this->request->method() === 'POST') {
$data = $this->request->post();
if (!$data) {
return $this->error(__('Parameter %s can not be empty', ['']));
}
$data = $this->applyInputFilter($data);
$data = $this->excludeFields($data);
// 不允许前端填写,统一后端根据管理员所属“顶级角色组(pid=0)”自动回填
if (array_key_exists('admin_group_id', $data)) {
unset($data['admin_group_id']);
}
if (!$this->auth->isSuperAdmin()) {
unset($data['admin_id']);
}
$nextAdminId = array_key_exists('admin_id', $data) ? $data['admin_id'] : ($row['admin_id'] ?? null);
if ($nextAdminId !== null && $nextAdminId !== '') {
$topGroupId = Db::name('admin_group_access')
->alias('aga')
->join('admin_group ag', 'aga.group_id = ag.id')
->where('aga.uid', $nextAdminId)
->where('ag.pid', 0)
->value('ag.id');
if ($topGroupId === null || $topGroupId === '') {
return $this->error(__('Record not found'));
}
$data['admin_group_id'] = $topGroupId;
}
$result = false;
$this->model->startTrans();
try {
if ($this->modelValidate) {
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
if (class_exists($validate)) {
$validate = new $validate();
if ($this->modelSceneValidate) {
$validate->scene('edit');
}
$data[$pk] = $row[$pk];
$validate->check($data);
}
}
$result = $row->save($data);
$this->model->commit();
} catch (Throwable $e) {
$this->model->rollback();
return $this->error($e->getMessage());
}
if ($result !== false) {
return $this->success(__('Update successful'));
}
return $this->error(__('No rows updated'));
}
return $this->success('', [
'row' => $row
]);
}
/**
* 查看
* @throws Throwable
*/
protected function _index(): Response
{
// 如果是 select 则转发到 select 方法,若未重写该方法,其实还是继续执行 index
if ($this->request && $this->request->get('select')) {
return $this->select($this->request);
}
/**
* 1. withJoin 不可使用 alias 方法设置表别名,别名将自动使用关联模型名称(小写下划线命名规则)
* 2. 以下的别名设置了主表别名,同时便于拼接查询参数等
* 3. paginate 数据集可使用链式操作 each(function($item, $key) {}) 遍历处理
*/
list($where, $alias, $limit, $order) = $this->queryBuilder();
$res = $this->model
->withJoin($this->withJoinTable, $this->withJoinType)
->with($this->withJoinTable)
->visible(['adminGroup' => ['name'], 'admin' => ['username']])
->alias($alias)
->where($where)
->order($order)
->paginate($limit);
$list = $this->buildChannelListWithRealtimeUserCounts($res->items());
return $this->success('', [
'list' => $list,
'total' => $res->total(),
'remark' => get_route_remark(),
]);
}
/**
* 列表 user_count 按 game_user.game_channel_id 实时 COUNT与库字段无关用户增删改时会回写 game_channel.user_count
*
* @param iterable<int|string, mixed> $items
* @return list<array<string, mixed>>
*/
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<string|int> $ids
* @return list<int|string>
*/
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<string, mixed> $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 中对应的方法至此进行重写
*/
}