v4.0.18版本提交

This commit is contained in:
2026-09-08 10:07:34 +08:00
parent 4bdb09068c
commit 23ba639e9b
46 changed files with 1922 additions and 162 deletions
@@ -1,5 +1,9 @@
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import {
STORE_STAFF_DEFAULT_PERMISSIONS,
StoreStaffRole,
} from '@dukang/shared-types';
import {
assertCanDeleteStore,
isMobilePhone,
@@ -23,9 +27,11 @@ import {
import { AnalyticsService } from '../analytics/analytics.service';
import type {
CreateStoreAccountDto,
CreateStoreAccountStaffDto,
CreateStoreDto,
CreateStoreMediaDto,
UpdateStoreAccountDto,
UpdateStoreAccountStaffDto,
UpdateStoreDto,
UpdateStoreMediaDto,
UpdateStoreStatusDto,
@@ -1103,7 +1109,7 @@ export class AdminStoresService {
...account,
stores: account.bindings.map((b) => b.store),
store: account.bindings[0]?.store ?? null,
staff: account.childAccounts,
staff: account.childAccounts.map((row) => this.mapStoreStaff(row)),
});
}
@@ -1120,17 +1126,102 @@ export class AdminStoresService {
return serializeBigInt(account);
}
async createStoreStaff(parentAccountId: bigint, dto: CreateStoreAccountStaffDto, actorId: bigint) {
const parent = await this.assertPrimaryStoreAccount(parentAccountId, actorId);
const phone = dto.phone.trim();
if (!isMobilePhone(phone)) {
throw new BadRequestException('请输入正确的手机号码');
}
const existing = await this.prisma.storeAccount.findUnique({ where: { phone } });
if (existing) throw new BadRequestException('该手机号已被使用');
const name = dto.name.trim();
if (!name) throw new BadRequestException('请填写真实姓名');
const storeIds = await this.resolveStaffStoreIds(parent.id, dto.storeIds);
const staffRole = (dto.staffRole as StoreStaffRole | undefined) ?? StoreStaffRole.CASHIER;
const permissions = dto.permissions?.length
? dto.permissions
: [...STORE_STAFF_DEFAULT_PERMISSIONS];
const account = await this.prisma.storeAccount.create({
data: {
phone,
name,
isPrimary: 0,
parentAccountId: parent.id,
staffRole,
permissions,
status: 'ACTIVE',
isTest: parent.isTest,
bindings: {
create: storeIds.map((storeId) => ({ storeId })),
},
},
include: this.staffBindingsInclude,
});
return serializeBigInt(this.mapStoreStaff(account));
}
async updateStoreStaff(
parentAccountId: bigint,
staffId: bigint,
dto: UpdateStoreAccountStaffDto,
actorId: bigint,
) {
const parent = await this.assertPrimaryStoreAccount(parentAccountId, actorId);
const staff = await this.assertStaffOwned(parent.id, staffId);
const data: Prisma.StoreAccountUpdateInput = {};
if (dto.name !== undefined) {
const name = dto.name.trim();
if (!name) throw new BadRequestException('请填写真实姓名');
data.name = name;
}
if (dto.staffRole !== undefined) data.staffRole = dto.staffRole as StoreStaffRole;
if (dto.permissions !== undefined) data.permissions = dto.permissions;
if (dto.status !== undefined) data.status = dto.status as 'ACTIVE' | 'DISABLED';
if (dto.phone !== undefined) {
const phone = dto.phone.trim();
if (!isMobilePhone(phone)) {
throw new BadRequestException('请输入正确的手机号码');
}
const phoneTaken = await this.prisma.storeAccount.findUnique({ where: { phone } });
if (phoneTaken && phoneTaken.id !== staff.id) {
throw new BadRequestException('该手机号已被使用');
}
data.phone = phone;
if (phone !== staff.phone) {
data.wxOpenId = null;
data.wxUnionId = null;
}
}
if (dto.storeIds !== undefined) {
const storeIds = await this.resolveStaffStoreIds(parent.id, dto.storeIds);
await this.prisma.$transaction([
this.prisma.storeAccountStore.deleteMany({ where: { storeAccountId: staff.id } }),
this.prisma.storeAccountStore.createMany({
data: storeIds.map((storeId) => ({ storeAccountId: staff.id, storeId })),
}),
this.prisma.storeAccount.update({ where: { id: staff.id }, data }),
]);
} else if (Object.keys(data).length) {
await this.prisma.storeAccount.update({ where: { id: staff.id }, data });
}
const updated = await this.prisma.storeAccount.findUniqueOrThrow({
where: { id: staff.id },
include: this.staffBindingsInclude,
});
return serializeBigInt(this.mapStoreStaff(updated));
}
/** HQ 删除门店子账号(非主账号) */
async deleteStoreStaff(parentAccountId: bigint, staffId: bigint, actorId: bigint) {
await this.assertStoreAccountInScope(actorId, parentAccountId);
const parent = await this.prisma.storeAccount.findUnique({ where: { id: parentAccountId } });
if (!parent || parent.isPrimary !== 1) {
throw new BadRequestException('主账号不存在');
}
const staff = await this.prisma.storeAccount.findFirst({
where: { id: staffId, parentAccountId, isPrimary: 0 },
});
if (!staff) throw new NotFoundException('子账号不存在');
const parent = await this.assertPrimaryStoreAccount(parentAccountId, actorId);
const staff = await this.assertStaffOwned(parent.id, staffId);
const pending = await this.prisma.redeemPendingRecord.count({
where: { storeAccountId: staffId },
@@ -1139,7 +1230,76 @@ export class AdminStoresService {
throw new BadRequestException('该子账号仍有待处理核销单,无法删除');
}
await this.prisma.storeAccount.delete({ where: { id: staffId } });
await this.prisma.storeAccount.delete({ where: { id: staff.id } });
return { ok: true };
}
private readonly staffBindingsInclude = {
bindings: {
include: {
store: { select: { id: true, name: true, status: true } },
},
},
} as const;
private async assertPrimaryStoreAccount(parentAccountId: bigint, actorId: bigint) {
await this.assertStoreAccountInScope(actorId, parentAccountId);
const parent = await this.prisma.storeAccount.findUnique({ where: { id: parentAccountId } });
if (!parent || parent.isPrimary !== 1) {
throw new BadRequestException('主账号不存在');
}
return parent;
}
private async assertStaffOwned(parentAccountId: bigint, staffId: bigint) {
const staff = await this.prisma.storeAccount.findFirst({
where: { id: staffId, parentAccountId, isPrimary: 0 },
});
if (!staff) throw new NotFoundException('子账号不存在');
return staff;
}
/** 子账号只能绑定主账号已管理的门店 */
private async resolveStaffStoreIds(primaryAccountId: bigint, storeIds: string[]) {
const unique = [...new Set(storeIds.map((id) => id.trim()).filter(Boolean))];
if (!unique.length) throw new BadRequestException('请至少绑定一家门店');
const ids = unique.map((id) => BigInt(id));
const owned = await this.prisma.storeAccountStore.findMany({
where: { storeAccountId: primaryAccountId, storeId: { in: ids } },
select: { storeId: true },
});
if (owned.length !== ids.length) {
throw new BadRequestException('只能绑定主账号已管理的门店');
}
return ids;
}
private mapStoreStaff(row: {
id: bigint;
name: string;
phone: string;
staffRole: string | null;
permissions: Prisma.JsonValue;
status: string;
lastLoginAt: Date | null;
createdAt: Date;
bindings: Array<{ store: { id: bigint; name: string; status: string } }>;
}) {
return {
id: row.id.toString(),
name: row.name,
phone: row.phone,
staffRole: row.staffRole ?? StoreStaffRole.CASHIER,
permissions: Array.isArray(row.permissions) ? row.permissions : [],
status: row.status,
storeIds: row.bindings.map((b) => b.store.id.toString()),
stores: row.bindings.map((b) => ({
id: b.store.id.toString(),
name: b.store.name,
status: b.store.status,
})),
lastLoginAt: row.lastLoginAt?.toISOString() ?? null,
createdAt: row.createdAt.toISOString(),
};
}
}