# 属性模版 — 测试用例

> **依据：** 《属性模版功能需求.md》v1.0、《属性模版技术方案.md》v1.0  
> **关联：** 《店铺商品列表测试用例.md》（商品选用模版协作）、《店铺商品分类测试用例.md》（店铺上下文/批量删模式）  
> **范围：** 商家端 `/agri/seller/attrTemplate`；`AttrTemplateServiceImpl` / `SellerAttrTemplateController`；`IAttrTemplateFacade`；`IGoodsFacade.existsByAttrTemplateId`  
> **排除：** 平台端模版 CRUD、C 端展示 E2E、模版导入导出、商品发品完整表单 E2E（v1.x 另册）  
> **环境：** RuoYi v3.9.2-springboot2；MySQL 5.7.39；请求头 **`X-Shop-Id`** 标识当前店铺

---

## 用例编号说明

| 前缀 | 类型 | 工具 |
|------|------|------|
| SAT-UT-xxx | 单元测试 | JUnit 5 + Mockito |
| SAT-API-xxx | 接口测试 | MockMvc / Apifox |
| SAT-UI-xxx | 界面测试 | Playwright（Chromium / Chrome 通道） |

---

## 测试数据约定

| 实体 | 说明 |
|------|------|
| 商户 A | 店铺 **101**（张三农资店）、**102**（李四农资店）；经营账号 `13800001001` |
| 店铺 101 模版 | **1=宠物**（属性：类型/品种；无规格）；**2=手机**（属性：屏幕/生产日期/品牌；规格：颜色/内存）；**3=待删**（属性：备注；**无商品引用**）；**4=引用中**（属性：产地；**有商品引用**） |
| 店铺 102 模版 | **501=他店模版**（属性：规格A） |
| 商品协作 | `goodsId=9001`，`shop_id=101`，`attr_template_id=4`，`goods_status=2`（出售中），`del_flag=0` |
| Token | 经营账号；`X-Shop-Id=101`；含 `agri:seller:attrTemplate:*` |
| 员工 Token | 子管理员；默认 **无** 属性模版菜单权限 |
| 已删店 | 店铺 **103**（`biz_shop.del_flag=2`） |

**名称列表期望（模版 2）：** `屏幕,生产日期,品牌,颜色,内存`（先属性项、后规格项，逗号分隔）。

---

## 一、单元测试

### SAT-UT-001 列表范围为当前店铺有效模版

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-001 |
| **测试模块** | 属性模版 |
| **测试项** | selectList 数据范围 |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 + Mockito |
| **测试目的** | 验证 AT1：仅当前店铺未删除模版 |
| **前置条件** | 店铺 101 有模版 1～4；店铺 102 有 501；模版 1 del_flag=0 |
| **测试步骤** | `SellerShopContext` 设 shopId=101，调用 `selectTemplateList` |
| **预期结果** | 返回 4 条；**不含** 501 及 del_flag=2 记录 |

### SAT-UT-002 列表按 create_time 降序

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-002 |
| **测试模块** | 属性模版 |
| **测试项** | 列表排序 |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 |
| **测试目的** | 验证 §5.1 默认排序 |
| **前置条件** | 模版 1 创建时间早于模版 2 |
| **测试步骤** | 调用 selectList 无 keyword |
| **预期结果** | 首行 templateId=2（较新者靠前） |

### SAT-UT-003 buildNameList 先属性后规格

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-003 |
| **测试模块** | 属性模版 |
| **测试项** | buildNameList |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 |
| **测试目的** | 验证 AT6 |
| **前置条件** | 模版 2 含属性项 屏幕/生产日期/品牌，规格项 颜色/内存 |
| **测试步骤** | 调用 `buildNameList(2)` |
| **预期结果** | 返回 `屏幕,生产日期,品牌,颜色,内存` |

### SAT-UT-004 keyword 模版名称模糊匹配

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-004 |
| **测试模块** | 属性模版 |
| **测试项** | keyword 检索 |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 |
| **测试目的** | 验证 §5.4 |
| **前置条件** | 存在「宠物」「手机」 |
| **测试步骤** | keyword=「手」；再 keyword=「1」（按 ID） |
| **预期结果** | 前者仅命中「手机」；后者 **不** 按 templateId 命中 |

### SAT-UT-005 requireShopId 未选店铺失败

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-005 |
| **测试模块** | 属性模版 |
| **测试项** | requireShopId |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 |
| **测试目的** | 验证 AT11、§6 |
| **前置条件** | `SellerShopContext.getShopId()` 为 null |
| **测试步骤** | 调用 list / insertTemplate |
| **预期结果** | 抛 `ServiceException`；msg 含「请先选择当前店铺」 |

