Parcourir la Source

会员管理代码

wwh il y a 2 semaines
Parent
commit
d676246772

+ 10 - 0
baqing-shop/src/main/java/com/ruoyi/web/modules/goods/constant/GoodsConstants.java

@@ -53,6 +53,16 @@ public final class GoodsConstants
53 53
 
54 54
     public static final String MSG_ATTR_TEMPLATE_INVALID = "请选择本店有效属性模版";
55 55
 
56
+    public static final String MSG_ATTR_ITEM_NAME_REQUIRED = "请输入属性项/规格项名称";
57
+
58
+    public static final String MSG_ATTR_ITEM_NAME_DUPLICATE = "属性项/规格项名称不可重复";
59
+
60
+    public static final String MSG_ATTR_CROSS_ITEM_NAME = "属性项与规格项名称不可相同";
61
+
62
+    public static final String MSG_ATTR_VALUE_REQUIRED = "请为每项至少添加一个属性值/规格值";
63
+
64
+    public static final String MSG_ATTR_VALUE_DUPLICATE = "同一项下属性值/规格值不可重复";
65
+
56 66
     public static final String MSG_DELETE_INVALID = "当前状态不可删除";
57 67
 
58 68
     public static final String MSG_UNFINISHED_ORDER = "该商品存在未完成订单,无法下架";

+ 25 - 0
baqing-shop/src/main/java/com/ruoyi/web/modules/goods/controller/SellerGoodsController.java

@@ -28,6 +28,8 @@ import com.ruoyi.web.modules.goods.dto.GoodsOffShelfDTO;
28 28
 import com.ruoyi.web.modules.goods.dto.GoodsSaveDTO;
29 29
 import com.ruoyi.web.modules.goods.exception.GoodsBatchOperationException;
30 30
 import com.ruoyi.web.modules.goods.service.IGoodsService;
31
+import com.ruoyi.web.modules.template.facade.IAttrTemplateFacade;
32
+import com.ruoyi.web.modules.template.vo.AttrTemplateDetailVO;
31 33
 
32 34
 /**
33 35
  * 商家端商品管理
@@ -48,6 +50,9 @@ public class SellerGoodsController extends BaseController
48 50
     @Autowired
49 51
     private IGoodsServiceFacade goodsServiceFacade;
50 52
 
53
+    @Autowired
54
+    private IAttrTemplateFacade attrTemplateFacade;
55
+
51 56
     @PreAuthorize("@ss.hasPermi('agri:seller:goods:list')")
52 57
     @GetMapping("/list")
53 58
     public TableDataInfo list(BizGoods query)
@@ -81,6 +86,26 @@ public class SellerGoodsController extends BaseController
81 86
         return success(data);
82 87
     }
83 88
 
89
+    @PreAuthorize("@ss.hasPermi('agri:seller:goods:list') or @ss.hasPermi('agri:seller:goods:add') or @ss.hasPermi('agri:seller:goods:edit')")
90
+    @GetMapping("/attrTemplateOptions")
91
+    public AjaxResult attrTemplateOptions()
92
+    {
93
+        return success(attrTemplateFacade.listOptionsByShopId(SellerShopContext.getShopId()));
94
+    }
95
+
96
+    @PreAuthorize("@ss.hasPermi('agri:seller:goods:list') or @ss.hasPermi('agri:seller:goods:add') or @ss.hasPermi('agri:seller:goods:edit')")
97
+    @GetMapping("/attrTemplate/{templateId}")
98
+    public AjaxResult attrTemplateDetail(@PathVariable("templateId") Long templateId)
99
+    {
100
+        AttrTemplateDetailVO detail = attrTemplateFacade.getDetailForGoods(templateId, SellerShopContext.getShopId());
101
+        Map<String, Object> data = new HashMap<>();
102
+        data.put("templateId", detail.getTemplateId());
103
+        data.put("templateName", detail.getTemplateName());
104
+        data.put("attributes", detail.getAttributes());
105
+        data.put("specs", detail.getSpecs());
106
+        return success(data);
107
+    }
108
+
84 109
     @PreAuthorize("@ss.hasPermi('agri:seller:goods:query')")
85 110
     @GetMapping("/{goodsId}")
86 111
     public AjaxResult getInfo(@PathVariable("goodsId") Long goodsId)

+ 26 - 0
baqing-shop/src/main/java/com/ruoyi/web/modules/goods/dto/GoodsSaveDTO.java

@@ -30,6 +30,12 @@ public class GoodsSaveDTO
30 30
 
31 31
     private List<Long> serviceIds;
32 32
 
33
+    /** 商品属性项(可选;选用模版后可修改) */
34
+    private List<GoodsAttrItemDTO> attributes;
35
+
36
+    /** 商品规格项(可选;选用模版后可修改) */
37
+    private List<GoodsAttrItemDTO> specs;
38
+
33 39
     /** 禁止通过保存接口修改,传入则拒收 */
34 40
     private String goodsStatus;
35 41
 
@@ -133,6 +139,26 @@ public class GoodsSaveDTO
133 139
         this.serviceIds = serviceIds;
134 140
     }
135 141
 
142
+    public List<GoodsAttrItemDTO> getAttributes()
143
+    {
144
+        return attributes;
145
+    }
146
+
147
+    public void setAttributes(List<GoodsAttrItemDTO> attributes)
148
+    {
149
+        this.attributes = attributes;
150
+    }
151
+
152
+    public List<GoodsAttrItemDTO> getSpecs()
153
+    {
154
+        return specs;
155
+    }
156
+
157
+    public void setSpecs(List<GoodsAttrItemDTO> specs)
158
+    {
159
+        this.specs = specs;
160
+    }
161
+
136 162
     public String getGoodsStatus()
137 163
     {
138 164
         return goodsStatus;

+ 28 - 0
baqing-shop/src/main/java/com/ruoyi/web/modules/goods/service/impl/GoodsServiceImpl.java

@@ -23,16 +23,20 @@ import com.ruoyi.web.modules.goodsservice.facade.IGoodsServiceFacade;
23 23
 import com.ruoyi.web.modules.goodsservice.mapper.BizGoodsServiceMapper;
24 24
 import com.ruoyi.web.modules.goods.constant.GoodsConstants;
25 25
 import com.ruoyi.web.modules.goods.domain.BizGoods;
26
+import com.ruoyi.web.modules.goods.domain.BizGoodsAttr;
26 27
 import com.ruoyi.web.modules.goods.domain.BizGoodsServiceSnapshot;
27 28
 import com.ruoyi.web.modules.goods.dto.GoodsAuditDTO;
29
+import com.ruoyi.web.modules.goods.dto.GoodsAttrItemDTO;
28 30
 import com.ruoyi.web.modules.goods.dto.GoodsBatchIdsDTO;
29 31
 import com.ruoyi.web.modules.goods.dto.GoodsOffShelfDTO;
30 32
 import com.ruoyi.web.modules.goods.dto.GoodsSaveDTO;
31 33
 import com.ruoyi.web.modules.goods.exception.GoodsBatchOperationException;
32 34
 import com.ruoyi.web.modules.goods.facade.IGoodsOrderFacade;
35
+import com.ruoyi.web.modules.goods.mapper.BizGoodsAttrMapper;
33 36
 import com.ruoyi.web.modules.goods.mapper.BizGoodsMapper;
34 37
 import com.ruoyi.web.modules.goods.mapper.BizGoodsServiceSnapshotMapper;
35 38
 import com.ruoyi.web.modules.goods.service.IGoodsService;
39
+import com.ruoyi.web.modules.goods.support.GoodsAttrSupport;
36 40
 import com.ruoyi.web.modules.goods.support.GoodsSnGenerator;
37 41
 import com.ruoyi.web.modules.goods.support.GoodsStatusUtils;
38 42
 import com.ruoyi.web.modules.goods.vo.GoodsDetailVO;
@@ -45,6 +49,7 @@ import com.ruoyi.web.modules.store.constant.ShopConstants;
45 49
 import com.ruoyi.web.modules.store.domain.BizShop;
46 50
 import com.ruoyi.web.modules.store.facade.IShopGlobalConfigFacade;
47 51
 import com.ruoyi.web.modules.store.mapper.BizShopMapper;
52
+import com.ruoyi.web.modules.template.constant.AttrTemplateConstants;
48 53
 import com.ruoyi.web.modules.template.domain.BizGoodsAttrTemplate;
49 54
 import com.ruoyi.web.modules.template.mapper.BizGoodsAttrTemplateMapper;
50 55
 
@@ -87,6 +92,9 @@ public class GoodsServiceImpl implements IGoodsService
87 92
     @Autowired
88 93
     private BizGoodsAttrTemplateMapper attrTemplateMapper;
89 94
 
95
+    @Autowired
96
+    private BizGoodsAttrMapper goodsAttrMapper;
97
+
90 98
     @Override
91 99
     public List<GoodsListVO> selectPlatformList(BizGoods query)
92 100
     {
@@ -150,6 +158,7 @@ public class GoodsServiceImpl implements IGoodsService
150 158
         goods.setCreateBy(operator);
151 159
         goodsMapper.insert(goods);
152 160
         saveServiceSnapshots(goods.getGoodsId(), dto.getServiceIds());
161
+        saveGoodsAttrs(goods.getGoodsId(), dto.getAttributes(), dto.getSpecs());
153 162
         return goods.getGoodsId();
154 163
     }
155 164
 
@@ -176,6 +185,7 @@ public class GoodsServiceImpl implements IGoodsService
176 185
         update.setUpdateBy(operator);
177 186
         goodsMapper.updateSeller(update);
178 187
         saveServiceSnapshots(dto.getGoodsId(), dto.getServiceIds());
188
+        saveGoodsAttrs(dto.getGoodsId(), dto.getAttributes(), dto.getSpecs());
179 189
     }
