管理后台左侧列表调整
This commit is contained in:
@@ -8,6 +8,7 @@ export const HqOperationAction = {
|
||||
PARTNER_ACCOUNT_UPDATE: 'PARTNER_ACCOUNT_UPDATE',
|
||||
HQ_ACCOUNT_CREATE: 'HQ_ACCOUNT_CREATE',
|
||||
HQ_ACCOUNT_UPDATE: 'HQ_ACCOUNT_UPDATE',
|
||||
HQ_PERMISSION_UPDATE: 'HQ_PERMISSION_UPDATE',
|
||||
USER_DELETE: 'USER_DELETE',
|
||||
USER_BATCH_DELETE: 'USER_BATCH_DELETE',
|
||||
ORDER_SHIP: 'ORDER_SHIP',
|
||||
@@ -50,7 +51,8 @@ export const HQ_OPERATION_ACTION_LABELS: Record<string, string> = {
|
||||
[HqOperationAction.PARTNER_ACCOUNT_CREATE]: '新增合伙人账户',
|
||||
[HqOperationAction.PARTNER_ACCOUNT_UPDATE]: '编辑合伙人账户',
|
||||
[HqOperationAction.HQ_ACCOUNT_CREATE]: '新增 HQ 管理员',
|
||||
[HqOperationAction.HQ_ACCOUNT_UPDATE]: '编辑 HQ 管理员/权限',
|
||||
[HqOperationAction.HQ_ACCOUNT_UPDATE]: '编辑 HQ 管理员',
|
||||
[HqOperationAction.HQ_PERMISSION_UPDATE]: '配置 HQ 权限',
|
||||
[HqOperationAction.USER_DELETE]: '删除用户',
|
||||
[HqOperationAction.USER_BATCH_DELETE]: '批量删除用户',
|
||||
[HqOperationAction.ORDER_SHIP]: '订单发货',
|
||||
|
||||
@@ -4,6 +4,31 @@ import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import type { AdminHqAccountsQueryDto } from './dto/admin-query.dto';
|
||||
import type { CreateHqAccountDto, UpdateHqAccountDto } from './dto/admin-mutate.dto';
|
||||
import { hashPassword } from '../../common/crypto/password.util';
|
||||
|
||||
function mapHqAccountRow(account: {
|
||||
id: bigint;
|
||||
phone: string;
|
||||
loginName: string | null;
|
||||
passwordHash: string | null;
|
||||
name: string;
|
||||
adminRole: string;
|
||||
status: string;
|
||||
lastLoginAt: Date | null;
|
||||
createdAt: Date;
|
||||
}) {
|
||||
return {
|
||||
id: account.id,
|
||||
phone: account.phone,
|
||||
loginName: account.loginName,
|
||||
hasPassword: !!account.passwordHash,
|
||||
name: account.name,
|
||||
adminRole: account.adminRole,
|
||||
status: account.status,
|
||||
lastLoginAt: account.lastLoginAt,
|
||||
createdAt: account.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AdminHqAccountsService {
|
||||
@@ -23,42 +48,130 @@ export class AdminHqAccountsService {
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
select: {
|
||||
id: true,
|
||||
phone: true,
|
||||
loginName: true,
|
||||
passwordHash: true,
|
||||
name: true,
|
||||
adminRole: true,
|
||||
status: true,
|
||||
lastLoginAt: true,
|
||||
createdAt: true,
|
||||
},
|
||||
}),
|
||||
this.prisma.hqAccount.count({ where }),
|
||||
]);
|
||||
return serializeBigInt({ items, total, page, pageSize });
|
||||
return serializeBigInt({
|
||||
items: items.map(mapHqAccountRow),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
async detail(id: bigint) {
|
||||
const account = await this.prisma.hqAccount.findUnique({ where: { id } });
|
||||
const account = await this.prisma.hqAccount.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
id: true,
|
||||
phone: true,
|
||||
loginName: true,
|
||||
passwordHash: true,
|
||||
name: true,
|
||||
adminRole: true,
|
||||
status: true,
|
||||
lastLoginAt: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
if (!account) throw new NotFoundException('HQ 账号不存在');
|
||||
return serializeBigInt(account);
|
||||
return serializeBigInt(mapHqAccountRow(account));
|
||||
}
|
||||
|
||||
async create(dto: CreateHqAccountDto) {
|
||||
const exists = await this.prisma.hqAccount.findUnique({ where: { phone: dto.phone } });
|
||||
if (exists) throw new BadRequestException('手机号已存在');
|
||||
const adminRole = (dto.adminRole ?? 'OPS') as 'SUPER_ADMIN' | 'OPS' | 'FINANCE' | 'CUSTOMER_SERVICE';
|
||||
|
||||
if (dto.credentialType === 'phone') {
|
||||
if (!dto.phone?.trim()) throw new BadRequestException('请填写手机号');
|
||||
const phone = dto.phone.trim();
|
||||
const exists = await this.prisma.hqAccount.findUnique({ where: { phone } });
|
||||
if (exists) throw new BadRequestException('手机号已存在');
|
||||
const account = await this.prisma.hqAccount.create({
|
||||
data: { phone, name: dto.name, adminRole },
|
||||
});
|
||||
return serializeBigInt(mapHqAccountRow({ ...account, passwordHash: null }));
|
||||
}
|
||||
|
||||
if (!dto.loginName?.trim() || !dto.password) {
|
||||
throw new BadRequestException('账号密码模式需填写用户名和密码');
|
||||
}
|
||||
const loginName = dto.loginName.trim();
|
||||
const loginTaken = await this.prisma.hqAccount.findUnique({ where: { loginName } });
|
||||
if (loginTaken) throw new BadRequestException('用户名已存在');
|
||||
|
||||
const phone = dto.phone?.trim() || (await this.generatePlaceholderPhone());
|
||||
const phoneTaken = await this.prisma.hqAccount.findUnique({ where: { phone } });
|
||||
if (phoneTaken) throw new BadRequestException('手机号已存在');
|
||||
|
||||
const account = await this.prisma.hqAccount.create({
|
||||
data: {
|
||||
phone: dto.phone,
|
||||
phone,
|
||||
loginName,
|
||||
passwordHash: hashPassword(dto.password),
|
||||
name: dto.name,
|
||||
adminRole: (dto.adminRole ?? 'OPS') as 'SUPER_ADMIN' | 'OPS' | 'FINANCE' | 'CUSTOMER_SERVICE',
|
||||
adminRole,
|
||||
},
|
||||
});
|
||||
return serializeBigInt(account);
|
||||
return serializeBigInt(mapHqAccountRow(account));
|
||||
}
|
||||
|
||||
async update(id: bigint, dto: UpdateHqAccountDto) {
|
||||
const current = await this.prisma.hqAccount.findUnique({ where: { id } });
|
||||
if (!current) throw new NotFoundException('HQ 账号不存在');
|
||||
|
||||
if (dto.loginName !== undefined) {
|
||||
const loginName = dto.loginName.trim();
|
||||
if (!loginName) throw new BadRequestException('用户名不能为空');
|
||||
const conflict = await this.prisma.hqAccount.findFirst({
|
||||
where: { loginName, id: { not: id } },
|
||||
});
|
||||
if (conflict) throw new BadRequestException('用户名已存在');
|
||||
}
|
||||
|
||||
const account = await this.prisma.hqAccount.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(dto.name !== undefined ? { name: dto.name } : {}),
|
||||
...(dto.loginName !== undefined ? { loginName: dto.loginName.trim() } : {}),
|
||||
...(dto.password ? { passwordHash: hashPassword(dto.password) } : {}),
|
||||
...(dto.adminRole !== undefined
|
||||
? { adminRole: dto.adminRole as 'SUPER_ADMIN' | 'OPS' | 'FINANCE' | 'CUSTOMER_SERVICE' }
|
||||
: {}),
|
||||
...(dto.status !== undefined ? { status: dto.status as 'ACTIVE' | 'DISABLED' } : {}),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
phone: true,
|
||||
loginName: true,
|
||||
passwordHash: true,
|
||||
name: true,
|
||||
adminRole: true,
|
||||
status: true,
|
||||
lastLoginAt: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
return serializeBigInt(account);
|
||||
return serializeBigInt(mapHqAccountRow(account));
|
||||
}
|
||||
|
||||
private async generatePlaceholderPhone(): Promise<string> {
|
||||
for (let i = 0; i < 8; i += 1) {
|
||||
const suffix = `${Date.now()}${Math.floor(Math.random() * 1000)}`.slice(-8);
|
||||
const phone = `199${suffix}`;
|
||||
const exists = await this.prisma.hqAccount.findUnique({ where: { phone } });
|
||||
if (!exists) return phone;
|
||||
}
|
||||
throw new BadRequestException('无法生成占位手机号,请手动填写');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Body, Controller, Get, Param, Put, UseGuards } from '@nestjs/common';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import { SuperAdminGuard } from '../../common/guards/super-admin.guard';
|
||||
import { HqOperation } from '../../common/hq-operation/hq-operation.decorator';
|
||||
import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants';
|
||||
import { AdminHqPermissionsService } from './admin-hq-permissions.service';
|
||||
import { SaveHqAccountPermissionsDto, SaveHqRolePermissionsDto } from './dto/admin-mutate.dto';
|
||||
|
||||
@Controller('admin/hq-permissions')
|
||||
@UseGuards(HqAuthGuard, SuperAdminGuard)
|
||||
export class AdminHqPermissionsController {
|
||||
constructor(private readonly service: AdminHqPermissionsService) {}
|
||||
|
||||
@Get('catalog')
|
||||
catalog() {
|
||||
return this.service.catalog();
|
||||
}
|
||||
|
||||
@Get('roles/:role')
|
||||
getRolePermissions(@Param('role') role: string) {
|
||||
return this.service.getRolePermissions(role);
|
||||
}
|
||||
|
||||
@Put('roles/:role')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.HQ_PERMISSION_UPDATE,
|
||||
refType: 'HQ_ROLE',
|
||||
refIdParam: 'role',
|
||||
includeBody: true,
|
||||
})
|
||||
saveRolePermissions(@Param('role') role: string, @Body() dto: SaveHqRolePermissionsDto) {
|
||||
return this.service.saveRolePermissions(role, dto.permissionKeys);
|
||||
}
|
||||
|
||||
@Get('accounts/:id')
|
||||
getAccountPermissions(@Param('id') id: string) {
|
||||
return this.service.getAccountPermissions(BigInt(id));
|
||||
}
|
||||
|
||||
@Put('accounts/:id')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.HQ_PERMISSION_UPDATE,
|
||||
refType: 'HQ_ACCOUNT',
|
||||
refIdParam: 'id',
|
||||
includeBody: true,
|
||||
})
|
||||
saveAccountPermissions(@Param('id') id: string, @Body() dto: SaveHqAccountPermissionsDto) {
|
||||
return this.service.saveAccountPermissions(BigInt(id), dto.permissionKeys);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import {
|
||||
HQ_PERMISSION_CATALOG,
|
||||
HQ_ROLE_DEFAULT_PERMISSIONS,
|
||||
type HqPermissionKey,
|
||||
} from '@dukang/shared-types';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
|
||||
const VALID_PERMISSION_KEYS = new Set<string>(HQ_PERMISSION_CATALOG.map((p) => p.key));
|
||||
|
||||
function assertPermissionKeys(keys: string[]) {
|
||||
const invalid = keys.filter((key) => !VALID_PERMISSION_KEYS.has(key));
|
||||
if (invalid.length) {
|
||||
throw new BadRequestException(`无效权限项: ${invalid.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AdminHqPermissionsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
catalog() {
|
||||
return {
|
||||
permissions: HQ_PERMISSION_CATALOG,
|
||||
roles: Object.entries(HQ_ROLE_DEFAULT_PERMISSIONS).map(([role, permissionKeys]) => ({
|
||||
role,
|
||||
permissionKeys,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async getRolePermissions(role: string) {
|
||||
const rows = await this.prisma.hqRolePermission.findMany({
|
||||
where: { adminRole: role as 'SUPER_ADMIN' | 'OPS' | 'FINANCE' | 'CUSTOMER_SERVICE' },
|
||||
select: { permissionKey: true },
|
||||
});
|
||||
const permissionKeys =
|
||||
rows.length > 0
|
||||
? rows.map((r) => r.permissionKey)
|
||||
: [...(HQ_ROLE_DEFAULT_PERMISSIONS[role] ?? [])];
|
||||
return { role, permissionKeys };
|
||||
}
|
||||
|
||||
async saveRolePermissions(role: string, permissionKeys: string[]) {
|
||||
if (role === 'SUPER_ADMIN') {
|
||||
throw new BadRequestException('超级管理员拥有全部权限,无需配置');
|
||||
}
|
||||
assertPermissionKeys(permissionKeys);
|
||||
const adminRole = role as 'OPS' | 'FINANCE' | 'CUSTOMER_SERVICE';
|
||||
await this.prisma.$transaction([
|
||||
this.prisma.hqRolePermission.deleteMany({ where: { adminRole } }),
|
||||
...(permissionKeys.length
|
||||
? [
|
||||
this.prisma.hqRolePermission.createMany({
|
||||
data: permissionKeys.map((permissionKey) => ({ adminRole, permissionKey })),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
]);
|
||||
return this.getRolePermissions(role);
|
||||
}
|
||||
|
||||
async getAccountPermissions(accountId: bigint) {
|
||||
const account = await this.prisma.hqAccount.findUnique({
|
||||
where: { id: accountId },
|
||||
select: { id: true, name: true, phone: true, loginName: true, adminRole: true, status: true },
|
||||
});
|
||||
if (!account) throw new NotFoundException('HQ 账号不存在');
|
||||
if (account.adminRole === 'SUPER_ADMIN') {
|
||||
return serializeBigInt({
|
||||
account,
|
||||
permissionKeys: HQ_PERMISSION_CATALOG.map((p) => p.key),
|
||||
rolePermissionKeys: HQ_PERMISSION_CATALOG.map((p) => p.key),
|
||||
userPermissionKeys: [],
|
||||
effectivePermissionKeys: HQ_PERMISSION_CATALOG.map((p) => p.key),
|
||||
});
|
||||
}
|
||||
|
||||
const [rolePerms, userPerms] = await Promise.all([
|
||||
this.getRolePermissions(account.adminRole),
|
||||
this.prisma.hqAccountPermission.findMany({
|
||||
where: { hqAccountId: accountId },
|
||||
select: { permissionKey: true },
|
||||
}),
|
||||
]);
|
||||
const userPermissionKeys = userPerms.map((p) => p.permissionKey);
|
||||
const effectivePermissionKeys = [
|
||||
...new Set([...rolePerms.permissionKeys, ...userPermissionKeys]),
|
||||
] as HqPermissionKey[];
|
||||
|
||||
return serializeBigInt({
|
||||
account,
|
||||
permissionKeys: userPermissionKeys,
|
||||
rolePermissionKeys: rolePerms.permissionKeys,
|
||||
userPermissionKeys,
|
||||
effectivePermissionKeys,
|
||||
});
|
||||
}
|
||||
|
||||
async saveAccountPermissions(accountId: bigint, permissionKeys: string[]) {
|
||||
const account = await this.prisma.hqAccount.findUnique({ where: { id: accountId } });
|
||||
if (!account) throw new NotFoundException('HQ 账号不存在');
|
||||
if (account.adminRole === 'SUPER_ADMIN') {
|
||||
throw new BadRequestException('超级管理员拥有全部权限,无需配置');
|
||||
}
|
||||
assertPermissionKeys(permissionKeys);
|
||||
await this.prisma.$transaction([
|
||||
this.prisma.hqAccountPermission.deleteMany({ where: { hqAccountId: accountId } }),
|
||||
...(permissionKeys.length
|
||||
? [
|
||||
this.prisma.hqAccountPermission.createMany({
|
||||
data: permissionKeys.map((permissionKey) => ({ hqAccountId: accountId, permissionKey })),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
]);
|
||||
return this.getAccountPermissions(accountId);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,17 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsArray, IsBoolean, IsIn, IsNotEmpty, IsNumber, IsObject, IsOptional, IsString, Min } from 'class-validator';
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsIn,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Min,
|
||||
MinLength,
|
||||
ValidateIf,
|
||||
} from 'class-validator';
|
||||
|
||||
export class UpdateStoreStatusDto {
|
||||
@IsString()
|
||||
@@ -413,9 +425,22 @@ export class UpdateDeliveryDto {
|
||||
}
|
||||
|
||||
export class CreateHqAccountDto {
|
||||
@IsIn(['phone', 'password'])
|
||||
credentialType: 'phone' | 'password';
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@ValidateIf((o: CreateHqAccountDto) => o.credentialType === 'password')
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
phone: string;
|
||||
loginName?: string;
|
||||
|
||||
@ValidateIf((o: CreateHqAccountDto) => o.credentialType === 'password')
|
||||
@IsString()
|
||||
@MinLength(6)
|
||||
password?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@@ -427,6 +452,14 @@ export class CreateHqAccountDto {
|
||||
}
|
||||
|
||||
export class UpdateHqAccountDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
loginName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(6)
|
||||
password?: string;
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
@@ -440,6 +473,18 @@ export class UpdateHqAccountDto {
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export class SaveHqRolePermissionsDto {
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
permissionKeys: string[];
|
||||
}
|
||||
|
||||
export class SaveHqAccountPermissionsDto {
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
permissionKeys: string[];
|
||||
}
|
||||
|
||||
export class CreateProductDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
|
||||
@@ -46,6 +46,8 @@ import { AdminRedeemDebugController } from './admin-redeem-debug.controller';
|
||||
import { AdminRedeemDebugService } from './admin-redeem-debug.service';
|
||||
import { AdminWechatBindingsController } from './admin-wechat-bindings.controller';
|
||||
import { AdminWechatBindingsService } from './admin-wechat-bindings.service';
|
||||
import { AdminHqPermissionsController } from './admin-hq-permissions.controller';
|
||||
import { AdminHqPermissionsService } from './admin-hq-permissions.service';
|
||||
|
||||
@Module({
|
||||
imports: [IamModule, TradeModule, BenefitModule, CommonModule, IntegrationsModule, RedeemModule],
|
||||
@@ -75,6 +77,7 @@ import { AdminWechatBindingsService } from './admin-wechat-bindings.service';
|
||||
AdminRedeemDebugController,
|
||||
AdminPromoCodesController,
|
||||
AdminWechatBindingsController,
|
||||
AdminHqPermissionsController,
|
||||
],
|
||||
providers: [
|
||||
AdminDashboardService,
|
||||
@@ -98,6 +101,7 @@ import { AdminWechatBindingsService } from './admin-wechat-bindings.service';
|
||||
AdminRedeemDebugService,
|
||||
AdminPromoCodesService,
|
||||
AdminWechatBindingsService,
|
||||
AdminHqPermissionsService,
|
||||
SuperAdminGuard,
|
||||
],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user