Răsfoiți Sursa

026连锁门店复制商品设计文档(暂缓实现)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
qmj 1 zi în urmă
părinte
comite
afe44e57f3

+ 2 - 2
.specify/feature.json

@@ -1,3 +1,3 @@
 {
-  "feature_directory": "specs/024-flash-delivery"
-}
+  "feature_directory": "specs/026-store-menu-copy"
+}

+ 35 - 0
specs/026-store-menu-copy/checklists/requirements.md

@@ -0,0 +1,35 @@
+# Specification Quality Checklist: 连锁门店复制商品
+
+**Purpose**: Validate specification completeness and quality before proceeding to planning
+**Created**: 2026-09-03
+**Feature**: [spec.md](../spec.md)
+
+## Content Quality
+
+- [x] No implementation details (languages, frameworks, APIs)
+- [x] Focused on user value and business needs
+- [x] Written for non-technical stakeholders
+- [x] All mandatory sections completed
+
+## Requirement Completeness
+
+- [x] No [NEEDS CLARIFICATION] markers remain
+- [x] Requirements are testable and unambiguous
+- [x] Success criteria are measurable
+- [x] Success criteria are technology-agnostic (no implementation details)
+- [x] All acceptance scenarios are defined
+- [x] Edge cases are identified
+- [x] Scope is clearly bounded
+- [x] Dependencies and assumptions identified
+
+## Feature Readiness
+
+- [x] All functional requirements have clear acceptance criteria
+- [x] User scenarios cover primary flows
+- [x] Feature meets measurable outcomes defined in Success Criteria
+- [x] No implementation details leak into specification
+
+## Notes
+
+- 所有关键决策(范围收窄至轻量版+、差异覆盖由用户整体选择、权限口径、零 DDL)已在规格前的设计讨论中与需求方确认,无遗留澄清项。
+- 实体小节中的表名(pos_store 等)为项目既有业务名词,保留以便与现有系统对照,不属于实现泄漏。

+ 130 - 0
specs/026-store-menu-copy/spec.md

