Files
webman-buildadmin/app/admin/controller/auth/Admin.php
2026-05-29 11:06:24 +08:00

752 lines
27 KiB
PHP

<?php
declare(strict_types=1);
namespace app\admin\controller\auth;
use ba\Random;
use ba\Tree;
use Throwable;
use support\think\Db;
use support\validation\Validator;
use support\validation\ValidationException;
use app\common\controller\Backend;
use app\common\service\AdminCommissionDistributionService;
use app\admin\model\Admin as AdminModel;
use support\Response;
use Webman\Http\Request;
class Admin extends Backend
{
/**
* 分红比例余量查询(表单提示用)
*/
protected array $noNeedPermission = ['commissionShareRemainder'];
protected ?object $model = null;
protected array|string $preExcludeFields = ['create_time', 'update_time', 'password', 'salt', 'login_failure', 'last_login_time', 'last_login_ip'];
protected array|string $quickSearchField = ['username', 'nickname'];
/** 使用 parent_admin_id 树过滤,不用角色组 dataLimit */
protected string|int|bool $dataLimit = false;
protected string $dataLimitField = 'id';
protected function initController(Request $request): ?Response
{
$this->model = new AdminModel();
return null;
}
public function index(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
if ($request->get('select') ?? $request->post('select')) {
$selectRes = $this->select($request);
if ($selectRes !== null) {
return $selectRes;
}
}
list($where, $alias, $limit, $order) = $this->queryBuilder();
$adminAlias = $alias[strtolower($this->model->getTable())] ?? 'admin';
$query = $this->model
->withoutField('login_failure,password,salt')
->alias($alias)
->where($where);
$visibleIds = AdminCommissionDistributionService::getVisibleAdminIdsForOperator(
intval($this->auth->id),
$this->auth->isSuperAdmin()
);
if ($visibleIds !== []) {
$query->where($adminAlias . '.id', 'in', $visibleIds);
}
$topGroup = $request->get('top_group') ?? $request->post('top_group');
if ($topGroup === '1' || $topGroup === 1 || $topGroup === true) {
$query = $query
->join('admin_group_access aga', $adminAlias . '.id = aga.uid')
->join('admin_group ag', 'aga.group_id = ag.id')
->where('ag.pid', 0)
->distinct(true);
}
$rows = $query->order($order)->select()->toArray();
$rows = $this->enrichAdminRows($rows);
foreach ($rows as $k => $row) {
$parentId = intval($row['parent_admin_id'] ?? 0);
$rows[$k]['pid'] = $parentId > 0 ? $parentId : 0;
}
$tree = Tree::instance()->assembleChild($rows, 'pid', 'id');
return $this->success('', [
'list' => $tree,
'total' => count($rows),
'remark' => get_route_remark(),
]);
}
/**
* 查询同上级下剩余可分配分红比例(表单提示)
*/
public function commissionShareRemainder(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
$parentAdminId = intval($request->get('parent_admin_id', 0));
$excludeId = intval($request->get('exclude_id', 0));
if ($parentAdminId <= 0) {
return $this->success('', [
'used_rate' => '0.00',
'remaining_rate' => '100.00',
'parent_has_no_share' => false,
]);
}
if (!$this->canManageAdminId($parentAdminId)) {
return $this->error(__('You have no permission'));
}
$stats = AdminCommissionDistributionService::getShareRemainder(
$parentAdminId,
$excludeId > 0 ? $excludeId : null
);
return $this->success('', [
'used_rate' => $stats['used_rate'],
'remaining_rate' => $stats['remaining_rate'],
'parent_has_no_share' => bccomp($stats['remaining_rate'], '0', 2) <= 0,
]);
}
/**
* 远程下拉(重写:支持 top_group=1 仅返回顶级组管理员)
*/
protected function _select(): Response
{
if (empty($this->model)) {
return $this->success('', [
'list' => [],
'total' => 0,
]);
}
$pk = $this->model->getPk();
$fields = [$pk];
$quickSearchArr = is_array($this->quickSearchField) ? $this->quickSearchField : explode(',', (string) $this->quickSearchField);
foreach ($quickSearchArr as $f) {
$f = trim((string) $f);
if ($f === '') {
continue;
}
$f = str_contains($f, '.') ? substr($f, strrpos($f, '.') + 1) : $f;
if ($f !== '' && !in_array($f, $fields, true)) {
$fields[] = $f;
}
}
list($where, $alias, $limit, $order) = $this->queryBuilder();
$modelTable = strtolower($this->model->getTable());
$mainAlias = ($alias[$modelTable] ?? $modelTable) . '.';
$selectFields = [];
foreach ($fields as $f) {
$f = trim((string) $f);
if ($f === '') {
continue;
}
$selectFields[] = $mainAlias . $f . ' as ' . $f;
}
$qualifiedOrder = [];
if (is_array($order)) {
foreach ($order as $k => $v) {
$k = (string) $k;
$qualifiedOrder[str_contains($k, '.') ? $k : ($mainAlias . $k)] = $v;
}
}
$query = $this->model
->field($selectFields)
->alias($alias)
->where($where);
$visibleIds = AdminCommissionDistributionService::getVisibleAdminIdsForOperator(
intval($this->auth->id),
$this->auth->isSuperAdmin()
);
if ($visibleIds !== []) {
$query->where($mainAlias . 'id', 'in', $visibleIds);
}
$topGroup = $this->request ? ($this->request->get('top_group') ?? $this->request->post('top_group')) : null;
if ($topGroup === '1' || $topGroup === 1 || $topGroup === true) {
$query = $query
->join('admin_group_access aga', $mainAlias . 'id = aga.uid')
->join('admin_group ag', 'aga.group_id = ag.id')
->where('ag.pid', 0)
->distinct(true);
}
$res = $query
->order($qualifiedOrder ?: $order)
->paginate($limit);
return $this->success('', [
'list' => $res->items(),
'total' => $res->total(),
]);
}
public function add(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
if ($request->method() === 'POST') {
$data = $request->post();
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(__('Please select exactly one role group'));
}
if ($this->modelValidate) {
try {
$rules = [
'username' => 'required|string|regex:/^[a-zA-Z][a-zA-Z0-9_]{2,15}$/|unique:admin,username',
'nickname' => 'required|string',
'password' => 'required|string|regex:/^(?!.*[&<>"\'\n\r]).{6,32}$/',
'email' => 'email|unique:admin,email',
'mobile' => 'regex:/^1[3-9]\d{9}$/|unique:admin,mobile',
'group_arr' => 'required|array',
];
$messages = [
'username.regex' => __('Please input correct username'),
'password.regex' => __('Please input correct password'),
];
Validator::make($data, $rules, $messages)->validate();
} catch (ValidationException $e) {
return $this->error($e->getMessage());
}
}
$passwd = $data['password'] ?? '';
$data = $this->excludeFields($data);
$creatorChannelId = $this->getCreatorChannelId();
$groupChannelId = $this->resolveChannelIdFromPrimaryGroup($data['group_arr'] ?? []);
if (!$this->auth->isSuperAdmin()) {
if ($creatorChannelId === null || $creatorChannelId === '') {
return $this->error(__('You have no permission'));
}
if ($groupChannelId === null || $groupChannelId === '') {
return $this->error(__('Selected role group is not bound to a channel'));
}
if ((string) $groupChannelId !== (string) $creatorChannelId) {
return $this->error(__('Selected role group channel does not match current account'));
}
$data['channel_id'] = $creatorChannelId;
$data['parent_admin_id'] = $this->auth->id;
} else {
$postedChannel = $data['channel_id'] ?? null;
if ($postedChannel === null || $postedChannel === '') {
$data['channel_id'] = ($groupChannelId === null || $groupChannelId === '') ? null : $groupChannelId;
}
}
$parentErr = $this->normalizeParentAndShareFields($data, null);
if ($parentErr !== null) {
return $this->error($parentErr);
}
$data['invite_code'] = $this->generateUniqueInviteCode();
$result = false;
if (!empty($data['group_arr'])) {
$authRes = $this->checkGroupAuth($data['group_arr']);
if ($authRes !== null) {
return $authRes;
}
}
$this->model->startTrans();
try {
$result = $this->model->save($data);
if (!empty($data['group_arr'])) {
$groupAccess = [];
foreach ($data['group_arr'] as $datum) {
$groupAccess[] = [
'uid' => $this->model->id,
'group_id' => $datum,
];
}
Db::name('admin_group_access')->insertAll($groupAccess);
}
$this->model->commit();
if (!empty($passwd)) {
$this->model->resetPassword($this->model->id, $passwd);
}
} 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'));
}
public function edit(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
$pk = $this->model->getPk();
$id = $request->get($pk) ?? $request->post($pk);
$row = $this->model->find($id);
if (!$row) {
return $this->error(__('Record not found'));
}
if (!$this->canManageAdminId(intval($row['id']))) {
return $this->error(__('You have no permission'));
}
if ($request->method() === 'POST') {
$data = $request->post();
if (!$data) {
return $this->error(__('Parameter %s can not be empty', ['']));
}
$isSelfEdit = (int) $this->auth->id === (int) $id;
if ($isSelfEdit) {
unset($data['group_arr'], $data['group_name_arr'], $data['parent_admin_id'], $data['commission_share_rate']);
}
$editGroupArr = null;
if (array_key_exists('group_arr', $data)) {
$data = $this->normalizeSingleGroup($data);
if (!$this->hasSingleGroup($data['group_arr'] ?? null)) {
return $this->error(__('Please select exactly one role group'));
}
$editGroupArr = $data['group_arr'];
} elseif (!$isSelfEdit) {
return $this->error(__('Please select exactly one role group'));
}
if ($this->modelValidate) {
try {
$rules = [
'username' => 'required|string|regex:/^[a-zA-Z][a-zA-Z0-9_]{2,15}$/|unique:admin,username,' . $id,
'nickname' => 'required|string',
'password' => 'nullable|string|regex:/^(?!.*[&<>"\'\n\r]).{6,32}$/',
'email' => 'email|unique:admin,email,' . $id,
'mobile' => 'regex:/^1[3-9]\d{9}$/|unique:admin,mobile,' . $id,
];
if (array_key_exists('group_arr', $data)) {
$rules['group_arr'] = 'required|array';
}
$messages = [
'username.regex' => __('Please input correct username'),
'password.regex' => __('Please input correct password'),
];
Validator::make($data, $rules, $messages)->validate();
} catch (ValidationException $e) {
return $this->error($e->getMessage());
}
}
if ($this->auth->id == $data['id'] && ($data['status'] ?? '') == 'disable') {
return $this->error(__('Please use another administrator account to disable the current account!'));
}
if (!empty($data['password'])) {
$this->model->resetPassword($row->id, $data['password']);
}
$groupAccess = [];
if (!$isSelfEdit && !empty($editGroupArr)) {
$checkGroups = [];
$rowGroupArr = $row->group_arr ?? [];
foreach ($editGroupArr as $datum) {
if (!in_array($datum, $rowGroupArr)) {
$checkGroups[] = $datum;
}
$groupAccess[] = [
'uid' => $id,
'group_id' => $datum,
];
}
$authRes = $this->checkGroupAuth($checkGroups);
if ($authRes !== null) {
return $authRes;
}
}
$data = $this->excludeFields($data);
unset($data['invite_code'], $data['group_arr'], $data['group_name_arr']);
if (!$isSelfEdit && $editGroupArr !== null) {
$creatorChannelId = $this->getCreatorChannelId();
$groupChannelId = $this->resolveChannelIdFromPrimaryGroup($editGroupArr);
if (!$this->auth->isSuperAdmin()) {
if ($creatorChannelId === null || $creatorChannelId === '') {
return $this->error(__('You have no permission'));
}
if ($groupChannelId === null || $groupChannelId === '') {
return $this->error(__('Selected role group is not bound to a channel'));
}
if ((string) $groupChannelId !== (string) $creatorChannelId) {
return $this->error(__('Selected role group channel does not match current account'));
}
$data['channel_id'] = $creatorChannelId;
} else {
$data['channel_id'] = ($groupChannelId === null || $groupChannelId === '') ? null : $groupChannelId;
}
}
if (!$isSelfEdit) {
if (!$this->auth->isSuperAdmin()) {
unset($data['parent_admin_id']);
}
$parentErr = $this->normalizeParentAndShareFields($data, intval($id));
if ($parentErr !== null) {
return $this->error($parentErr);
}
}
$result = false;
$this->model->startTrans();
try {
$result = $row->save($data);
if (!$isSelfEdit) {
Db::name('admin_group_access')
->where('uid', $id)
->delete();
if ($groupAccess) {
Db::name('admin_group_access')->insertAll($groupAccess);
}
}
$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'));
}
unset($row['salt'], $row['login_failure']);
$row['password'] = '';
$rowData = $row->toArray();
$enriched = $this->enrichAdminRows([$rowData]);
$rowData = $enriched[0] ?? $rowData;
$parentId = intval($rowData['parent_admin_id'] ?? 0);
if ($parentId > 0) {
$remainder = AdminCommissionDistributionService::getShareRemainder($parentId, intval($id));
$rowData['share_remainder'] = $remainder;
}
return $this->success('', [
'row' => $rowData,
]);
}
public function del(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
$ids = $request->get('ids') ?? $request->post('ids') ?? [];
$ids = is_array($ids) ? $ids : [];
if ($ids === []) {
return $this->error(__('Parameter error'));
}
$data = $this->model->where($this->model->getPk(), 'in', $ids)->select();
$count = 0;
$this->model->startTrans();
try {
foreach ($data as $v) {
if ($v->id == $this->auth->id) {
continue;
}
if (!$this->canManageAdminId(intval($v->id))) {
continue;
}
$childCount = Db::name('admin')->where('parent_admin_id', $v->id)->count();
if ($childCount > 0) {
$this->model->rollback();
return $this->error(__('Cannot delete administrator with sub-agents'));
}
$count += $v->delete();
Db::name('admin_group_access')
->where('uid', $v['id'])
->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'));
}
public function select(Request $request): Response
{
return parent::select($request);
}
private function checkGroupAuth(array $groups): ?Response
{
if ($this->auth->isSuperAdmin()) {
return null;
}
$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!'));
}
}
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'];
}
return null;
}
/**
* @param array<int|string> $groupIds
*/
private function resolveChannelIdFromPrimaryGroup(array $groupIds): mixed
{
if ($groupIds === []) {
return null;
}
$gid = $groupIds[0];
if ($gid === null || $gid === '') {
return null;
}
return Db::name('admin_group')->where('id', $gid)->value('channel_id');
}
private function canManageAdminId(int $adminId): bool
{
if ($this->auth->isSuperAdmin()) {
return true;
}
if ($adminId === intval($this->auth->id)) {
return true;
}
$visible = AdminCommissionDistributionService::getVisibleAdminIdsForOperator(
intval($this->auth->id),
false
);
return in_array($adminId, $visible, true);
}
/**
* @param array<string, mixed> $data
*/
private function normalizeParentAndShareFields(array &$data, ?int $editAdminId): ?string
{
$parentId = isset($data['parent_admin_id']) && $data['parent_admin_id'] !== '' && $data['parent_admin_id'] !== null
? intval($data['parent_admin_id'])
: 0;
if ($parentId <= 0 && $editAdminId !== null && $editAdminId > 0) {
$existingParent = Db::name('admin')->where('id', $editAdminId)->value('parent_admin_id');
$parentId = intval($existingParent ?? 0);
}
if ($parentId <= 0) {
$data['parent_admin_id'] = null;
$data['commission_share_rate'] = null;
return null;
}
if ($editAdminId !== null && $parentId === $editAdminId) {
return (string) __('Cannot set yourself as parent administrator');
}
$parent = Db::name('admin')->where('id', $parentId)->find();
if (!is_array($parent)) {
return (string) __('Invalid parent administrator');
}
if (!$this->canManageAdminId($parentId) && !$this->auth->isSuperAdmin()) {
return (string) __('You have no permission');
}
$channelId = $data['channel_id'] ?? null;
if ($channelId !== null && $channelId !== '' && (string) ($parent['channel_id'] ?? '') !== (string) $channelId) {
return (string) __('Parent administrator must belong to the same channel');
}
if (($channelId === null || $channelId === '') && !empty($parent['channel_id'])) {
$data['channel_id'] = $parent['channel_id'];
}
$shareErr = AdminCommissionDistributionService::validateCommissionShareRate(
$parentId,
$data['commission_share_rate'] ?? null,
$editAdminId
);
if ($shareErr !== null) {
return $shareErr;
}
$data['commission_share_rate'] = bcadd(strval($data['commission_share_rate'] ?? '0'), '0', 2);
$data['parent_admin_id'] = $parentId;
return null;
}
/**
* @param array<int, array<string, mixed>> $rows
* @return array<int, array<string, mixed>>
*/
private function enrichAdminRows(array $rows): array
{
if ($rows === []) {
return [];
}
$adminIds = [];
$channelIds = [];
$parentIds = [];
foreach ($rows as $row) {
$aid = intval($row['id'] ?? 0);
if ($aid > 0) {
$adminIds[] = $aid;
}
$cid = $row['channel_id'] ?? null;
if ($cid !== null && $cid !== '') {
$channelIds[] = intval($cid);
}
$pid = intval($row['parent_admin_id'] ?? 0);
if ($pid > 0) {
$parentIds[] = $pid;
}
}
$channelNames = $channelIds !== []
? Db::name('channel')->where('id', 'in', array_unique($channelIds))->column('name', 'id')
: [];
$parentNames = $parentIds !== []
? Db::name('admin')->where('id', 'in', array_unique($parentIds))->column('username', 'id')
: [];
$groupMap = [];
if ($adminIds !== []) {
$accessRows = Db::name('admin_group_access')->where('uid', 'in', $adminIds)->select()->toArray();
$groupIds = array_unique(array_map(static fn(array $r): int => intval($r['group_id'] ?? 0), $accessRows));
$groupNames = $groupIds !== []
? Db::name('admin_group')->where('id', 'in', $groupIds)->column('name', 'id')
: [];
foreach ($accessRows as $access) {
$uid = intval($access['uid'] ?? 0);
$gid = intval($access['group_id'] ?? 0);
if ($uid <= 0 || $gid <= 0) {
continue;
}
if (!isset($groupMap[$uid])) {
$groupMap[$uid] = [];
}
$name = $groupNames[$gid] ?? '';
if ($name !== '') {
$groupMap[$uid][] = $name;
}
}
}
foreach ($rows as $k => $row) {
$cid = $row['channel_id'] ?? null;
$rows[$k]['channel_name'] = ($cid !== null && $cid !== '') ? strval($channelNames[intval($cid)] ?? '') : '';
$pid = intval($row['parent_admin_id'] ?? 0);
$rows[$k]['parent_admin_username'] = $pid > 0 ? strval($parentNames[$pid] ?? '') : '';
$aid = intval($row['id'] ?? 0);
$rows[$k]['group_name_arr'] = $groupMap[$aid] ?? [];
}
return $rows;
}
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 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;
}
}