门店审核功能
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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}` : ''}`,
|
||||
})),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user