### SAT-UT-006 店铺已逻辑删除 assertShopMaintable 失败

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-006 |
| **测试模块** | 属性模版 |
| **测试项** | assertShopMaintable |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 + Mockito |
| **测试目的** | 验证删店后不可维护 |
| **前置条件** | shopId=103，`biz_shop.del_flag=2` |
| **测试步骤** | 调用 insertTemplate / selectList |
| **预期结果** | 失败；msg 含「店铺已关闭，无法维护属性模版」 |

### SAT-UT-007 新增模版名称本店唯一-成功

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-007 |
| **测试模块** | 属性模版 |
| **测试项** | assertTemplateNameUnique |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 |
| **测试目的** | 验证 AT3 正常路径 |
| **前置条件** | 店铺 101 无「家电」 |
| **测试步骤** | validateSaveDto templateName=家电 |
| **预期结果** | 校验通过 |

### SAT-UT-008 新增模版名称重复失败

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-008 |
| **测试模块** | 属性模版 |
| **测试项** | assertTemplateNameUnique |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 |
| **测试目的** | 验证 AT3 |
| **前置条件** | 已有 templateName=宠物 |
| **测试步骤** | insertTemplate templateName=宠物 |
| **预期结果** | 失败；msg 含「属性模版名称已存在」 |

### SAT-UT-009 编辑名称排除自身可保留

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-009 |
| **测试模块** | 属性模版 |
| **测试项** | 编辑名称唯一 |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 |
| **测试目的** | 验证 §8.2 排除自身 |
| **前置条件** | 编辑 templateId=1，名称仍为「宠物」 |
| **测试步骤** | updateTemplate 不改 templateName |
| **预期结果** | 校验通过 |

### SAT-UT-010 不同店铺同名允许

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-010 |
| **测试模块** | 属性模版 |
| **测试项** | 店间隔离 |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 |
| **测试目的** | 验证 AT11 |
| **前置条件** | 店铺 101 有「宠物」；店铺 102 无「宠物」 |
| **测试步骤** | shopId=102 创建 templateName=宠物 |
| **预期结果** | 成功；两店各一条「宠物」 |

### SAT-UT-011 无属性项且无规格项失败

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-011 |
| **测试模块** | 属性模版 |
| **测试项** | validateSaveDto 至少一项 |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 |
| **测试目的** | 验证 AT4、AT-A2 |
| **前置条件** | attributes=[]，specs=[] |
| **测试步骤** | insertTemplate |
| **预期结果** | 失败；msg 含「请至少添加一个属性项或规格项」 |

### SAT-UT-012 模版名称为空失败

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-012 |
| **测试模块** | 属性模版 |
| **测试项** | templateName 必填 |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 |
| **测试目的** | 验证 AT3 |
| **前置条件** | templateName="" 或 null |
| **测试步骤** | insertTemplate |
| **预期结果** | 失败；msg 含「请输入属性模版名称」 |

### SAT-UT-013 属性项段内名称重复失败

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-013 |
| **测试模块** | 属性模版 |
| **测试项** | 属性项重名 |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 |
| **测试目的** | 验证 AT5 |
| **前置条件** | attributes 含两项 itemName=品牌 |
| **测试步骤** | validateSaveDto |
| **预期结果** | 失败；msg 含「属性项/规格项名称不可重复」 |

### SAT-UT-014 属性项与规格项跨段重名失败

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-014 |
| **测试模块** | 属性模版 |
| **测试项** | 跨段 itemName 唯一 |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 |
| **测试目的** | 验证 §7.2.3、AT5 |
| **前置条件** | attributes 含「颜色」；specs 含「颜色」 |
| **测试步骤** | validateSaveDto |
| **预期结果** | 失败；msg 含「属性项与规格项名称不可相同」 |

### SAT-UT-015 属性项无值失败

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-015 |
| **测试模块** | 属性模版 |
| **测试项** | 每项至少一值 |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 |
| **测试目的** | 验证 AT5、AT-A4 |
| **前置条件** | attributes=[{itemName:品牌, values:[]}] |
| **测试步骤** | insertTemplate |
| **预期结果** | 失败；msg 含「请为每项至少添加一个属性值/规格值」 |

