store-menu-copy-app-api.md 7.5 KB

连锁门店复制商品(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. 前置接口:门店列表

选择源门店/展示入口前,先取当前账号有权限的门店:

GET /chanting/store/storelistlist?page=1&size=100
token: <商家登录令牌>
  • 返回 data 为分页对象 IPage<PosStore>,App 主要用 records[].id(门店 id)和 records[].posName(门店名);total 判断门店数是否 ≥ 2。
  • 返回范围:主账号 = 名下全部门店;分管账号 = 授权范围内门店;摊位主 = 仅绑定摊位(1 家,无入口)。

4. 接口:复制预览

POST /chanting/food/menu-copy/preview
token: <商家登录令牌>
Content-Type: application/json

{
  "sourceStoreId": 101,
  "targetStoreId": 202
}

响应 data(StoreMenuCopyPreviewResponse):

{
  "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. 接口:复制执行

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):

{
  "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 的同名节点。