# 连锁门店复制商品(026):App 端接入说明 **日期**:2026-10-12 **分支**:`test-202609v2`(后端已实现并过测试;商家 PC 端 foodie-store 已上线同款功能,本文档供 App 端复用同一组接口) **一句话**:连锁商家在 App 里把一家门店的全部分类+商品**复制**到另一家门店;复制前可**预览差异**,同名但内容不同的商品由商家选择**保留本店版本**或**用源店覆盖**;可重复执行(幂等)。 **功能规格**:`specs/026-store-menu-copy/spec.md`;PC 端参考实现:`foodie-store/src/components/StoreMenuCopyDialog.vue`。 --- ## 1. 功能与角色 - 面向**多家门店的商家**:主账号(`userType=1`)可对自己名下所有门店操作;分管账号(022 子账号)仅当源、目标两店都在其授权范围内可用。 - 摊位商家(`userType=4`)只有一个绑定摊位,**不出现**复制入口。 - 核心价值:新开门店初始化菜单、向其他门店推新品/改价,替代逐条录入。 - 本功能**零 DDL、无同步状态表**:差异一律实时计算,预览差异为 0 即等价「已是最新」。 ## 2. 通用约定 | 项 | 说明 | |---|---| | 鉴权 | 请求头 `token: <商家登录令牌>`;服务端校验操作者对源、目标两店**均有权限**,越权返回错误且不产生任何写入 | | 响应包裹 | `{ "code": 200, "msg": "...", "data": ... }`;业务失败 `code=500`,`msg` 为服务端本地化文案 | | 错误文案语言 | 请求带查询参数 `lang`(如 `lang=vi` / `zh` / `en`,缺省繁中),**App 直接展示 msg,不要自己映射错误文案** | | 超时 | 预览与执行建议设 **≥ 15 秒**(PC 端即 15s;后端 100 商品规模 < 10 秒) | | 入口控制 | 当前账号可访问门店数 < 2 时,App 不显示「复制商品」入口;源店与目标店不能是同一家 | ## 3. 前置接口:门店列表 选择源门店/展示入口前,先取当前账号有权限的门店: ```http GET /chanting/store/storelistlist?page=1&size=100 token: <商家登录令牌> ``` - 返回 `data` 为分页对象 `IPage`,App 主要用 `records[].id`(门店 id)和 `records[].posName`(门店名);`total` 判断门店数是否 ≥ 2。 - 返回范围:主账号 = 名下全部门店;分管账号 = 授权范围内门店;摊位主 = 仅绑定摊位(1 家,无入口)。 ## 4. 接口:复制预览 ```http POST /chanting/food/menu-copy/preview token: <商家登录令牌> Content-Type: application/json { "sourceStoreId": 101, "targetStoreId": 202 } ``` 响应 `data`(`StoreMenuCopyPreviewResponse`): ```json { "code": 200, "data": { "sourceStoreId": 101, "targetStoreId": 202, "addedCategoryCount": 5, "mergedCategoryCount": 2, "addedProductCount": 12, "identicalProductCount": 23, "differentProductCount": 3, "differences": [ { "sourceProductId": 5001, "targetProductId": 6102, "name": "麦辣鸡腿堡", "language": "3", "fields": [ { "field": "price", "sourceValue": "75", "targetValue": "65" } ] } ] } } ``` **计数含义**: | 字段 | 含义 | |---|---| | `addedCategoryCount` | 将新建的分类数 | | `mergedCategoryCount` | 同名分类合并数(目标店已有,复用) | | `addedProductCount` | 将新增的商品数 | | `identicalProductCount` | 同名且内容一致、将跳过的商品数 | | `differentProductCount` | 同名但内容不同的商品数(差异明细见 `differences`) | **差异明细 `differences[]`**:每个差异商品含 `sourceProductId` / `targetProductId` / `name` / `language`(多语言行各自独立匹配),`fields[]` 为字段级前后值。`field` 取值固定为以下 7 个之一: | `field` | 含义 | 前端展示建议 | |---|---|---| | `price` | 价格 | 金额 | | `image` | 图片 | 缩略图对比(URL 引用,直接展示) | | `introduce` | 介绍 | 文本,过长截断 | | `foodSku` | 属性(SKU) | 原始串,建议只提示「有变化」 | | `sort` | 排序 | 数字 | | `recommend` | 推荐 | 0/1 | | `specs` | 规格关联集合 | 建议只提示「有变化」 | **判定口径**:分类按(分类名+语言)匹配;商品按(商品名+语言)匹配;内容比对字段即上表 7 项。 ## 5. 接口:复制执行 ```http POST /chanting/food/menu-copy/execute token: <商家登录令牌> Content-Type: application/json { "sourceStoreId": 101, "targetStoreId": 202, "conflictStrategy": "KEEP_TARGET" } ``` - `conflictStrategy` 差异商品处理方式: - `KEEP_TARGET`:保留本店版本(**默认**,传空/不传即此值) - `OVERWRITE_TARGET`:用源店版本覆盖差异商品的内容字段 - 大小写不敏感;其他值返回错误 - 执行为**单事务**:全部成功或全部回滚;**幂等**:同参数重复执行,第二次新增 0、覆盖 0。 响应 `data`(`StoreMenuCopyResult`): ```json { "code": 200, "data": { "addedCategoryCount": 5, "addedProductCount": 12, "skippedProductCount": 23, "overwrittenProductCount": 0 } } ``` ## 6. 错误响应 `code=500`,`msg` 为按 `lang` 下发的本地化文案(App 直接展示)。可能的 key 与繁中文案: | msg key | 繁中文案 | 触发场景 | |---|---|---| | `menu.copy.store.required` | 來源門店和目標門店不能為空 | 缺 sourceStoreId / targetStoreId | | `menu.copy.store.same` | 來源門店和目標門店不能相同 | 两店相同 | | `menu.copy.strategy.invalid` | 差異商品處理方式無效 | conflictStrategy 非法值 | | `merchant.store.access.denied` | 無權存取該店鋪 | 任一店无权限 / 非商家账号 | ## 7. 业务规则要点(App 交互需对齐) - **覆盖不破坏本店状态**:`OVERWRITE_TARGET` 只更新内容字段(价格、图片、介绍、属性、排序、推荐、规格关联),**不改**目标店商品的 id、上架状态、审核状态——目标店临时下架的商品覆盖后仍保持下架。 - **新增商品**继承源店的上架与审核状态,复制后可直接被顾客下单;图片直接引用源店 URL,不复制文件。 - **规格**:目标店缺模板时按(规格名+语言)新建模板与规格值;已存在的规格模板自身的规格值**不会被修改**(门店内共享资产)。 - **多语言**:同一商品各语言行独立匹配、独立复制,不做跨语言合并。 - **预览与执行之间存在秒级时间窗**:若期间菜单被改动,以执行时快照为准,结果计数可能与预览略有出入(单商家操作场景可接受)。 ## 8. 推荐 App 交互流程(对齐 PC 端) 1. 商品管理页当前门店即**目标店**;门店数 < 2 隐藏「复制商品」入口。 2. 弹窗选择**源门店**(选项排除目标店)→ 调预览接口。 3. 展示 5 个计数;`differentProductCount > 0` 时可展开差异明细(商品名 + 字段前后值),并提供单选: - 保留本店版本(`KEEP_TARGET`,默认) - 用源店覆盖(`OVERWRITE_TARGET`,选中时给出醒目确认提示) 4. 无可复制内容(新增分类=0、新增商品=0,且未选覆盖)时,执行按钮置灰。 5. 确认执行 → 展示结果 4 计数(新增分类/新增商品/跳过/覆盖)→ 刷新商品列表。 6. 幂等可重跑:再次执行全部命中跳过,可当「同步校验」用。 ## 9. App 自身多语言 App 端功能 UI 文案需覆盖 **vi / zh / tw / en** 四语。PC 端 `menuCopy.*` 文案 key 可直接参考 `foodie-store/src/lang/{zh,tw,en,vi}.js` 的同名节点。