# 杜康好客 · v3.4.17 门店套餐审核快捷入口 > **2026-08-16** · PRD §套餐变更审核 · admin-web / API(ops) > 目标:把「套餐变更审核」能力前置到**门店详情-套餐页签**与**门店列表操作栏**,让总部在不离开列表/详情流的情况下即可发现待审套餐并一键进入审核(含线上 vs 待审对照)。 ## 范围 | 项 | 交付 | |----|------| | A. 门店详情·套餐页签 待审提醒 | 存在 `status=PENDING` 的套餐变更时,页签顶部黄色 Alert + 「审核 / 对比」按钮 | | B. 审核 / 对比 跳转 | 复用现有 `StorePackageAuditsPage`,跳转 `/store-package-audits?requestId=xxx` 自动打开审核抽屉(抽屉内已含线上 vs 待审并排对照) | | C. 门店列表 操作栏快捷入口 | 有待审套餐时显示「审核套餐」「对比」两个按钮,跳转同一地址(与详情页审核功能一致) | | D. 列表接口补 `pendingPackageAuditId` | `GET /admin/stores` 返回每家店当前待审套餐变更的 `requestId`(一次批量查询,无 schema 变更) | | E. 审核后提醒自动刷新 | 审核完成 dispatch `admin:package-audit-changed`,套餐页签监听后刷新提醒 | ## A / B. 门店详情·套餐页签 组件:`apps/admin-web/src/components/AdminStorePackagesSection.tsx` - 现有 `GET /admin/stores/:storeId/packages` 已返回 `pendingRequest`(含 `id`/`status`/`packages`),此前被组件丢弃;本次**捕获 `pendingRequest`** 存入 state。 - 当 `pendingRequest.status === 'PENDING'`: - 页签顶部渲染 ``:「该门店有待审核套餐」。 - 提醒内放「审核」「对比」两个按钮,均 `navigate('/store-package-audits?requestId=' + pendingRequest.id)`。 - 空套餐门店同样展示该提醒(不依赖 `live` 是否有数据)。 - 跳转后由 `StorePackageAuditsPage` 读取 `?requestId=` 自动打开抽屉:抽屉内左侧「线上已审核套餐」、右侧「待审核套餐」并排 `diffPackages` 对照;抽屉右上「通过 / 驳回」即审核动作。两套按钮语义:审核=处置,对比=看差异,落点同一页面。 ## C. 门店列表·操作栏 组件:`apps/admin-web/src/pages/StoresPage.tsx` - `StoreRow` 增加 `pendingPackageAuditId?: string | null`。 - 操作列(原「详情 / 评价」)在 `pendingPackageAuditId` 存在时追加「审核套餐」「对比」按钮,均 `navigate('/store-package-audits?requestId=' + row.pendingPackageAuditId)`。 - 操作列宽度 140 → 220,允许换行,避免按钮被挤压。 ## D. 列表接口补 `pendingPackageAuditId` 后端:`server/dukang-api/src/modules/ops/admin-stores.service.ts` 的 `listStores` - 取回 `items` 后,用一次查询批量取出待审变更: `storePackageChangeRequest.findMany({ where: { storeId: { in: storeIds }, status: 'PENDING' }, select: { id, storeId } })` - 建 `storeId -> requestId` 映射,逐店附加 `pendingPackageAuditId`(无则 `null`)。 - `mapStoreCompat` 为 `{ ...store }` 展开透传,新字段不会被丢弃;**无 Prisma schema 变更**,发布可 `--skip-db`。 ## E. 审核后提醒自动刷新 - `StorePackageAuditsPage` 审核成功时调用 `notifyPackageAuditChanged()`(dispatch `admin:package-audit-changed`)。 - `AdminStorePackagesSection` 监听该事件,重新拉取 `pendingRequest` 并刷新提醒(仅更新 `pendingRequest`,不触碰正在编辑的 `items`,避免覆盖未保存套餐)。 ## 关键接口 / 文件 | 位置 | 说明 | |------|------| | `GET /admin/stores` | 列表,新增 `pendingPackageAuditId`(ops/admin/* 透传,无 schema 变更) | | `GET /admin/stores/:storeId/packages` | 返回 `pendingRequest`(store-package.service.ts,未改,仅前端消费) | | `GET /admin/store-package-audits/:requestId` | 审核详情(含 `livePackages` 对照) | | `PUT /admin/store-package-audits/:requestId/audit` | 通过 / 驳回 | | `apps/admin-web/src/components/AdminStorePackagesSection.tsx` | 套餐页签提醒 + 按钮 | | `apps/admin-web/src/pages/StorePackageAuditsPage.tsx` | `?requestId=` 自动开抽屉 + 变更明细(含 `fieldChanges` / `diffText` / `TextDiff`) | | `apps/admin-web/src/pages/StoresPage.tsx` | 列表操作栏快捷入口 | ## F. 套餐变更对比·逐字段 / 逐字明细 组件:`apps/admin-web/src/pages/StorePackageAuditsPage.tsx` > 需求:审核抽屉原本只给每条套餐「新增 / 删除 / 变更 / 未变」粗粒度标记,看不出具体哪里变了。本次在「待审核套餐」卡片底部新增**变更明细**块,逐字段列出差异;文本类字段进一步做**逐字(LCS)差异定位**,精确高亮哪些字被增 / 删。 - `fieldChanges(live, proposed)`:逐字段比对,产出 `{ label, old, now, kind }` 明细: - `kind: 'value'`(整体替换展示 `旧 → 新`):**价格**、**图片(N 张 → M 张)**。 - `kind: 'text'`(走逐字差异):**套餐名称、菜品内容、可用时间、其他说明**。 - `diffText(a, b)`:LCS 动态规划(`dp[i][j]`)+ 回溯,产出 `equal / delete / insert` 段落并合并相邻同类型;时间/空间 `O(|a|·|b|)`,套餐字段长度可控,无性能风险。 - `TextDiff({ oldText, newText })`:渲染两行—— - 「原:」行用**红色删除线**标出被删的字(`delete` 段),其余正常; - 「新:」行用**绿色**标出新增的字(`insert` 段),其余正常。 - 一眼定位到具体改动的字,而非整段替换。 - 抽屉顶部汇总条:**新增 X · 删除 Y · 变更 Z · 未变 W**(由 `diffPackages` 的 `changes` 统计)。 ### 已知局限 - 套餐按**名称**(空则按位置)配对;若某条套餐**改名**,会误判为「删除旧名 + 新增新名」而非「变更」,改名本身不会进入逐字明细。如需把改名也识别为「变更」,需升级 `packageKey` 配对策略(名称变了但其余字段相近 → 视为变更)。 ## 验收 - [ ] 某门店有待审套餐变更时:门店详情-套餐页签顶部出现黄色「有待审核套餐」提醒,且「审核 / 对比」可点。 - [ ] 点「审核」或「对比」均跳到套餐审核页并自动打开该门店变更抽屉,可见线上 vs 待审对照与通过/驳回。 - [ ] 门店列表操作栏对该门店出现「审核套餐」「对比」按钮,点击同样跳转并自动打开抽屉。 - [ ] 在审核页完成审核后,返回门店详情-套餐页签,提醒消失(或被事件即时刷新)。 - [ ] 无待审套餐的门店:列表与详情页签均不出现上述入口。 - [ ] 抽屉中「变更」套餐的「待审核」卡片底部出现「变更明细」:价格/图片以 `旧 → 新` 展示;菜品内容/可用时间/其他说明/套餐名称以**逐字差异**展示(原行红删、新行绿增),能精确定位到具体改动的字。