日期:2026-10-12
分支:test-202609v2(后端已实现并过测试;商家 PC 端 foodie-store 已上线同款功能,本文档供 App 端复用同一组接口)
一句话:连锁商家在 App 里把一家门店的全部分类+商品复制到另一家门店;复制前可预览差异,同名但内容不同的商品由商家选择保留本店版本或用源店覆盖;可重复执行(幂等)。
功能规格:specs/026-store-menu-copy/spec.md;PC 端参考实现:foodie-store/src/components/StoreMenuCopyDialog.vue。
userType=1)可对自己名下所有门店操作;分管账号(022 子账号)仅当源、目标两店都在其授权范围内可用。userType=4)只有一个绑定摊位,不出现复制入口。| 项 | 说明 |
|---|---|
| 鉴权 | 请求头 token: <商家登录令牌>;服务端校验操作者对源、目标两店均有权限,越权返回错误且不产生任何写入 |
| 响应包裹 | { "code": 200, "msg": "...", "data": ... };业务失败 code=500,msg 为服务端本地化文案 |
| 错误文案语言 | 请求带查询参数 lang(如 lang=vi / zh / en,缺省繁中),App 直接展示 msg,不要自己映射错误文案 |
| 超时 | 预览与执行建议设 ≥ 15 秒(PC 端即 15s;后端 100 商品规模 < 10 秒) |
| 入口控制 | 当前账号可访问门店数 < 2 时,App 不显示「复制商品」入口;源店与目标店不能是同一家 |
选择源门店/展示入口前,先取当前账号有权限的门店:
GET /chanting/store/storelistlist?page=1&size=100
token: <商家登录令牌>
data 为分页对象 IPage<PosStore>,App 主要用 records[].id(门店 id)和 records[].posName(门店名);total 判断门店数是否 ≥ 2。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 项。
POST /chanting/food/menu-copy/execute
token: <商家登录令牌>
Content-Type: application/json
{
"sourceStoreId": 101,
"targetStoreId": 202,
"conflictStrategy": "KEEP_TARGET"
}
conflictStrategy 差异商品处理方式:
KEEP_TARGET:保留本店版本(默认,传空/不传即此值)OVERWRITE_TARGET:用源店版本覆盖差异商品的内容字段响应 data(StoreMenuCopyResult):
{
"code": 200,
"data": {
"addedCategoryCount": 5,
"addedProductCount": 12,
"skippedProductCount": 23,
"overwrittenProductCount": 0
}
}
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 |
無權存取該店鋪 | 任一店无权限 / 非商家账号 |
OVERWRITE_TARGET 只更新内容字段(价格、图片、介绍、属性、排序、推荐、规格关联),不改目标店商品的 id、上架状态、审核状态——目标店临时下架的商品覆盖后仍保持下架。differentProductCount > 0 时可展开差异明细(商品名 + 字段前后值),并提供单选:
KEEP_TARGET,默认)OVERWRITE_TARGET,选中时给出醒目确认提示)App 端功能 UI 文案需覆盖 vi / zh / tw / en 四语。PC 端 menuCopy.* 文案 key 可直接参考 foodie-store/src/lang/{zh,tw,en,vi}.js 的同名节点。