Sfoglia il codice sorgente

feat: 一键自动关联ERP商品+批量查询批量UPDATE优化

- 新增 POST /erp-goods/auto-bind 端点
- 按 ERP goodsCode 匹配本地 product.name,唯一匹配自动绑定
- 批量查询绑定状态(IN)和产品匹配(100条/批)
- UPDATE 使用 CASE WHEN 批量写入(100条/批)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 5 giorni fa
parent
commit
7232919fd9

+ 4 - 0
haha-admin-web/src/api/erpGoods.ts

@@ -21,3 +21,7 @@ export const bindErpGoods = (productId: number, erpGoodsId: number) =>
 /** 解除本地商品与 ERP 商品的关联 */
 export const unbindErpGoods = (productId: number) =>
   http.request<Result>("post", "/erp-goods/unbind", { data: { productId } });
+
+/** 一键自动关联:按 goodsCode 匹配本地商品名称 */
+export const autoBindErpGoods = () =>
+  http.request<Result>("post", "/erp-goods/auto-bind");

+ 27 - 1
haha-admin-web/src/views/erp-goods/index.vue

@@ -10,7 +10,8 @@ import {
   syncErpGoods,
   getErpGoodsList,
   bindErpGoods,
-  unbindErpGoods
+  unbindErpGoods,
+  autoBindErpGoods
 } from "@/api/erpGoods";
 import { getProductList } from "@/api/product";
 
@@ -29,6 +30,24 @@ const bindDialogVisible = ref(false);
 const currentErpGoods = ref<any>(null);
 const selectedProductId = ref<number | null>(null);
 const bindLoading = ref(false);
