v4.0.18版本提交
This commit is contained in:
@@ -16,9 +16,11 @@ import {
|
||||
} from './dto/admin-query.dto';
|
||||
import {
|
||||
CreateStoreAccountDto,
|
||||
CreateStoreAccountStaffDto,
|
||||
CreateStoreDto,
|
||||
CreateStoreMediaDto,
|
||||
UpdateStoreAccountDto,
|
||||
UpdateStoreAccountStaffDto,
|
||||
UpdateStoreDto,
|
||||
UpdateStoreMediaDto,
|
||||
UpdateStoreStatusDto,
|
||||
@@ -113,6 +115,37 @@ export class AdminStoreAccountsController {
|
||||
return this.service.updateStoreAccount(BigInt(id), dto, user.actorId);
|
||||
}
|
||||
|
||||
@Post(':id/staff')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.STORE_ACCOUNT_STAFF_CREATE,
|
||||
refType: 'STORE_ACCOUNT',
|
||||
refIdField: 'id',
|
||||
includeBody: true,
|
||||
})
|
||||
createStaff(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: CreateStoreAccountStaffDto,
|
||||
) {
|
||||
return this.service.createStoreStaff(BigInt(id), dto, user.actorId);
|
||||
}
|
||||
|
||||
@Put(':id/staff/:staffId')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.STORE_ACCOUNT_STAFF_UPDATE,
|
||||
refType: 'STORE_ACCOUNT',
|
||||
refIdParam: 'staffId',
|
||||
includeBody: true,
|
||||
})
|
||||
updateStaff(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Param('id') id: string,
|
||||
@Param('staffId') staffId: string,
|
||||
@Body() dto: UpdateStoreAccountStaffDto,
|
||||
) {
|
||||
return this.service.updateStoreStaff(BigInt(id), BigInt(staffId), dto, user.actorId);
|
||||
}
|
||||
|
||||
@Delete(':id/staff/:staffId')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.STORE_ACCOUNT_STAFF_DELETE,
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Prisma, type WecomReportPush } from '@prisma/client';
|
||||
import {
|
||||
formatWecomReportMarkdown,
|
||||
isWecomReportKind,
|
||||
wecomReportCountedOrderWhere,
|
||||
wecomReportCutoff,
|
||||
wecomReportPeriod,
|
||||
wecomReportShouldFire,
|
||||
@@ -274,6 +275,7 @@ export class AdminWecomReportsService implements OnModuleInit {
|
||||
private async loadStats(start: Date, cutoff: Date): Promise<WecomReportStats> {
|
||||
const userBase = { status: 1, mergedIntoUserId: null } as const;
|
||||
const partnerBase = { isPrimary: 1 } as const;
|
||||
const countedOrder = wecomReportCountedOrderWhere();
|
||||
const paid = { payStatus: 'PAID' as const };
|
||||
|
||||
const [
|
||||
@@ -304,15 +306,15 @@ export class AdminWecomReportsService implements OnModuleInit {
|
||||
}),
|
||||
this.prisma.store.count({ where: { createdAt: { lt: cutoff } } }),
|
||||
this.prisma.store.count({ where: { createdAt: { gte: start, lt: cutoff } } }),
|
||||
this.prisma.order.count({ where: { createdAt: { lt: cutoff } } }),
|
||||
this.prisma.order.count({ where: { createdAt: { gte: start, lt: cutoff } } }),
|
||||
this.prisma.order.count({ where: { ...countedOrder, createdAt: { lt: cutoff } } }),
|
||||
this.prisma.order.count({ where: { ...countedOrder, createdAt: { gte: start, lt: cutoff } } }),
|
||||
this.prisma.order.aggregate({
|
||||
_sum: { payAmount: true },
|
||||
where: { ...paid, paidAt: { lt: cutoff } },
|
||||
where: { ...countedOrder, ...paid, paidAt: { lt: cutoff } },
|
||||
}),
|
||||
this.prisma.order.aggregate({
|
||||
_sum: { payAmount: true },
|
||||
where: { ...paid, paidAt: { gte: start, lt: cutoff } },
|
||||
where: { ...countedOrder, ...paid, paidAt: { gte: start, lt: cutoff } },
|
||||
}),
|
||||
this.prisma.redeemRecord.count({ where: { createdAt: { lt: cutoff } } }),
|
||||
this.prisma.redeemRecord.count({ where: { createdAt: { gte: start, lt: cutoff } } }),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayMaxSize,
|
||||
ArrayMinSize,
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsIn,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
ValidateIf,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { AccountStatus, StoreStaffRole } from '@dukang/shared-types';
|
||||
|
||||
export class UpdateStoreStatusDto {
|
||||
@IsString()
|
||||
@@ -341,6 +343,61 @@ export class UpdateStoreAccountDto {
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export class CreateStoreAccountStaffDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
phone: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@IsString({ each: true })
|
||||
storeIds: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsIn(Object.values(StoreStaffRole))
|
||||
staffRole?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
export class UpdateStoreAccountStaffDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsIn(Object.values(StoreStaffRole))
|
||||
staffRole?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
permissions?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsIn(Object.values(AccountStatus))
|
||||
status?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
storeIds?: string[];
|
||||
}
|
||||
|
||||
export class CreatePartnerDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
|
||||
Reference in New Issue
Block a user