### SAT-UT-016 同项下属性值重复失败

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-016 |
| **测试模块** | 属性模版 |
| **测试项** | 值唯一 |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 |
| **测试目的** | 验证 AT5 |
| **前置条件** | 品牌 values=[小米, 小米] |
| **测试步骤** | validateSaveDto |
| **预期结果** | 失败；msg 含「同一项下属性值/规格值不可重复」 |

### SAT-UT-017 仅属性段保存成功

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-017 |
| **测试模块** | 属性模版 |
| **测试项** | insertTemplate 仅属性 |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 + Mockito |
| **测试目的** | 验证 AT4 属性段可独立成模 |
| **前置条件** | specs=[]；attributes=[{itemName:类型, values:[犬类]}] |
| **测试步骤** | insertTemplate |
| **预期结果** | 成功；item_type=1 有记录；item_type=2 无记录 |

### SAT-UT-018 仅规格段保存成功

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-018 |
| **测试模块** | 属性模版 |
| **测试项** | insertTemplate 仅规格 |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 |
| **测试目的** | 验证 AT4 规格段可独立成模 |
| **前置条件** | attributes=[]；specs=[{itemName:颜色, values:[红]}] |
| **测试步骤** | insertTemplate |
| **预期结果** | 成功；item_type=2 有记录 |

### SAT-UT-019 编辑全量替换项与值

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-019 |
| **测试模块** | 属性模版 |
| **测试项** | updateTemplate replace |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 + DB 断言 |
| **测试目的** | 验证技术方案 §3.4 PUT 策略 |
| **前置条件** | 模版 1 原属性项=类型/品种 |
| **测试步骤** | PUT 改为 attributes=[{itemName:体重, values:[小型]}] |
| **预期结果** | 旧 item/value **物理删除**；仅保留新结构；nameList=体重 |

### SAT-UT-020 编辑模版不更新商品 attr 快照

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-020 |
| **测试模块** | 属性模版 |
| **测试项** | AT8 不追溯 |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 + Mockito |
| **测试目的** | 验证 AT8、§10.2 |
| **前置条件** | goodsId=9001 引用 templateId=4；biz_goods_attr 有历史行 |
| **测试步骤** | updateTemplate(4) 修改属性项；verify 无 goodsMapper 更新 |
| **预期结果** | 模版更新成功；**未调用** 更新 `biz_goods` / `biz_goods_attr` |

### SAT-UT-021 编辑模版不改动商品状态

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-021 |
| **测试模块** | 属性模版 |
| **测试项** | AT10 |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 + DB 断言 |
| **测试目的** | 验证 AT10 |
| **前置条件** | goods 9001 goods_status=2，attr_template_id=4 |
| **测试步骤** | updateTemplate(4) |
| **预期结果** | 模版成功；goods 9001 goods_status **仍为** 2 |

### SAT-UT-022 删除无引用模版成功

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-022 |
| **测试模块** | 属性模版 |
| **测试项** | logicDelete |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 |
| **测试目的** | 验证 §9、AT12 |
| **前置条件** | templateId=3；existsByAttrTemplateId=false |
| **测试步骤** | deleteTemplate(3) |
| **预期结果** | del_flag=2；selectList **不含** 3 |

### SAT-UT-023 删除有商品引用失败

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-023 |
| **测试模块** | 属性模版 |
| **测试项** | existsByAttrTemplateId |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 + Mockito |
| **测试目的** | 验证 AT7 |
| **前置条件** | templateId=4；goodsFacade.existsByAttrTemplateId=true |
| **测试步骤** | deleteTemplate(4) |
| **预期结果** | 失败；msg 含「该属性模版已被商品使用，无法删除」 |

### SAT-UT-024 批量删除整批成功

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-024 |
| **测试模块** | 属性模版 |
| **测试项** | 批量删 |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 |
| **测试目的** | 验证 §9.1 正常批量 |
| **前置条件** | templateIds=[1,3] 均无商品引用（测试前解绑 goods 或 mock false） |
| **测试步骤** | deleteTemplateBatch([1,3]) |
| **预期结果** | 全部 del_flag=2 |

### SAT-UT-025 批量删除含引用项整批失败

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-025 |
| **测试模块** | 属性模版 |
| **测试项** | AttrTemplateBatchDeleteException |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 |
| **测试目的** | 验证 AT7 整批失败 |
| **前置条件** | ids=[3,4]；4 有商品引用 |
| **测试步骤** | deleteTemplateBatch |
| **预期结果** | 抛批量异常；**3、4 均未删除**；reasons 含模版 4 |

