Browse Source

销售管理模块后端代码

lbyzx123 1 day ago
parent
commit
6c4fab8f9e

+ 535 - 236
ruoyi-admin/src/main/java/com/ruoyi/web/sales/controller/SalesOrderController.java

@@ -30,7 +30,9 @@ import javax.servlet.http.HttpServletResponse;
 import java.net.InetAddress;
 import java.net.NetworkInterface;
 import java.math.BigDecimal;
+import java.sql.Timestamp;
 import java.time.LocalDate;
+import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -237,9 +239,189 @@ public class SalesOrderController {
         util.exportExcel(response, list, "销售订单");
     }
 
-    @ApiOperation("销售订单货品明细列表")
+    @ApiOperation("白条分配订单货品明细(销售日期、物流线/市场/客户、产品类型)")
     @PostMapping("/goodsDetailList")
-    public AjaxResult goodsDetailList(@RequestBody Map<String, String> params, HttpServletRequest request) throws Exception {
+    public AjaxResult goodsDetailList(@RequestBody(required = false) Map<String, String> params, HttpServletRequest request) throws Exception {
+        if (params == null) {
+            params = Collections.emptyMap();
+        }
+        String saleDate = params.get("saleDate");
+        if (StringUtils.isBlank(saleDate)) {
+            throw new Exception("销售日期不能为空");
+        }
+        saleDate = saleDate.trim();
+        String orgId = tokenService.getLoginOrgId(request);
+        String lineNum = trimOrNull(params.get("lineNum"));
+        String marketNum = trimOrNull(params.get("marketNum"));
+        String customerNum = trimOrNull(params.get("customerNum"));
+        List<String> productTypeList = parseCommaSeparatedList(trimOrNull(params.get("productTypes")));
+        if (!StringUtils.isNotEmpty(productTypeList)) {
+            productTypeList = new ArrayList<String>();
+            productTypeList.add("002");
+        }
+
+        Map<Long, BigDecimal> allocByGoodsId = loadAllocNumByGoodsLineId(
+                orgId, saleDate, lineNum, marketNum, customerNum, productTypeList);
+        List<Map<String, Object>> list = salesOrderGoodsMapper.listGoodsDetailByConditions(
+                orgId, saleDate, lineNum, marketNum, customerNum, productTypeList);
+        fillGoodsLineUnalloc(list, allocByGoodsId);
+        return success(list);
+    }
+
+    /** 分配表 sale_id 对应订单货品明细主键 sog.id */
+    private Map<Long, BigDecimal> loadAllocNumByGoodsLineId(String orgId, String saleDate,
+                                                            String lineNum, String marketNum, String customerNum,
+                                                            List<String> productTypeList) {
+        List<Map<String, Object>> rows = salesOrderGoodsMapper.listAllocNumByOrderGoodsLine(
+                orgId, saleDate, lineNum, marketNum, customerNum, productTypeList);
+        Map<Long, BigDecimal> map = new HashMap<Long, BigDecimal>();
+        if (StringUtils.isEmpty(rows)) {
+            return map;
+        }
+        for (Map<String, Object> row : rows) {
+            if (row == null) {
+                continue;
+            }
+            Long goodsId = toLongValue(row.get("sale_id"));
+            if (goodsId == null) {
+                continue;
+            }
+            map.put(goodsId, toBigDecimal(row.get("alloc_num")));
+        }
+        return map;
+    }
+
+    private void fillGoodsLineUnalloc(List<Map<String, Object>> list, Map<Long, BigDecimal> allocByGoodsId) {
+        if (StringUtils.isEmpty(list)) {
+            return;
+        }
+        for (Map<String, Object> row : list) {
+            if (row == null) {
+                continue;
+            }
+            Long goodsId = toLongValue(row.get("id"));
+            BigDecimal alloc = goodsId == null ? BigDecimal.ZERO : allocByGoodsId.getOrDefault(goodsId, BigDecimal.ZERO);
+            BigDecimal ass = toBigDecimal(row.get("ass_num"));
+            BigDecimal unalloc = ass.subtract(alloc);
+            if (unalloc.compareTo(BigDecimal.ZERO) < 0) {
+                unalloc = BigDecimal.ZERO;
+            }
+            row.put("alloc_num", alloc);
+            row.put("unalloc_num", unalloc);
+        }
+    }
+
+    private static String trimOrNull(String s) {
+        return StringUtils.isBlank(s) ? null : s.trim();
+    }
+
+    /** 逗号分隔编码列表(如产品类型 {@code 002,003});空或 null 解析结果为空列表前由调用方处理默认 */
+    private static List<String> parseCommaSeparatedList(String s) {
+        if (StringUtils.isBlank(s)) {
+            return null;
+        }
+        List<String> out = new ArrayList<String>();
+        for (String p : s.split(",")) {
+            String t = p == null ? null : p.trim();
+            if (StringUtils.isNotBlank(t)) {
+                out.add(t);
+            }
+        }
+        return out.isEmpty() ? null : out;
+    }
+
+    private static Long toLongValue(Object v) {
+        if (v == null) {
+            return null;
+        }
+        if (v instanceof Long) {
+            return (Long) v;
+        }
+        if (v instanceof Integer) {
+            return ((Integer) v).longValue();
+        }
+        if (v instanceof Number) {
+            return ((Number) v).longValue();
+        }
+        String s = String.valueOf(v).trim();
+        if (StringUtils.isBlank(s)) {
+            return null;
+        }
+        try {
+            return new BigDecimal(s.replace(",", "")).longValue();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    /** JDBC/MyBatis 常见日期类型转为 {@link Date},无法解析时返回 null */
+    private static Date toUtilDate(Object v) {
+        if (v == null) {
+            return null;
+        }
+        if (v instanceof Date) {
+            return (Date) v;
+        }
+        if (v instanceof Timestamp) {
+            return new Date(((Timestamp) v).getTime());
+        }
+        if (v instanceof java.sql.Date) {
+            return new Date(((java.sql.Date) v).getTime());
+        }
+        if (v instanceof java.time.LocalDateTime) {
+            return Date.from(((java.time.LocalDateTime) v).atZone(ZoneId.systemDefault()).toInstant());
+        }
+        if (v instanceof LocalDate) {
+            return Date.from(((LocalDate) v).atStartOfDay(ZoneId.systemDefault()).toInstant());
+        }
+        return null;
+    }
+
+    /**
+     * 按白条分配表汇总每条订单明细({@code sales_order_goods.id} = {@code pwsa.sale_id})的最大发货日期写入 {@code delivery_date}。
+     */
+    private void attachMaxDeliveryDatePerGoodsLine(String orgId, List<Map<String, Object>> detailRows) {
+        if (StringUtils.isEmpty(detailRows)) {
+            return;
+        }
+        LinkedHashSet<Long> saleIds = new LinkedHashSet<Long>();
+        for (Map<String, Object> row : detailRows) {
+            if (row == null) {
+                continue;
+            }
+            Long id = toLongValue(row.get("id"));
+            if (id != null) {
+                saleIds.add(id);
+            }
+        }
+        Map<Long, Date> deliveryBySaleId = new HashMap<Long, Date>(Math.max(8, saleIds.size()));
+        if (!saleIds.isEmpty()) {
+            List<Map<String, Object>> agg = salesOrderGoodsMapper.listMaxDeliveryDateBySaleIds(orgId, new ArrayList<Long>(saleIds));
+            if (StringUtils.isNotEmpty(agg)) {
+                for (Map<String, Object> r : agg) {
+                    if (r == null) {
+                        continue;
+                    }
+                    Long sid = toLongValue(r.get("sale_id"));
+                    if (sid == null) {
+                        continue;
+                    }
+                    deliveryBySaleId.put(sid, toUtilDate(r.get("delivery_date")));
+                }
+            }
+        }
+        for (Map<String, Object> row : detailRows) {
+            if (row == null) {
+                continue;
+            }
+            Long id = toLongValue(row.get("id"));
+            row.put("delivery_date", id == null ? null : deliveryBySaleId.get(id));
+        }
+    }
+
+    @ApiOperation("按品种级别统计订单未分配片数、可分配片数")
+    @PostMapping("/allocationStatsByVarietyGrade")
+    public AjaxResult allocationStatsByVarietyGrade(@RequestBody Map<String, String> params, HttpServletRequest request) throws Exception {
         String saleDate = params == null ? null : params.get("saleDate");
         if (StringUtils.isBlank(saleDate)) {
             throw new Exception("销售日期不能为空");
@@ -248,14 +430,116 @@ public class SalesOrderController {
         String lineNum = params.get("lineNum");
         String marketNum = params.get("marketNum");
         String customerNum = params.get("customerNum");
-        List<Map<String, Object>> list = salesOrderGoodsMapper.listGoodsDetailByConditions(
+        List<String> productTypeList = parseCommaSeparatedList(trimOrNull(params.get("productTypeList")));
+        if (productTypeList == null) {
+            productTypeList = parseCommaSeparatedList(trimOrNull(params.get("productTypes")));
+        }
+        if (!StringUtils.isNotEmpty(productTypeList)) {
+            productTypeList = new ArrayList<String>();
+            productTypeList.add("002");
+        }
+
+        List<Map<String, Object>> assSummary = salesOrderGoodsMapper.summarizeAssNumByVarietyAndGrade(
                 orgId,
                 saleDate.trim(),
                 StringUtils.isBlank(lineNum) ? null : lineNum.trim(),
                 StringUtils.isBlank(marketNum) ? null : marketNum.trim(),
-                StringUtils.isBlank(customerNum) ? null : customerNum.trim()
+                StringUtils.isBlank(customerNum) ? null : customerNum.trim(),
+                productTypeList
         );
-        return success(list);
+        List<Map<String, Object>> availSummary = salesOrderGoodsMapper.summarizeAvailWhiteStripByVarietyGrade(
+                orgId, saleDate.trim(), productTypeList);
+        List<Map<String, Object>> allocSummary = salesOrderGoodsMapper.summarizeAllocWhiteStripByVarietyGrade(
+                orgId, saleDate.trim(), productTypeList);
+
+        Map<String, BigDecimal> assMap = new LinkedHashMap<String, BigDecimal>();
+        Map<String, BigDecimal> allocMap = new LinkedHashMap<String, BigDecimal>();
+        Map<String, BigDecimal> availMap = new LinkedHashMap<String, BigDecimal>();
+        Map<String, String> varietyMap = new LinkedHashMap<String, String>();
+        Map<String, String> varietyNameMap = new LinkedHashMap<String, String>();
+        Map<String, String> gradeMap = new LinkedHashMap<String, String>();
+
+        for (Map<String, Object> row : assSummary) {
+            String variety = row == null || row.get("variety") == null ? null : String.valueOf(row.get("variety"));
+            String grade = row == null || row.get("grade") == null ? null : String.valueOf(row.get("grade"));
+            if (StringUtils.isBlank(variety) || StringUtils.isBlank(grade)) {
+                continue;
+            }
+            String key = variety.trim() + "\0" + grade.trim();
+            assMap.put(key, toBigDecimal(row.get("ass_num")));
+            varietyMap.put(key, variety.trim());
+            gradeMap.put(key, grade.trim());
+            Object vn = row.get("variety_name");
+            if (vn != null && StringUtils.isNotBlank(String.valueOf(vn))) {
+                varietyNameMap.put(key, String.valueOf(vn));
+            }
+        }
+        for (Map<String, Object> row : allocSummary) {
+            String variety = row == null || row.get("variety") == null ? null : String.valueOf(row.get("variety"));
+            String grade = row == null || row.get("grade") == null ? null : String.valueOf(row.get("grade"));
+            if (StringUtils.isBlank(variety) || StringUtils.isBlank(grade)) {
+                continue;
+            }
+            String key = variety.trim() + "\0" + grade.trim();
+            allocMap.put(key, toBigDecimal(row.get("alloc_ws_num")));
+            varietyMap.putIfAbsent(key, variety.trim());
+            gradeMap.putIfAbsent(key, grade.trim());
+            Object vn = row.get("variety_name");
+            if (vn != null && StringUtils.isNotBlank(String.valueOf(vn))) {
+                varietyNameMap.putIfAbsent(key, String.valueOf(vn));
+            }
+        }
+        for (Map<String, Object> row : availSummary) {
+            String variety = row == null || row.get("variety") == null ? null : String.valueOf(row.get("variety"));
+            String grade = row == null || row.get("grade") == null ? null : String.valueOf(row.get("grade"));
+            if (StringUtils.isBlank(variety) || StringUtils.isBlank(grade)) {
+                continue;
+            }
+            String key = variety.trim() + "\0" + grade.trim();
+            availMap.put(key, toBigDecimal(row.get("avail_ws_num")));
+            varietyMap.putIfAbsent(key, variety.trim());
+            gradeMap.putIfAbsent(key, grade.trim());
+            Object vn = row.get("variety_name");
+            if (vn != null && StringUtils.isNotBlank(String.valueOf(vn))) {
+                varietyNameMap.putIfAbsent(key, String.valueOf(vn));
+            }
+        }
+
+        LinkedHashSet<String> keys = new LinkedHashSet<String>();
+        keys.addAll(assMap.keySet());
+        keys.addAll(availMap.keySet());
+        keys.addAll(allocMap.keySet());
+
+        List<Map<String, Object>> result = new ArrayList<Map<String, Object>>();
+        for (String key : keys) {
+            BigDecimal assNum = assMap.getOrDefault(key, BigDecimal.ZERO);
+            BigDecimal allocNum = allocMap.getOrDefault(key, BigDecimal.ZERO);
+            BigDecimal orderUnAlloc = assNum.subtract(allocNum);
+            if (orderUnAlloc.compareTo(BigDecimal.ZERO) < 0) {
+                orderUnAlloc = BigDecimal.ZERO;
+            }
+            BigDecimal availNum = availMap.getOrDefault(key, BigDecimal.ZERO);
+
+            Map<String, Object> row = new LinkedHashMap<String, Object>();
+            row.put("variety", varietyMap.get(key));
+            row.put("varietyName", varietyNameMap.get(key));
+            row.put("grade", gradeMap.get(key));
+            row.put("orderUnAllocNum", orderUnAlloc);
+            row.put("availAllocNum", availNum);
+            result.add(row);
+        }
+        result.sort((a, b) -> {
+            String av = a.get("variety") == null ? "" : String.valueOf(a.get("variety"));
+            String bv = b.get("variety") == null ? "" : String.valueOf(b.get("variety"));
+            int c = av.compareTo(bv);
+            if (c != 0) {
+                return c;
+            }
+            String ag = a.get("grade") == null ? "" : String.valueOf(a.get("grade"));
+            String bg = b.get("grade") == null ? "" : String.valueOf(b.get("grade"));
+            return ag.compareTo(bg);
+        });
+        return success(result);
     }
 
     @ApiOperation("销售订单审核")
@@ -400,9 +684,7 @@ public class SalesOrderController {
                 .orderByAsc("market_num")
                 .orderByAsc("id");
         List<BaseMarket> allMarkets = baseMarketService.list(marketQw);
-        Map<String, List<BaseMarket>> marketsByLineNum = allMarkets.stream()
-                .filter(m -> StringUtils.isNotBlank(m.getLineNum()))
-                .collect(Collectors.groupingBy(m -> m.getLineNum().trim(), LinkedHashMap::new, Collectors.toList()));
+        Map<String, List<BaseMarket>> marketsByLineNum = groupMarketsByLineNum(allMarkets);
 
         QueryWrapper<RelCustomerMarket> relQw = new QueryWrapper<RelCustomerMarket>()
                 .eq("org_id", orgId)
@@ -413,260 +695,171 @@ public class SalesOrderController {
         List<BaseCustomer> customers = customerService.list(new QueryWrapper<BaseCustomer>()
                 .eq("org_id", orgId)
                 .and(w -> w.eq("del_flag", "0").or().isNull("del_flag")));
-        Map<String, String> customerNameMap = customers.stream()
-                .filter(c -> StringUtils.isNotBlank(c.getCustomerNum()))
-                .collect(Collectors.toMap(BaseCustomer::getCustomerNum,
-                        c -> c.getCustomerName() != null ? c.getCustomerName() : "",
-                        (a, b) -> a, LinkedHashMap::new));
+        Map<String, String> customerNameMap = buildCustomerNumToNameMap(customers);
 
-        Map<String, List<RelCustomerMarket>> relsByLineAndMarket = new LinkedHashMap<String, List<RelCustomerMarket>>();
-        for (RelCustomerMarket r : allRels) {
-            if (StringUtils.isBlank(r.getLineNum()) || StringUtils.isBlank(r.getMarketNum())) {
-                continue;
-            }
-            String key = r.getLineNum().trim() + "\0" + r.getMarketNum().trim();
-            relsByLineAndMarket.computeIfAbsent(key, k -> new ArrayList<RelCustomerMarket>()).add(r);
-        }
-
-        List<Map<String, Object>> tree = new ArrayList<Map<String, Object>>();
-        for (BaseLine line : lines) {
-            String lineNum = line.getLineNum() == null ? "" : line.getLineNum().trim();
-            Map<String, Object> lineNode = new LinkedHashMap<String, Object>();
-            lineNode.put("id", "line:" + lineNum + ":" + line.getId());
-            lineNode.put("label", StringUtils.isNotBlank(line.getLineName()) ? line.getLineName() : lineNum);
-            lineNode.put("type", "line");
-            lineNode.put("lineNum", lineNum);
-            lineNode.put("lineName", line.getLineName());
-
-            List<Map<String, Object>> marketChildren = new ArrayList<Map<String, Object>>();
-            List<BaseMarket> markets = marketsByLineNum.getOrDefault(lineNum, new ArrayList<BaseMarket>());
-            for (BaseMarket market : markets) {
-                String marketNum = market.getMarketNum() == null ? "" : market.getMarketNum().trim();
-                Map<String, Object> marketNode = new LinkedHashMap<String, Object>();
-                marketNode.put("id", "market:" + lineNum + ":" + marketNum + ":" + market.getId());
-                marketNode.put("label", StringUtils.isNotBlank(market.getMarketName()) ? market.getMarketName() : marketNum);
-                marketNode.put("type", "market");
-                marketNode.put("lineNum", lineNum);
-                marketNode.put("marketNum", marketNum);
-                marketNode.put("marketName", market.getMarketName());
-
-                String relKey = lineNum + "\0" + marketNum;
-                List<RelCustomerMarket> rels = relsByLineAndMarket.getOrDefault(relKey, new ArrayList<RelCustomerMarket>());
-                Set<String> seenCustomer = new LinkedHashSet<String>();
-                List<Map<String, Object>> customerChildren = new ArrayList<Map<String, Object>>();
-                for (RelCustomerMarket rel : rels) {
-                    String customerNum = rel.getCustomerNum() == null ? "" : rel.getCustomerNum().trim();
-                    if (StringUtils.isBlank(customerNum) || !seenCustomer.add(customerNum)) {
-                        continue;
-                    }
-                    String customerName = StringUtils.isNotBlank(rel.getCustomerName())
-                            ? rel.getCustomerName()
-                            : customerNameMap.getOrDefault(customerNum, customerNum);
-                    Map<String, Object> custNode = new LinkedHashMap<String, Object>();
-                    custNode.put("id", "customer:" + lineNum + ":" + marketNum + ":" + customerNum + ":" + rel.getId());
-                    custNode.put("label", customerName);
-                    custNode.put("type", "customer");
-                    custNode.put("lineNum", lineNum);
-                    custNode.put("marketNum", marketNum);
-                    custNode.put("customerNum", customerNum);
-                    custNode.put("customerName", customerName);
-                    custNode.put("relId", rel.getId());
-                    custNode.put("leaf", true);
-                    customerChildren.add(custNode);
-                }
-                marketNode.put("children", customerChildren);
-                marketChildren.add(marketNode);
-            }
-            lineNode.put("children", marketChildren);
-            tree.add(lineNode);
-        }
-        return success(tree);
+        Map<String, List<RelCustomerMarket>> relsByLineAndMarket = groupRelsByLineAndMarket(allRels);
+        return success(buildLineMarketCustomerElTree(lines, marketsByLineNum, relsByLineAndMarket, customerNameMap, null, null, null));
     }
 
-    @ApiOperation("物流线-市场-客户三级树(按销售日期和产品类型统计goodsCount")
+    @ApiOperation("物流线-市场-客户三级树(白条分配),按销售日期和产品类型统计goodsCount")
     @GetMapping("/lineMarketCustomerStats")
     public AjaxResult lineMarketCustomerStats(
             @ApiParam(value = "销售日期(yyyy-MM-dd)", required = true)
             @RequestParam("saleDate")
-            @DateTimeFormat(pattern = "yyyy-MM-dd") Date saleDate,
+            @DateTimeFormat(pattern = "yyyy-MM-dd") String saleDate,
             @ApiParam(value = "产品类型(productType),不传默认002")
             @RequestParam(value = "productType", required = false) String productType,
             HttpServletRequest request) {
         if (saleDate == null) {
             return AjaxResult.error("销售日期不能为空");
         }
-        String statProductType = StringUtils.isBlank(productType) ? "002" : productType.trim();
+        List<String> productTypeList = new ArrayList<>();
+        productTypeList.add(StringUtils.isBlank(productType) ? "002" : productType.trim());
         String orgId = tokenService.getLoginOrgId(request);
 
-        List<SalesOrder> saleDateOrders = salesOrderService.list(new QueryWrapper<SalesOrder>()
-                .eq("org_id", orgId)
-                .eq("del_flag", "0")
-                .eq("sale_date", saleDate)
-                .orderByDesc("id"));
-        if (saleDateOrders == null || saleDateOrders.isEmpty()) {
-            return success(new ArrayList<Map<String, Object>>());
-        }
-        Set<String> orderNums = saleDateOrders.stream()
-                .map(SalesOrder::getOrderNum)
-                .filter(StringUtils::isNotBlank)
-                .map(String::trim)
-                .collect(Collectors.toCollection(LinkedHashSet::new));
-        if (orderNums.isEmpty()) {
-            return success(new ArrayList<Map<String, Object>>());
-        }
-        Map<String, String> customerByOrderNum = saleDateOrders.stream()
-                .filter(o -> StringUtils.isNotBlank(o.getOrderNum()) && StringUtils.isNotBlank(o.getCustomerNum()))
-                .collect(Collectors.toMap(o -> o.getOrderNum().trim(), o -> o.getCustomerNum().trim(), (a, b) -> a, LinkedHashMap::new));
-        Set<String> orderCustomers = new LinkedHashSet<String>(customerByOrderNum.values());
-        if (orderCustomers.isEmpty()) {
+        Set<String> lineNumSet = new HashSet<String>();
+        Set<String> marketNumSet = new HashSet<String>();
+        Set<String> customerNumSet = new HashSet<String>();
+        Map<String, BigDecimal> lineAssNumCountMap = new HashMap<String, BigDecimal>();
+        Map<String, BigDecimal> marketAssNumCountMap = new HashMap<String, BigDecimal>();
+        Map<String, BigDecimal> customerAssNumCountMap = new HashMap<String, BigDecimal>();
+        List<Map<String, Object>> list = salesOrderGoodsMapper.listLineMarketCustomerByOrders(orgId, saleDate, productTypeList);
+        collectLineMarketCustomerFromOrderRows(list, lineNumSet, marketNumSet, customerNumSet,
+                lineAssNumCountMap, marketAssNumCountMap, customerAssNumCountMap);
+        if (lineNumSet.isEmpty() || marketNumSet.isEmpty() || customerNumSet.isEmpty()) {
             return success(new ArrayList<Map<String, Object>>());
         }
 
-        List<SalesOrderGoods> orderGoodsList = new ArrayList<SalesOrderGoods>();
-        for (List<String> batch : partitionList(new ArrayList<String>(orderNums), SQL_IN_BATCH_SIZE)) {
-            orderGoodsList.addAll(salesOrderGoodsService.list(new QueryWrapper<SalesOrderGoods>()
-                    .eq("org_id", orgId)
-                    .eq("del_flag", "0")
-                    .in("order_num", batch)));
-        }
-        Set<String> goodsNums = orderGoodsList.stream()
-                .map(SalesOrderGoods::getGoodsNum)
-                .filter(StringUtils::isNotBlank)
-                .map(String::trim)
-                .collect(Collectors.toCollection(LinkedHashSet::new));
-        Set<String> allowedGoodsNums = new LinkedHashSet<String>();
-        if (!goodsNums.isEmpty()) {
-            List<String> goodsNumList = new ArrayList<String>(goodsNums);
-            for (List<String> batch : partitionList(goodsNumList, SQL_IN_BATCH_SIZE)) {
-                List<BaseMaterial> materials = baseMaterialService.list(new QueryWrapper<BaseMaterial>()
-                        .eq("org_id", orgId)
-                        .eq("del_flag", "0")
-                        .eq("product_type", statProductType)
-                        .in("goods_num", batch));
-                for (BaseMaterial m : materials) {
-                    if (m != null && StringUtils.isNotBlank(m.getGoodsNum())) {
-                        allowedGoodsNums.add(m.getGoodsNum().trim());
-                    }
-                }
-            }
+        LineMarketCustomerTreeContext ctx = loadLineMarketCustomerTreeContext(orgId, lineNumSet, marketNumSet, customerNumSet);
+        return success(buildLineMarketCustomerElTree(ctx.lines, ctx.marketsByLineNum, ctx.relsByLineAndMarket, ctx.customerNameMap,
+                lineAssNumCountMap, marketAssNumCountMap, customerAssNumCountMap));
+    }
+
+
+    @ApiOperation("物流线-市场-客户三级树(分割品副产品分配)")
+    @PostMapping("/segmentedProductsCustomerTree")
+    public AjaxResult segmentedProductsCustomerTree(@RequestBody(required = false) Map<String, String> params,
+            HttpServletRequest request) throws Exception {
+        if (params == null) {
+            params = Collections.emptyMap();
         }
 
-        Map<String, BigDecimal> customerGoodsCountMap = new LinkedHashMap<String, BigDecimal>();
-        for (String customerNum : orderCustomers) {
-            customerGoodsCountMap.put(customerNum, BigDecimal.ZERO);
+        String productionDate = params.get("productionDate");
+        if (StringUtils.isBlank(productionDate)) {
+            throw new Exception("生产日期不能为空");
         }
-        for (SalesOrderGoods goods : orderGoodsList) {
-            if (goods == null || StringUtils.isBlank(goods.getOrderNum()) || StringUtils.isBlank(goods.getGoodsNum())) {
-                continue;
-            }
-            String goodsNum = goods.getGoodsNum().trim();
-            if (!allowedGoodsNums.contains(goodsNum)) {
-                continue;
-            }
-            String customerNum = customerByOrderNum.get(goods.getOrderNum().trim());
-            if (StringUtils.isBlank(customerNum)) {
-                continue;
-            }
-            BigDecimal qty = parseMoney(goods.getAssNum());
-            customerGoodsCountMap.put(customerNum, customerGoodsCountMap.getOrDefault(customerNum, BigDecimal.ZERO).add(qty == null ? BigDecimal.ZERO : qty));
+        List<String> productTypeList = parseCommaSeparatedList(trimOrNull(params.get("productTypes")));
+        if (!StringUtils.isNotEmpty(productTypeList)) {
+            throw new Exception("产品类型不能为空");
         }
 
-        List<RelCustomerMarket> allRels = new ArrayList<RelCustomerMarket>();
-        List<String> customerList = new ArrayList<String>(orderCustomers);
-        for (List<String> batch : partitionList(customerList, SQL_IN_BATCH_SIZE)) {
-            allRels.addAll(relCustomerMarketService.list(new QueryWrapper<RelCustomerMarket>()
-                    .eq("org_id", orgId)
-                    .and(w -> w.eq("del_flag", "0").or().isNull("del_flag"))
-                    .in("customer_num", batch)
-                    .orderByDesc("id")));
-        }
-        if (allRels.isEmpty()) {
+        String orgId = tokenService.getLoginOrgId(request);
+
+        Set<String> lineNumSet = new HashSet<String>();
+        Set<String> marketNumSet = new HashSet<String>();
+        Set<String> customerNumSet = new HashSet<String>();
+        List<Map<String, Object>> list = salesOrderGoodsMapper.listLineMarketCustomerByOrders(orgId, productionDate, productTypeList);
+        collectLineMarketCustomerFromOrderRows(list, lineNumSet, marketNumSet, customerNumSet, null, null, null);
+        if (lineNumSet.isEmpty() || marketNumSet.isEmpty() || customerNumSet.isEmpty()) {
             return success(new ArrayList<Map<String, Object>>());
         }
 
-        LinkedHashSet<String> relLineNums = new LinkedHashSet<String>();
-        HashSet<String> relPairs = new HashSet<String>();
-        for (RelCustomerMarket r : allRels) {
-            if (r == null || StringUtils.isBlank(r.getLineNum()) || StringUtils.isBlank(r.getMarketNum())) {
-                continue;
-            }
-            String ln = r.getLineNum().trim();
-            String mn = r.getMarketNum().trim();
-            relLineNums.add(ln);
-            relPairs.add(ln + "\0" + mn);
+        LineMarketCustomerTreeContext ctx = loadLineMarketCustomerTreeContext(orgId, lineNumSet, marketNumSet, customerNumSet);
+        return success(buildLineMarketCustomerElTree(ctx.lines, ctx.marketsByLineNum, ctx.relsByLineAndMarket, ctx.customerNameMap, null, null, null));
+    }
+
+    @ApiOperation("分割品副产品订单货品明细(销售日期 sale_date、物流线/市场/客户、产品类型;并附白条分配最大发货日期)")
+    @PostMapping("/segmentedProductGoodsDetailList")
+    public AjaxResult segmentedProductGoodsDetailList(@RequestBody(required = false) Map<String, String> params,
+                                                      HttpServletRequest request) throws Exception {
+        if (params == null) {
+            params = Collections.emptyMap();
         }
-        if (relLineNums.isEmpty()) {
-            return success(new ArrayList<Map<String, Object>>());
+        String productionDate = params.get("productionDate");
+        if (StringUtils.isBlank(productionDate)) {
+            throw new Exception("销售日期不能为空");
         }
-
-        List<BaseLine> lines = new ArrayList<BaseLine>();
-        Map<String, BaseLine> lineByNum = new LinkedHashMap<String, BaseLine>();
-        for (List<String> batch : partitionList(new ArrayList<String>(relLineNums), SQL_IN_BATCH_SIZE)) {
-            List<BaseLine> chunk = baseLineService.list(new QueryWrapper<BaseLine>()
-                    .eq("org_id", orgId)
-                    .in("line_num", batch)
-                    .and(w -> w.eq("del_flag", "0").or().isNull("del_flag"))
-                    .orderByAsc("line_num")
-                    .orderByAsc("id"));
-            for (BaseLine line : chunk) {
-                if (line != null && StringUtils.isNotBlank(line.getLineNum())) {
-                    String k = line.getLineNum().trim();
-                    lineByNum.putIfAbsent(k, line);
-                }
-            }
+        String orgId = tokenService.getLoginOrgId(request);
+        String lineNum = trimOrNull(params.get("lineNum"));
+        String marketNum = trimOrNull(params.get("marketNum"));
+        String customerNum = trimOrNull(params.get("customerNum"));
+        List<String> productTypeList = parseCommaSeparatedList(trimOrNull(params.get("productTypes")));
+        if (!StringUtils.isNotEmpty(productTypeList)) {
+            throw new Exception("产品类型不能为空");
         }
-        lines.addAll(lineByNum.values());
-        lines.sort(Comparator.comparing(l -> l.getLineNum() == null ? "" : l.getLineNum().trim()));
 
-        Map<String, BaseMarket> marketDedupe = new LinkedHashMap<String, BaseMarket>();
-        for (List<String> batch : partitionList(new ArrayList<String>(relLineNums), SQL_IN_BATCH_SIZE)) {
-            List<BaseMarket> chunk = baseMarketService.list(new QueryWrapper<BaseMarket>()
-                    .eq("org_id", orgId)
-                    .in("line_num", batch)
-                    .and(w -> w.eq("del_flag", "0").or().isNull("del_flag"))
-                    .orderByAsc("market_num")
-                    .orderByAsc("id"));
-            for (BaseMarket m : chunk) {
-                if (m == null || StringUtils.isBlank(m.getLineNum()) || StringUtils.isBlank(m.getMarketNum())) {
-                    continue;
-                }
-                String ln = m.getLineNum().trim();
-                String mn = m.getMarketNum().trim();
-                if (!relPairs.contains(ln + "\0" + mn)) {
-                    continue;
-                }
-                String mk = ln + "\0" + mn;
-                marketDedupe.putIfAbsent(mk, m);
-            }
+        List<Map<String, Object>> list = salesOrderGoodsMapper.listSegmentedProductOrderGoodsDetail(
+                orgId, productionDate.trim(), lineNum, marketNum, customerNum, productTypeList);
+        attachMaxDeliveryDatePerGoodsLine(orgId, list);
+        return success(list);
+    }
+
+    /** 市场按所属物流线分组(保留插入顺序)。 */
+    private static Map<String, List<BaseMarket>> groupMarketsByLineNum(List<BaseMarket> allMarkets) {
+        if (allMarkets == null || allMarkets.isEmpty()) {
+            return new LinkedHashMap<String, List<BaseMarket>>();
         }
-        Map<String, List<BaseMarket>> marketsByLineNum = marketDedupe.values().stream()
+        return allMarkets.stream()
                 .filter(m -> StringUtils.isNotBlank(m.getLineNum()))
                 .collect(Collectors.groupingBy(m -> m.getLineNum().trim(), LinkedHashMap::new, Collectors.toList()));
+    }
 
-        List<BaseCustomer> customers = new ArrayList<BaseCustomer>();
-        for (List<String> batch : partitionList(customerList, SQL_IN_BATCH_SIZE)) {
-            customers.addAll(customerService.list(new QueryWrapper<BaseCustomer>()
-                    .eq("org_id", orgId)
-                    .and(w -> w.eq("del_flag", "0").or().isNull("del_flag"))
-                    .in("customer_num", batch)));
-        }
-        Map<String, String> customerNameMap = customers.stream()
-                .filter(c -> StringUtils.isNotBlank(c.getCustomerNum()))
-                .collect(Collectors.toMap(BaseCustomer::getCustomerNum,
-                        c -> c.getCustomerName() != null ? c.getCustomerName() : "",
-                        (a, b) -> a, LinkedHashMap::new));
-
+    /** 客户关系按 line_num + market_num 分组。 */
+    private static Map<String, List<RelCustomerMarket>> groupRelsByLineAndMarket(List<RelCustomerMarket> allRels) {
         Map<String, List<RelCustomerMarket>> relsByLineAndMarket = new LinkedHashMap<String, List<RelCustomerMarket>>();
+        if (allRels == null || allRels.isEmpty()) {
+            return relsByLineAndMarket;
+        }
         for (RelCustomerMarket r : allRels) {
-            if (StringUtils.isBlank(r.getLineNum()) || StringUtils.isBlank(r.getMarketNum())) {
+            if (r == null || StringUtils.isBlank(r.getLineNum()) || StringUtils.isBlank(r.getMarketNum())) {
                 continue;
             }
             String key = r.getLineNum().trim() + "\0" + r.getMarketNum().trim();
             relsByLineAndMarket.computeIfAbsent(key, k -> new ArrayList<RelCustomerMarket>()).add(r);
         }
+        return relsByLineAndMarket;
+    }
+
+    private static Map<String, String> buildCustomerNumToNameMap(List<BaseCustomer> customers) {
+        if (customers == null || customers.isEmpty()) {
+            return new LinkedHashMap<String, String>();
+        }
+        return customers.stream()
+                .filter(c -> StringUtils.isNotBlank(c.getCustomerNum()))
+                .collect(Collectors.toMap(BaseCustomer::getCustomerNum,
+                        c -> c.getCustomerName() != null ? c.getCustomerName() : "",
+                        (a, b) -> a, LinkedHashMap::new));
+    }
 
+    /**
+     * 物流线 → 市场 → 客户 三级 el-tree 节点(与 rel、客户名称补充逻辑一致)。
+     */
+    private static List<Map<String, Object>> buildLineMarketCustomerElTree(
+            List<BaseLine> lines,
+            Map<String, List<BaseMarket>> marketsByLineNum,
+            Map<String, List<RelCustomerMarket>> relsByLineAndMarket,
+            Map<String, String> customerNameMap,
+            Map<String, BigDecimal> lineAssNumCountMap,
+            Map<String, BigDecimal> marketAssNumCountMap,
+            Map<String, BigDecimal> customerAssNumCountMap) {
         List<Map<String, Object>> tree = new ArrayList<Map<String, Object>>();
+        if (lines == null || lines.isEmpty()) {
+            return tree;
+        }
+        Map<String, String> nameMap = customerNameMap != null ? customerNameMap : new LinkedHashMap<String, String>();
+        if (lineAssNumCountMap == null) {
+            lineAssNumCountMap = new HashMap<>();
+        }
+        if (marketAssNumCountMap == null) {
+            marketAssNumCountMap = new HashMap<>();
+        }
+        if (customerAssNumCountMap == null) {
+            customerAssNumCountMap = new HashMap<>();
+        }
         for (BaseLine line : lines) {
+            if (line == null) {
+                continue;
+            }
             String lineNum = line.getLineNum() == null ? "" : line.getLineNum().trim();
             Map<String, Object> lineNode = new LinkedHashMap<String, Object>();
             lineNode.put("id", "line:" + lineNum + ":" + line.getId());
@@ -674,11 +867,16 @@ public class SalesOrderController {
             lineNode.put("type", "line");
             lineNode.put("lineNum", lineNum);
             lineNode.put("lineName", line.getLineName());
+            lineNode.put("goodsCount", lineAssNumCountMap.getOrDefault(lineNum, BigDecimal.ZERO));
 
-            BigDecimal lineGoodsCount = BigDecimal.ZERO;
             List<Map<String, Object>> marketChildren = new ArrayList<Map<String, Object>>();
-            List<BaseMarket> markets = marketsByLineNum.getOrDefault(lineNum, new ArrayList<BaseMarket>());
+            List<BaseMarket> markets = marketsByLineNum == null
+                    ? new ArrayList<BaseMarket>()
+                    : marketsByLineNum.getOrDefault(lineNum, new ArrayList<BaseMarket>());
             for (BaseMarket market : markets) {
+                if (market == null) {
+                    continue;
+                }
                 String marketNum = market.getMarketNum() == null ? "" : market.getMarketNum().trim();
                 Map<String, Object> marketNode = new LinkedHashMap<String, Object>();
                 marketNode.put("id", "market:" + lineNum + ":" + marketNum + ":" + market.getId());
@@ -687,21 +885,25 @@ public class SalesOrderController {
                 marketNode.put("lineNum", lineNum);
                 marketNode.put("marketNum", marketNum);
                 marketNode.put("marketName", market.getMarketName());
+                marketNode.put("goodsCount", marketAssNumCountMap.getOrDefault(lineNum + "\0" + marketNum, BigDecimal.ZERO));
 
-                BigDecimal marketGoodsCount = BigDecimal.ZERO;
                 String relKey = lineNum + "\0" + marketNum;
-                List<RelCustomerMarket> rels = relsByLineAndMarket.getOrDefault(relKey, new ArrayList<RelCustomerMarket>());
+                List<RelCustomerMarket> rels = relsByLineAndMarket == null
+                        ? new ArrayList<RelCustomerMarket>()
+                        : relsByLineAndMarket.getOrDefault(relKey, new ArrayList<RelCustomerMarket>());
                 Set<String> seenCustomer = new LinkedHashSet<String>();
                 List<Map<String, Object>> customerChildren = new ArrayList<Map<String, Object>>();
                 for (RelCustomerMarket rel : rels) {
+                    if (rel == null) {
+                        continue;
+                    }
                     String customerNum = rel.getCustomerNum() == null ? "" : rel.getCustomerNum().trim();
                     if (StringUtils.isBlank(customerNum) || !seenCustomer.add(customerNum)) {
                         continue;
                     }
-                    BigDecimal customerGoodsCount = customerGoodsCountMap.getOrDefault(customerNum, BigDecimal.ZERO);
                     String customerName = StringUtils.isNotBlank(rel.getCustomerName())
                             ? rel.getCustomerName()
-                            : customerNameMap.getOrDefault(customerNum, customerNum);
+                            : nameMap.getOrDefault(customerNum, customerNum);
                     Map<String, Object> custNode = new LinkedHashMap<String, Object>();
                     custNode.put("id", "customer:" + lineNum + ":" + marketNum + ":" + customerNum + ":" + rel.getId());
                     custNode.put("label", customerName);
@@ -711,27 +913,112 @@ public class SalesOrderController {
                     custNode.put("customerNum", customerNum);
                     custNode.put("customerName", customerName);
                     custNode.put("relId", rel.getId());
-                    custNode.put("goodsCount", customerGoodsCount);
                     custNode.put("leaf", true);
+                    custNode.put("goodsCount", customerAssNumCountMap.getOrDefault(lineNum + "\0" + marketNum + "\0" + customerNum, BigDecimal.ZERO));
                     customerChildren.add(custNode);
-                    marketGoodsCount = marketGoodsCount.add(customerGoodsCount);
-                }
-                if (customerChildren.isEmpty()) {
-                    continue;
                 }
-                marketNode.put("goodsCount", marketGoodsCount);
                 marketNode.put("children", customerChildren);
                 marketChildren.add(marketNode);
-                lineGoodsCount = lineGoodsCount.add(marketGoodsCount);
             }
-            if (marketChildren.isEmpty()) {
-                continue;
-            }
-            lineNode.put("goodsCount", lineGoodsCount);
             lineNode.put("children", marketChildren);
             tree.add(lineNode);
         }
-        return success(tree);
+        return tree;
+    }
+
+    /** 线-市场-客户树:主数据与 rel 分组结果(供 {@link #loadLineMarketCustomerTreeContext} 使用)。 */
+    private static final class LineMarketCustomerTreeContext {
+        final List<BaseLine> lines;
+        final Map<String, List<BaseMarket>> marketsByLineNum;
+        final Map<String, List<RelCustomerMarket>> relsByLineAndMarket;
+        final Map<String, String> customerNameMap;
+
+        LineMarketCustomerTreeContext(List<BaseLine> lines,
+                                      Map<String, List<BaseMarket>> marketsByLineNum,
+                                      Map<String, List<RelCustomerMarket>> relsByLineAndMarket,
+                                      Map<String, String> customerNameMap) {
+            this.lines = lines;
+            this.marketsByLineNum = marketsByLineNum;
+            this.relsByLineAndMarket = relsByLineAndMarket;
+            this.customerNameMap = customerNameMap;
+        }
+    }
+
+    /**
+     * 从 {@code listLineMarketCustomerByOrders} 结果收集线/市场/客户编号;
+     * 三张 ass 汇总 Map 均非 null 时按 {@code ass_num} 累加(否则仅收集编号)。
+     */
+    private void collectLineMarketCustomerFromOrderRows(List<Map<String, Object>> rows,
+                                                        Set<String> lineNumSet,
+                                                        Set<String> marketNumSet,
+                                                        Set<String> customerNumSet,
+                                                        Map<String, BigDecimal> lineAssNumCountMap,
+                                                        Map<String, BigDecimal> marketAssNumCountMap,
+                                                        Map<String, BigDecimal> customerAssNumCountMap) {
+        if (StringUtils.isEmpty(rows)) {
+            return;
+        }
+        boolean sumAss = lineAssNumCountMap != null && marketAssNumCountMap != null && customerAssNumCountMap != null;
+        for (Map<String, Object> map : rows) {
+            if (map == null) {
+                continue;
+            }
+            String lineNum = map.get("line_num") == null ? null : String.valueOf(map.get("line_num")).trim();
+            String marketNum = map.get("market_num") == null ? null : String.valueOf(map.get("market_num")).trim();
+            String customerNum = map.get("customer_num") == null ? null : String.valueOf(map.get("customer_num")).trim();
+            if (StringUtils.isAnyBlank(lineNum, marketNum, customerNum)) {
+                continue;
+            }
+            lineNumSet.add(lineNum);
+            marketNumSet.add(marketNum);
+            customerNumSet.add(customerNum);
+            if (sumAss) {
+                BigDecimal assNum = toBigDecimal(map.get("ass_num"));
+                lineAssNumCountMap.merge(lineNum, assNum, BigDecimal::add);
+                marketAssNumCountMap.merge(lineNum + "\0" + marketNum, assNum, BigDecimal::add);
+                customerAssNumCountMap.merge(lineNum + "\0" + marketNum + "\0" + customerNum, assNum, BigDecimal::add);
+            }
+        }
+    }
+
+    /**
+     * 按编号集合加载物流线、市场、客户关系与客户名称(树组装共用)。
+     */
+    private LineMarketCustomerTreeContext loadLineMarketCustomerTreeContext(String orgId,
+                                                                          Set<String> lineNumSet,
+                                                                          Set<String> marketNumSet,
+                                                                          Set<String> customerNumSet) {
+        QueryWrapper<BaseLine> lineQw = new QueryWrapper<BaseLine>()
+                .eq("org_id", orgId)
+                .in("line_num", new ArrayList<String>(lineNumSet))
+                .and(w -> w.eq("del_flag", "0").or().isNull("del_flag"))
+                .orderByAsc("line_name");
+        List<BaseLine> lines = baseLineService.list(lineQw);
+
+        QueryWrapper<BaseMarket> marketQw = new QueryWrapper<BaseMarket>()
+                .eq("org_id", orgId)
+                .in("market_num", new ArrayList<String>(marketNumSet))
+                .and(w -> w.eq("del_flag", "0").or().isNull("del_flag"))
+                .orderByAsc("market_name");
+        List<BaseMarket> allMarkets = baseMarketService.list(marketQw);
+        Map<String, List<BaseMarket>> marketsByLineNum = groupMarketsByLineNum(allMarkets);
+
+        QueryWrapper<RelCustomerMarket> relQw = new QueryWrapper<RelCustomerMarket>()
+                .eq("org_id", orgId)
+                .in("customer_num", new ArrayList<String>(customerNumSet))
+                .and(w -> w.eq("del_flag", "0").or().isNull("del_flag"))
+                .orderByDesc("id");
+        List<RelCustomerMarket> allRels = relCustomerMarketService.list(relQw);
+
+        List<BaseCustomer> allCustomers = customerService.list(new QueryWrapper<BaseCustomer>()
+                .eq("org_id", orgId)
+                .in("customer_num", new ArrayList<String>(customerNumSet))
+                .and(w -> w.eq("del_flag", "0").or().isNull("del_flag"))
+                .orderByAsc("customer_name"));
+        Map<String, String> customerNameMap = buildCustomerNumToNameMap(allCustomers);
+
+        Map<String, List<RelCustomerMarket>> relsByLineAndMarket = groupRelsByLineAndMarket(allRels);
+        return new LineMarketCustomerTreeContext(lines, marketsByLineNum, relsByLineAndMarket, customerNameMap);
     }
 
     private static <T> List<List<T>> partitionList(List<T> list, int batchSize) {
@@ -1068,6 +1355,18 @@ public class SalesOrderController {
         }
     }
 
+    private BigDecimal toBigDecimal(Object value) {
+        if (value == null) {
+            return BigDecimal.ZERO;
+        }
+        if (value instanceof BigDecimal) {
+            return (BigDecimal) value;
+        }
+        String s = String.valueOf(value);
+        BigDecimal n = parseMoney(s);
+        return n == null ? BigDecimal.ZERO : n;
+    }
+
     private String formatMoney(BigDecimal n) {
         if (n == null) {
             return null;

+ 244 - 1
ruoyi-admin/src/main/java/com/ruoyi/web/sales/mapper/SalesOrderGoodsMapper.java

@@ -10,6 +10,10 @@ import java.util.Map;
 
 public interface SalesOrderGoodsMapper extends BaseMapper<SalesOrderGoods> {
 
+    /**
+     * 白条分配订单货品明细(销售日期、物流线/市场/客户、产品类型)
+     * productTypeList 非空时按物料产品类型 {@code bm.product_type} 过滤;为空或 null 不按产品类型限制。
+     */
     @Select({
             "<script>",
             "select sog.*, so.line_num, so.market_num, so.customer_num, so.customer_name, bm.variety, bm.variety_name, bm.goods_level ",
@@ -26,6 +30,12 @@ public interface SalesOrderGoodsMapper extends BaseMapper<SalesOrderGoods> {
             "  <if test='lineNum != null and lineNum != \"\"'> and so.line_num = #{lineNum} </if>",
             "  <if test='marketNum != null and marketNum != \"\"'> and so.market_num = #{marketNum} </if>",
             "  <if test='customerNum != null and customerNum != \"\"'> and so.customer_num = #{customerNum} </if>",
+            "  <if test='productTypeList != null and productTypeList.size() &gt; 0'>",
+            "    and bm.product_type in ",
+            "    <foreach collection='productTypeList' item='pt' open='(' separator=',' close=')'>",
+            "      #{pt}",
+            "    </foreach>",
+            "  </if>",
             "order by so.id desc, sog.id asc",
             "</script>"
     })
@@ -33,5 +43,238 @@ public interface SalesOrderGoodsMapper extends BaseMapper<SalesOrderGoods> {
                                                           @Param("saleDate") String saleDate,
                                                           @Param("lineNum") String lineNum,
                                                           @Param("marketNum") String marketNum,
-                                                          @Param("customerNum") String customerNum);
+                                                          @Param("customerNum") String customerNum,
+                                                          @Param("productTypeList") List<String> productTypeList);
+
+    /**
+     * 按订单明细(sale_id 对应 sales_order_goods.id)统计白条已分配条数。
+     * 关联 {@code base_material} 与明细列表一致;productTypeList 非空时追加 {@code bm.product_type in (...)}。
+     */
+    @Select({
+            "<script>",
+            "select pwsa.sale_id as sale_id, MAX(pwsa.variety) as variety, MAX(pwsa.grade) as grade, count(*) as alloc_num ",
+            "from production_white_strip_allocation pwsa, sales_order_goods sog, sales_order so, base_material bm ",
+            "where pwsa.sale_id = sog.id ",
+            "  and pwsa.sale_num = so.order_num ",
+            "  and sog.order_num = so.order_num ",
+            "  and sog.goods_num = bm.goods_num ",
+            "  and bm.org_id = #{orgId} ",
+            "  and bm.del_flag = '0' ",
+            "  and pwsa.del_flag = '0' ",
+            "  and sog.del_flag = '0' ",
+            "  and so.del_flag = '0' ",
+            "  and sog.org_id = #{orgId} ",
+            "  and so.org_id = #{orgId} ",
+            "  and pwsa.org_id = #{orgId} ",
+            "  and so.sale_date = #{saleDate} ",
+            "  <if test='lineNum != null and lineNum != \"\"'> and so.line_num = #{lineNum} </if>",
+            "  <if test='marketNum != null and marketNum != \"\"'> and so.market_num = #{marketNum} </if>",
+            "  <if test='customerNum != null and customerNum != \"\"'> and so.customer_num = #{customerNum} </if>",
+            "  <if test='productTypeList != null and productTypeList.size() &gt; 0'>",
+            "    and bm.product_type in ",
+            "    <foreach collection='productTypeList' item='pt' open='(' separator=',' close=')'>",
+            "      #{pt}",
+            "    </foreach>",
+            "  </if>",
+            "group by pwsa.sale_id",
+            "</script>"
+    })
+    List<Map<String, Object>> listAllocNumByOrderGoodsLine(@Param("orgId") String orgId,
+                                                           @Param("saleDate") String saleDate,
+                                                           @Param("lineNum") String lineNum,
+                                                           @Param("marketNum") String marketNum,
+                                                           @Param("customerNum") String customerNum,
+                                                           @Param("productTypeList") List<String> productTypeList);
+
+    /**
+     * 按品种、级别汇总辅助计量数量。
+     * productTypeList 非空时按物料产品类型 {@code bm.product_type} 过滤。
+     * variety_name 使用 MAX,避免与 group by bm.variety,bm.goods_level 在 ONLY_FULL_GROUP_BY 下冲突。
+     */
+    @Select({
+            "<script>",
+            "select bm.variety, MAX(bm.variety_name) as variety_name, bm.goods_level as grade, sum(sog.ass_num) as ass_num ",
+            "from sales_order_goods sog, sales_order so, base_material bm ",
+            "where sog.order_num = so.order_num ",
+            "  and sog.goods_num = bm.goods_num ",
+            "  and sog.del_flag = '0' ",
+            "  and so.del_flag = '0' ",
+            "  and bm.del_flag = '0' ",
+            "  and so.org_id = #{orgId} ",
+            "  and sog.org_id = #{orgId} ",
+            "  and bm.org_id = #{orgId} ",
+            "  and so.sale_date = #{saleDate} ",
+            "  <if test='lineNum != null and lineNum != \"\"'> and so.line_num = #{lineNum} </if>",
+            "  <if test='marketNum != null and marketNum != \"\"'> and so.market_num = #{marketNum} </if>",
+            "  <if test='customerNum != null and customerNum != \"\"'> and so.customer_num = #{customerNum} </if>",
+            "  <if test='productTypeList != null and productTypeList.size() &gt; 0'>",
+            "    and bm.product_type in ",
+            "    <foreach collection='productTypeList' item='pt' open='(' separator=',' close=')'>",
+            "      #{pt}",
+            "    </foreach>",
+            "  </if>",
+            "group by bm.variety, bm.goods_level",
+            "</script>"
+    })
+    List<Map<String, Object>> summarizeAssNumByVarietyAndGrade(@Param("orgId") String orgId,
+                                                               @Param("saleDate") String saleDate,
+                                                               @Param("lineNum") String lineNum,
+                                                               @Param("marketNum") String marketNum,
+                                                               @Param("customerNum") String customerNum,
+                                                               @Param("productTypeList") List<String> productTypeList);
+
+    /**
+     * 统计可用白条数量(按品种、级别)。
+     * 与物料表关联:货品、品种、级别一致;productTypeList 非空时按 {@code bm.product_type} 过滤。
+     * variety_name 使用 MAX,避免与 group by bm.variety,bm.goods_level 在 ONLY_FULL_GROUP_BY 下冲突。
+     */
+    @Select({
+            "<script>",
+            "select bm.variety, MAX(bm.variety_name) as variety_name, pr.grade, count(*) as avail_ws_num ",
+            "from production_regular pr, base_material bm ",
+            "where pr.goods_num = bm.goods_num ",
+            "  and pr.variety = bm.variety ",
+            "  and pr.grade = bm.goods_level ",
+            "  and pr.production_date = #{productionDate} ",
+            "  and pr.is_fenpei = 0 ",
+            "  and pr.del_flag = '0' ",
+            "  and bm.del_flag = '0' ",
+            "  and pr.org_id = #{orgId} ",
+            "  and bm.org_id = #{orgId} ",
+            "  <if test='productTypeList != null and productTypeList.size() &gt; 0'>",
+            "    and bm.product_type in ",
+            "    <foreach collection='productTypeList' item='pt' open='(' separator=',' close=')'>",
+            "      #{pt}",
+            "    </foreach>",
+            "  </if>",
+            "group by bm.variety, bm.goods_level",
+            "</script>"
+    })
+    List<Map<String, Object>> summarizeAvailWhiteStripByVarietyGrade(@Param("orgId") String orgId,
+                                                                   @Param("productionDate") String productionDate,
+                                                                   @Param("productTypeList") List<String> productTypeList);
+
+    /**
+     * 统计已分配白条数量(按品种、级别)。
+     * 与物料表关联:material_code 与货品、品种、级别一致;productTypeList 非空时按 {@code bm.product_type} 过滤。
+     * variety_name 使用 MAX,避免与 group by pwsa.variety,pwsa.grade 在 ONLY_FULL_GROUP_BY 下冲突。
+     */
+    @Select({
+            "<script>",
+            "select pwsa.variety, MAX(bm.variety_name) as variety_name, pwsa.grade, count(*) as alloc_ws_num ",
+            "from production_white_strip_allocation pwsa, base_material bm ",
+            "where pwsa.material_code = bm.goods_num ",
+            "  and pwsa.variety = bm.variety ",
+            "  and pwsa.grade = bm.goods_level ",
+            "  and pwsa.production_date = #{productionDate} ",
+            "  and pwsa.del_flag = '0' ",
+            "  and bm.del_flag = '0' ",
+            "  and pwsa.org_id = #{orgId} ",
+            "  and bm.org_id = #{orgId} ",
+            "  <if test='productTypeList != null and productTypeList.size() &gt; 0'>",
+            "    and bm.product_type in ",
+            "    <foreach collection='productTypeList' item='pt' open='(' separator=',' close=')'>",
+            "      #{pt}",
+            "    </foreach>",
+            "  </if>",
+            "group by pwsa.variety, pwsa.grade",
+            "</script>"
+    })
+    List<Map<String, Object>> summarizeAllocWhiteStripByVarietyGrade(@Param("orgId") String orgId,
+                                                                     @Param("productionDate") String productionDate,
+                                                                     @Param("productTypeList") List<String> productTypeList);
+
+    /**
+     * 查询销售订单的客户信息(物流线、市场、客户,distinct)。仅订单与物料,不关联生产表。
+     * {@code productionDate} 用于筛选订单 {@code so.sale_date};productTypeList 非空时按 {@code bm.product_type} 过滤。
+     */
+    @Select({
+            "<script>",
+            "select so.line_num, so.market_num, so.customer_num, sum(sog.ass_num) as ass_num ",
+            "from sales_order_goods sog, sales_order so, base_material bm ",
+            "where sog.order_num = so.order_num ",
+            "  and sog.goods_num = bm.goods_num ",
+            "  and sog.del_flag = '0' ",
+            "  and so.del_flag = '0' ",
+            "  and bm.del_flag = '0' ",
+            "  and so.org_id = #{orgId} ",
+            "  and sog.org_id = #{orgId} ",
+            "  and bm.org_id = #{orgId} ",
+            "  and so.sale_date = #{productionDate} ",
+            "  <if test='productTypeList != null and productTypeList.size() &gt; 0'>",
+            "    and bm.product_type in ",
+            "    <foreach collection='productTypeList' item='pt' open='(' separator=',' close=')'>",
+            "      #{pt}",
+            "    </foreach>",
+            "  </if>",
+            "order by so.line_num, so.market_num, so.customer_num",
+            "</script>"
+    })
+    List<Map<String, Object>> listLineMarketCustomerByOrders(@Param("orgId") String orgId,
+                                                                           @Param("productionDate") String productionDate,
+                                                                           @Param("productTypeList") List<String> productTypeList);
+
+    /**
+     * 分割品副产品:订单货品明细。
+     * {@code productionDate} 用于筛选订单 {@code so.sale_date};productTypeList 非空时按 {@code bm.product_type} 过滤。
+     */
+    @Select({
+            "<script>",
+            "select sog.*, so.line_num, so.market_num, so.customer_num, so.customer_name, ",
+            "       so.document_date as order_document_date, so.sale_date as order_sale_date, ",
+            "       bm.variety, bm.variety_name, bm.goods_level, bm.product_type ",
+            "from sales_order_goods sog, sales_order so, base_material bm ",
+            "where sog.order_num = so.order_num ",
+            "  and sog.goods_num = bm.goods_num ",
+            "  and sog.del_flag = '0' ",
+            "  and so.del_flag = '0' ",
+            "  and bm.del_flag = '0' ",
+            "  and so.org_id = #{orgId} ",
+            "  and sog.org_id = #{orgId} ",
+            "  and bm.org_id = #{orgId} ",
+            "  and so.sale_date = #{productionDate} ",
+            "  <if test='lineNum != null and lineNum != \"\"'> and so.line_num = #{lineNum} </if>",
+            "  <if test='marketNum != null and marketNum != \"\"'> and so.market_num = #{marketNum} </if>",
+            "  <if test='customerNum != null and customerNum != \"\"'> and so.customer_num = #{customerNum} </if>",
+            "  <if test='productTypeList != null and productTypeList.size() &gt; 0'>",
+            "    and bm.product_type in ",
+            "    <foreach collection='productTypeList' item='pt' open='(' separator=',' close=')'>",
+            "      #{pt}",
+            "    </foreach>",
+            "  </if>",
+            "order by so.id desc, sog.id asc",
+            "</script>"
+    })
+    List<Map<String, Object>> listSegmentedProductOrderGoodsDetail(@Param("orgId") String orgId,
+                                                                   @Param("productionDate") String productionDate,
+                                                                   @Param("lineNum") String lineNum,
+                                                                   @Param("marketNum") String marketNum,
+                                                                   @Param("customerNum") String customerNum,
+                                                                   @Param("productTypeList") List<String> productTypeList);
+
+    /**
+     * 按订单明细主键({@code pwsa.sale_id} = {@code sales_order_goods.id})查询白条分配的最大发货日期。
+     * saleIds 为空时不命中任何行。
+     */
+    @Select({
+            "<script>",
+            "select pwsa.sale_id as sale_id, MAX(pwsa.delivery_date) as delivery_date ",
+            "from production_white_strip_allocation pwsa ",
+            "where pwsa.del_flag = '0' ",
+            "  and pwsa.org_id = #{orgId} ",
+            "  <if test='saleIds != null and saleIds.size() &gt; 0'>",
+            "    and pwsa.sale_id in ",
+            "    <foreach collection='saleIds' item='sid' open='(' separator=',' close=')'>",
+            "      #{sid}",
+            "    </foreach>",
+            "  </if>",
+            "  <if test='saleIds == null or saleIds.size() == 0'>",
+            "    and 1 = 0 ",
+            "  </if>",
+            "group by pwsa.sale_id",
+            "</script>"
+    })
+    List<Map<String, Object>> listMaxDeliveryDateBySaleIds(@Param("orgId") String orgId,
+                                                          @Param("saleIds") List<Long> saleIds);
+
 }