Explorar o código

fix(product-sku): specs字段规范化为合法JSON避免MySQL JSON列报错

- 后端 ProductSkuService.create/update 增加 normalizeSpecsToJson():
  将 "颜色:红色;尺码:M" 文本格式转为 {"颜色":"红色","尺码":"M"} JSON
- 前端 ProductSkuEditor handleSave 发送前同样做前端规范化
- 根因: product_skus.specs 列为 MySQL JSON NOT NULL, 前端输入普通文本被拒绝
Xiaogang Liao hai 2 meses
pai
achega
d93bf20c79

+ 57 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ProductSkuService.java

@@ -8,7 +8,9 @@ import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
 import java.util.Date;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 
 @Service
 public class ProductSkuService {
@@ -26,6 +28,7 @@ public class ProductSkuService {
 
     @Transactional
     public ProductSku create(ProductSku sku) {
+        sku.setSpecs(normalizeSpecsToJson(sku.getSpecs()));
         sku.setCreateTime(new Date());
         sku.setUpdateTime(new Date());
         if (sku.getEnabled() == null) {
@@ -37,6 +40,9 @@ public class ProductSkuService {
 
     @Transactional
     public void update(ProductSku sku) {
+        if (sku.getSpecs() != null) {
+            sku.setSpecs(normalizeSpecsToJson(sku.getSpecs()));
+        }
         sku.setUpdateTime(new Date());
         skuMapper.updateById(sku);
     }
@@ -69,4 +75,55 @@ public class ProductSkuService {
         }
         return sku.getStock() >= quantity;
     }
+
+    /**
+     * 将 specs 规格字符串规范化为合法 JSON 对象。
+     *
+     * product_skus.specs 列为 MySQL JSON NOT NULL,只接受合法 JSON。
+     * 前端 ProductSkuEditor 允许用户输入 "颜色:红色;尺码:M" 这种格式,
+     * 直接插入会被 MySQL 拒绝(Invalid JSON text)。
+     * 此方法将 "k1:v1;k2:v2" 格式转为 {"k1":"v1","k2":"v2"};
+     * 如果已经是合法 JSON 字符串则原样返回。
+     */
+    static String normalizeSpecsToJson(String specs) {
+        if (specs == null || specs.trim().isEmpty()) {
+            return "{}";
+        }
+        String trimmed = specs.trim();
+        // 已是合法 JSON 对象/数组,直接返回
+        if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
+            return trimmed;
+        }
+        // 将 k1:v1;k2:v2 格式转为 JSON 对象
+        Map<String, String> map = new LinkedHashMap<>();
+        String[] pairs = trimmed.split("[;;]");
+        for (String pair : pairs) {
+            pair = pair.trim();
+            if (pair.isEmpty()) continue;
+            String[] kv = pair.split("[::]", 2);
+            if (kv.length == 2) {
+                map.put(kv[0].trim(), kv[1].trim());
+            }
+        }
+        if (map.isEmpty()) {
+            return "{}";
+        }
+        // 手动构建 JSON 字符串(避免引入 Jackson 依赖)
+        StringBuilder sb = new StringBuilder("{");
+        boolean first = true;
+        for (Map.Entry<String, String> entry : map.entrySet()) {
+            if (!first) sb.append(",");
+            first = false;
+            sb.append("\"").append(escapeJson(entry.getKey())).append("\":");
+            sb.append("\"").append(escapeJson(entry.getValue())).append("\"");
+        }
+        sb.append("}");
+        return sb.toString();
+    }
+
+    private static String escapeJson(String s) {
+        if (s == null) return "";
+        return s.replace("\\", "\\\\").replace("\"", "\\\"")
+                .replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t");
+    }
 }

+ 18 - 1
cfc-web/src/views/admin/components/ProductSkuEditor.vue

@@ -210,6 +210,23 @@ export default {
         return specs
       }
     },
+    normalizeSpecsToJson(specs) {
+      if (!specs || !specs.trim()) return '{}'
+      const trimmed = specs.trim()
+      if (trimmed.startsWith('{') || trimmed.startsWith('[')) return trimmed
+      const map = {}
+      trimmed.split(/[;;]/).forEach(pair => {
+        pair = pair.trim()
+        if (!pair) return
+        const idx = pair.indexOf(':')
+        const idx2 = pair.indexOf(':')
+        const ci = idx === -1 ? idx2 : (idx2 === -1 ? idx : Math.min(idx, idx2))
+        if (ci > 0) {
+          map[pair.substring(0, ci).trim()] = pair.substring(ci + 1).trim()
+        }
+      })
+      return Object.keys(map).length ? JSON.stringify(map) : '{}'
+    },
     formatPrice(val) {
       if (!val) return '¥0.00'
       return '¥' + parseFloat(val).toFixed(2)
@@ -221,7 +238,7 @@ export default {
         try {
           const payload = {
             productId: this.productId,
-            specs: this.skuForm.specs,
+            specs: this.normalizeSpecsToJson(this.skuForm.specs),
             price: parseFloat(this.skuForm.price),
             originalPrice: this.skuForm.originalPrice ? parseFloat(this.skuForm.originalPrice) : null,
             stock: this.skuForm.stock,