From e0f5977dd914a410c9555d330975416aeb7272ff Mon Sep 17 00:00:00 2001 From: jacy <18049821889@163.com> Date: Tue, 14 Jul 2026 15:25:17 +0800 Subject: [PATCH] =?UTF-8?q?=E9=97=A8=E5=BA=97=E5=AE=A1=E6=A0=B8=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin-web/src/lib/constants.ts | 6 + apps/admin-web/src/pages/StoresPage.tsx | 129 +++++++++++++++++- apps/h5-partner/src/lib/storeStatus.ts | 21 +++ apps/h5-partner/src/pages/HomePage.tsx | 46 ++++++- apps/h5-partner/src/pages/StoreDetailPage.tsx | 76 +++++++++-- apps/h5-partner/src/pages/StoreListPage.tsx | 60 ++++++-- packages/shared-types/src/enums.ts | 12 ++ packages/shared-types/src/partner-log.ts | 7 +- server/dukang-api/.env.example | 2 + server/dukang-api/prisma/schema.prisma | 12 +- .../src/modules/ops/admin-stores.service.ts | 47 ++++++- .../src/modules/ops/dto/admin-query.dto.ts | 4 + .../src/modules/store/store.service.ts | 97 ++++++++++++- 13 files changed, 479 insertions(+), 40 deletions(-) diff --git a/apps/admin-web/src/lib/constants.ts b/apps/admin-web/src/lib/constants.ts index faef146..bcaf6c4 100644 --- a/apps/admin-web/src/lib/constants.ts +++ b/apps/admin-web/src/lib/constants.ts @@ -38,6 +38,12 @@ export const STORE_STATUS_LABELS: Record = { CLOSED: '已关闭', }; +export const STORE_AUDIT_STATUS_LABELS: Record = { + PENDING: '待审核', + APPROVED: '已通过', + REJECTED: '已驳回', +}; + export const ACCOUNT_STATUS_LABELS: Record = { ACTIVE: '正常', DISABLED: '停用', diff --git a/apps/admin-web/src/pages/StoresPage.tsx b/apps/admin-web/src/pages/StoresPage.tsx index 4565d2d..d4ab23d 100644 --- a/apps/admin-web/src/pages/StoresPage.tsx +++ b/apps/admin-web/src/pages/StoresPage.tsx @@ -20,7 +20,7 @@ import { } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { request, type Paginated } from '../lib/api'; -import { ADMIN_OPTIONS_PAGE_SIZE, STORE_STATUS_LABELS, fmtTime } from '../lib/constants'; +import { ADMIN_OPTIONS_PAGE_SIZE, STORE_AUDIT_STATUS_LABELS, STORE_STATUS_LABELS, fmtTime } from '../lib/constants'; import { validateStoreCreateStep1, validateStoreCreateStep3, @@ -42,6 +42,8 @@ type StoreRow = { name: string; phone: string; status: string; + auditStatus?: string; + rejectReason?: string | null; cityName: string; district: string; address: string; @@ -73,6 +75,7 @@ export default function StoresPage() { const qs = new URLSearchParams(); if (filters.name) qs.set('name', filters.name); if (filters.status) qs.set('status', filters.status); + if (filters.auditStatus) qs.set('auditStatus', filters.auditStatus); if (filters.phone) qs.set('phone', filters.phone); return qs; }, @@ -80,6 +83,9 @@ export default function StoresPage() { ); const [detail, setDetail] = useState | null>(null); const [drawerOpen, setDrawerOpen] = useState(false); + const [rejectOpen, setRejectOpen] = useState(false); + const [rejectReason, setRejectReason] = useState(''); + const [auditing, setAuditing] = useState(false); const [createOpen, setCreateOpen] = useState(false); const [createStep, setCreateStep] = useState(0); const [createError, setCreateError] = useState(''); @@ -228,9 +234,26 @@ export default function StoresPage() { { title: '城市', dataIndex: 'cityName', width: 80 }, { title: '电话', dataIndex: 'phone', width: 120 }, { - title: '状态', dataIndex: 'status', width: 90, + title: '营业状态', dataIndex: 'status', width: 90, render: (s) => {STORE_STATUS_LABELS[s] || s}, }, + { + title: '审核', dataIndex: 'auditStatus', width: 100, + render: (s, row) => { + const status = s || 'APPROVED'; + const color = status === 'PENDING' ? 'orange' : status === 'REJECTED' ? 'red' : 'green'; + return ( + + {STORE_AUDIT_STATUS_LABELS[status] || status} + {status === 'REJECTED' && row.rejectReason ? ( + + {row.rejectReason} + + ) : null} + + ); + }, + }, { title: '开城合伙人', dataIndex: ['partner', 'companyName'], width: 120 }, { title: '介绍', dataIndex: 'intro', width: 160, ellipsis: true, render: (v) => v || '—' }, { title: '店长', dataIndex: ['account', 'name'], width: 90, render: (v) => v || '—' }, @@ -268,9 +291,17 @@ export default function StoresPage() {
{ setFilters(v); setPage(1); }}> - + ({ value, label }))} + /> + @@ -278,7 +309,41 @@ export default function StoresPage() { pagination={{ current: page, pageSize, total: data?.total ?? 0, showSizeChanger: true, onChange: (p, ps) => { setPage(p); setPageSize(ps); } }} /> setDrawerOpen(false)} extra={detail && ( - + + {String(detail.auditStatus || '') === 'PENDING' || String(detail.auditStatus || '') === 'REJECTED' ? ( + <> + + + + ) : null}
@@ -449,6 +536,40 @@ export default function StoresPage() { + + setRejectOpen(false)} + onOk={async () => { + if (!detail || !rejectReason.trim()) return; + setAuditing(true); + try { + const updated = await request>(`/admin/stores/${detail.id}/audit`, { + method: 'PUT', + body: JSON.stringify({ approved: false, remark: rejectReason.trim() }), + }); + message.success('已驳回,原因已同步合伙人端'); + setDetail({ ...detail, ...updated, auditStatus: 'REJECTED', rejectReason: rejectReason.trim() }); + setRejectOpen(false); + void reload(); + } finally { + setAuditing(false); + } + }} + > + 驳回原因将展示给合伙人,请说明需修改的内容。 + setRejectReason(e.target.value)} + /> + ); } diff --git a/apps/h5-partner/src/lib/storeStatus.ts b/apps/h5-partner/src/lib/storeStatus.ts index 80d8780..80a4400 100644 --- a/apps/h5-partner/src/lib/storeStatus.ts +++ b/apps/h5-partner/src/lib/storeStatus.ts @@ -1,3 +1,20 @@ +export type PartnerStoreAuditStatus = 'PENDING' | 'APPROVED' | 'REJECTED' | string; + +export function storeAuditLabel(auditStatus?: string | null): string { + const s = String(auditStatus || 'APPROVED').toUpperCase(); + if (s === 'PENDING') return '待总部审核'; + if (s === 'REJECTED') return '审核驳回'; + if (s === 'APPROVED') return '审核通过'; + return auditStatus || '—'; +} + +export function storeAuditPillClass(auditStatus?: string | null): string { + const s = String(auditStatus || 'APPROVED').toUpperCase(); + if (s === 'PENDING') return 'partner-status-pill--paused'; + if (s === 'REJECTED') return 'partner-status-pill--closed'; + return 'partner-status-pill--open'; +} + export type StoreStatusValue = 'OPEN' | 'PAUSED' | 'CLOSED'; export function storeStatusLabel(status: string): string { @@ -14,3 +31,7 @@ export function storeStatusPillClass(status: string): string { if (s === 'PAUSED') return 'partner-status-pill--paused'; return 'partner-status-pill--closed'; } + +export function canPartnerOpenStore(auditStatus?: string | null): boolean { + return String(auditStatus || '').toUpperCase() === 'APPROVED'; +} diff --git a/apps/h5-partner/src/pages/HomePage.tsx b/apps/h5-partner/src/pages/HomePage.tsx index 2fb08b3..ab9325b 100644 --- a/apps/h5-partner/src/pages/HomePage.tsx +++ b/apps/h5-partner/src/pages/HomePage.tsx @@ -16,6 +16,7 @@ export default function HomePage() { const [dash, setDash] = useState | null>(null); const [stores, setStores] = useState>>([]); const [leaderboardPreview, setLeaderboardPreview] = useState([]); + const [notifOpen, setNotifOpen] = useState(false); useEffect(() => { if (!isLoggedIn()) { navigate('/login'); return; } @@ -29,6 +30,10 @@ export default function HomePage() { const storeCount = Number(dash?.storeCount || stores.length || 0); const orderCount = Number(dash?.orderCount || 0); const pendingAuditCount = Number(dash?.pendingAuditCount || 0); + const notifications = Array.isArray(dash?.notifications) + ? (dash.notifications as Array>) + : []; + const hasUnread = notifications.length > 0; const activeStores = stores.filter((s) => String(s.status).toUpperCase() === 'OPEN').length; const abnormalStores = Math.max(0, storeCount - activeStores); const revenue = orderCount * 128.45; @@ -49,9 +54,14 @@ export default function HomePage() {

工作台

-
person @@ -59,6 +69,33 @@ export default function HomePage() {
+ {notifOpen && ( +
+
+

审核通知

+ +
+ {notifications.length === 0 ? ( +

暂无审核通知

+ ) : ( + notifications.slice(0, 8).map((n) => ( + setNotifOpen(false)} + > +
+

{String(n.title || '门店审核')}

+

{String(n.content || '')}

+
+ + )) + )} +
+ )} +

@@ -172,14 +209,15 @@ export default function HomePage() { previewStores.map((s) => { const storeId = String(s.id); const status = String(s.status || 'PAUSED').toUpperCase(); + const audit = String(s.auditStatus || 'APPROVED').toUpperCase(); return (

{String(s.name || '未命名门店')}

{String(s.address || s.district || '')}

- - {storeStatusLabel(status)} + + {audit === 'PENDING' ? '待审核' : audit === 'REJECTED' ? '已驳回' : storeStatusLabel(status)} ); diff --git a/apps/h5-partner/src/pages/StoreDetailPage.tsx b/apps/h5-partner/src/pages/StoreDetailPage.tsx index 33878fa..8205722 100644 --- a/apps/h5-partner/src/pages/StoreDetailPage.tsx +++ b/apps/h5-partner/src/pages/StoreDetailPage.tsx @@ -6,7 +6,14 @@ import { request } from '../lib/api'; import { toastSuccess } from '../lib/toast'; import { usePartnerSession } from '../contexts/PartnerSessionContext'; import { isSubAccount } from '../lib/partnerAccess'; -import { storeStatusLabel, storeStatusPillClass, type StoreStatusValue } from '../lib/storeStatus'; +import { + canPartnerOpenStore, + storeAuditLabel, + storeAuditPillClass, + storeStatusLabel, + storeStatusPillClass, + type StoreStatusValue, +} from '../lib/storeStatus'; const STATUS_OPTIONS: StoreStatusValue[] = ['OPEN', 'PAUSED', 'CLOSED']; @@ -48,6 +55,15 @@ export default function StoreDetailPage() { async function changeStatus(next: StoreStatusValue) { if (!id || next === status || statusSaving) return; if (status === 'CLOSED') return; + const auditStatus = String(store?.auditStatus || 'APPROVED').toUpperCase(); + if (next === 'OPEN' && !canPartnerOpenStore(auditStatus)) { + setActionError( + auditStatus === 'REJECTED' + ? `门店审核未通过${store?.rejectReason ? `:${String(store.rejectReason)}` : ''}` + : '门店尚在总部审核中,通过后方可开门', + ); + return; + } if (next === 'CLOSED') { const ok = window.confirm('关闭后不可恢复营业,确认关闭该门店?'); if (!ok) return; @@ -71,6 +87,11 @@ export default function StoreDetailPage() { async function saveBasic() { if (!id || saving || status === 'CLOSED') return; + const auditStatus = String(store?.auditStatus || 'APPROVED').toUpperCase(); + if (auditStatus === 'PENDING') { + setActionError('门店审核中,暂不可修改资料'); + return; + } setSaving(true); setActionError(''); try { @@ -84,7 +105,7 @@ export default function StoreDetailPage() { }), }); applyStore(data); - toastSuccess('已保存'); + toastSuccess(auditStatus === 'REJECTED' ? '已保存并重新提交审核' : '已保存'); } catch (e) { setActionError(e instanceof Error ? e.message : '保存失败'); } finally { @@ -106,7 +127,11 @@ export default function StoreDetailPage() { const envPhotos = Array.isArray(store.media) ? (store.media as Array<{ url?: string; bizType?: string }>).filter((m) => m.bizType === 'ENV') : []; - const readOnly = subReadonly || status === 'CLOSED'; + const auditStatus = String(store.auditStatus || 'APPROVED').toUpperCase(); + const auditPending = auditStatus === 'PENDING'; + const auditRejected = auditStatus === 'REJECTED'; + const readOnly = subReadonly || status === 'CLOSED' || auditPending; + const canOpen = canPartnerOpenStore(auditStatus); return (
@@ -114,6 +139,37 @@ export default function StoreDetailPage() {
{actionError &&

{actionError}

} + +
+
+

审核状态

+ + {storeAuditLabel(auditStatus)} + +
+ {auditPending && ( +

+ 资料已提交总部审核,通过后即可开门营业。审核期间不可修改资料。 +

+ )} + {auditRejected && ( +
+

+ 审核未通过 +

+

+ {String(store.rejectReason || '未填写驳回原因')} +

+

+ 请按驳回原因修改资料并保存,将自动重新提交审核。 +

+
+ )} + {auditStatus === 'APPROVED' && ( +

总部审核已通过,可将门店设为营业中。

+ )} +
+ {!subReadonly && (
@@ -129,10 +185,10 @@ export default function StoreDetailPage() { key={s} type="button" className={status === s ? 'active' : ''} - disabled={readOnly || statusSaving} + disabled={readOnly || statusSaving || (s === 'OPEN' && !canOpen) || status === 'CLOSED'} onClick={() => void changeStatus(s)} > - {storeStatusLabel(s)} + {s === 'OPEN' && !canOpen ? '待审核' : storeStatusLabel(s)} ))}
@@ -195,10 +251,14 @@ export default function StoreDetailPage() {

管理信息

-
- +
+

{storeStatusLabel(status)}

+
+ +

{storeAuditLabel(auditStatus)}

+
@@ -207,7 +267,7 @@ export default function StoreDetailPage() { {!subReadonly && ( )} diff --git a/apps/h5-partner/src/pages/StoreListPage.tsx b/apps/h5-partner/src/pages/StoreListPage.tsx index d6da1ff..5fe95e3 100644 --- a/apps/h5-partner/src/pages/StoreListPage.tsx +++ b/apps/h5-partner/src/pages/StoreListPage.tsx @@ -3,14 +3,23 @@ import { Link, useNavigate } from 'react-router-dom'; import { isLoggedIn, request } from '../lib/api'; import { usePartnerSession } from '../contexts/PartnerSessionContext'; import { isSubAccount } from '../lib/partnerAccess'; -import { storeStatusLabel, storeStatusPillClass, type StoreStatusValue } from '../lib/storeStatus'; +import { + canPartnerOpenStore, + storeAuditLabel, + storeAuditPillClass, + storeStatusLabel, + storeStatusPillClass, + type StoreStatusValue, +} from '../lib/storeStatus'; -type StatusFilter = 'ALL' | StoreStatusValue; +type StatusFilter = 'ALL' | StoreStatusValue | 'PENDING_AUDIT' | 'REJECTED'; const FILTERS: { key: StatusFilter; label: string }[] = [ { key: 'ALL', label: '全部' }, { key: 'OPEN', label: '营业中' }, { key: 'PAUSED', label: '暂时闭店' }, + { key: 'PENDING_AUDIT', label: '待审核' }, + { key: 'REJECTED', label: '已驳回' }, { key: 'CLOSED', label: '关闭' }, ]; @@ -35,11 +44,20 @@ export default function StoreListPage() { const filtered = useMemo(() => stores.filter((s) => { const matchQ = !q || String(s.name).includes(q) || String(s.address).includes(q); - const matchStatus = filter === 'ALL' || String(s.status).toUpperCase() === filter; + const audit = String(s.auditStatus || 'APPROVED').toUpperCase(); + const status = String(s.status).toUpperCase(); + let matchStatus = true; + if (filter === 'PENDING_AUDIT') matchStatus = audit === 'PENDING'; + else if (filter === 'REJECTED') matchStatus = audit === 'REJECTED'; + else if (filter !== 'ALL') matchStatus = status === filter; return matchQ && matchStatus; }), [stores, q, filter]); - async function updateStatus(storeId: string, next: StoreStatusValue) { + async function updateStatus(storeId: string, next: StoreStatusValue, auditStatus?: string) { + if (next === 'OPEN' && !canPartnerOpenStore(auditStatus)) { + setError(auditStatus === 'REJECTED' ? '门店审核未通过,请查看驳回原因并修改后重新提交' : '门店尚在总部审核中,通过后方可开门'); + return; + } if (next === 'CLOSED') { const ok = window.confirm('关闭后不可恢复营业,确认关闭该门店?'); if (!ok) return; @@ -98,9 +116,11 @@ export default function StoreListPage() { {filtered.map((s) => { const storeId = String(s.id); const currentStatus = String(s.status).toUpperCase() as StoreStatusValue; + const auditStatus = String(s.auditStatus || 'APPROVED').toUpperCase(); const dim = currentStatus === 'CLOSED'; const storeName = String(s.name || '未命名门店'); const busy = updatingId === storeId; + const canOpen = canPartnerOpenStore(auditStatus); return (
@@ -109,10 +129,24 @@ export default function StoreListPage() {

门店名称

{storeName}

{String(s.address || s.district || '')}

+ {auditStatus !== 'APPROVED' && ( +

+ {storeAuditLabel(auditStatus)} + {auditStatus === 'REJECTED' && s.rejectReason ? `:${String(s.rejectReason)}` : ''} +

+ )} +
+
+ {auditStatus !== 'APPROVED' ? ( + + {storeAuditLabel(auditStatus)} + + ) : ( + + {storeStatusLabel(currentStatus)} + + )}
- - {storeStatusLabel(currentStatus)} -
{!readonly && ( @@ -121,8 +155,8 @@ export default function StoreListPage() { type="button" className="btn btn-outline" style={{ fontSize: 12, padding: '8px 12px' }} - disabled={busy || currentStatus === 'CLOSED' || currentStatus === 'PAUSED'} - onClick={() => void updateStatus(storeId, 'PAUSED')} + disabled={busy || currentStatus === 'CLOSED' || currentStatus === 'PAUSED' || auditStatus === 'PENDING'} + onClick={() => void updateStatus(storeId, 'PAUSED', auditStatus)} > 暂时闭店 @@ -131,7 +165,7 @@ export default function StoreListPage() { className="btn btn-outline" style={{ fontSize: 12, padding: '8px 12px', borderColor: 'var(--color-subtle-gray)', color: 'var(--color-subtle-gray)' }} disabled={busy || currentStatus === 'CLOSED'} - onClick={() => void updateStatus(storeId, 'CLOSED')} + onClick={() => void updateStatus(storeId, 'CLOSED', auditStatus)} > 关闭 @@ -140,10 +174,10 @@ export default function StoreListPage() { type="button" className="btn btn-outline" style={{ fontSize: 12, padding: '8px 12px' }} - disabled={busy} - onClick={() => void updateStatus(storeId, 'OPEN')} + disabled={busy || !canOpen} + onClick={() => void updateStatus(storeId, 'OPEN', auditStatus)} > - 恢复营业 + {canOpen ? '开门营业' : '待审核通过'} )} diff --git a/packages/shared-types/src/enums.ts b/packages/shared-types/src/enums.ts index a3346a3..7a3f121 100644 --- a/packages/shared-types/src/enums.ts +++ b/packages/shared-types/src/enums.ts @@ -105,6 +105,18 @@ export enum StoreStatus { CLOSED = 'CLOSED', } +export enum StoreAuditStatus { + PENDING = 'PENDING', + APPROVED = 'APPROVED', + REJECTED = 'REJECTED', +} + +export const STORE_AUDIT_STATUS_LABELS: Record = { + [StoreAuditStatus.PENDING]: '待审核', + [StoreAuditStatus.APPROVED]: '已通过', + [StoreAuditStatus.REJECTED]: '已驳回', +}; + export enum PartnerStaffRole { PARTNER = 'PARTNER', INTERNAL = 'INTERNAL', diff --git a/packages/shared-types/src/partner-log.ts b/packages/shared-types/src/partner-log.ts index 2297611..900d660 100644 --- a/packages/shared-types/src/partner-log.ts +++ b/packages/shared-types/src/partner-log.ts @@ -18,7 +18,12 @@ export const PARTNER_LOG_EVENT_CATEGORIES: Record 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, }, }); diff --git a/server/dukang-api/src/modules/ops/dto/admin-query.dto.ts b/server/dukang-api/src/modules/ops/dto/admin-query.dto.ts index 709220d..b9e9021 100644 --- a/server/dukang-api/src/modules/ops/dto/admin-query.dto.ts +++ b/server/dukang-api/src/modules/ops/dto/admin-query.dto.ts @@ -85,6 +85,10 @@ export class AdminStoresQueryDto extends PaginationQueryDto { @IsOptional() @IsString() phone?: string; + + @IsOptional() + @IsString() + auditStatus?: string; } export class AdminStoreAccountsQueryDto extends PaginationQueryDto { diff --git a/server/dukang-api/src/modules/store/store.service.ts b/server/dukang-api/src/modules/store/store.service.ts index 1ce69a4..a50a30f 100644 --- a/server/dukang-api/src/modules/store/store.service.ts +++ b/server/dukang-api/src/modules/store/store.service.ts @@ -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}` : ''}`, + })), + ), }; }