180 190
 
181 191
     @Override
@@ -357,6 +367,7 @@ public class GoodsServiceImpl implements IGoodsService
357 367
         for (Long goodsId : dto.getGoodsIds())
358 368
         {
359 369
             snapshotMapper.deleteByGoodsId(goodsId);
370
+            goodsAttrMapper.deleteByGoodsId(goodsId);
360 371
         }
361 372
     }
362 373
 
@@ -444,6 +455,9 @@ public class GoodsServiceImpl implements IGoodsService
444 455
         vo.setCanEdit(true);
445 456
         List<GoodsServiceSnapshotVO> services = snapshotMapper.selectVisibleByGoodsId(goods.getGoodsId());
446 457
         vo.setServices(services);
458
+        List<BizGoodsAttr> attrRows = goodsAttrMapper.selectByGoodsId(goods.getGoodsId());
459
+        vo.setAttributes(GoodsAttrSupport.groupByItem(attrRows, AttrTemplateConstants.ITEM_TYPE_ATTR));
460
+        vo.setSpecs(GoodsAttrSupport.groupByItem(attrRows, AttrTemplateConstants.ITEM_TYPE_SPEC));
447 461
         if (platform)
448 462
         {
449 463
             fillShopMerchantName(vo, goods.getShopId());
@@ -524,6 +538,20 @@ public class GoodsServiceImpl implements IGoodsService
524 538
         }
525 539
     }
526 540
 
541
+    private void saveGoodsAttrs(Long goodsId, List<GoodsAttrItemDTO> attributes, List<GoodsAttrItemDTO> specs)
542
+    {
543
+        goodsAttrMapper.deleteByGoodsId(goodsId);
544
+        if ((attributes == null || attributes.isEmpty()) && (specs == null || specs.isEmpty()))
545
+        {
546
+            return;
547
+        }
548
+        List<BizGoodsAttr> rows = GoodsAttrSupport.flatten(goodsId, attributes, specs);
549
+        if (!rows.isEmpty())
550
+        {
551
+            goodsAttrMapper.batchInsert(rows);
552
+        }
553
+    }
554
+
527 555
     private BizGoods buildGoodsFromDto(GoodsSaveDTO dto)
