= [
{ title: '姓名', dataIndex: 'name' },
+ { title: '用户名', dataIndex: 'loginName', width: 120, render: (v) => v || '—' },
{ title: '手机', dataIndex: 'phone', width: 130 },
+ {
+ title: '登录方式',
+ width: 110,
+ render: (_, r) => (
+
+ {r.hasPassword ? 密码 : null}
+ 短信
+
+ ),
+ },
{ title: '角色', dataIndex: 'adminRole', width: 110, render: (r) => ROLE_LABELS[r] || r },
{ title: '状态', dataIndex: 'status', width: 90, render: (s) => {ACCOUNT_STATUS_LABELS[s] || s} },
{ title: '最后登录', dataIndex: 'lastLoginAt', width: 160, render: fmtTime },
@@ -55,7 +75,12 @@ export default function HqAccountsPage() {
render: (_, row) => (
),
@@ -66,7 +91,18 @@ export default function HqAccountsPage() {
HQ 账户
- {isSuperAdmin && }
+ {isSuperAdmin && (
+
+ )}
@@ -82,7 +118,9 @@ export default function HqAccountsPage() {
diff --git a/apps/admin-web/src/pages/HqPermissionsPage.tsx b/apps/admin-web/src/pages/HqPermissionsPage.tsx
new file mode 100644
index 0000000..11ae3ed
--- /dev/null
+++ b/apps/admin-web/src/pages/HqPermissionsPage.tsx
@@ -0,0 +1,264 @@
+import { useEffect, useMemo, useState } from 'react';
+import {
+ Alert,
+ Button,
+ Card,
+ Checkbox,
+ Col,
+ Form,
+ Row,
+ Select,
+ Space,
+ Tabs,
+ Tag,
+ Typography,
+ message,
+} from 'antd';
+import {
+ HQ_ADMIN_ROLES,
+ HQ_PERMISSION_CATALOG,
+ type HqPermissionKey,
+} from '@dukang/shared-types';
+import { request, type HqProfile } from '../lib/api';
+
+type RolePermRes = { role: string; permissionKeys: string[] };
+type AccountOption = { id: string; name: string; phone: string; loginName: string | null; adminRole: string };
+type AccountPermRes = {
+ account: AccountOption;
+ permissionKeys: string[];
+ rolePermissionKeys: string[];
+ userPermissionKeys: string[];
+ effectivePermissionKeys: string[];
+};
+
+const ROLE_LABELS = Object.fromEntries(HQ_ADMIN_ROLES.map((r) => [r.value, r.label]));
+
+function PermissionChecklist({
+ value,
+ onChange,
+ disabled,
+}: {
+ value: string[];
+ onChange: (keys: string[]) => void;
+ disabled?: boolean;
+}) {
+ return (
+ onChange(checked as string[])}
+ >
+
+ {HQ_PERMISSION_CATALOG.map((item) => (
+
+ {item.label}
+
+ ))}
+
+
+ );
+}
+
+export default function HqPermissionsPage() {
+ const [profile, setProfile] = useState(null);
+ const [role, setRole] = useState('OPS');
+ const [roleKeys, setRoleKeys] = useState([]);
+ const [roleLoading, setRoleLoading] = useState(false);
+ const [roleSaving, setRoleSaving] = useState(false);
+
+ const [accounts, setAccounts] = useState([]);
+ const [accountId, setAccountId] = useState();
+ const [accountKeys, setAccountKeys] = useState([]);
+ const [roleInheritedKeys, setRoleInheritedKeys] = useState([]);
+ const [accountLoading, setAccountLoading] = useState(false);
+ const [accountSaving, setAccountSaving] = useState(false);
+
+ const previewEffectiveKeys = useMemo(
+ () => [...new Set([...roleInheritedKeys, ...accountKeys])],
+ [roleInheritedKeys, accountKeys],
+ );
+
+ const isSuperAdmin = profile?.adminRole === 'SUPER_ADMIN';
+
+ useEffect(() => {
+ request('/admin/auth/me').then(setProfile).catch(() => {});
+ }, []);
+
+ useEffect(() => {
+ if (!isSuperAdmin) return;
+ request<{ items: AccountOption[] }>('/admin/hq-accounts?page=1&pageSize=100')
+ .then((res) => setAccounts(res.items))
+ .catch(() => {});
+ }, [isSuperAdmin]);
+
+ useEffect(() => {
+ if (!isSuperAdmin || !role) return;
+ setRoleLoading(true);
+ request(`/admin/hq-permissions/roles/${role}`)
+ .then((res) => setRoleKeys(res.permissionKeys))
+ .finally(() => setRoleLoading(false));
+ }, [isSuperAdmin, role]);
+
+ useEffect(() => {
+ if (!isSuperAdmin || !accountId) return;
+ setAccountLoading(true);
+ request(`/admin/hq-permissions/accounts/${accountId}`)
+ .then((res) => {
+ setAccountKeys(res.userPermissionKeys);
+ setRoleInheritedKeys(res.rolePermissionKeys);
+ })
+ .finally(() => setAccountLoading(false));
+ }, [isSuperAdmin, accountId]);
+
+ async function saveRolePermissions() {
+ setRoleSaving(true);
+ try {
+ const res = await request(`/admin/hq-permissions/roles/${role}`, {
+ method: 'PUT',
+ body: JSON.stringify({ permissionKeys: roleKeys }),
+ });
+ setRoleKeys(res.permissionKeys);
+ message.success('角色权限已保存');
+ } catch (e) {
+ message.error(e instanceof Error ? e.message : '保存失败');
+ } finally {
+ setRoleSaving(false);
+ }
+ }
+
+ async function saveAccountPermissions() {
+ if (!accountId) return;
+ setAccountSaving(true);
+ try {
+ const res = await request(`/admin/hq-permissions/accounts/${accountId}`, {
+ method: 'PUT',
+ body: JSON.stringify({ permissionKeys: accountKeys }),
+ });
+ setAccountKeys(res.userPermissionKeys);
+ setRoleInheritedKeys(res.rolePermissionKeys);
+ message.success('用户权限已保存');
+ } catch (e) {
+ message.error(e instanceof Error ? e.message : '保存失败');
+ } finally {
+ setAccountSaving(false);
+ }
+ }
+
+ if (!isSuperAdmin) {
+ return (
+
+ );
+ }
+
+ return (
+
+
权限分配
+
+ 按角色配置基础权限;按用户可追加专属权限。最终生效权限 = 角色权限 ∪ 用户权限(超级管理员始终拥有全部权限)。
+
+
+
+
+
+
+ {role === 'SUPER_ADMIN' ? (
+
+ ) : (
+ <>
+
+
+
+
+ >
+ )}
+
+ ),
+ },
+ {
+ key: 'user',
+ label: '按用户分配',
+ children: (
+
+
+
+
+ {!accountId ? (
+
+ ) : (
+ <>
+
+ 角色继承:
+ {roleInheritedKeys.map((key) => {
+ const item = HQ_PERMISSION_CATALOG.find((p) => p.key === key);
+ return (
+
+ {item?.label || key}
+
+ );
+ })}
+
+
+ 下方勾选为用户专属追加权限(保存后与角色权限合并生效)。
+
+
+
+ 合并生效:
+ {previewEffectiveKeys.map((key) => {
+ const item = HQ_PERMISSION_CATALOG.find((p) => p.key === (key as HqPermissionKey));
+ return {item?.label || key};
+ })}
+
+
+
+
+ >
+ )}
+
+ ),
+ },
+ ]}
+ />
+
+ );
+}
diff --git a/packages/shared-types/src/hq-permissions.ts b/packages/shared-types/src/hq-permissions.ts
new file mode 100644
index 0000000..8565a4b
--- /dev/null
+++ b/packages/shared-types/src/hq-permissions.ts
@@ -0,0 +1,45 @@
+export const HQ_PERMISSION_CATALOG = [
+ { key: 'dashboard', label: '概览' },
+ { key: 'users', label: '用户管理' },
+ { key: 'wechat_bindings', label: '微信绑定' },
+ { key: 'products', label: '商品管理' },
+ { key: 'orders', label: '订单管理' },
+ { key: 'stores', label: '门店管理' },
+ { key: 'partners', label: '开城管理' },
+ { key: 'benefit', label: '好客权益' },
+ { key: 'deliveries', label: '配送单' },
+ { key: 'tickets', label: '工单中心' },
+ { key: 'resources', label: 'OSS 资源库' },
+ { key: 'logs', label: '日志' },
+ { key: 'hq_permissions', label: '权限分配' },
+ { key: 'hq_accounts', label: 'HQ 账户' },
+] as const;
+
+export type HqPermissionKey = (typeof HQ_PERMISSION_CATALOG)[number]['key'];
+
+export const HQ_ADMIN_ROLES = [
+ { value: 'SUPER_ADMIN', label: '超级管理员' },
+ { value: 'OPS', label: '运营' },
+ { value: 'FINANCE', label: '财务' },
+ { value: 'CUSTOMER_SERVICE', label: '客服' },
+] as const;
+
+export const HQ_ROLE_DEFAULT_PERMISSIONS: Record = {
+ SUPER_ADMIN: HQ_PERMISSION_CATALOG.map((p) => p.key),
+ OPS: [
+ 'dashboard',
+ 'users',
+ 'wechat_bindings',
+ 'products',
+ 'orders',
+ 'stores',
+ 'partners',
+ 'benefit',
+ 'deliveries',
+ 'tickets',
+ 'resources',
+ 'logs',
+ ],
+ FINANCE: ['dashboard', 'orders', 'stores', 'partners', 'benefit', 'logs'],
+ CUSTOMER_SERVICE: ['dashboard', 'users', 'orders', 'tickets', 'logs'],
+};
diff --git a/packages/shared-types/src/index.ts b/packages/shared-types/src/index.ts
index 8fddc1c..c04afc5 100644
--- a/packages/shared-types/src/index.ts
+++ b/packages/shared-types/src/index.ts
@@ -13,3 +13,4 @@ export * from './user-log';
export * from './store-log';
export * from './partner-log';
export * from './promo';
+export * from './hq-permissions';
diff --git a/server/dukang-api/prisma/schema.prisma b/server/dukang-api/prisma/schema.prisma
index 18e6289..2f456b7 100644
--- a/server/dukang-api/prisma/schema.prisma
+++ b/server/dukang-api/prisma/schema.prisma
@@ -506,9 +506,31 @@ model HqAccount {
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.DateTime(3)
+ permissions HqAccountPermission[]
+
@@map("hq_account")
}
+model HqRolePermission {
+ adminRole HqAdminRole @map("admin_role")
+ permissionKey String @map("permission_key") @db.VarChar(64)
+ createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
+
+ @@id([adminRole, permissionKey])
+ @@map("hq_role_permission")
+}
+
+model HqAccountPermission {
+ hqAccountId BigInt @map("hq_account_id") @db.UnsignedBigInt
+ permissionKey String @map("permission_key") @db.VarChar(64)
+ createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
+
+ hqAccount HqAccount @relation(fields: [hqAccountId], references: [id], onDelete: Cascade)
+
+ @@id([hqAccountId, permissionKey])
+ @@map("hq_account_permission")
+}
+
// ─── USER ─────────────────────────────────────────────
model User {
diff --git a/server/dukang-api/src/common/hq-operation/hq-operation.constants.ts b/server/dukang-api/src/common/hq-operation/hq-operation.constants.ts
index 40669e0..92b71f1 100644
--- a/server/dukang-api/src/common/hq-operation/hq-operation.constants.ts
+++ b/server/dukang-api/src/common/hq-operation/hq-operation.constants.ts
@@ -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 = {
[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]: '订单发货',
diff --git a/server/dukang-api/src/modules/ops/admin-hq-accounts.service.ts b/server/dukang-api/src/modules/ops/admin-hq-accounts.service.ts
index a289c1e..c582a7a 100644
--- a/server/dukang-api/src/modules/ops/admin-hq-accounts.service.ts
+++ b/server/dukang-api/src/modules/ops/admin-hq-accounts.service.ts
@@ -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 {
+ 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('无法生成占位手机号,请手动填写');
}
}
diff --git a/server/dukang-api/src/modules/ops/admin-hq-permissions.controller.ts b/server/dukang-api/src/modules/ops/admin-hq-permissions.controller.ts
new file mode 100644
index 0000000..345bb38
--- /dev/null
+++ b/server/dukang-api/src/modules/ops/admin-hq-permissions.controller.ts
@@ -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);
+ }
+}
diff --git a/server/dukang-api/src/modules/ops/admin-hq-permissions.service.ts b/server/dukang-api/src/modules/ops/admin-hq-permissions.service.ts
new file mode 100644
index 0000000..452162c
--- /dev/null
+++ b/server/dukang-api/src/modules/ops/admin-hq-permissions.service.ts
@@ -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(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);
+ }
+}
diff --git a/server/dukang-api/src/modules/ops/dto/admin-mutate.dto.ts b/server/dukang-api/src/modules/ops/dto/admin-mutate.dto.ts
index 90b3573..71f0694 100644
--- a/server/dukang-api/src/modules/ops/dto/admin-mutate.dto.ts
+++ b/server/dukang-api/src/modules/ops/dto/admin-mutate.dto.ts
@@ -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()
diff --git a/server/dukang-api/src/modules/ops/ops.module.ts b/server/dukang-api/src/modules/ops/ops.module.ts
index 1dcccdd..84a2de0 100644
--- a/server/dukang-api/src/modules/ops/ops.module.ts
+++ b/server/dukang-api/src/modules/ops/ops.module.ts
@@ -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,
],
})