### SAT-UT-026 跨店 templateId 操作失败

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-026 |
| **测试模块** | 属性模版 |
| **测试项** | shop_id 校验 |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 |
| **测试目的** | 验证 AT11 |
| **前置条件** | shopId=101；templateId=501 属店铺 102 |
| **测试步骤** | getDetail(501) / updateTemplate(501) / deleteTemplate(501) |
| **预期结果** | 失败；msg 含「属性模版不存在」或等价无权限 |

### SAT-UT-027 listOptionsByShopId 仅本店有效模版

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-027 |
| **测试模块** | 属性模版 |
| **测试项** | IAttrTemplateFacade |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 |
| **测试目的** | 验证 AT9 下拉数据源 |
| **前置条件** | 店铺 101 有模版 1、2；店铺 102 有 501 |
| **测试步骤** | `listOptionsByShopId(101)` |
| **预期结果** | 返回 templateId 1、2；含 templateName、nameList；**不含** 501 |

### SAT-UT-028 getDetailForGoods 结构完整

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UT-028 |
| **测试模块** | 属性模版 |
| **测试项** | getDetailForGoods |
| **测试类型** | 单元测试 |
| **测试工具** | JUnit 5 |
| **测试目的** | 验证商品预填结构 |
| **前置条件** | templateId=2 属 shop 101 |
| **测试步骤** | `getDetailForGoods(2, 101)` |
| **预期结果** | attributes 3 项、specs 2 项；每项 values 非空数组 |

---

## 二、接口测试

### SAT-API-001 GET /list 分页与字段

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-API-001 |
| **测试模块** | 属性模版 |
| **测试项** | GET /list |
| **测试类型** | 接口测试 |
| **测试工具** | MockMvc |
| **测试目的** | 验证 §3.2、AT1 |
| **前置条件** | Token + `X-Shop-Id=101`；`agri:seller:attrTemplate:list` |
| **测试步骤** | `GET /agri/seller/attrTemplate/list?pageNum=1&pageSize=15` |
| **预期结果** | `code=200`；rows 含 templateName、nameList；total≥4 |

### SAT-API-002 GET /list keyword 检索

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-API-002 |
| **测试模块** | 属性模版 |
| **测试项** | GET /list keyword |
| **测试类型** | 接口测试 |
| **测试工具** | Apifox |
| **测试目的** | 验证 §5.4 |
| **前置条件** | 同上 |
| **测试步骤** | `GET .../list?keyword=手机` |
| **预期结果** | 仅返回 templateName 含「手机」的记录 |

### SAT-API-003 GET /list 缺 X-Shop-Id 失败

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-API-003 |
| **测试模块** | 属性模版 |
| **测试项** | 店铺上下文 |
| **测试类型** | 接口测试 |
| **测试工具** | MockMvc |
| **测试目的** | 验证 §6 |
| **前置条件** | 请求 **不带** X-Shop-Id |
| **测试步骤** | GET /list |
| **预期结果** | 失败；msg 含「请先选择当前店铺」 |

### SAT-API-004 GET /{templateId} 详情回显

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-API-004 |
| **测试模块** | 属性模版 |
| **测试项** | GET /{id} |
| **测试类型** | 接口测试 |
| **测试工具** | MockMvc |
| **测试目的** | 验证 §3.3 |
| **前置条件** | `agri:seller:attrTemplate:query` |
| **测试步骤** | `GET /agri/seller/attrTemplate/2` |
| **预期结果** | `code=200`；data.attributes、data.specs 与测试数据一致 |

### SAT-API-005 POST / 新增完整模版成功

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-API-005 |
| **测试模块** | 属性模版 |
| **测试项** | POST / |
| **测试类型** | 接口测试 |
| **测试工具** | Apifox |
| **测试目的** | 验证 §7 主流程 |
| **前置条件** | `agri:seller:attrTemplate:add` |
| **测试步骤** | POST body：`{"templateName":"家电","attributes":[{"itemName":"功率","values":["1000W"]}],"specs":[{"itemName":"尺寸","values":["大号","小号"]}]}` |
| **预期结果** | `code=200`；GET /list 可见「家电」；nameList=功率,尺寸 |

### SAT-API-006 POST / 模版名称重复失败

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-API-006 |
| **测试模块** | 属性模版 |
| **测试项** | POST / 名称 |
| **测试类型** | 接口测试 |
| **测试工具** | Apifox |
| **测试目的** | 验证 AT3 |
| **前置条件** | 已有「宠物」 |
| **测试步骤** | POST templateName=宠物 + 合法 attributes |
| **预期结果** | 失败；msg 含「属性模版名称已存在」 |