528 556
     {
529 557
         BizGoods goods = new BizGoods();

+ 25 - 0
baqing-shop/src/main/java/com/ruoyi/web/modules/goods/vo/GoodsDetailVO.java

@@ -4,6 +4,7 @@ import java.math.BigDecimal;
4 4
 import java.util.ArrayList;
5 5
 import java.util.Date;
6 6
 import java.util.List;
7
+import com.ruoyi.web.modules.goods.dto.GoodsAttrItemDTO;
7 8
 
8 9
 /**
9 10
  * 商品详情
@@ -44,6 +45,10 @@ public class GoodsDetailVO extends GoodsListVO
44 45
 
45 46
     private List<GoodsServiceSnapshotVO> services = new ArrayList<>();
46 47
 
48
+    private List<GoodsAttrItemDTO> attributes = new ArrayList<>();
49
+
50
+    private List<GoodsAttrItemDTO> specs = new ArrayList<>();
51
+
47 52
     public Long getShopId()
48 53
     {
49 54
         return shopId;
@@ -213,4 +218,24 @@ public class GoodsDetailVO extends GoodsListVO
213 218
     {
214 219
         this.services = services;
215 220
     }
221
+
222
+    public List<GoodsAttrItemDTO> getAttributes()
223
+    {
224
+        return attributes;
225
+    }
226
+
227
+    public void setAttributes(List<GoodsAttrItemDTO> attributes)
228
+    {
229
+        this.attributes = attributes;
230
+    }
231
+
232
+    public List<GoodsAttrItemDTO> getSpecs()
233
+    {
234
+        return specs;
235
+    }
236
+
237
+    public void setSpecs(List<GoodsAttrItemDTO> specs)
238
+    {
239
+        this.specs = specs;
240
+    }
216 241
 }

+ 41 - 0
baqing-shop/src/test/java/com/ruoyi/web/modules/goods/controller/SellerGoodsControllerTest.java

@@ -42,6 +42,10 @@ import com.ruoyi.web.modules.goods.exception.GoodsBatchOperationException;
42 42
 import com.ruoyi.web.modules.goods.service.IGoodsService;
43 43
 import com.ruoyi.web.modules.goods.vo.GoodsDetailVO;
44 44
 import com.ruoyi.web.modules.goods.vo.GoodsListVO;
45
+import com.ruoyi.web.modules.template.dto.AttrTemplateItemDTO;
46
+import com.ruoyi.web.modules.template.facade.IAttrTemplateFacade;
47
+import com.ruoyi.web.modules.template.vo.AttrTemplateDetailVO;
48
+import com.ruoyi.web.modules.template.vo.AttrTemplateOptionVO;
45 49
 
46 50
 @ExtendWith(MockitoExtension.class)
47 51
 class SellerGoodsControllerTest
@@ -58,6 +62,9 @@ class SellerGoodsControllerTest
58 62
     @Mock
59 63
     private IGoodsServiceFacade goodsServiceFacade;
60 64
 
65
+    @Mock
66
+    private IAttrTemplateFacade attrTemplateFacade;
67
+
61 68
     @Spy
62 69
     @InjectMocks
63 70
     private SellerGoodsController controller;
@@ -195,6 +202,40 @@ class SellerGoodsControllerTest
195 202
                 .andExpect(jsonPath("$.data[0].label").value("肥料 > 复合肥"));
196 203
     }
197 204
 
205
+    @Test
206
+    void attrTemplateDetail_returnsPrefillData() throws Exception
207
+    {
208
+        AttrTemplateItemDTO attr = new AttrTemplateItemDTO();
209
+        attr.setItemName("品牌");
210
+        attr.setValues(Collections.singletonList("华为"));
211
+        AttrTemplateDetailVO detail = new AttrTemplateDetailVO();
212
+        detail.setTemplateId(4L);
213
+        detail.setTemplateName("手机");
214
+        detail.setAttributes(Collections.singletonList(attr));
215
+        detail.setSpecs(Collections.emptyList());
216
+        when(attrTemplateFacade.getDetailForGoods(4L, 101L)).thenReturn(detail);
217
+
218
+        mockMvc.perform(get("/agri/seller/goods/attrTemplate/4"))
219
+                .andExpect(status().isOk())
220
+                .andExpect(jsonPath("$.code").value(200))
221
+                .andExpect(jsonPath("$.data.templateName").value("手机"))
222
+                .andExpect(jsonPath("$.data.attributes[0].itemName").value("品牌"));
223
+    }
224
+
225
+    @Test
226
+    void attrTemplateOptions_returnsList() throws Exception
227
+    {
228
+        AttrTemplateOptionVO option = new AttrTemplateOptionVO();
229
+        option.setTemplateId(4L);
230
+        option.setTemplateName("手机");
231
+        when(attrTemplateFacade.listOptionsByShopId(101L)).thenReturn(Collections.singletonList(option));
232
+
233
+        mockMvc.perform(get("/agri/seller/goods/attrTemplateOptions"))
234
+                .andExpect(status().isOk())
235
+                .andExpect(jsonPath("$.code").value(200))
236
+                .andExpect(jsonPath("$.data[0].templateName").value("手机"));
237
+    }
238
+
198 239
     private GoodsSaveDTO buildSaveDto()
199 240
     {
200 241
         GoodsSaveDTO dto = new GoodsSaveDTO();

+ 42 - 0
baqing-shop/src/test/java/com/ruoyi/web/modules/goods/service/GoodsServiceImplTest.java

@@ -29,11 +29,13 @@ import com.ruoyi.web.modules.goodsservice.mapper.BizGoodsServiceMapper;
29 29
 import com.ruoyi.web.modules.goods.constant.GoodsConstants;
30 30
 import com.ruoyi.web.modules.goods.domain.BizGoods;
31 31
 import com.ruoyi.web.modules.goods.dto.GoodsAuditDTO;
32
+import com.ruoyi.web.modules.goods.dto.GoodsAttrItemDTO;
32 33
 import com.ruoyi.web.modules.goods.dto.GoodsBatchIdsDTO;
33 34
 import com.ruoyi.web.modules.goods.dto.GoodsOffShelfDTO;
34 35
 import com.ruoyi.web.modules.goods.dto.GoodsSaveDTO;
35 36
 import com.ruoyi.web.modules.goods.exception.GoodsBatchOperationException;
36 37
 import com.ruoyi.web.modules.goods.facade.IGoodsOrderFacade;
38
+import com.ruoyi.web.modules.goods.mapper.BizGoodsAttrMapper;
37 39
 import com.ruoyi.web.modules.goods.mapper.BizGoodsMapper;
38 40
 import com.ruoyi.web.modules.goods.mapper.BizGoodsServiceSnapshotMapper;
39 41
 import com.ruoyi.web.modules.goods.service.impl.GoodsServiceImpl;
@@ -85,6 +87,9 @@ class GoodsServiceImplTest
85 87
     @Mock
86 88
     private BizGoodsAttrTemplateMapper attrTemplateMapper;
87 89
 
90
+    @Mock
91
+    private BizGoodsAttrMapper goodsAttrMapper;
92
+
88 93
     @InjectMocks
89 94
     private GoodsServiceImpl goodsService;
90 95
 
@@ -439,6 +444,42 @@ class GoodsServiceImplTest
439 444
         verify(goodsMapper, never()).insert(any());
440 445
     }
441 446
 
447
+    @Test
448
+    void insert_withAttributes_persistsGoodsAttrRows()
449
+    {
450
+        when(goodsSnGenerator.nextSn()).thenReturn("G20260526000004");
451
+        BizGoodsCategory platformCat = new BizGoodsCategory();
452
+        platformCat.setCategoryLevel(CategoryConstants.LEVEL_TWO);
453
+        when(categoryMapper.selectById(100L, null)).thenReturn(platformCat);
454
+        when(goodsMapper.insert(any())).thenAnswer(inv -> {
455
+            BizGoods g = inv.getArgument(0);
456
+            g.setGoodsId(102L);
457
+            return 1;
458
+        });
459
+
460
+        GoodsAttrItemDTO attr = new GoodsAttrItemDTO();
461
+        attr.setItemName("品牌");
462
+        attr.setValues(Collections.singletonList("华为"));
463
+        GoodsAttrItemDTO spec = new GoodsAttrItemDTO();
464
+        spec.setItemName("内存");
465
+        spec.setValues(Arrays.asList("8G", "16G"));
466
+
467
+        GoodsSaveDTO dto = new GoodsSaveDTO();
468
+        dto.setCategoryId(100L);
469
+        dto.setGoodsName("手机");
470
+        dto.setMainPic("/a.jpg");
471
+        dto.setDetailContent("detail");
472
+        dto.setSalePrice(new BigDecimal("1999"));
473
+        dto.setStock(10);
474
+        dto.setAttributes(Collections.singletonList(attr));
475
+        dto.setSpecs(Collections.singletonList(spec));
476
+
477
+        goodsService.insertSellerGoods(10L, dto, "seller");
478
+
479
+        verify(goodsAttrMapper).deleteByGoodsId(102L);
480
+        verify(goodsAttrMapper).batchInsert(any());
481
+    }
482
+
442 483
     @Test
443 484
     void delete_draft_success()
444 485
     {
@@ -450,6 +491,7 @@ class GoodsServiceImplTest
450 491
 
451 492
         verify(goodsMapper).logicDeleteByIds(any(), eq(10L), eq("seller"));
452 493
         verify(snapshotMapper).deleteByGoodsId(7L);
494
+        verify(goodsAttrMapper).deleteByGoodsId(7L);
453 495
     }
454 496
 
455 497
     @Test

+ 14 - 6
doc/店铺后台/商品管理/商品列表/商品列表前端技术方案.md

@@ -70,15 +70,18 @@
70 70
 └── 添加/编辑弹窗(720px · v1.0 单规格)
71 71
     ├── 商品分类 categoryId(必填,平台二级)
72 72
     ├── 店铺商品分类 shopCategoryId(选填,本店二级)
73
-    ├── 属性模版 attrTemplateId(选填;下拉 `/agri/seller/attrTemplate/options`)
73
+    ├── 属性模版 attrTemplateId(选填)
74
+    │   ├── 下拉 GET /agri/seller/goods/attrTemplateOptions
75
+    │   ├── 选用预填 GET /agri/seller/goods/attrTemplate/{templateId}
76
+    │   └── 属性 attributes[] / 规格 specs[](itemName + values[],可增删改)
74 77
     ├── 商品名称、主图、销售价、库存
75 78
     ├── 商品详情 detailContent(Editor 富文本)
76 79
     └── 服务说明 serviceIds(多选;新建默认勾选 defaultShow)
77 80
 ```
78 81
 
79
-**v1.0 未实现(后续扩展):** 多图 gallery、多规格 SKU、运费模版、属性/规格明细编辑(`biz_goods_attr`)、独立 form 路由页。
82
+**v1.0 未实现(后续扩展):** 多图 gallery、多规格 SKU、运费模版、独立 form 路由页。
80 83
 
81
-**v1.0 后端已支持:** 保存/详情 `attrTemplateId`(模版引用 ID);前端发品表单待接入下拉与回显
84
+**已实现(后端):** `attrTemplateId` + `attributes`/`specs` 保存与详情回显(`biz_goods_attr`);前端发品表单待接入
82 85
 
83 86
 ---
84 87
 
@@ -101,7 +104,8 @@
101 104
 | `listSellerGoods` | GET | `/list` | 分页列表 |
102 105
 | `sellerGoodsCategoryOptions` | GET | `/categoryOptions` | 平台二级分类 |
103 106
 | `sellerGoodsShopCategoryOptions` | GET | `/shopCategoryOptions` | 本店二级店铺分类 |
104
-| `sellerAttrTemplateOptions` | GET | `/agri/seller/attrTemplate/options` | 本店属性模版(属性模版模块) |
107
+| `sellerAttrTemplateOptions` | GET | `/attrTemplateOptions` | 本店属性模版下拉 |
108
+| `getSellerAttrTemplateDetail` | GET | `/attrTemplate/{templateId}` | 选用模版预填 attributes/specs |
105 109
 | `sellerGoodsServiceOptions` | GET | `/serviceOptions` | `{ all, defaultShow }` |
106 110
 | `getSellerGoods` | GET | `/{goodsId}` | 详情 |
107 111
 | `addSellerGoods` | POST | `/` | 新增 → status=0 |
@@ -140,6 +144,8 @@
140 144
 | categoryId | 必填 | 必填 | 平台二级 |
141 145
 | shopCategoryId | 选填 | 选填 | 本店二级 |
142 146
 | attrTemplateId | 选填 | 选填 | 本店有效属性模版 |
147
+| attributes | 选填 | 选填 | `[{ itemName, values[] }]` 属性项 |
148
+| specs | 选填 | 选填 | `[{ itemName, values[] }]` 规格项 |
143 149
 | goodsName | 必填 | 必填 | max 200 |
144 150
 | mainPic | 必填 | 必填 | image-upload |
145 151
 | detailContent | 必填 | 必填 | 富文本 |
@@ -170,6 +176,7 @@
170 176
 | services[] | 服务快照;编辑时回显 `serviceIds` |
171 177
 | shopCategoryPath | 店铺分类路径 |
172 178
 | attrTemplateId / attrTemplateName | 属性模版引用与名称 |
179
+| attributes / specs | 属性/规格快照(同保存体结构) |
173 180
 
174 181
 ---
175 182
 
@@ -194,8 +201,8 @@
194 201
 - [ ] 添加 → 未上架 → 提交上架(免审开/关两种策略)
195 202
 - [ ] 出售中编辑保存后状态不变
196 203
 - [ ] 批量操作含非法状态时后端报错
204
+- [ ] 选模版 → 预填 attributes/specs → 修改后保存 → 详情回显一致
197 205
 - [ ] 分类/店铺分类/属性模版/服务下拉数据正确
198
-- [ ] 保存带 attrTemplateId 后详情回显 attrTemplateName
199 206
 
200 207
 ---
201 208
 
@@ -205,7 +212,8 @@
205 212
 |------|------|
206 213
 | v1.0 | 首版:列表 + v1.0 单规格发品弹窗 + 详情抽屉 + 状态流转操作 |
207 214
 | v1.1 | 对齐后端 `attrTemplateId`:保存体/详情字段;下拉走属性模版模块 |
215
+| v1.2 | 对齐 `biz_goods_attr`:attributes/specs 保存体;goods 侧预填/下拉 API |
208 216
 
209 217
 ---
210 218
 
211
-*文档版本:v1.1 · 关联《商品列表功能需求.md》v1.3、《店铺商品列表技术方案.md》v1.2*
219
+*文档版本:v1.2 · 关联《商品列表功能需求.md》v1.4、《店铺商品列表技术方案.md》v1.4*

+ 7 - 7
doc/店铺后台/商品管理/商品列表/商品列表功能需求.md

@@ -268,7 +268,7 @@
268 268
 | 基础信息 | 编号、名称、主图/详情图、售价、库存、销量、重量、条码、关键词、简述等 |
269 269
 | 分类 | **商品分类**(一级 > 二级)、**店铺商品分类**(一级 > 二级,若有)、**属性模版**(名称,若选用) |
270 270
 | 规格与运费 | 销售规格(统一/多规格)、规格明细(市场价、库存);快递运费(固定/模版) |
271
-| 属性 | 基于 **属性模版** 扩展的属性项、规格项及其值 |
271
+| 属性 | 基于 **属性模版** 扩展的属性项、规格项及其值(详情 `attributes`/`specs` 快照) |
272 272
 | 详情内容 | 图文详情(图片块 + 文本块) |
273 273
 | 服务快照 | 已勾选 **商品服务** 项的快照(只读) |
274 274
 | 状态信息 | 当前状态;**待审核/审核失败** 展示提交时间、审核时间(若有)、**驳回原因** |
@@ -298,7 +298,7 @@
298 298
 | 基础 | 商品条码 | 否 | — |
299 299
 | 基础 | 搜索关键词 | 否 | — |
300 300
 | 基础 | 商品简述 | 视产品 | — |
301
-| 属性 | 属性模版 | 否 | 下拉选用本店模版(数据源见 **属性模版** 模块);保存 **引用 ID**;属性/规格键值明细 **v1.x** |
301
+| 属性 | 属性模版 | 否 | 下拉选用本店模版;保存 **引用 ID** + **`attributes`/`specs` 快照** 至 `biz_goods_attr` |
302 302
 | 销售 | 销售规格 | 是 | **统一规格 / 多规格** |
303 303
 | 销售 | 规格明细 | 是 | 每种规格:**市场价、库存**(首期单规格时一行) |
304 304
 | 物流 | 快递运费 | 是 | **固定运费 / 运费模版** |
@@ -321,7 +321,7 @@
321 321
 | 当前店铺有效 | 停业店仍可发品;**已逻辑删除** 店铺不可用 |
322 322
 | 商品分类 | 平台已维护 **平台二级分类**(至少一条可选);商家 **只选** |
323 323
 | 商品服务 | 平台已配置目录则可选;无目录时服务区域为空 |
324
-| 属性模版 | 可选;下拉来自 **属性模版** 模块 `/agri/seller/attrTemplate/options`;无模版时可不传 |
324
+| 属性模版 | 可选;下拉 `GET /agri/seller/goods/attrTemplateOptions`;选用后 `GET .../attrTemplate/{id}` 预填;无模版时可手工维护属性/规格 |
325 325
 
326 326
 ---
327 327
 
@@ -600,7 +600,7 @@
600 600
 | **GL15** | 须携带 **当前店铺上下文**;切换店铺后数据范围随之变化 |
601 601
 | **GL16** | 商品创建后 **不可换店** |
602 602
 | **GL17** | **任意状态** 均可 **查看详情**;审核失败须展示 **驳回原因** |
603
-| **GL18** | 发品可选 **属性模版**;若传须为 **当前店铺** 有效模版;保存写入引用;模版项/值快照 **biz_goods_attr** 另案(v1.x) |
603
+| **GL18** | 发品可选 **属性模版**;若传须为 **当前店铺** 有效模版;保存写入 `attr_template_id` 与 **`biz_goods_attr` 快照**(用户可改后再存) |
604 604
 
605 605
 ---
606 606
 
@@ -625,8 +625,7 @@
625 625
 
626 626
 | 项 | 说明 |
627 627
 |----|------|
628
-| 属性模版 **维护** | 见 **属性模版** 模块;本模块 **仅选用 + 保存引用** |
629
-| 属性/规格 **明细落库** | `biz_goods_attr` 写入 **v1.x**;当前仅 `attr_template_id` |
628
+| 属性模版 **维护** | 见 **属性模版** 模块;本模块 **选用 + 保存引用与快照** |
630 629
 | 运费模版 **维护** | 假定平台或它模块已提供可选模版 |
631 630
 | 多规格 SKU 完整能力 | 草稿含多规格字段;复杂 SKU 以商品管理全模块为准 |
632 631
 | 平台代商家改价/改库存 | 平台侧能力 |
@@ -665,7 +664,8 @@
665 664
 | **v1.1** | 对齐《店铺商品分类功能需求》v1.1:店铺商品分类为 **两级**,发品 **仅挂二级** |
666 665
 | **v1.2** | 统一「商品分类 = 平台二级 / 店铺商品分类 = 本店二级」口径;与《店铺商品列表技术方案》v1.1 及实现对齐 |
667 666
 | **v1.3** | 对齐属性模版协作:发品可选 `attrTemplateId` 并保存引用;属性/规格明细 `biz_goods_attr` 仍标 v1.x |
667
+| **v1.4** | `biz_goods_attr` 已实现:`attributes`/`specs` 保存与详情回显;商品侧预填/下拉接口 |
668 668
 
669 669
 ---
670 670
 
671
-*文档版本:v1.3 · 关联《关联需求分析.md》v1.6、《商品审核功能需求.md》v1.0、《商品分类功能需求.md》v1.5、《商品服务管理功能需求.md》v1.0.1、《店铺商品分类功能需求.md》v1.1、《属性模版功能需求.md》v1.0 · 草稿《商品列表功能需求-草稿.md》保持不变。*
671
+*文档版本:v1.4 · 关联《店铺商品列表技术方案.md》v1.4、《属性模版功能需求.md》v1.1 · 草稿《商品列表功能需求-草稿.md》保持不变。*

+ 63 - 26
doc/店铺后台/商品管理/商品列表/店铺商品列表技术方案.md

@@ -1,7 +1,7 @@
1 1
 # 店铺商品列表 — 技术方案
2 2
 
3
-> **依据:** 《商品列表功能需求.md》v1.3  
4
-> **关联:** 《关联需求分析.md》v1.6;平台《商品审核技术方案》v1.1、《商品分类技术方案》v1.3、《商品服务管理技术方案》v1.0.1;商家《店铺商品分类技术方案》v1.2、《属性模版技术方案》v1.2  
3
+> **依据:** 《商品列表功能需求.md》v1.4  
4
+> **关联:** 《关联需求分析.md》v1.6;平台《商品审核技术方案》v1.1、《商品分类技术方案》v1.3、《商品服务管理技术方案》v1.0.1;商家《店铺商品分类技术方案》v1.2、《属性模版技术方案》v1.3  
5 5
 > **范围:** 商家端 **当前店铺** 商品列表、检索、发品、编辑、提交上架、下架、删除;**复用** 平台商品主表与状态机,**不另建** 商家商品表。  
6 6
 > **原则:** `goods_status` 仅经 submit / audit / offShelf 变更(P17);`X-Shop-Id` 店铺上下文;批量含非法状态 **整批失败**。
7 7
 
@@ -56,7 +56,7 @@ biz_goods_category
56 56
 | **商品分类** | `category_id` ← `IPlatformCategoryService.selectPlatformLevel2Options()` |
57 57
 | **店铺商品分类** | `shop_category_id` ← `IShopGoodsCategoryFacade.listOptionsByShopId()` |
58 58
 | **商品服务** | 发品多选 ← `IGoodsServiceFacade`;保存写快照 |
59
-| **属性模版** | `attr_template_id` ← 保存时 `assertAttrTemplateIfPresent`;下拉 ← `/agri/seller/attrTemplate/options` 或 `IAttrTemplateFacade` |
59
+| **属性模版** | 选用模版预填 `attributes`/`specs`;保存 `attr_template_id` + **先删后插** `biz_goods_attr` |
60 60
 | **店铺设置** | `IShopGlobalConfigFacade.getDefaultAuditPass()` 决定 submit 目标态 |
61 61
 | **订单管理** | 下架前校验 **O10 未完成订单**(**待实现**,`IGoodsOrderFacade` 占位) |
62 62
 | **店铺管理** | `IGoodsShopFacade.hasBlockingGoodsForShopDelete`(`GoodsFacadeImpl`)删店前置 |
@@ -73,12 +73,14 @@ baqing-shop/src/main/java/com/ruoyi/web/modules/goods/
73 73
 │   └── GoodsAuditController.java            # /agri/goodsAudit(平台审核菜单薄封装)
74 74
 ├── service/
75 75
 │   ├── IGoodsService.java
76
-│   └── impl/GoodsServiceImpl.java           # 商家 + 平台共用;注入 template 模块 Mapper 校验 attrTemplateId
76
+│   └── impl/GoodsServiceImpl.java           # saveServiceSnapshots + saveGoodsAttrs
77 77
 ├── domain/
78 78
 │   ├── BizGoods.java
79
-│   └── BizGoodsServiceSnapshot.java
79
+│   ├── BizGoodsServiceSnapshot.java
80
+│   └── BizGoodsAttr.java
80 81
 ├── dto/
81 82
 │   ├── GoodsSaveDTO.java
83
+│   ├── GoodsAttrItemDTO.java                # attributes[] / specs[] 项
82 84
 │   ├── GoodsAuditDTO.java
83 85
 │   ├── GoodsBatchIdsDTO.java                # 批量提交 / 批量删除
84 86
 │   └── GoodsOffShelfDTO.java                # 批量下架
@@ -89,7 +91,8 @@ baqing-shop/src/main/java/com/ruoyi/web/modules/goods/
89 91
 │   └── GoodsPurchaseVO.java                 # C 端/采购 Facade 用
90 92
 ├── mapper/
91 93
 │   ├── BizGoodsMapper.java
92
-│   └── BizGoodsServiceSnapshotMapper.java
94
+│   ├── BizGoodsServiceSnapshotMapper.java
95
+│   └── BizGoodsAttrMapper.java
93 96
 ├── facade/
94 97
 │   ├── IGoodsOrderFacade.java               # 下架 O10(DefaultGoodsOrderFacade 占位)
95 98
 │   ├── IGoodsPurchaseFacade.java
@@ -101,16 +104,19 @@ baqing-shop/src/main/java/com/ruoyi/web/modules/goods/
101 104
 ├── support/
102 105
 │   ├── GoodsSnGenerator.java
103 106
 │   ├── GoodsStatusUtils.java
107
+│   ├── GoodsAttrSupport.java                # 属性/规格校验与扁平化/聚合
104 108
 │   └── GoodsBatchResponseSupport.java
105 109
 ├── exception/GoodsBatchOperationException.java
106 110
 └── constant/GoodsConstants.java
107 111
 
108 112
 baqing-shop/src/main/resources/mapper/goods/
109 113
 ├── BizGoodsMapper.xml
110
-└── BizGoodsServiceSnapshotMapper.xml
114
+├── BizGoodsServiceSnapshotMapper.xml
115
+└── BizGoodsAttrMapper.xml
111 116
 
112 117
 sql/biz_goods.sql                              # 含 shop_category_id、attr_template_id
113 118
 sql/biz_goods_service_snapshot.sql
119
+sql/biz_goods_attr.sql
114 120
 ```
115 121
 
116 122
 **跨模块依赖(接口与实现分离):**
@@ -121,8 +127,7 @@ sql/biz_goods_service_snapshot.sql
121 127
 | 店铺商品分类下拉 | `category/facade/IShopGoodsCategoryFacade.java` | `category/facade/impl/ShopGoodsCategoryFacadeImpl.java` |
122 128
 | 平台二级分类下拉 | `category/service/IPlatformCategoryService.java` | `SellerGoodsController` 或 `SellerPlatformCategoryController` |
123 129
 | 分类路径 | `category/facade/ICategoryFacade.java` | `CategoryFacadeImpl` |
124
-| 属性模版校验 | `template/mapper/BizGoodsAttrTemplateMapper.java` | `GoodsServiceImpl` 直接注入 |
125
-| 属性模版下拉 | `template/facade/IAttrTemplateFacade.java` | 前端亦可调 `/agri/seller/attrTemplate/options` |
130
+| 属性模版校验/预填 | `template/facade/IAttrTemplateFacade.java` | `SellerGoodsController` `/attrTemplateOptions`、`/attrTemplate/{id}`;亦可调 `/agri/seller/attrTemplate/*` |
126 131
 | 商品服务目录 | `goodsservice/facade/IGoodsServiceFacade.java` | 发品 `serviceOptions` |
127 132
 | 店铺上下文 | `category/support/SellerShopContext.java` | 拦截器 `SellerShopContextInterceptor` |
128 133
 | 店铺上下文 API | `store/controller/SellerShopContextController.java` | `GET /agri/seller/context` |
@@ -139,6 +144,7 @@ sql/biz_goods_service_snapshot.sql
139 144
 |------|------------|
140 145
 | **`biz_goods`** | 商品主表;商家 CRUD + 状态字段 |
141 146
 | **`biz_goods_service_snapshot`** | 发品勾选服务的 **展示快照** |
147
+| **`biz_goods_attr`** | 发品属性/规格 **展示快照**(选用模版后可改) |
142 148
 | `biz_goods_category` | join 分类路径;**不** 由本模块维护 |
143 149
 | `biz_shop` | 店铺上下文、删店 join |
144 150
 | `biz_shop_global_config` | 全局默认审核开关 |
@@ -169,7 +175,7 @@ sql/biz_goods_service_snapshot.sql
169 175
 | del_flag | char(1) | 0 存在 2 逻辑删除 |
170 176
 | create_by / create_time / update_by / update_time | | 审计 |
171 177
 
172
-**首期未落库、后续扩展(需求 §8 完整表单):** 多图、重量、条码、关键词、简述、多规格 SKU、运费模版、**属性/规格键值(`biz_goods_attr`)** 等 → 见 §2.5;**`attr_template_id` 引用已实现**,不阻塞 v1.0 列表/单规格发品
178
+**首期未落库、后续扩展(需求 §8 完整表单):** 多图、重量、条码、关键词、简述、多规格 SKU、运费模版等 → 见 §2.5;**`attr_template_id` + `biz_goods_attr` 已实现**
173 179
 
174 180
 ### 2.3 增量 DDL
175 181
 
@@ -187,7 +193,20 @@ ALTER TABLE `biz_goods`
187 193
 
188 194
 完整建表见 `sql/biz_goods.sql`。
189 195
 
190
-### 2.4 `biz_goods_service_snapshot`
196
+### 2.4 `biz_goods_attr`
197
+
198
+| 字段 | 类型 | 说明 |
199
+|------|------|------|
200
+| attr_id | bigint PK | |
201
+| goods_id | bigint NOT NULL | 商品 ID |
202
+| attr_type | char(1) | **1** 属性项 **2** 规格项 |
203
+| item_name | varchar(64) | 项名称 |
204
+| value_text | varchar(128) | 单值一行;多项多行 |
205
+| sort_no | int | 展示顺序 |
206
+
207
+保存商品时 **先删后插** 该商品全部属性行;删除商品时物理删除关联行。模版编辑 **不追溯** 已保存商品(AT8)。
208
+
209
+### 2.5 `biz_goods_service_snapshot`
191 210
 
192 211
 | 字段 | 说明 |
193 212
 |------|------|
@@ -198,18 +217,17 @@ ALTER TABLE `biz_goods`
198 217
 
199 218
 保存商品时 **先删后插** 该商品全部快照行。
200 219
 
201
-### 2.5 扩展表(非 v1.0 · 概要)
220
+### 2.6 扩展表(非 v1.0 · 概要)
202 221
 
203 222
 | 表(规划) | 用途 |
204 223
 |------------|------|
205 224
 | `biz_goods_pic` | 多图 gallery |
206 225
 | `biz_goods_sku` | 多规格 SKU(市场价、库存) |
207 226
 | `biz_goods_freight` | 固定运费 / 运费模版 ID |
208
-| `biz_goods_attr` | 属性项/规格项键值 |
209 227
 
210
-v1.0 **仅** 使用主表 `sale_price`/`stock`/`main_pic`/`detail_content`。
228
+v1.0 **使用** 主表 `sale_price`/`stock`/`main_pic`/`detail_content` + 服务快照 + **属性/规格快照**
211 229
 
212
-### 2.6 索引
230
+### 2.7 索引
213 231
 
214 232
 | 索引 | 字段 | 用途 |
215 233
 |------|------|------|
@@ -218,9 +236,10 @@ v1.0 **仅** 使用主表 `sale_price`/`stock`/`main_pic`/`detail_content`。
218 236
 | idx_category_id | category_id, del_flag | 分类检索 |
219 237
 | idx_shop_category_id | shop_category_id, del_flag | 店铺分类删校验 |
220 238
 | idx_attr_template_id | attr_template_id, del_flag | 属性模版删校验 |
239
+| idx_goods_attr | goods_id, attr_type, sort_no | 详情聚合属性/规格 |
221 240
 | idx_submit_time | submit_time | 待审核排序(平台侧) |
222 241
 
223
-### 2.7 商家列表 SQL 约定
242
+### 2.8 商家列表 SQL 约定
224 243
 
225 244
 ```sql
226 245
 WHERE g.del_flag = '0'
@@ -228,7 +247,7 @@ WHERE g.del_flag = '0'
228 247
   -- 含 goods_status='0',与平台列表不同
229 248
 ```
230 249
 
231
-### 2.8 字典
250
+### 2.9 字典
232 251
 
233 252
 | dict_type | 说明 |
234 253
 |-----------|------|
@@ -337,6 +356,7 @@ WHERE g.del_flag = '0'
337 356
 | 列表字段 | 同 §5.1 |
338 357
 | shopId, categoryId, shopCategoryId | |
339 358
 | attrTemplateId, attrTemplateName | 选用模版 ID 与名称(详情回显) |
359
+| attributes[], specs[] | 属性/规格快照(`GoodsAttrItemDTO`:`itemName` + `values[]`) |
340 360
 | detailContent, stock | |
341 361
 | rejectReason, submitTime, auditTime, offShelfTime | |
342 362
 | services[] | 服务快照列表 |
@@ -351,7 +371,7 @@ WHERE g.del_flag = '0'
351 371
 | 权限 | `agri:seller:goods:add` |
352 372
 | 日志 | title=商家商品 |
353 373
 
354
-**Body(GoodsSaveDTO · v1.0):**
374
+**Body(GoodsSaveDTO):**
355 375
 
356 376
 ```json
357 377
 {
@@ -363,7 +383,13 @@ WHERE g.del_flag = '0'
363 383
   "detailContent": "<p>详情</p>",
364 384
   "salePrice": 128.00,
365 385
   "stock": 500,
366
-  "serviceIds": [1, 3]
386
+  "serviceIds": [1, 3],
387
+  "attributes": [
388
+    { "itemName": "品牌", "values": ["华为"] }
389
+  ],
390
+  "specs": [
391
+    { "itemName": "内存", "values": ["8G", "16G"] }
392
+  ]
367 393
 }
368 394
 ```
369 395
 
@@ -372,6 +398,7 @@ WHERE g.del_flag = '0'
372 398
 | categoryId | 须为 **平台二级** 有效分类 |
373 399
 | shopCategoryId | 若传,须属 **当前店** 二级分类 |
374 400
 | attrTemplateId | 若传,须属 **当前店** 有效属性模版 |
401
+| attributes / specs | 可选;若传则项名非空、每项至少一值、同段项名/值不重复、属性与规格项名不可相同 |
375 402
 | serviceIds | 须全部存在于未删除服务目录 |
376 403
 | goodsStatus | **禁止** 传入 |
377 404
 
@@ -387,7 +414,7 @@ WHERE g.del_flag = '0'
387 414
 | Body | 同 §5.3 + **goodsId** 必填 |
388 415
 
389 416
 - **不修改** `goods_status`、`shop_id`、`goods_sn`。  
390
-- 各状态均可编辑(P14);保存后刷新服务快照。
417
+- 各状态均可编辑(P14);保存后刷新服务快照与 **属性/规格快照**
391 418
 
392 419
 ---
393 420
 
@@ -459,9 +486,9 @@ WHERE g.del_flag = '0'
459 486
 | GET | `/agri/seller/goods/shopCategoryOptions` | `list` | 本店二级「店铺商品分类」`IShopGoodsCategoryFacade.listOptionsByShopId(shopId, false)` |
460 487
 | GET | `/agri/seller/category/platformLevel2Options` | 同上 | 与上同源(分类模块只读入口,可选) |
461 488
 | GET | `/agri/seller/goods/serviceOptions` | `list` | `{ all: [], defaultShow: [] }` |
462
-| GET | `/agri/seller/attrTemplate/options` | 属性模版模块 | 本店属性模版下拉(**非** goods 子路径;见《属性模版技术方案》§3.6) |
463
-
464
-> **规划(可选挂载):** `GET /agri/seller/goods/attrTemplateOptions` 委托 `IAttrTemplateFacade`,与上同源。
489
+| GET | `/agri/seller/goods/attrTemplateOptions` | `list` / `add` / `edit` | 本店属性模版下拉;委托 `IAttrTemplateFacade.listOptionsByShopId` |
490
+| GET | `/agri/seller/goods/attrTemplate/{templateId}` | 同上 | 选用模版预填;返回 `attributes`、`specs`(委托 `getDetailForGoods`) |
491
+| GET | `/agri/seller/attrTemplate/options` | 属性模版模块 | 与 `attrTemplateOptions` **同源**(亦可直调) |
465 492
 
466 493
 ---
467 494
 
@@ -490,6 +517,11 @@ insertSellerGoods
490 517
   → goodsSn = GoodsSnGenerator.next()
491 518
   → insert status=0(含 attr_template_id)
492 519
   → saveServiceSnapshots
520
+  → saveGoodsAttrs(先删后插 biz_goods_attr)
521
+
522
+updateSellerGoods
523
+  → 同上校验(goodsId 属当前店)
524
+  → updateSeller + saveServiceSnapshots + saveGoodsAttrs
493 525
 
494 526
 submitGoods
495 527
   → status in (0,3,4)
@@ -504,6 +536,7 @@ offShelfSeller
504 536
 deleteSellerGoodsBatch
505 537
   → status in (0,3,4)
506 538
   → logic delete + 整批失败策略
539
+  → delete service snapshot + goods_attr rows
507 540
   → [待] orderFacade 校验 O10(下架时)
508 541
 ```
509 542
 
@@ -515,6 +548,8 @@ deleteSellerGoodsBatch
515 548
 | shop_category_id | `biz_goods_category.category_id`,`shop_id = #{shopId}`,`parent_id > 0` |
516 549
 | attr_template_id | `biz_goods_attr_template.template_id`,`shop_id = #{shopId}`,`del_flag = '0'` |
517 550
 
551
+**错误 msg(属性/规格):** 项名为空 / 重复 / 与对端重名 / 值为空 / 值重复 → 见 `GoodsConstants.MSG_ATTR_*`。
552
+
518 553
 **错误 msg(属性模版):** 无效或非本店模版 → `请选择本店有效属性模版`(`GoodsConstants.MSG_ATTR_TEMPLATE_INVALID`)。
519 554
 
520 555
 ---
@@ -542,9 +577,9 @@ deleteSellerGoodsBatch
542 577
 | **v1.0** | 列表/详情/单规格 CRUD/单独 submit·offShelf/服务快照/平台分类与服务下拉 | **已实现** |
543 578
 | **v1.0 补全** | `shop_category_id` + 店铺分类下拉 + 删除 + 批量 submit/offShelf/delete | **已实现** |
544 579
 | **v1.0 补全·模版引用** | `attr_template_id` 保存/详情回显 + 本店模版校验 | **已实现** |
580
+| **v1.0 补全·属性快照** | `biz_goods_attr` 写入/详情回显;预填接口;`GoodsAttrSupport` | **已实现** |
545 581
 | **v1.0 补全·O10** | 下架前未完成订单校验(`IGoodsOrderFacade`) | **待实现** |
546
-| **v1.x** | 多图、多规格 SKU、运费、`biz_goods_attr` 写入、关键词检索扩展 | 规划 |
547
-| **v1.x·可选** | `/agri/seller/goods/attrTemplateOptions` 挂载 | 规划 |
582
+| **v1.x** | 多图、多规格 SKU、运费、关键词检索扩展 | 规划 |
548 583
 
549 584
 ---
550 585
 
@@ -563,6 +598,7 @@ deleteSellerGoodsBatch
563 598
 | T9 | 跨店 goodsId → MSG_NOT_OWNER |
564 599
 | T10 | 服务快照:保存后详情与目录变更隔离 |
565 600
 | T11 | attrTemplateId:本店有效模版写入;跨店/无效 ID → 失败 |
601
+| T12 | attributes/specs:保存后详情回显;校验项名/值 |
566 602
 
567 603
 ---
568 604
 
@@ -574,7 +610,8 @@ deleteSellerGoodsBatch
574 610
 | **v1.1** | 同步代码:v1.0 补全已实现;统一平台/店铺分类口径;O10 仍标待实现 |
575 611
 | **v1.2** | 同步 `attr_template_id`:GoodsSaveDTO/详情/Mapper/校验;下拉走属性模版模块 `/options` |
576 612
 | **v1.3** | §1.3 模块落位与代码对齐:`IGoodsFacade` 跨包、`template` 依赖、快照/订单 Facade |
613
+| **v1.4** | `biz_goods_attr` 保存/回显;`attributes`/`specs` 请求体;`/goods/attrTemplateOptions` 与预填接口 |
577 614
 
578 615
 ---
579 616
 
580
-*文档版本:v1.3 · 关联《商品列表功能需求.md》v1.3、《商品审核技术方案.md》v1.1、《商品分类技术方案.md》v1.3、《商品服务管理技术方案.md》v1.0.1、《店铺商品分类技术方案.md》v1.2、《属性模版技术方案.md》v1.2 · 技术栈 RuoYi v3.9.2-springboot2 + MySQL 5.7.39。*
617
+*文档版本:v1.4 · 关联《商品列表功能需求.md》v1.4、《属性模版技术方案.md》v1.3 · 技术栈 RuoYi v3.9.2-springboot2 + MySQL 5.7.39。*

+ 63 - 6
doc/店铺后台/商品管理/商品列表/店铺商品列表测试用例.md

@@ -1,11 +1,11 @@
1 1
 # 店铺商品列表 — 测试用例
2 2
 
3
-> **依据:** 《商品列表功能需求.md》v1.3、《店铺商品列表技术方案.md》v1.2  
3
+> **依据:** 《商品列表功能需求.md》v1.4、《店铺商品列表技术方案.md》v1.4  
4 4
 > **关联:** 《商品审核功能需求.md》《商品分类功能需求.md》《店铺商品分类功能需求.md》v1.1、《属性模版功能需求.md》、《角色管理测试用例.md》  
5
-> **范围:** 商家端 `/agri/seller/goods`;`GoodsServiceImpl`(商家 scope)/ `SellerGoodsController`;`IGoodsFacade` / 服务快照 / 属性模版引用协作  
6
-> **排除:** 平台端审核/监管 UI E2E、C 端下单、多规格 SKU/运费模版完整表单、属性模版 **维护** UI、**biz_goods_attr** 明细写入  
5
+> **范围:** 商家端 `/agri/seller/goods`;`GoodsServiceImpl`(商家 scope)/ `SellerGoodsController`;`IGoodsFacade` / 服务快照 / 属性模版与 **biz_goods_attr** 协作  
6
+> **排除:** 平台端审核/监管 UI E2E、C 端下单、多规格 SKU/运费模版完整表单、属性模版 **维护** UI  
7 7
 > **环境:** RuoYi v3.9.2-springboot2;MySQL 5.7.39;请求头 **`X-Shop-Id`** 标识当前店铺  
8
-> **实现分期:** v1.0 + v1.0 补全 **已实现**(含 shopCategoryId、attrTemplateId、删除、批量 submit/offShelf);**O10 订单阻断仍待实现**
8
+> **实现分期:** v1.0 补全 **已实现**(含 shopCategoryId、attrTemplateId、**biz_goods_attr**、删除、批量 submit/offShelf);**O10 仍待实现**
9 9
 
10 10
 ---
11 11
 
@@ -349,6 +349,34 @@
349 349
 | **测试步骤** | insertSellerGoods(101, dto含attrTemplateId) |
350 350
 | **预期结果** | 失败;msg 含「属性模版」;不调用 insert |
351 351
 
352
+### SGL-UT-026 保存写入 biz_goods_attr
353
+
354
+| 要素 | 内容 |
355
+|------|------|
356
+| **用例编号** | SGL-UT-026 |
357
+| **测试模块** | 店铺商品列表 |
358
+| **测试项** | saveGoodsAttrs |
359
+| **测试类型** | 单元测试 |
360
+| **测试工具** | JUnit 5 + Mockito |
361
+| **测试目的** | 验证 GL18:attributes/specs 先删后插 |
362
+| **前置条件** | 合法 GoodsSaveDTO;attributes 含「品牌」→「华为」;specs 含「内存」→「8G」「16G」 |
363
+| **测试步骤** | insertSellerGoods(101, dto) |
364
+| **预期结果** | `goodsAttrMapper.deleteByGoodsId` + `batchInsert` 被调用 |
365
+
366
+### SGL-UT-027 属性项校验失败
367
+
368
+| 要素 | 内容 |
369
+|------|------|
370
+| **用例编号** | SGL-UT-027 |
371
+| **测试模块** | 店铺商品列表 |
372
+| **测试项** | GoodsAttrSupport 校验 |
373
+| **测试类型** | 单元测试 |
374
+| **测试工具** | JUnit 5 |
375
+| **测试目的** | 属性与规格项名相同拒绝 |
376
+| **前置条件** | attributes 与 specs 均含 itemName=「内存」 |
377
+| **测试步骤** | insertSellerGoods |
378
+| **预期结果** | 失败;msg 含「不可相同」 |
379
+
352 380
 ### SGL-UT-021 删除可删状态成功
353 381
 
354 382
 | 要素 | 内容 |
@@ -787,6 +815,34 @@
787 815
 | **测试步骤** | 商家 GET /1002 |
788 816
 | **预期结果** | goods_status=3;rejectReason 非空 |
789 817
 
818
+### SGL-API-029 属性模版预填
819
+
820
+| 要素 | 内容 |
821
+|------|------|
822
+| **用例编号** | SGL-API-029 |
823
+| **测试模块** | 店铺商品列表 |
824
+| **测试项** | GET /attrTemplate/{templateId} |
825
+| **测试类型** | 接口测试 |
826
+| **测试工具** | MockMvc |
827
+| **测试目的** | 验证选用模版预填 attributes/specs |
828
+| **前置条件** | templateId=4 属 shop 101 |
829
+| **测试步骤** | GET /agri/seller/goods/attrTemplate/4 |
830
+| **预期结果** | code=200;data.attributes / data.specs 结构与模版详情一致 |
831
+
832
+### SGL-API-030 保存带 attributes 后详情回显
833
+
834
+| 要素 | 内容 |
835
+|------|------|
836
+| **用例编号** | SGL-API-030 |
837
+| **测试模块** | 店铺商品列表 |
838
+| **测试项** | POST + GET 详情 |
839
+| **测试类型** | 接口测试 |
840
+| **测试工具** | Apifox |
841
+| **测试目的** | 验证 biz_goods_attr 写入与聚合回显 |
842
+| **前置条件** | 新建商品 body 含 attributes/specs |
843
+| **测试步骤** | POST 保存 → GET 详情 |
844
+| **预期结果** | 详情 attributes/specs 与保存体一致 |
845
+
790 846
 ---
791 847
 
792 848
 ## 三、界面测试(Playwright)
@@ -1048,7 +1104,7 @@
1048 1104
 | GL9 删除状态限制 | UT-021,022 | API-020,021 | UI-016 |
1049 1105
 | GL10 批量整批失败 | — | API-022,023 | UI-012 |
1050 1106
 | GL11 分类二级/店铺分类 | UT-007,020 | API-024,026 | UI-004 |
1051
-| GL18 属性模版引用 | UT-024,025 | — | — |
1107
+| GL18 属性模版引用 | UT-024,025,026,027 | API-029,030 | — |
1052 1108
 | GL12 服务快照 | UT-018,019 | API-025 | UI-017 |
1053 1109
 | GL13 保存不可改态 | UT-006 | API-010 | — |
1054 1110
 | GL14 外部变更不级联 | — | API-028 | — |
@@ -1068,7 +1124,8 @@
1068 1124
 | **v1.0** | 首版:23 单元 + 28 接口 + 17 UI;覆盖 GL1~GL17 及状态机/检索/权限 |
1069 1125
 | **v1.1** | 同步代码 v1.0 补全已实现;O10 仍标待实现;统一平台/店铺分类口径 |
1070 1126
 | **v1.2** | 新增 GL18 / UT-024、025:attrTemplateId 保存与校验 |
1127
+| **v1.3** | 新增 UT-026/027、API-029/030:biz_goods_attr 保存与预填 |
1071 1128
 
1072 1129
 ---
1073 1130
 
1074
-*文档版本:v1.2 · 关联《商品列表功能需求.md》v1.3、《店铺商品列表技术方案.md》v1.2*
1131
+*文档版本:v1.3 · 关联《商品列表功能需求.md》v1.4、《店铺商品列表技术方案.md》v1.4*

+ 2 - 1
doc/店铺后台/商品管理/属性模版/属性模版功能需求.md

@@ -541,7 +541,8 @@
541 541
 |------|------|
542 542
 | **v1.0** | 首版定稿:对齐草稿 §1~§6 及原型;关联《关联需求分析》《店铺管理》《商品列表》及平台商品相关需求 |
543 543
 | **v1.1** | 对齐商品列表协作:发品保存 `attr_template_id` 已实现(见《店铺商品列表技术方案》v1.2) |
544
+| **v1.2** | 对齐 `biz_goods_attr` 快照保存与预填流程(见《店铺商品列表技术方案》v1.4) |
544 545
 
545 546
 ---
546 547
 
547
-*文档版本:v1.1 · 关联《属性模版功能需求-草稿》· 平台侧见《关联需求分析.md》《商品分类功能需求.md》《商品服务管理功能需求.md》《商品审核功能需求.md》· 商家侧见《商品列表功能需求.md》v1.3、《店铺商品分类功能需求.md》*
548
+*文档版本:v1.2 · 商家侧见《商品列表功能需求.md》v1.4*

+ 12 - 8
doc/店铺后台/商品管理/属性模版/属性模版技术方案.md

@@ -1,7 +1,7 @@
1 1
 # 属性模版 — 技术方案
2 2
 
3 3
 > **依据:** 《属性模版功能需求.md》v1.0  
4
-> **关联:** 《关联需求分析.md》v1.6;平台《商品服务管理技术方案》v1.0.1、《商品分类技术方案》v1.3、《商品审核技术方案》v1.1;商家《店铺商品列表技术方案》v1.3、《店铺商品分类技术方案》v1.2  
4
+> **关联:** 《关联需求分析.md》v1.6;平台《商品服务管理技术方案》v1.0.1、《商品分类技术方案》v1.3、《商品审核技术方案》v1.1;商家《店铺商品列表技术方案》v1.4、《店铺商品分类技术方案》v1.2  
5 5
 > **范围:** 商家端 **当前店铺** 属性模版 CRUD;为 **商品列表 · 属性模版** 字段提供下拉与详情预填;**平台不参与** 模版维护。  
6 6
 > **原则:** `shop_id` 店铺隔离;逻辑删除;批量删除 **整批失败**;编辑模版 **不 UPDATE** 已有商品属性/规格(快照原则)。
7 7
 
@@ -40,7 +40,7 @@ biz_goods_attr_template(shop_id = 当前店铺)
40 40
41 41
 【商品列表】选用模版 → 预填表单 → 保存
42 42
         ├── biz_goods.attr_template_id(引用关系,删模版校验)
43
-        └── biz_goods_attr(商品侧属性/规格快照,v1.x 落库
43
+        └── biz_goods_attr(商品侧属性/规格快照,**已实现**
44 44
45 45
 【平台审核/监管】读 biz_goods + biz_goods_attr,不读模版表
46 46
 ```
@@ -102,6 +102,7 @@ sql/agri_seller_attr_template_menu.sql             # 菜单(待补)
102 102
 | 场景 | 商品模块代码 |
103 103
 |------|--------------|
104 104
 | 保存校验 `attrTemplateId` | `goods/service/impl/GoodsServiceImpl` 注入 `BizGoodsAttrTemplateMapper` |
105
+| 保存属性/规格快照 | `goods/mapper/BizGoodsAttrMapper` + `GoodsAttrSupport` + `saveGoodsAttrs` |
105 106
 | 删模版引用校验 | `category/facade/IGoodsFacade.existsByAttrTemplateId` ← `goods/facade/impl/GoodsFacadeImpl` |
106 107
 
107 108
 ### 1.4 跨模块 Facade
@@ -170,7 +171,7 @@ sql/agri_seller_attr_template_menu.sql             # 菜单(待补)
170 171
 | `biz_goods` | `attr_template_id` bigint **NULL** | 发品选用模版 ID;**删模版校验** |
171 172
 | `biz_goods_attr` | 商品属性/规格 **快照** | 保存商品时写入;**不随模版编辑追溯** |
172 173
 
173
-**`biz_goods_attr`(v1.x 与发品完整表单同步落库):**
174
+**`biz_goods_attr`(商品列表协作 · 已实现):**
174 175
 
175 176
 | 字段 | 类型 | 说明 |
176 177
 |------|------|------|
@@ -181,7 +182,7 @@ sql/agri_seller_attr_template_menu.sql             # 菜单(待补)
181 182
 | value_text | varchar(128) | 单值一行;多值多行 |
182 183
 | sort_no | int | |
183 184
 
184
-> **v1.0 本模块:** 可先完成模版 CRUD + `attr_template_id` + Facade;`biz_goods_attr` 写入随商品列表 v1.x 一并实现
185
+> **说明:** 建表见 `sql/biz_goods_attr.sql`;写入逻辑在 **商品模块** `GoodsServiceImpl.saveGoodsAttrs`
185 186
 
186 187
 ### 2.6 索引
187 188
 
@@ -390,13 +391,15 @@ INSERT 新 item/value(sort_no 按数组顺序 0,1,2…)
390 391
 
391 392
 商品模块亦可调用 `IAttrTemplateFacade.listOptionsByShopId(shopId)`;选用后 `getDetailForGoods(templateId, shopId)` 预填表单。
392 393
 
393
-**商品侧挂载(规划):**
394
+**商品侧挂载(已实现):**
394 395
 
395 396
 | 方法 | 路径 | 说明 |
396 397
 |------|------|------|
397 398
 | GET | `/agri/seller/goods/attrTemplateOptions` | 委托 `IAttrTemplateFacade.listOptionsByShopId` |
398 399
 | GET | `/agri/seller/goods/attrTemplate/{templateId}` | 委托 `getDetailForGoods`(选用预填) |
399 400
 
401
+保存商品时在 `GoodsSaveDTO` 携带 `attributes` / `specs`,商品模块 **先删后插** `biz_goods_attr`。
402
+
400 403
 ---
401 404
 
402 405
 ## 4. 对外协作
@@ -407,7 +410,7 @@ INSERT 新 item/value(sort_no 按数组顺序 0,1,2…)
407 410
 |------|------|
408 411
 | 发品下拉 | `GET /options` 或 Facade |
409 412
 | 选用预填 | `GET /{templateId}` / `getDetailForGoods` |
410
-| 保存商品 | 写 `biz_goods.attr_template_id`(**已实现**);**先删后插** `biz_goods_attr`(v1.x) |
413
+| 保存商品 | 写 `biz_goods.attr_template_id` + **先删后插** `biz_goods_attr`(**已实现**) |
411 414
 | 删模版校验 | `IGoodsFacade.existsByAttrTemplateId` |
412 415
 
413 416
 **`existsByAttrTemplateId` SQL:**
@@ -543,7 +546,7 @@ buildNameList(templateId)      // 列表聚合列
543 546
 |------|------|------|
544 547
 | **v1.0** | 模版三表 CRUD;`/options`;`IGoodsFacade.existsByAttrTemplateId`;菜单权限 | **已实现** |
545 548
 | **v1.0·商品协作** | 商品保存/详情 `attr_template_id`;`assertAttrTemplateIfPresent` | **已实现** |
546
-| **v1.x** | `biz_goods_attr` 写入;`/agri/seller/goods/attrTemplateOptions` 挂载 | 规划 |
549
+| **v1.0·属性快照** | `biz_goods_attr` 写入/回显;商品侧预填/下拉 API | **已实现** |
547 550
 | **非本期** | 模版导入导出、版本历史、平台代维、与分类自动绑定 | — |
548 551
 
549 552
 ---
@@ -555,7 +558,8 @@ buildNameList(templateId)      // 列表聚合列
555 558
 | **v1.0** | 首版:三表模型;`/agri/seller/attrTemplate`;与商品列表/平台审核协作;对齐《属性模版功能需求》v1.0 |
556 559
 | **v1.1** | 同步实现:模版 CRUD + 删引用校验;商品侧 `attr_template_id` 保存/回显已实现 |
557 560
 | **v1.2** | §1.3 模块落位与代码对齐:包名 `template`、Mapper XML 路径、跨模块 Facade 说明 |
561
+| **v1.3** | 商品协作:`biz_goods_attr`、预填/下拉 API、保存体 attributes/specs |
558 562
 
559 563
 ---
560 564
 
561
-*文档版本:v1.2 · 关联《属性模版功能需求.md》v1.1 · 技术栈 RuoYi v3.9.2-springboot2 + MySQL 5.7.39*
565
+*文档版本:v1.3 · 关联《属性模版功能需求.md》v1.1、《店铺商品列表技术方案.md》v1.4*

+ 10 - 9
doc/店铺后台/商品管理/属性模版/属性模版测试用例.md

@@ -684,19 +684,19 @@
684 684
 | **测试步骤** | GET /list |
685 685
 | **预期结果** | HTTP 403 |
686 686
 
687
-### SAT-API-019 GET /goods/attrTemplateOptions 协作(v1.x
687
+### SAT-API-019 GET /goods/attrTemplateOptions 协作(已实现
688 688
 
689 689
 | 要素 | 内容 |
690 690
 |------|------|
691 691
 | **用例编号** | SAT-API-019 |
692 692
 | **测试模块** | 属性模版 |
693
-| **测试项** | 商品侧下拉 |
693
+| **测试项** | 商品侧下拉 + 预填 |
694 694
 | **测试类型** | 接口测试 |
695
-| **测试工具** | Apifox |
696
-| **测试目的** | 验证技术方案 §3.6 商品挂载 |
697
-| **前置条件** | 商品模块已实现 `/agri/seller/goods/attrTemplateOptions` |
698
-| **测试步骤** | GET 该路径,X-Shop-Id=101 |
699
-| **预期结果** | 与 GET /attrTemplate/options **数据一致** |
695
+| **测试工具** | Apifox / MockMvc |
696
+| **测试目的** | 验证 AT9:商品发品下拉与预填 API |
697
+| **前置条件** | shop 101 有模版;`X-Shop-Id=101` |
698
+| **测试步骤** | GET `/agri/seller/goods/attrTemplateOptions`;GET `/agri/seller/goods/attrTemplate/4` |
699
+| **预期结果** | options 与 `/attrTemplate/options` 一致;预填含 attributes/specs |
700 700
 
701 701
 ---
702 702
 
@@ -968,8 +968,9 @@
968 968
 | 版本 | 说明 |
969 969
 |------|------|
970 970
 | **v1.0** | 首版:28 单元 + 19 接口 + 17 UI;覆盖 AT1~AT12 及正常/异常/协作流程 |
971
-| **v1.1** | 商品协作:`attr_template_id` 保存/回显已实现;`biz_goods_attr` 仍标 v1.x |
971
+| **v1.1** | 商品协作:`attr_template_id` 保存/回显已实现 |
972
+| **v1.2** | `biz_goods_attr` 与商品侧预填 API 已实现;SAT-API-019 标已实现 |
972 973
 
973 974
 ---
974 975
 
975
-*文档版本:v1.1 · 关联《属性模版功能需求.md》v1.1、《属性模版技术方案.md》v1.1*
976
+*文档版本:v1.2 · 关联《属性模版功能需求.md》v1.1、《属性模版技术方案.md》v1.3*

+ 12 - 0
sql/biz_goods.sql

@@ -44,3 +44,15 @@ CREATE TABLE IF NOT EXISTS `biz_goods_service_snapshot` (
44 44
   PRIMARY KEY (`snapshot_id`),
45 45
   KEY `idx_goods_id` (`goods_id`)
46 46
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品服务展示快照';
47
+
48
+-- 商品属性/规格快照
49
+CREATE TABLE IF NOT EXISTS `biz_goods_attr` (
50
+  `attr_id`    bigint(20)  NOT NULL AUTO_INCREMENT,
51
+  `goods_id`   bigint(20)  NOT NULL COMMENT '商品ID',
52
+  `attr_type`  char(1)     NOT NULL COMMENT '1属性2规格',
53
+  `item_name`  varchar(64) NOT NULL COMMENT '属性项/规格项名称',
54
+  `value_text` varchar(128) NOT NULL COMMENT '属性值/规格值',
55
+  `sort_no`    int(11)     NOT NULL DEFAULT '0' COMMENT '展示顺序',
56
+  PRIMARY KEY (`attr_id`),
57
+  KEY `idx_goods_id` (`goods_id`,`attr_type`,`sort_no`)
58
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品属性规格快照';

+ 11 - 0
sql/biz_goods_attr.sql

@@ -0,0 +1,11 @@
1
+-- 商品属性/规格快照(发品保存;不随属性模版编辑追溯)
2
+CREATE TABLE IF NOT EXISTS `biz_goods_attr` (
3
+  `attr_id`    bigint(20)  NOT NULL AUTO_INCREMENT,
4
+  `goods_id`   bigint(20)  NOT NULL COMMENT '商品ID',
5
+  `attr_type`  char(1)     NOT NULL COMMENT '1属性2规格',
6
+  `item_name`  varchar(64) NOT NULL COMMENT '属性项/规格项名称',
7
+  `value_text` varchar(128) NOT NULL COMMENT '属性值/规格值',
8
+  `sort_no`    int(11)     NOT NULL DEFAULT '0' COMMENT '展示顺序',
9
+  PRIMARY KEY (`attr_id`),
10
+  KEY `idx_goods_id` (`goods_id`,`attr_type`,`sort_no`)
11
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品属性规格快照';