@@ -0,0 +1,130 @@
+# 功能规格:连锁门店复制商品(菜单复制)
+
+**功能标识**:`026-store-menu-copy`
+
+**创建日期**:2026-09-03
+
+**状态**:设计定稿,**待排期实现**(2026-09-03 与需求方确认设计后暂缓开发;恢复时从 plan → tasks → implement 继续,禅道对外文档见同目录 zentao-design.md)
+
+**输入**:连锁商家(如麦当劳型)各门店商品菜单基本一致,新开门店或补齐菜单时逐条重录商品费时费力。需要一个「复制商品」功能,把一家门店的分类+商品一次性复制到另一家门店;复制前可预览差异,同名但内容不同的商品由商家选择保留本店版本或用源店版本覆盖;不破坏目标门店已有商品;可重复执行(幂等)。
+
+## 用户场景与测试
+
+### 用户故事 1 - 把源门店菜单复制到新开门店(Priority: P1)
+
+连锁商家主账号新开了一家门店(空店),在商家 PC 端商品管理页选择该门店,点击「复制商品」,选择一家老门店作为源,确认后系统把源门店全部分类和商品(含价格、图片、介绍、属性、规格、排序、推荐)复制到新门店,新门店立即可对外营业。
+
+**为什么这个优先级**:这是功能的核心价值——开新店初始化菜单,替代逐条录入;只要这一个故事成立,功能即产生价值(MVP)。
+
+**独立测试**:商家 A 名下有门店 X(有 5 分类 38 商品)和空门店 Y,执行复制 X→Y 后,Y 拥有同名同内容的 5 分类 38 商品,商品上架与审核状态继承源店。
+
+**验收场景**:
+
+1. **假如** 商家有两家门店且目标店为空,**当** 商家从源店复制到目标店,**那么** 目标店生成与源店一致的全部分类与商品(名字、价格、图片、介绍、属性、规格、排序、推荐)。
+2. **假如** 复制完成,**当** 商家查看目标店商品列表,**那么** 新增商品的图片正常显示(图片为 URL 引用,不重复上传文件)。
+3. **假如** 源店某商品为上架状态,**当** 复制到空店,**那么** 新店该商品为上架状态、可直接被顾客下单。
+4. **假如** 商品带规格(如大/中/杯加价),**当** 复制到空店,**那么** 目标店生成同名规格模板与规格值,且商品正确关联,顾客下单可选规格。
+
+---
+
+### 用户故事 2 - 复制前预览差异,回答「差多少/是否已同步」(Priority: P1)
+
+商家选择源门店后、确认复制前,系统展示差异概览:将新增几个分类(同名合并几个)、新增几个商品、同名且内容一致跳过几个、同名但内容不同几个;差异商品可展开查看明细(商品名+哪个字段变了+前后值)。商家据此决定是否复制以及差异商品如何处理。
+
+**为什么这个优先级**:预览是复制决策的依据,也是轻量版的「同步状态」——差异为 0 即已同步,无需任何状态记录。
+
+**独立测试**:门店 X 有商品「麦辣鸡腿堡」价格 75,门店 Y 同名商品价格 65;打开复制弹窗选 X→Y,预览显示「内容不同 1 个:麦辣鸡腿堡 价格 65 → 75」。
+
+**验收场景**:
+
+1. **假如** 两店菜单完全一致,**当** 商家打开复制弹窗,**那么** 预览显示新增 0、差异 0(等价于「已是最新」)。
+2. **假如** 源店有 38 个商品、目标店已有其中 26 个(23 个内容一致、3 个内容不同),**当** 打开预览,**那么** 显示「新增分类 X(同名合并 Y)、新增商品 12、一致跳过 23、内容不同 3」,且 3 个差异商品可展开查看字段级前后值。
+3. **假如** 预览与执行之间商家未做任何修改,**当** 确认复制,**那么** 实际执行结果与预览计数一致。
+
+---
+
+### 用户故事 3 - 复制到非空门店:差异商品由商家选择保留或覆盖(Priority: P2)
+
+目标门店已有商品时仍允许复制(追加模式):同名分类合并、同名且内容一致的商品跳过、**同名但内容不同的商品由商家整体选择「保留本店版本(默认)」或「用源店版本覆盖」**。覆盖只更新内容字段,不动目标店商品的上架状态与审核状态。
+
+**为什么这个优先级**:解除「仅空店可复制」的死限制(商家先手建了几个商品就废掉功能),同时给连锁商家一个手动「推新品/改价」到其他门店的途径;但它是破坏性决策,排在预览之后。
+
+**独立测试**:门店 Y 有「麦辣鸡腿堡」价格 65(本店自改)、目标店今日已下架「薯条」;选覆盖复制 X→Y 后,「麦辣鸡腿堡」价格变 75,「薯条」内容对齐源店但**仍保持本店下架状态**。
+
+**验收场景**:
+
+1. **假如** 差异商品 3 个且商家选「保留本店版本」,**当** 确认复制,**那么** 3 个差异商品完全不变,仅新增缺失的分类与商品。
+2. **假如** 差异商品 3 个且商家选「用源店版本覆盖」,**当** 确认复制,**那么** 3 个商品的**内容字段**(价格、图片、介绍、属性、规格、排序、推荐)对齐源店,但上架状态、审核状态与商品 id 保持本店原样。
+3. **假如** 目标店某差异商品今日临时下架,**当** 覆盖复制,**那么** 该商品内容被更新但不会因此被拉回上架。
+
+---
+
+### 用户故事 4 - 权限与入口控制(Priority: P2)
+
+只有同时具备源、目标两店操作权限的商家账号可以执行复制:主账号对自己名下所有门店可用;分管账号仅当两店都在其授权范围内可用。源门店与目标门店不能是同一家。商家名下门店数不足 2 家时,PC 端不显示「复制商品」入口。
+
+**为什么这个优先级**:复制是跨店写操作,必须防止越权改动未授权门店的菜单。
+
+**独立测试**:分管账号甲被授权门店 A、B,未授权 C;甲复制 A→B 成功;甲复制 A→C(伪造参数)被后端拒绝;甲的界面上不出现涉及 C 的选项。
+
+**验收场景**:
+
+1. **假如** 分管账号甲负责 A、B,**当** 甲发起 A→B 复制,**那么** 系统允许。
+2. **假如** 甲未负责 C,**当** 甲以伪造门店 id 发起 A→C 复制,**那么** 后端拒绝且 C 数据不变。
+3. **假如** 请求中源门店与目标门店相同,**当** 发起预览或复制,**那么** 系统拒绝并提示。
+4. **假如** 商家名下只有 1 家门店,**当** 其打开商品管理页,**那么** 看不到「复制商品」按钮。
+
+### 边界场景
+
+- 源门店没有任何分类/商品时:预览显示无可复制内容,复制按钮置灰或提示,不产生写入。
+- 同名但语言不同的商品(如本店只有中文「可乐」、源店另有英文「Cola」):按(名字+语言)判定为不同商品,英文行作为新增复制(多语言行各自独立匹配)。
+- 源店存在两个同名分类(历史脏数据):目标店与第一个匹配的分类合并,不产生额外重复。
+- 复制执行中商家在另一端同时改菜单:以复制事务开始时读到的数据为准,整批成功或整批回滚(单商家操作,不做跨端加锁)。
+- 超大菜单(数百商品):批量写入,正常网络下整次复制在 10 秒内完成,不超时。
+- 商品图片:直接引用源店图片 URL,不复制图片文件;后续删除源店商品不影响目标店(图片为共享 URL)。
+
+## 需求
+
+### 功能需求
+
+- **FR-001**: 系统 MUST 提供复制预览能力:给定源门店与目标门店,返回新增分类数(含同名合并数)、新增商品数、同名一致跳过数、内容不同商品数及差异明细(商品名、语言、变更字段、前后值)。
+- **FR-002**: 系统 MUST 提供复制执行能力:按预览口径写入新增分类/商品,并按商家选择处理差异商品(保留或覆盖)。
+- **FR-003**: 分类匹配规则 MUST 为(分类名+语言)相同视为同一分类:目标店已有则复用(新增商品挂入),否则新建分类。
+- **FR-004**: 商品匹配规则 MUST 为(商品名+语言)相同视为同一商品;内容比对字段为:价格、图片、介绍、属性(foodSku)、规格集合(模板+规格值)、排序、推荐。
+- **FR-005**: 新增商品 MUST 继承源店的上架状态与审核状态,图片直接引用源店 URL;所属分类指向目标店合并/新建后的分类。
+- **FR-006**: 覆盖差异商品 MUST 只更新内容字段(同 FR-004 比对字段),MUST NOT 修改目标店商品的 id、上架状态、审核状态。
+- **FR-007**: 差异商品处理方式 MUST 默认为「保留本店版本」,覆盖为显式选择。
+- **FR-008**: 复制 MUST 幂等:同参数重复执行,第二次新增 0、覆盖 0(全部命中跳过)。
+- **FR-009**: 系统 MUST 校验操作者对源、目标两店均有权限,且两店不能相同;无权限或不合法时拒绝且不产生任何写入。
+- **FR-010**: 复制执行 MUST 在单事务内完成:全部成功或全部回滚。
+- **FR-011**: 规格 MUST 随商品复制:新增商品在目标店按(规格名+语言)复用已有规格模板,缺失则新建模板与规格值,并重建商品与规格的关联;覆盖时仅把商品的规格**关联集合**对齐源店(缺失模板同样复用/新建),MUST NOT 修改目标店已有规格模板自身的规格值——规格模板是门店内共享资产,改动会波及其他商品;规格值的同步不在本功能范围。
+- **FR-012**: PC 商家端商品管理页 MUST 提供「复制商品」入口(当前查看门店为目标店),门店数不足 2 家时隐藏;弹窗文案、按钮、结果提示 MUST 覆盖四种界面语言(中/繁/英/越)。
+- **FR-013**: 复制完成后 MUST 向商家反馈结果计数(新增分类、新增商品、跳过、覆盖)并刷新商品列表。
+- **FR-014**: 本功能 MUST NOT 引入数据库结构变更(零 DDL)、MUST NOT 新增同步状态存储——差异一律实时计算。
+
+### 关键实体
+
+- **门店(pos_store)**:连锁商家的经营单元,归属商家主账号(user_id);一个主账号多家门店即「连锁」。
+- **分类(pos_fenlei)**:门店内的商品分类(名称、图片、排序、语言),挂门店(mendid)。
+- **商品(pos_food)**:门店内可售单品(名称、价格、图片、介绍、属性、排序、推荐、语言、上架状态、审核状态),挂门店(mdid)+分类(fl_id)。
+- **规格(food_specs / food_specs_value / food_spec_relation)**:规格模板(店级作用域)→ 规格值(加价选项)→ 商品关联(多对多)。
+- **商家账号(主账号/分管账号)**:主账号拥有全部门店;分管账号按 022 授权范围访问门店。
+
+## 成功标准
+
+### 可度量结果
+
+- **SC-001**: 连锁商家把一家门店的完整菜单(30 个商品以内)复制到另一家门店,全程(含预览确认)操作时间在 1 分钟以内,对比逐条录入(30 个商品约需 1 小时以上)效率提升 90% 以上。
+- **SC-002**: 100 个商品规模的门店单次复制在 10 秒内完成且无超时报错。
+- **SC-003**: 同参数重复执行第二次复制,新增与覆盖计数均为 0(幂等可验证)。
+- **SC-004**: 单店商家在界面上感知不到本功能(无入口、无文案干扰)。
+- **SC-005**: 覆盖操作后,目标门店商品的上架状态 100% 保持不变(抽查验证)。
+
+## 假设
+
+- 分管账号的门店授权模型沿用 022(merchantStoreAccessService 权限口径),本功能不新增角色或权限类型。
+- 商家 App(uni-app)端不在本期实现范围;本功能输出的接口与交互设计可供 App 端后续直接复用。
+- 不做连锁菜单统一管理/定时同步/逐条勾选差异(原方案 3),若后续真实使用提出需求再立项。
+- 商品多语言行为按现状:同一商品的各语言行相互独立,复制按(名字+语言)逐行匹配,不做跨语言语义合并。
+- 图片存储为共享 URL,无需复制文件,也不处理图片清理。
+- 预览与执行之间存在秒级时间窗,两窗口径一致即可,不保证绝对实时一致(单商家操作场景可接受)。