### SAT-API-007 POST / 无属性无规格失败

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-API-007 |
| **测试模块** | 属性模版 |
| **测试项** | POST / 结构 |
| **测试类型** | 接口测试 |
| **测试工具** | MockMvc |
| **测试目的** | 验证 AT4 |
| **前置条件** | attributes=[]，specs=[] |
| **测试步骤** | POST 创建 |
| **预期结果** | 失败；msg 含「请至少添加一个属性项或规格项」 |

### SAT-API-008 POST / 属性规格跨段重名失败

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-API-008 |
| **测试模块** | 属性模版 |
| **测试项** | POST / 跨段重名 |
| **测试类型** | 接口测试 |
| **测试工具** | Apifox |
| **测试目的** | 验证 §12 |
| **前置条件** | attributes 与 specs 均含 itemName=内存 |
| **测试步骤** | POST 创建 |
| **预期结果** | 失败；msg 含「属性项与规格项名称不可相同」 |

### SAT-API-009 PUT / 编辑成功且全量替换

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-API-009 |
| **测试模块** | 属性模版 |
| **测试项** | PUT / |
| **测试类型** | 接口测试 |
| **测试工具** | MockMvc |
| **测试目的** | 验证 §8 |
| **前置条件** | `agri:seller:attrTemplate:edit` |
| **测试步骤** | PUT templateId=1，改 templateName=宠物用品，attributes 改为单一项「适用对象」 |
| **预期结果** | `code=200`；GET /1 同步；旧项「类型/品种」**不存在** |

### SAT-API-010 PUT / 编辑有引用模版允许且商品不变

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-API-010 |
| **测试模块** | 属性模版 |
| **测试项** | PUT / AT8 |
| **测试类型** | 接口测试 |
| **测试工具** | Apifox + DB |
| **测试目的** | 验证 AT8、§8.2 |
| **前置条件** | templateId=4 被 goods 9001 引用 |
| **测试步骤** | PUT 修改 attributes；查 goods 9001 与 biz_goods_attr |
| **预期结果** | 模版更新成功；goods 9001 属性快照 **不变**；goods_status **不变** |

### SAT-API-011 DELETE /{id} 无引用成功

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-API-011 |
| **测试模块** | 属性模版 |
| **测试项** | DELETE /{id} |
| **测试类型** | 接口测试 |
| **测试工具** | MockMvc |
| **测试目的** | 验证 §9 正常删除 |
| **前置条件** | `agri:seller:attrTemplate:remove`；templateId=3 无引用 |
| **测试步骤** | `DELETE /agri/seller/attrTemplate/3` |
| **预期结果** | `code=200`；/list、/options **不含** 3 |

### SAT-API-012 DELETE /{id} 有商品引用失败

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-API-012 |
| **测试模块** | 属性模版 |
| **测试项** | DELETE 商品阻断 |
| **测试类型** | 接口测试 |
| **测试工具** | Apifox |
| **测试目的** | 验证 AT7 |
| **前置条件** | templateId=4 关联 goods 9001 |
| **测试步骤** | DELETE /4 |
| **预期结果** | 失败；msg 含「已被商品使用」；模版仍存在 |

### SAT-API-013 DELETE 批量含引用整批失败

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-API-013 |
| **测试模块** | 属性模版 |
| **测试项** | DELETE 批量 |
| **测试类型** | 接口测试 |
| **测试工具** | MockMvc |
| **测试目的** | 验证 §9.1 整批失败 |
| **前置条件** | ids=3,4；4 有引用 |
| **测试步骤** | `DELETE /agri/seller/attrTemplate/3,4` |
| **预期结果** | 失败；msg 含「整批操作已取消」；3、4 **均未删** |

### SAT-API-014 GET /options 发品下拉

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-API-014 |
| **测试模块** | 属性模版 |
| **测试项** | GET /options |
| **测试类型** | 接口测试 |
| **测试工具** | Apifox |
| **测试目的** | 验证 §3.6、AT9 |
| **前置条件** | shop 101 有效模版 1、2、4 |
| **测试步骤** | `GET /agri/seller/attrTemplate/options` |
| **预期结果** | `code=200`；data 数组含 templateId、templateName、nameList；**不分页** |

