门店审核功能

This commit is contained in:
2026-07-14 15:25:17 +08:00
parent d61a43cc8c
commit e0f5977dd9
13 changed files with 479 additions and 40 deletions
+2
View File
@@ -22,6 +22,8 @@ ALIYUN_SMS_ACCESS_KEY_SECRET=
MOCK_PAY=true
MOCK_DELIVERY_AUTO=true
AUTO_APPROVE_STORE=true
# 开启总部人工审核时改为 false:合伙人录店 → 待审核 → HQ 通过后可开门;驳回须填原因并在合伙人端展示
# AUTO_APPROVE_STORE=false
# 微信 OAuth Mock(preV1 本地联调):须在微信内置浏览器内打开 H5,走 OAuth 回跳带 mock code
# 非微信浏览器不会发起 /login/wechat 请求。生产请 MOCK_WECHAT=false 并配置 WX_APP_ID / WX_APP_SECRET。
MOCK_WECHAT=true
+11 -1
View File
@@ -176,6 +176,12 @@ enum StoreStatus {
CLOSED
}
enum StoreAuditStatus {
PENDING
APPROVED
REJECTED
}
enum UserSourceType {
ORGANIC
PROMO_CODE
@@ -734,7 +740,10 @@ model Store {
avgPrice Decimal? @map("avg_price") @db.Decimal(10, 2)
rating Decimal? @db.Decimal(3, 2)
tags Json?
status StoreStatus @default(PAUSED)
status StoreStatus @default(PAUSED)
auditStatus StoreAuditStatus @default(APPROVED) @map("audit_status")
rejectReason String? @map("reject_reason") @db.VarChar(512)
auditedAt DateTime? @map("audited_at") @db.DateTime(3)
openTime String? @map("open_time") @db.VarChar(8)
closeTime String? @map("close_time") @db.VarChar(8)
settlementRate Decimal @default(0.60) @map("settlement_rate") @db.Decimal(5, 4)
@@ -753,6 +762,7 @@ model Store {
@@index([cityId, status])
@@index([partnerAccountId])
@@index([auditStatus, createdAt])
@@map("store_store")
}
@@ -28,6 +28,9 @@ export class AdminStoresService {
const where: Prisma.StoreWhereInput = {};
if (query.name) where.name = { contains: query.name };
if (query.status) where.status = query.status as Prisma.EnumStoreStatusFilter['equals'];
if (query.auditStatus) {
where.auditStatus = query.auditStatus as Prisma.EnumStoreAuditStatusFilter['equals'];
}
if (query.cityId) where.cityId = BigInt(query.cityId);
if (query.partnerId) where.partnerAccountId = BigInt(query.partnerId);
if (query.phone) where.phone = { contains: query.phone };
@@ -57,6 +60,7 @@ export class AdminStoresService {
items: items.map((s) =>
mapStoreCompat({
...s,
partner: s.partnerAccount,
account: s.bindings[0]?.storeAccount ?? null,
bindings: undefined,
}),
@@ -118,11 +122,33 @@ export class AdminStoresService {
async auditStore(id: bigint, dto: { approved: boolean; remark?: string }) {
const store = await this.prisma.store.findUnique({ where: { id } });
if (!store) throw new NotFoundException('门店不存在');
const status = dto.approved ? 'OPEN' : 'PAUSED';
if (store.auditStatus !== 'PENDING' && store.auditStatus !== 'REJECTED') {
// 允许对已通过门店再次驳回/通过(总部纠错);PENDING/REJECTED/APPROVED 均可审核
}
if (!dto.approved) {
const reason = dto.remark?.trim();
if (!reason) throw new BadRequestException('驳回时必须填写原因');
}
const now = new Date();
const updated = await this.prisma.store.update({
where: { id },
data: { status },
data: dto.approved
? {
// 审核通过后保持闭店,由合伙人自行开门
status: store.status === 'CLOSED' ? 'CLOSED' : 'PAUSED',
auditStatus: 'APPROVED',
rejectReason: null,
auditedAt: now,
}
: {
status: 'PAUSED',
auditStatus: 'REJECTED',
rejectReason: dto.remark!.trim(),
auditedAt: now,
},
});
await this.prisma.commonEvent.create({
data: {
eventType: 'STORE_AUDIT',
@@ -130,10 +156,20 @@ export class AdminStoresService {
refId: id,
actorType: 'HQ',
status: dto.approved ? 'APPROVED' : 'REJECTED',
remark: dto.remark ?? (dto.approved ? '审核通过' : '审核驳回'),
remark: dto.approved
? (dto.remark?.trim() || '审核通过,可开门营业')
: dto.remark!.trim(),
param1: dto.approved ? 'APPROVE' : 'REJECT',
param1Desc: 'audit_action',
},
});
return serializeBigInt(updated);
return serializeBigInt({
...updated,
notifyHint: dto.approved
? '已通过审核,合伙人可在端内开门营业'
: '已驳回,驳回原因已同步至合伙人端',
});
}
async updateStore(id: bigint, dto: UpdateStoreDto) {
@@ -217,6 +253,9 @@ export class AdminStoresService {
openTime: '10:00',
closeTime: '22:00',
status: 'OPEN',
auditStatus: 'APPROVED',
auditedAt: new Date(),
rejectReason: null,
},
});
@@ -85,6 +85,10 @@ export class AdminStoresQueryDto extends PaginationQueryDto {
@IsOptional()
@IsString()
phone?: string;
@IsOptional()
@IsString()
auditStatus?: string;
}
export class AdminStoreAccountsQueryDto extends PaginationQueryDto {
@@ -197,6 +197,9 @@ export class StoreService {
openTime: body.openTime ? String(body.openTime) : '10:00',
closeTime: body.closeTime ? String(body.closeTime) : '22:00',
status: this.config.autoApproveStore ? 'OPEN' : 'PAUSED',
auditStatus: this.config.autoApproveStore ? 'APPROVED' : 'PENDING',
auditedAt: this.config.autoApproveStore ? new Date() : null,
rejectReason: null,
},
});
@@ -325,6 +328,18 @@ export class StoreService {
if (!['OPEN', 'PAUSED', 'CLOSED'].includes(status)) {
throw new BadRequestException('无效的门店状态');
}
if (status === 'OPEN') {
if (store.auditStatus === 'PENDING') {
throw new BadRequestException('门店尚在总部审核中,通过后方可开门');
}
if (store.auditStatus === 'REJECTED') {
throw new BadRequestException(
store.rejectReason
? `门店审核未通过:${store.rejectReason}`
: '门店审核未通过,请查看驳回原因并重新提交',
);
}
}
const updated = await this.prisma.store.update({
where: { id: storeId },
@@ -358,6 +373,9 @@ export class StoreService {
if (store.status === 'CLOSED') {
throw new BadRequestException('门店已关闭,不可编辑');
}
if (store.auditStatus === 'PENDING') {
throw new BadRequestException('门店审核中,暂不可修改资料');
}
const name = body.name !== undefined ? String(body.name).trim() : undefined;
const phone = body.phone !== undefined ? String(body.phone).trim() : undefined;
@@ -373,6 +391,7 @@ export class StoreService {
throw new BadRequestException('门店简介须为 10~500 字');
}
const resubmitAudit = store.auditStatus === 'REJECTED';
await this.prisma.store.update({
where: { id: storeId },
data: {
@@ -380,8 +399,33 @@ export class StoreService {
...(phone !== undefined ? { phone } : {}),
...(address !== undefined ? { address } : {}),
...(introRaw !== undefined ? { intro: introRaw || null } : {}),
...(resubmitAudit
? {
auditStatus: 'PENDING' as const,
rejectReason: null,
auditedAt: null,
status: store.status === 'OPEN' ? ('PAUSED' as const) : store.status,
}
: {}),
},
});
if (resubmitAudit) {
await this.prisma.commonEvent.create({
data: {
eventType: 'STORE_AUDIT',
refType: 'STORE',
refId: storeId,
actorType: 'PARTNER',
actorId: partnerAccountId,
status: 'PENDING',
param1: 'RESUBMIT',
param1Desc: 'audit_type',
remark: '合伙人修改资料后重新提交审核',
},
});
}
return this.partnerGetStore(partnerAccountId, storeId);
}
@@ -398,6 +442,13 @@ export class StoreService {
where: { storeAccountId_storeId: { storeAccountId, storeId } },
include: { store: true },
});
if (status === 'OPEN' && binding.store.auditStatus !== 'APPROVED') {
throw new BadRequestException(
binding.store.auditStatus === 'REJECTED'
? `门店审核未通过${binding.store.rejectReason ? `${binding.store.rejectReason}` : ''}`
: '门店尚在总部审核中,通过后方可营业',
);
}
const previousStatus = binding.store.status;
const store = await this.prisma.store.update({
where: { id: storeId },
@@ -423,34 +474,70 @@ export class StoreService {
select: { id: true },
});
const storeIds = partnerStoreIds.map((s) => s.id);
const [storeCount, orderCount, recentStores, pendingAuditCount] = await Promise.all([
const [storeCount, orderCount, recentStores, pendingAuditCount, recentNotices] = await Promise.all([
this.prisma.store.count({ where: { partnerAccountId: primaryId } }),
this.prisma.order.count({
where: await this.partnerCityService.buildPartnerOrderWhere(primaryId),
}),
this.prisma.store.findMany({
where: { partnerAccountId: primaryId },
select: { id: true, name: true, status: true, createdAt: true },
select: { id: true, name: true, status: true, auditStatus: true, createdAt: true },
orderBy: { createdAt: 'desc' },
take: 10,
}),
this.prisma.store.count({
where: { partnerAccountId: primaryId, auditStatus: 'PENDING' },
}),
storeIds.length === 0
? Promise.resolve(0)
: this.prisma.commonEvent.count({
? Promise.resolve([])
: this.prisma.commonEvent.findMany({
where: {
eventType: 'STORE_AUDIT',
status: 'PENDING',
refType: 'STORE',
refId: { in: storeIds },
status: { in: ['APPROVED', 'REJECTED'] },
actorType: 'HQ',
},
orderBy: { createdAt: 'desc' },
take: 20,
}),
]);
const storeNameMap = new Map(
(
await this.prisma.store.findMany({
where: { id: { in: recentNotices.map((n) => n.refId) } },
select: { id: true, name: true },
})
).map((s) => [s.id.toString(), s.name]),
);
return {
storeCount,
orderCount,
companyName: account.companyName ?? '',
recentStores: serializeBigInt(recentStores),
pendingAuditCount,
notifications: serializeBigInt(
recentNotices.map((n) => ({
id: n.id,
storeId: n.refId,
storeName: storeNameMap.get(n.refId.toString()) ?? '门店',
status: n.status,
remark: n.remark,
createdAt: n.createdAt,
title:
n.status === 'APPROVED'
? '门店审核已通过'
: n.status === 'REJECTED'
? '门店审核已驳回'
: '门店审核更新',
content:
n.status === 'APPROVED'
? `${storeNameMap.get(n.refId.toString()) ?? '门店'} 已通过总部审核,可开门营业`
: `${storeNameMap.get(n.refId.toString()) ?? '门店'} 未通过审核${n.remark ? `${n.remark}` : ''}`,
})),
),
};
}