Files
webman-buildadmin/app/admin/controller/user/User.php
2026-04-18 15:19:36 +08:00

545 lines
20 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\user;
use Throwable;
use app\common\controller\Backend;
use support\think\Db;
use support\Response;
use Webman\Http\Request as WebmanRequest;
/**
* 用户管理
*/
class User extends Backend
{
/**
* User模型对象
* @var object|null
* @phpstan-var \app\common\model\User|null
*/
protected ?object $model = null;
protected array|string $preExcludeFields = ['id', 'uuid', 'create_time', 'update_time', 'invite_code', 'coin', 'total_deposit_coin', 'total_withdraw_coin', 'bet_flow_coin'];
protected array $withJoinTable = ['channel', 'admin'];
protected string|array $quickSearchField = ['id', 'username', 'phone'];
protected bool $modelSceneValidate = true;
protected function initController(WebmanRequest $request): ?Response
{
$this->model = new \app\common\model\User();
return null;
}
/**
* 添加重写password 使用 Admin 同款加密uuid 为 10 位唯一对外标识)
* @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);
$inviteResolved = $this->applyInviteCodeToUserChannel($data);
if ($inviteResolved !== null) {
return $inviteResolved;
}
$data = $this->excludeFields($data);
$password = $data['password'] ?? null;
if (!is_string($password) || trim($password) === '') {
return $this->error(__('Parameter %s can not be empty', ['password']));
}
$data['password'] = hash_password($password);
$username = $data['username'] ?? '';
$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']));
}
$data['uuid'] = \app\common\model\User::generateUniquePublicCode10();
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) {
return $this->success(__('Added successfully'));
}
return $this->error(__('No rows were added'));
}
return $this->error(__('Parameter error'));
}
/**
* 编辑重写password 使用 Admin 同款加密uuid 创建后不因改名改渠道自动变更)
* @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);
if (array_key_exists('password', $data)) {
$password = $data['password'];
if (!is_string($password) || trim($password) === '') {
unset($data['password']);
} else {
$data['password'] = hash_password($password);
}
}
$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'));
}
// GET: 返回编辑数据时,剔除敏感字段
unset($row['password'], $row['salt'], $row['token'], $row['refresh_token']);
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(['channel' => ['name'], 'admin' => ['username']])
->alias($alias)
->where($where)
->order($order)
->paginate($limit);
return $this->success('', [
'list' => $res->items(),
'total' => $res->total(),
'remark' => get_route_remark(),
]);
}
/**
* 后台钱包加减点(不允许在用户编辑表单直接改余额)
*/
public function walletAdjust(WebmanRequest $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
if ($request->method() !== 'POST') {
return $this->error(__('Parameter error'));
}
$userIdRaw = $request->post('user_id');
$userId = is_numeric(strval($userIdRaw)) ? intval(strval($userIdRaw)) : 0;
if ($userId <= 0) {
return $this->error(__('Parameter error'));
}
$opRaw = $request->post('op');
$op = is_string($opRaw) ? trim($opRaw) : '';
if (!in_array($op, ['credit', 'deduct'], true)) {
return $this->error('操作类型不正确');
}
$amountRaw = $request->post('amount');
$amountText = is_string($amountRaw) || is_numeric($amountRaw) ? trim(strval($amountRaw)) : '';
if ($amountText === '' || !is_numeric($amountText)) {
return $this->error('金额格式不正确');
}
if (bccomp($amountText, '0', 4) <= 0) {
return $this->error('金额必须大于0');
}
$remarkRaw = $request->post('remark');
$remark = is_string($remarkRaw) ? trim($remarkRaw) : '';
$adminName = is_string($this->auth->username ?? null) ? $this->auth->username : ('#' . strval($this->auth->id));
$amountForRemark = self::formatAmountForDisplay($amountText);
if ($remark === '') {
$actionText = $op === 'credit' ? '加点' : '扣点';
$remark = '后台管理员(' . $adminName . '' . $actionText . $amountForRemark . '(值)';
}
$user = $this->model->where('id', $userId)->find();
if (!$user) {
return $this->error(__('Record not found'));
}
$dataLimitAdminIds = $this->getDataLimitAdminIds();
if ($dataLimitAdminIds && !in_array($user[$this->dataLimitField], $dataLimitAdminIds)) {
return $this->error(__('You have no permission'));
}
$channelIdRaw = $user['channel_id'] ?? null;
$channelId = is_numeric(strval($channelIdRaw)) ? intval(strval($channelIdRaw)) : null;
$before = strval($user['coin'] ?? '0');
$delta = self::normalizeAmountScale($amountText, 4);
if ($op === 'credit') {
$after = bcadd($before, $delta, 4);
$bizType = 'admin_credit';
$direction = 1;
} else {
if (bccomp($before, $delta, 4) < 0) {
return $this->error('余额不足,扣点失败');
}
$after = bcsub($before, $delta, 4);
$bizType = 'admin_deduct';
$direction = 2;
}
$now = time();
$idem = 'admin_adjust_' . $userId . '_' . $this->auth->id . '_' . $now . '_' . random_int(1000, 9999);
Db::startTrans();
try {
Db::name('user')->where('id', $userId)->update([
'coin' => $after,
'update_time' => $now,
]);
Db::name('user_wallet_record')->insert([
'user_id' => $userId,
'channel_id' => $channelId,
'biz_type' => $bizType,
'direction' => $direction,
'amount' => $delta,
'balance_before' => $before,
'balance_after' => $after,
'ref_type' => 'admin_user_wallet_adjust',
'ref_id' => null,
'idempotency_key' => $idem,
'operator_admin_id' => intval(strval($this->auth->id)),
'remark' => substr($remark, 0, 500),
'create_time' => $now,
]);
Db::commit();
} catch (Throwable $e) {
Db::rollback();
return $this->error($e->getMessage());
}
return $this->success('钱包调整成功', [
'user_id' => $userId,
'coin_before' => self::formatAmountForDisplay($before),
'coin_after' => self::formatAmountForDisplay($after),
'amount' => self::formatAmountForDisplay($delta),
'op' => $op,
]);
}
private static function normalizeAmountScale(string $amount, int $scale): string
{
$raw = trim(str_replace(',', '.', $amount));
if ($raw === '') {
return '0';
}
$negative = false;
if (str_starts_with($raw, '-')) {
$negative = true;
$raw = ltrim(substr($raw, 1));
}
if (!str_contains($raw, '.')) {
$v = ltrim($raw, '0');
$v = $v === '' ? '0' : $v;
return $negative ? ('-' . $v) : $v;
}
[$intPart, $fracPart] = explode('.', $raw, 2);
$intPart = ltrim($intPart, '0');
$intPart = $intPart === '' ? '0' : $intPart;
$fracPart = preg_replace('/\D+/', '', $fracPart) ?? '';
if (strlen($fracPart) > $scale) {
$fracPart = substr($fracPart, 0, $scale);
} else {
$fracPart = str_pad($fracPart, $scale, '0');
}
$v = $intPart . '.' . $fracPart;
return $negative ? ('-' . $v) : $v;
}
private static function formatAmountForDisplay(string $amount): string
{
$normalized = self::normalizeAmountScale($amount, 4);
$negative = false;
if (str_starts_with($normalized, '-')) {
$negative = true;
$normalized = substr($normalized, 1);
}
$parts = explode('.', $normalized, 2);
$intPart = $parts[0] ?? '0';
$fracPart = $parts[1] ?? '0000';
$displayFrac = substr($fracPart, 0, 2);
$v = $intPart . '.' . str_pad($displayFrac, 2, '0');
return $negative ? ('-' . $v) : $v;
}
/**
* 角色组 → 管理员树(仅当前账号可管理的角色组及其下管理员;用于游戏用户归属)
* 同一管理员若属于多个组,只挂在 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)));
}
/**
* 请求中的 `invite_code`(子代理 admin.invite_code解析为 `channel_id`,并写入 `register_invite_code`、`admin_id`。
* 若同时提交 `channel_id` / `admin_id`,须与邀请码对应记录一致。
*/
private function applyInviteCodeToUserChannel(array &$data): ?Response
{
if (!array_key_exists('invite_code', $data)) {
return null;
}
$raw = $data['invite_code'];
unset($data['invite_code']);
$inviteCode = is_string($raw) ? trim($raw) : '';
if ($inviteCode === '') {
return null;
}
$row = Db::name('admin')->field(['id', 'channel_id'])->where('invite_code', $inviteCode)->find();
if (!$row) {
return $this->error(__('Invite code does not exist'));
}
$cid = $row['channel_id'] ?? null;
if ($cid === null || $cid === '') {
return $this->error(__('Invite code not bound to channel'));
}
$cidInt = intval(trim((string) $cid));
if ($cidInt <= 0) {
return $this->error(__('Invite code not bound to channel'));
}
if (isset($data['channel_id']) && $data['channel_id'] !== null && $data['channel_id'] !== '') {
$existing = intval(trim((string) $data['channel_id']));
if ($existing > 0 && $existing !== $cidInt) {
return $this->error(__('Parameter error'));
}
}
$data['channel_id'] = $cidInt;
$data['register_invite_code'] = $inviteCode;
$aidRaw = $row['id'] ?? null;
if ($aidRaw === null || $aidRaw === '') {
return $this->error(__('Parameter error'));
}
$aidInt = intval(trim((string) $aidRaw));
if ($aidInt <= 0) {
return $this->error(__('Parameter error'));
}
if (isset($data['admin_id']) && $data['admin_id'] !== null && $data['admin_id'] !== '') {
$reqAid = intval(trim((string) $data['admin_id']));
if ($reqAid > 0 && $reqAid !== $aidInt) {
return $this->error(__('Parameter error'));
}
}
$data['admin_id'] = $aidInt;
return null;
}
/**
* 若需重写查看、编辑、删除等方法,请复制 @see \app\admin\library\traits\Backend 中对应的方法至此进行重写
*/
}