### SAT-API-015 跨店访问他店模版 ID 失败

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-API-015 |
| **测试模块** | 属性模版 |
| **测试项** | 跨店越权 |
| **测试类型** | 接口测试 |
| **测试工具** | MockMvc |
| **测试目的** | 验证 AT11 |
| **前置条件** | X-Shop-Id=101 |
| **测试步骤** | GET /501；PUT /501；DELETE /501 |
| **预期结果** | 均失败；不泄露 102 店数据 |

### SAT-API-016 切换店铺后列表数据隔离

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-API-016 |
| **测试模块** | 属性模版 |
| **测试项** | 多店切换 |
| **测试类型** | 接口测试 |
| **测试工具** | Apifox |
| **测试目的** | 验证 §6、AT11 |
| **前置条件** | 同一 Token |
| **测试步骤** | GET /list，`X-Shop-Id=101`；再 `X-Shop-Id=102` |
| **预期结果** | 101 含「宠物」；102 含「他店模版」；互不含对方数据 |

### SAT-API-017 已删店 103 维护失败

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-API-017 |
| **测试模块** | 属性模版 |
| **测试项** | 删店阻断 |
| **测试类型** | 接口测试 |
| **测试工具** | Apifox |
| **测试目的** | 验证 §2.2 |
| **前置条件** | X-Shop-Id=103 |
| **测试步骤** | GET /list；POST / |
| **预期结果** | 失败；msg 含「店铺已关闭，无法维护属性模版」 |

### SAT-API-018 无 list 权限 403

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-API-018 |
| **测试模块** | 属性模版 |
| **测试项** | @PreAuthorize |
| **测试类型** | 接口测试 |
| **测试工具** | MockMvc |
| **测试目的** | 验证 §2.7 权限 |
| **前置条件** | 员工 Token；角色 **未** 分配 attrTemplate 菜单 |
| **测试步骤** | GET /list |
| **预期结果** | HTTP 403 |

### SAT-API-019 GET /goods/attrTemplateOptions 协作（已实现）

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-API-019 |
| **测试模块** | 属性模版 |
| **测试项** | 商品侧下拉 + 预填 |
| **测试类型** | 接口测试 |
| **测试工具** | Apifox / MockMvc |
| **测试目的** | 验证 AT9：商品发品下拉与预填 API |
| **前置条件** | shop 101 有模版；`X-Shop-Id=101` |
| **测试步骤** | GET `/agri/seller/goods/attrTemplateOptions`；GET `/agri/seller/goods/attrTemplate/4` |
| **预期结果** | options 与 `/attrTemplate/options` 一致；预填含 attributes/specs |

---

## 三、界面测试（Playwright）

> **通用前置：** Playwright 启动 **Chromium（Chrome 通道）**；商家端 URL 以环境为准（如 `http://localhost:80`）；经营账号 `13800001001` / 密码见测试环境；登录后页头展示当前店铺；菜单 **商品管理 → 属性模版**（路由 `/agri/seller/attrTemplate`）。

### SAT-UI-001 进入列表展示模版与名称列表

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UI-001 |
| **测试模块** | 属性模版 |
| **测试项** | 列表页 |
| **测试类型** | 界面测试 |
| **测试工具** | Playwright (Chrome) |
| **测试目的** | 验证 AT1、§5.2 |
| **前置条件** | 已登录；当前店铺=张三农资店；101 有模版 1～4 |
| **测试步骤** | 1. `page.goto` 登录页并登录 2. 进入「属性模版」菜单 |
| **预期结果** | 页头展示「张三农资店」；表格含 **属性模版名称、名称列表、操作（编辑/删除）**；「手机」行 nameList 含 `屏幕,品牌,颜色,内存` |

### SAT-UI-002 按模版名称检索与重置

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UI-002 |
| **测试模块** | 属性模版 |
| **测试项** | 名称检索 |
| **测试类型** | 界面测试 |
| **测试工具** | Playwright |
| **测试目的** | 验证 §5.4 |
| **前置条件** | 存在「宠物」「手机」 |
| **测试步骤** | 检索框输入「宠」→ 搜索 → 重置 |
| **预期结果** | 搜索后仅「宠物」；重置后恢复全量 |

### SAT-UI-003 添加属性模版完整流程

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UI-003 |
| **测试模块** | 属性模版 |
| **测试项** | 添加模版 |
| **测试类型** | 界面测试 |
| **测试工具** | Playwright |
| **测试目的** | 验证 §7 主流程 |
| **前置条件** | 有 add 权限 |
| **测试步骤** | 1. 点「添加属性模版」2. 名称=「家电」3. 添加属性项「功率」值「1000W」4. 添加规格项「尺寸」值「大」「小」5. 确认 |
| **预期结果** | 成功提示；列表出现「家电」；nameList=`功率,尺寸` |

