Просмотр исходного кода

fix(product): 商品编辑正确回显并保存商品分类与所属体系

问题:编辑页选择商品分类/所属体系后保存,再次打开仍显示为空。

根因:
1. ProductDTO 未包含 categoryId / distributionSystemId,from() 也不拷贝,
   详情接口不返回这两个字段,编辑页永远无法回显(数据库实际已写入)。
2. updateById 默认忽略 null 字段,clearable 下拉清空后无法真正置 NULL。

修复:
- ProductDTO 增加两字段并在 from() 中映射。
- adminUpdate 对 categoryId / distributionSystemId 为 null 时显式置 NULL,
  与既有 minVisibleLevel 处理方式一致。

测试:ProductDTOMappingTest(回显映射)、ProductUpdateClearTest(清空落库),
均先红后绿。
Xiaogang Liao 4 дней назад
Родитель
Сommit
85fa3fd068

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/ProductDTO.java

@@ -45,6 +45,8 @@ public class ProductDTO {
     private String ppointSource;         // config=管理员配置,default=商品默认
     private Integer profitRate;         // 商品默认利润率(千分比,备用)
     private Integer sortOrder;           // 排序(值越小越靠前)
+    private Long categoryId;               // 商品类目ID
+    private Long distributionSystemId;     // 销售体系ID
 
     // 测评商品扩展信息
     private String assessmentType;
@@ -120,6 +122,8 @@ public class ProductDTO {
         d.rejectReason = p.getRejectReason();
         d.deliveryMethod = p.getDeliveryMethod();
         d.sortOrder = p.getSortOrder();
+        d.categoryId = p.getCategoryId();
+        d.distributionSystemId = p.getDistributionSystemId();
         if (p.getVisibleRoles() != null && !p.getVisibleRoles().isEmpty()) {
             try {
                 d.visibleRoles = JSON.parseArray(p.getVisibleRoles(), String.class);

+ 16 - 5
cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java

@@ -585,13 +585,24 @@ wrapper.in(Product::getId, productIds)
         // 最低可见级别:空串规整为 NULL(NULL=所有用户含未登录可见),
         // 避免管理端 el-select 清空提交 '' 导致商品对所有用户不可见
         boolean clearMinVisibleLevel = normalizeMinVisibleLevel(product);
+        // updateById 默认忽略 null 字段:clearable 下拉清空后需显式置 NULL
+        boolean clearCategory = product.getCategoryId() == null;
+        boolean clearDistributionSystem = product.getDistributionSystemId() == null;
         productMapper.updateById(product);
         nutritionProductService.syncFromProduct(product);
-        // updateById 默认忽略 null 字段,清空时需显式将 min_visible_level 置 NULL
-        if (clearMinVisibleLevel) {
-            productMapper.update(null, new LambdaUpdateWrapper<Product>()
-                    .eq(Product::getId, product.getId())
-                    .set(Product::getMinVisibleLevel, null));
+        if (clearMinVisibleLevel || clearCategory || clearDistributionSystem) {
+            LambdaUpdateWrapper<Product> clearWrapper = new LambdaUpdateWrapper<Product>()
+                    .eq(Product::getId, product.getId());
+            if (clearMinVisibleLevel) {
+                clearWrapper.set(Product::getMinVisibleLevel, null);
+            }
+            if (clearCategory) {
+                clearWrapper.set(Product::getCategoryId, null);
+            }
+            if (clearDistributionSystem) {
+                clearWrapper.set(Product::getDistributionSystemId, null);
+            }
+            productMapper.update(null, clearWrapper);
         }
         return Result.success(ProductDTO.from(productMapper.selectById(product.getId())));
     }

+ 29 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/ProductDTOMappingTest.java

@@ -0,0 +1,29 @@
+package com.etotem.cfc.unit;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.etotem.cfc.dto.ProductDTO;
+import com.etotem.cfc.entity.Product;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * 商品详情 DTO 映射:管理端编辑页依赖详情接口回显「商品分类」和「所属体系」。
+ */
+class ProductDTOMappingTest {
+
+    @Test
+    void from_保留商品分类与所属体系() {
+        Product p = new Product();
+        p.setId(7L);
+        p.setName("还原水机滤芯");
+        p.setCategoryId(1L);
+        p.setDistributionSystemId(3L);
+
+        JSONObject json = JSON.parseObject(JSON.toJSONString(ProductDTO.from(p)));
+
+        assertEquals(1L, json.getLong("categoryId"));
+        assertEquals(3L, json.getLong("distributionSystemId"));
+    }
+}

+ 99 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/ProductUpdateClearTest.java

@@ -0,0 +1,99 @@
+package com.etotem.cfc.unit;
+
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
+import com.etotem.cfc.entity.Product;
+import com.etotem.cfc.mapper.ProductMapper;
+import com.etotem.cfc.service.NutritionProductService;
+import com.etotem.cfc.service.ProductService;
+import org.apache.ibatis.builder.MapperBuilderAssistant;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import java.lang.reflect.Field;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.*;
+
+/**
+ * 商品编辑:分类/所属体系为 clearable 下拉,清空后必须真正写入 NULL,
+ * 不能因为 MyBatis-Plus updateById 忽略 null 字段而被静默保留。
+ */
+class ProductUpdateClearTest {
+
+    private ProductService service;
+    private ProductMapper productMapper;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        // LambdaUpdateWrapper 解析列名依赖 MyBatis-Plus 的 TableInfo 缓存
+        MapperBuilderAssistant assistant =
+                new MapperBuilderAssistant(new MybatisConfiguration(), "");
+        TableInfoHelper.initTableInfo(assistant, Product.class);
+
+        service = new ProductService();
+        productMapper = mock(ProductMapper.class);
+        set("productMapper", productMapper);
+        set("nutritionProductService", mock(NutritionProductService.class));
+    }
+
+    private void set(String name, Object value) throws Exception {
+        Field f = ProductService.class.getDeclaredField(name);
+        f.setAccessible(true);
+        f.set(service, value);
+    }
+
+    private Product existing() {
+        Product p = new Product();
+        p.setId(7L);
+        p.setName("还原水机滤芯");
+        p.setStatus("on_shelf");
+        p.setCategoryId(1L);
+        p.setDistributionSystemId(3L);
+        return p;
+    }
+
+    @Test
+    void adminUpdate_分类与体系为空时显式置NULL() {
+        Product existing = existing();
+        when(productMapper.selectById(7L)).thenReturn(existing);
+        when(productMapper.updateById(any(Product.class))).thenReturn(1);
+        when(productMapper.update(isNull(), any(Wrapper.class))).thenReturn(1);
+
+        Product form = new Product();
+        form.setId(7L);
+        form.setName("还原水机滤芯");
+        form.setCategoryId(null);
+        form.setDistributionSystemId(null);
+
+        assertEquals(200, service.adminUpdate(form).getCode());
+
+        ArgumentCaptor<Wrapper> captor = ArgumentCaptor.forClass(Wrapper.class);
+        verify(productMapper).update(isNull(), captor.capture());
+        String sqlSet = ((LambdaUpdateWrapper<Product>) captor.getValue()).getSqlSet();
+        assertTrue(sqlSet.contains("category_id"), sqlSet);
+        assertTrue(sqlSet.contains("distribution_system_id"), sqlSet);
+    }
+
+    @Test
+    void adminUpdate_选择了分类与体系时不额外置NULL() {
+        Product existing = existing();
+        when(productMapper.selectById(7L)).thenReturn(existing);
+        when(productMapper.updateById(any(Product.class))).thenReturn(1);
+        when(productMapper.update(isNull(), any(Wrapper.class))).thenReturn(1);
+
+        Product form = new Product();
+        form.setId(7L);
+        form.setName("还原水机滤芯");
+        form.setCategoryId(2L);
+        form.setDistributionSystemId(4L);
+
+        assertEquals(200, service.adminUpdate(form).getCode());
+
+        verify(productMapper, never()).update(isNull(), any(Wrapper.class));
+    }
+}