+const autoBinding = ref(false);
+
+async function handleAutoBind() {
+  autoBinding.value = true;
+  try {
+    const res = await autoBindErpGoods();
+    if (res.code === 200) {
+      ElMessage.success(res.message || "自动关联完成");
+      await fetchList();
+    } else {
+      ElMessage.error(res.message || "自动关联失败");
+    }
+  } catch (e) {
+    ElMessage.error("自动关联请求失败");
+  } finally {
+    autoBinding.value = false;
+  }
+}
 
 async function fetchList() {
   loading.value = true;
@@ -183,6 +202,13 @@ onMounted(() => {
         >
           从 ERP 同步
         </el-button>
+        <el-button
+          type="success"
+          :loading="autoBinding"
+          @click="handleAutoBind"
+        >
+          一键关联
+        </el-button>
       </div>
     </div>
 

+ 12 - 0
haha-admin/src/main/java/com/haha/admin/controller/ErpGoodsController.java

@@ -78,4 +78,16 @@ public class ErpGoodsController {
         erpGoodsService.unbindGoods(productId);
         return Result.success("解除关联成功");
     }
+
+    /**
+     * 一键自动关联:按 ERP goodsCode 匹配本地商品名称,唯一匹配则自动绑定
+     */
+    @RequirePermission("erp:goods:bind")
+    @Log(module = "ERP商品管理", operation = OperationType.UPDATE, summary = "一键自动关联ERP商品")
+    @PostMapping("/auto-bind")
+    public Result<Map<String, Integer>> autoBind() {
+        Map<String, Integer> result = erpGoodsService.autoBind();
+        return Result.success(String.format("关联 %d 条,跳过 %d 条",
+                result.get("bound"), result.get("skipped")), result);
+    }
 }

+ 6 - 0
haha-service/src/main/java/com/haha/service/ErpGoodsService.java

@@ -23,4 +23,10 @@ public interface ErpGoodsService extends IService<ErpGoods> {
     void bindGoods(Long productId, Long erpGoodsId);
 
     void unbindGoods(Long productId);
+
+    /**
+     * 一键自动关联:按 goodsCode 匹配本地商品名称,唯一匹配则自动绑定
+     * @return { bound: 成功关联数, skipped: 无匹配/多匹配跳过数 }
+     */
+    java.util.Map<String, Integer> autoBind();
 }

+ 101 - 0
haha-service/src/main/java/com/haha/service/impl/ErpGoodsServiceImpl.java

@@ -20,6 +20,7 @@ import com.qdb.sdk.model.response.SysGoodsVO;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -27,7 +28,13 @@ import org.springframework.util.StringUtils;
 
 import java.time.LocalDateTime;
 import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 @Slf4j
 @Service
@@ -39,6 +46,9 @@ public class ErpGoodsServiceImpl extends ServiceImpl<ErpGoodsMapper, ErpGoods> i
     @Autowired(required = false)
     private QdbClient qdbClient;
 
+    @Autowired
+    private JdbcTemplate jdbcTemplate;
+
     @Async("taskExecutor")
     @Override
     public void syncFromErp() {
@@ -171,6 +181,97 @@ public class ErpGoodsServiceImpl extends ServiceImpl<ErpGoodsMapper, ErpGoods> i
         log.info("商品解除ERP商品关联: productId={}, productName={}", productId, product.getName());
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, Integer> autoBind() {
+        List<ErpGoods> erpGoodsList = list(new LambdaQueryWrapper<ErpGoods>()
+                .eq(ErpGoods::getStatus, 1));
+        if (erpGoodsList.isEmpty()) {
+            Map<String, Integer> empty = new HashMap<>();
+            empty.put("bound", 0);
+            empty.put("skipped", 0);
+            return empty;
+        }
+
+        // 1. 收集所有 ERP 商品 ID,批量查已关联的本地商品
+        List<Long> allErpGoodsIds = erpGoodsList.stream().map(ErpGoods::getErpGoodsId).toList();
+        Set<Long> boundErpGoodsIds = new HashSet<>(productMapper.selectList(new LambdaQueryWrapper<Product>()
+                .in(Product::getErpGoodsId, allErpGoodsIds)
+                .eq(Product::getIsDeleted, 0))
+                .stream().map(Product::getErpGoodsId).toList());
+
+        // 2. 收集未关联的 ERP 商品 code,批量查本地商品
+        Map<String, Long> codeToErpGoodsId = new LinkedHashMap<>();
+        for (ErpGoods erp : erpGoodsList) {
+            if (!boundErpGoodsIds.contains(erp.getErpGoodsId())) {
+                codeToErpGoodsId.put(erp.getGoodsCode(), erp.getErpGoodsId());
+            }
+        }
+        int alreadyBoundSkip = erpGoodsList.size() - codeToErpGoodsId.size();
+
+        // 3. 分批查本地商品(每批 100 个 code)
+        Map<String, List<Product>> nameToProducts = new HashMap<>();
+        List<String> allCodes = new ArrayList<>(codeToErpGoodsId.keySet());
+        for (int i = 0; i < allCodes.size(); i += 100) {
+            int end = Math.min(i + 100, allCodes.size());
+            List<String> batch = allCodes.subList(i, end);
+            List<Product> batchProducts = productMapper.selectList(new LambdaQueryWrapper<Product>()
+                    .in(Product::getName, batch)
+                    .eq(Product::getIsDeleted, 0));
+            for (Product p : batchProducts) {
+                nameToProducts.computeIfAbsent(p.getName(), k -> new ArrayList<>()).add(p);
+            }
+        }
+
+        // 4. 收集唯一匹配的关联对
+        java.util.List<Long> bindProductIds = new ArrayList<>();
+        java.util.List<Long> bindErpIds = new ArrayList<>();
+        int skipped = alreadyBoundSkip;
+
+        for (Map.Entry<String, Long> entry : codeToErpGoodsId.entrySet()) {
+            String goodsCode = entry.getKey();
+            Long erpGoodsId = entry.getValue();
+            List<Product> matches = nameToProducts.getOrDefault(goodsCode, List.of());
+
+            if (matches.size() == 1) {
+                bindProductIds.add(matches.get(0).getId());
+                bindErpIds.add(erpGoodsId);
+                log.info("自动关联: erpGoodsCode={}, productName={}", goodsCode, matches.get(0).getName());
+            } else {
+                skipped++;
+            }
+        }
+
+        // 5. 批量 UPDATE 用 CASE WHEN(100 条一批)
+        int bound = 0;
+        for (int i = 0; i < bindProductIds.size(); i += 100) {
+            int end = Math.min(i + 100, bindProductIds.size());
+            List<Long> batchIds = bindProductIds.subList(i, end);
+            List<Long> batchErpIds = bindErpIds.subList(i, end);
+
+            StringBuilder sql = new StringBuilder(
+                    "UPDATE t_product SET erp_goods_id = CASE id ");
+            for (int j = 0; j < batchIds.size(); j++) {
+                sql.append("WHEN ").append(batchIds.get(j))
+                   .append(" THEN ").append(batchErpIds.get(j)).append(" ");
+            }
+            sql.append("END, update_time = NOW() WHERE id IN (");
+            for (int j = 0; j < batchIds.size(); j++) {
+                if (j > 0) sql.append(",");
+                sql.append(batchIds.get(j));
+            }
+            sql.append(")");
+
+            bound += jdbcTemplate.update(sql.toString());
+        }
+
+        log.info("一键自动关联完成: 成功 {} 条, 跳过 {} 条", bound, skipped);
+        Map<String, Integer> result = new HashMap<>();
+        result.put("bound", bound);
+        result.put("skipped", skipped);
+        return result;
+    }
+
     private void upsertErpGoods(SysGoodsVO goods) {
         log.info("同步ERP商品: goodsId={}, goodsCode={}, goodsName={}",
                 goods.getGoodsId(), goods.getGoodsCode(), goods.getGoodsName());