### SAT-UI-004 动态添加属性项与属性值

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UI-004 |
| **测试模块** | 属性模版 |
| **测试项** | +添加属性项 / +增加属性值 |
| **测试类型** | 界面测试 |
| **测试工具** | Playwright |
| **测试目的** | 验证 §7.3 |
| **前置条件** | 打开添加弹窗 |
| **测试步骤** | 点「+添加属性项」新增第二项；点「+增加属性值」为第一项追加值 |
| **预期结果** | 表单出现新行；可多值标签/输入框；可删除已加项/值 |

### SAT-UI-005 动态添加规格项与规格值

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UI-005 |
| **测试模块** | 属性模版 |
| **测试项** | +添加规格项 / +增加规格值 |
| **测试类型** | 界面测试 |
| **测试工具** | Playwright |
| **测试目的** | 验证 §7.3 规格段 |
| **前置条件** | 打开添加/编辑弹窗 |
| **测试步骤** | 在「规格」区点「+添加规格项」；为规格项点「+增加规格值」 |
| **预期结果** | 规格区独立展示；交互与属性段一致 |

### SAT-UI-006 模版名称为空拦截

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UI-006 |
| **测试模块** | 属性模版 |
| **测试项** | 名称必填 |
| **测试类型** | 界面测试 |
| **测试工具** | Playwright |
| **测试目的** | 验证 §12 |
| **前置条件** | 打开添加弹窗 |
| **测试步骤** | 名称留空；填合法属性项 → 确认 |
| **预期结果** | 前端校验或提示「请输入属性模版名称」 |

### SAT-UI-007 无属性无规格拦截

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UI-007 |
| **测试模块** | 属性模版 |
| **测试项** | 至少一项 |
| **测试类型** | 界面测试 |
| **测试工具** | Playwright |
| **测试目的** | 验证 AT4 |
| **前置条件** | 打开添加弹窗 |
| **测试步骤** | 仅填名称「空结构」；不添加任何属性/规格项 → 确认 |
| **预期结果** | 提示「请至少添加一个属性项或规格项」 |

### SAT-UI-008 模版名称重复提示

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UI-008 |
| **测试模块** | 属性模版 |
| **测试项** | 名称唯一 |
| **测试类型** | 界面测试 |
| **测试工具** | Playwright |
| **测试目的** | 验证 AT3 |
| **前置条件** | 已有「宠物」 |
| **测试步骤** | 添加模版名称=「宠物」+ 合法属性 → 确认 |
| **预期结果** | 提示「属性模版名称已存在」 |

### SAT-UI-009 编辑模版回显并保存

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UI-009 |
| **测试模块** | 属性模版 |
| **测试项** | 编辑 |
| **测试类型** | 界面测试 |
| **测试工具** | Playwright |
| **测试目的** | 验证 §8 |
| **前置条件** | 模版「手机」存在 |
| **测试步骤** | 点行「编辑」→ 修改品牌增加「三星」→ 确认 |
| **预期结果** | 弹窗回显原属性/规格结构；保存后 GET 详情含「三星」；列表 nameList 更新 |

### SAT-UI-010 删除无引用模版二次确认

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UI-010 |
| **测试模块** | 属性模版 |
| **测试项** | 单独删除 |
| **测试类型** | 界面测试 |
| **测试工具** | Playwright |
| **测试目的** | 验证 §9、AT12 |
| **前置条件** | 模版「待删」无商品引用 |
| **测试步骤** | 点「删除」→ 确认框点确定 |
| **预期结果** | 成功提示；列表 **不含**「待删」 |

### SAT-UI-011 删除有引用模版阻断

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UI-011 |
| **测试模块** | 属性模版 |
| **测试项** | 删除阻断 |
| **测试类型** | 界面测试 |
| **测试工具** | Playwright |
| **测试目的** | 验证 AT7 |
| **前置条件** | 模版「引用中」被 goods 9001 使用 |
| **测试步骤** | 点「删除」→ 确认 |
| **预期结果** | 提示「已被商品使用，无法删除」；行仍在列表 |

### SAT-UI-012 批量删除勾选与整批失败

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UI-012 |
| **测试模块** | 属性模版 |
| **测试项** | 批量删除 |
| **测试类型** | 界面测试 |
| **测试工具** | Playwright |
| **测试目的** | 验证 §9.1 |
| **前置条件** | 勾选「待删」与「引用中」 |
| **测试步骤** | 点「批量删除」→ 确认 |
| **预期结果** | 提示整批失败；两条 **均仍在** 列表 |