+ 148 - 0
specs/026-store-menu-copy/zentao-design.md

@@ -0,0 +1,148 @@
+# 连锁门店「复制商品」功能说明
+
+> 版本:v1.1 | 日期:2026-09-03 | 状态:设计已评审定稿
+> 适用读者:需求、测试
+
+---
+
+## 一、功能背景
+
+连锁商家(如麦当劳)一个账号下开了多家门店,各家门店的商品菜单基本一致。目前分类和商品都挂在单个门店下,商家新开一家门店只能在商家端逐条重新录入,30 个商品的菜单录入要 1 小时以上,还容易漏录、录错价格。
+
+本功能提供「复制商品」:把一家门店的菜单一键复制到另一家门店,开新店的菜单初始化 1 分钟内完成。
+
+## 二、功能范围
+
+- **复制的内容**:全部分类 + 全部商品,包括价格、图片、商品介绍、属性(加料等)、规格(大小杯/温度等)、排序、是否推荐
+- **不复制**:门店资料(店名、地址、统编、税号、客服电话、营业时间)——每家门店这些必然不同
+- **实现范围**:商家 PC 端(本期);商家 App 后续复用同一功能
+
+## 三、怎么用(交互)
+
+### 入口
+
+商家 PC 端「商品管理」页:
+
+- 顶部是门店切换按钮(现状已有),**当前选中的门店就是复制目标**
+- 「添加商品」按钮旁边新增「**复制商品**」按钮
+- 商家名下门店不足 2 家时,按钮自动隐藏(单店商家完全无感知)
+
+### 操作流程
+
+```
+点「复制商品」
+   ↓
+① 选源门店(下拉框,自动排除当前门店)
+   ↓ 选定后自动显示差异概览
+② 查看差异概览;如有"内容不同"的商品,选择处理方式
+   ↓
+③ 点「确定复制」→ 完成提示 → 商品列表自动刷新
+```
+
+### 弹窗示意
+
+```
+┌─ 复制商品 ──────────────────────┐
+│ 从「台北忠孝店」复制到「台北信义店」   │
+│                                │
+│ 新增分类 1(同名合并 4)           │
+│ 新增商品 12                      │
+│ 同名且内容一致,跳过 23            │
+│                                │
+│ ▸ 同名但内容不同:3 个(点击展开)   │
+│    麦辣鸡腿堡   价格 65 → 75       │
+│    薯条(大)     图片已更换          │
+│    可乐(大)     介绍已修改          │
+│                                │
+│ 这 3 个商品怎么处理?              │
+│ ○ 保留本店版本(默认)              │
+│ ● 用源店版本覆盖                  │
+│                                │
+│          [取消]    [确定复制]      │
+└────────────────────────────────┘
+```
+
+### 特殊状态
+
+| 场景 | 弹窗表现 |
+|---|---|
+| 两店菜单完全一致 | 显示「已是最新,无新增」,确定按钮不可点 |
+| 源门店没有任何商品 | 提示「该门店没有可复制的商品」,确定按钮不可点 |
+| 复制完成 | 提示「新增分类 X、新增商品 Y、跳过 Z、覆盖 W」 |
+
+## 四、业务规则(测试依据)
+
+先记住两条判定规则:
+
+- **同一个分类** = 名字 + 语言相同
+- **同一个商品** = 名字 + 语言相同(同名但语言不同算**不同**商品,会作为新增补进来——比如本店只有中文「可乐」,源店还有英文「Cola」,复制后英文版也补上)
+
+### 复制时对每个分类/商品的处理
+
+| 情况 | 系统行为 |
+|---|---|
+| 目标店没有这个分类 | 新建分类 |
+| 目标店已有同名分类 | 复用已有分类,新商品挂进去(**不会出现两个"汉堡"分类**) |
+| 目标店没有这个商品 | 新增,内容照搬源店,**上下架、审核状态跟源店一致**(复制完即可营业) |
+| 同名且内容一致 | 跳过,不动 |
+| 同名但内容不同 | **商家选择**:保留本店版本(默认)或 用源店版本覆盖 |
+
+「内容」指:价格、图片、商品介绍、属性(加料等)、规格、排序、是否推荐。
+
+### 覆盖的边界(重要)
+
+选「用源店版本覆盖」时:
+
+- **更新**:价格、图片、介绍、属性、排序、推荐、规格关联
+- **不动**:本店的**上下架状态**——例:目标店「薯条」今日临时下架,覆盖后价格图片对齐源店,但**仍是下架**
+- **不动**:本店的审核状态
+- **不动**:本店已有规格模板里的加价——例:本店「大小杯」模板"大杯+5"是本店自改价,覆盖后**仍是+5**(规格模板全店共享,改它会连带影响本店其他商品,所以不覆盖)
+
+### 其他规则
+
+- 源门店和目标门店不能选同一家
+- 权限:主账号可复制自己名下任意两家门店;分管账号只能复制授权范围内的门店
+- **可以反复执行**:菜单有变化后再复制一次,只补缺的;显示「新增 0、覆盖 0」就代表两边已一致
+- 复制要么全部完成、要么全部不做,不会出现复制一半的情况
+
+## 五、测试用例建议
+
+### 1. 主流程
+
+| 用例 | 前置 | 步骤 | 预期 |
+|---|---|---|---|
+| TC01 空店全量复制 | 商家有店 X(5 分类 38 商品,含带规格、带属性商品)、空店 Y | X→Y 复制(默认保留) | Y 生成 5 分类 38 商品,内容一致;规格可选且加价正确;图片正常显示 |
+| TC02 上下架状态继承 | 源店商品一半上架一半下架 | 复制到空店 | 新店商品上下架状态与源店一致;上架商品顾客端可下单 |
+| TC03 重复执行 | TC01 完成后 | 再执行一次相同复制 | 提示新增 0、覆盖 0;数据无变化 |
+| TC04 预览与执行一致 | X 有 38 商品,Y 已有 26 个(23 个一致 3 个差异) | 打开预览后确认复制 | 预览计数=新增 12、跳过 23、差异 3;实际执行结果与预览一致 |
+
+### 2. 差异与覆盖
+
+| 用例 | 前置 | 步骤 | 预期 |
+|---|---|---|---|
+| TC05 差异-保留(默认) | Y 的「麦辣鸡腿堡」价格 65,X 为 75 | 选保留复制 | Y 该商品价格仍 65,其余新增正常 |
+| TC06 差异-覆盖内容 | 同上 | 选覆盖复制 | Y 该商品价格变 75,图片/介绍/属性/排序/推荐对齐 X |
+| TC07 覆盖不动上下架 | Y 的「薯条」已下架 | 选覆盖复制 | 薯条内容对齐 X,但**仍为下架** |
+| TC08 覆盖不动审核 | Y 的商品审核状态与 X 不同 | 选覆盖复制 | Y 审核状态不变 |
+| TC09 覆盖不动规格加价 | Y 的「大小杯」模板"大杯+5",X 为"大杯+3" | 选覆盖复制 | Y 模板仍"大杯+5";若 X 多关联了「温度」规格,Y 商品补挂「温度」 |
+
+### 3. 匹配规则
+
+| 用例 | 前置 | 预期 |
+|---|---|---|
+| TC10 分类合并 | Y 已有「汉堡」分类 | 不产生第二个「汉堡」,新增商品挂入已有分类 |
+| TC11 多语言独立匹配 | Y 只有中文「可乐」,X 有中英两行 | 英文行新增、中文行按一致/差异规则处理 |
+| TC12 源店同名分类脏数据 | X 有两个同名分类 | 与第一个合并,不报错 |
+| TC13 名字不同内容相同 | X「麦辣鸡腿堡」、Y「辣鸡腿堡」价格相同 | 判定为不同商品,X 的作为新增 |
+
+### 4. 权限与边界
+
+| 用例 | 前置 | 预期 |
+|---|---|---|
+| TC14 单店隐藏入口 | 商家只有 1 家门店 | 商品管理页无「复制商品」按钮 |
+| TC15 源=目标 | 两个下拉选到同一家(或构造请求) | 系统拒绝,提示错误 |
+| TC16 分管账号越权 | 分管账号甲授权 A、B,未授权 C | A→B 成功;伪造参数 A→C 被拒绝,C 数据不变 |
+| TC17 源店为空 | 源店无分类无商品 | 提示无可复制,无任何写入 |
+| TC18 中途失败 | 模拟复制过程中出错 | 目标店无任何新增(不留半个菜单) |
+| TC19 大菜单性能 | 源店 100 商品 | 复制 10 秒内完成,无超时 |
+| TC20 多语言界面 | 分别切简中/繁中/英文/越南文打开弹窗 | 文案全部正确翻译,无乱码或显示 key |