### SAT-UI-013 批量删除全部可删成功

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UI-013 |
| **测试模块** | 属性模版 |
| **测试项** | 批量删除成功 |
| **测试类型** | 界面测试 |
| **测试工具** | Playwright |
| **测试目的** | 验证 §9 正常批量 |
| **前置条件** | 仅勾选无引用模版（测试数据「待删」） |
| **测试步骤** | 批量删除 → 确认 |
| **预期结果** | 成功；选中行从列表消失 |

### SAT-UI-014 切换店铺后列表刷新

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UI-014 |
| **测试模块** | 属性模版 |
| **测试项** | 多店上下文 |
| **测试类型** | 界面测试 |
| **测试工具** | Playwright |
| **测试目的** | 验证 §6、AT11 |
| **前置条件** | 商户 A 有店 101、102 |
| **测试步骤** | 当前店 101 见「宠物」→ 页头切换店铺为 102 → 回到属性模版 |
| **预期结果** | 页头店名变为李四农资店；列表 **不含**「宠物」；含「他店模版」 |

### SAT-UI-015 无权限账号不可见菜单

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UI-015 |
| **测试模块** | 属性模版 |
| **测试项** | 菜单权限 |
| **测试类型** | 界面测试 |
| **测试工具** | Playwright |
| **测试目的** | 验证 §2.7 |
| **前置条件** | 子管理员 Token；角色无 attrTemplate |
| **测试步骤** | 登录 → 查看商品管理菜单；直接 `goto` `/agri/seller/attrTemplate` |
| **预期结果** | 菜单 **无**「属性模版」；直链 403 或跳转无权限页 |

### SAT-UI-016 列表分页切换

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UI-016 |
| **测试模块** | 属性模版 |
| **测试项** | 分页 |
| **测试类型** | 界面测试 |
| **测试工具** | Playwright |
| **测试目的** | 验证 §5.1 |
| **前置条件** | 店铺 101 模版数 > 15（或 mock 分页） |
| **测试步骤** | 进入列表 → 切换每页 15 条 → 点第 2 页 |
| **预期结果** | 底部分页显示「第 x-y 条/总共 z 条」；翻页数据变化 |

### SAT-UI-017 取消添加不保存

| 要素 | 内容 |
|------|------|
| **用例编号** | SAT-UI-017 |
| **测试模块** | 属性模版 |
| **测试项** | 取消操作 |
| **测试类型** | 界面测试 |
| **测试工具** | Playwright |
| **测试目的** | 验证 §7.3 取消 |
| **前置条件** | 打开添加弹窗 |
| **测试步骤** | 填写名称与属性 → 点「取消」 |
| **预期结果** | 弹窗关闭；列表 **无** 新增行 |

---

## 四、规则覆盖矩阵

| 规则 | 单元 | 接口 | UI |
|------|:----:|:----:|:--:|
| AT1 当前店铺有效模版 | UT-001 | API-001 | UI-001 |
| AT2 仅商家端 | — | API-018 | UI-015 |
| AT3 名称本店唯一 | UT-007/008 | API-006 | UI-008 |
| AT4 至少一项 | UT-011/17/18 | API-007 | UI-007 |
| AT5 项/值校验 | UT-013～016 | API-008 | UI-006 |
| AT6 nameList | UT-003 | API-001 | UI-001 |
| AT7 删引用/整批失败 | UT-023/025 | API-012/013 | UI-011/012 |
| AT8 不追溯商品 | UT-020 | API-010 | — |
| AT9 下拉协作 | UT-027/028 | API-014/019 | — |
| AT10 不改商品状态 | UT-021 | API-010 | — |
| AT11 店铺上下文 | UT-005/010/026 | API-003/015/016 | UI-014 |
| AT12 逻辑删除 | UT-022 | API-011 | UI-010 |

---

## 五、修订记录

| 版本 | 说明 |
|------|------|
| **v1.0** | 首版：28 单元 + 19 接口 + 17 UI；覆盖 AT1～AT12 及正常/异常/协作流程 |
| **v1.1** | 商品协作：`attr_template_id` 保存/回显已实现 |
| **v1.2** | `biz_goods_attr` 与商品侧预填 API 已实现；SAT-API-019 标已实现 |

---

*文档版本：v1.2 · 关联《属性模版功能需求.md》v1.1、《属性模版技术方案.md》v1.3*
