package com.ruoyi.web.sales.controller;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.web.base.domain.BaseEmployee;
import com.ruoyi.web.base.domain.BaseLine;
import com.ruoyi.web.base.domain.BaseMaterial;
import com.ruoyi.web.base.domain.BaseMarket;
import com.ruoyi.web.base.service.IBaseEmployeeService;
import com.ruoyi.web.base.service.IBaseLineService;
import com.ruoyi.web.base.service.IBaseMaterialService;
import com.ruoyi.web.base.service.IBaseMarketService;
import com.ruoyi.web.sales.domain.*;
import com.ruoyi.web.sales.mapper.SalesOrderGoodsMapper;
import com.ruoyi.web.sales.service.*;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
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;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import static com.ruoyi.common.core.domain.AjaxResult.success;
import static com.ruoyi.common.utils.SecurityUtils.getUsername;

@RestController
@Api(tags = "销售订单")
@RequestMapping("/sales-order")
public class SalesOrderController {

    /** MySQL 等数据库对 IN 列表过大时性能差，分批查询 */
    private static final int SQL_IN_BATCH_SIZE = 800;

    @Autowired
    private ISalesOrderService salesOrderService;
    @Autowired
    private ISalesOrderGoodsService salesOrderGoodsService;
    @Autowired
    private SalesOrderGoodsMapper salesOrderGoodsMapper;
    @Autowired
    private IBaseCustomerService customerService;
    @Autowired
    private IRelCustomerMarketService relCustomerMarketService;
    @Autowired
    private IBaseMaterialService baseMaterialService;
    @Autowired
    private IBaseEmployeeService baseEmployeeService;
    @Autowired
    private IBaseLineService baseLineService;
    @Autowired
    private IBaseMarketService baseMarketService;
    @Autowired
    private ISalesPriceService salesPriceService;
    @Autowired
    private ISalesPriceItemService salesPriceItemService;
    @Autowired
    private TokenService tokenService;

    @ApiOperation("销售订单新增")
    @PostMapping("/add")
    @Transactional
    public AjaxResult add(@RequestBody SalesOrder order, HttpServletRequest request) throws Exception {
        String orgId = tokenService.getLoginOrgId(request);
        validateBeforeSave(order, orgId, true);
        fillCustomerAndMarketInfo(order, orgId);
        recalcGoodsSubTotalsAndOrderTotal(order);
        order.setOrgId(orgId);
        order.setDelFlag("0");
        order.setAuditStatus(1);
        String username = getUsername();
        order.setId(null);
        order.setCreateBy(username);
        order.setCreateTime(new Date());
        order.setUpdateBy(null);
        order.setUpdateTime(null);
        salesOrderService.save(order);
        saveGoods(order, orgId, username);
        return success();
    }

    @ApiOperation("销售订单修改")
    @PostMapping("/edit")
    @Transactional
    public AjaxResult edit(@RequestBody SalesOrder order, HttpServletRequest request) throws Exception {
        if (order.getId() == null) {
            throw new Exception("id不能为空");
        }
        SalesOrder old = salesOrderService.getById(order.getId());
        if (old == null || !"0".equals(old.getDelFlag())) {
            throw new Exception("销售订单不存在");
        }
        if (old.getAuditStatus() != null && old.getAuditStatus() == 1) {
            throw new Exception("已审核的销售订单不能修改，请先反审核");
        }
        String orgId = tokenService.getLoginOrgId(request);
        validateBeforeSave(order, orgId, false);
        fillCustomerAndMarketInfo(order, orgId);
        recalcGoodsSubTotalsAndOrderTotal(order);
        order.setOrgId(orgId);
        order.setAuditStatus(1);
        String username = getUsername();
        order.setCreateBy(old.getCreateBy());
        order.setCreateTime(old.getCreateTime());
        order.setUpdateBy(username);
        order.setUpdateTime(new Date());
        salesOrderService.updateById(order);
        /** 修改保存：先按原单据编号物理删除旧物料清单，再插入本次提交的明细 */
        String oldOrderNum = StringUtils.isNotBlank(old.getOrderNum()) ? old.getOrderNum().trim() : null;
        deleteSalesOrderGoodsByOrderNum(orgId, oldOrderNum);
        saveGoods(order, orgId, username);
        return success();
    }

    @ApiOperation("销售订单删除")
    @PostMapping("/delete")
    public AjaxResult delete(@RequestBody Map<String, String> params) throws Exception {
        String ids = params.get("ids");
        for (String id : ids.split(",")) {
            SalesOrder order = salesOrderService.getById(id);
            if (order == null || !"0".equals(order.getDelFlag())) {
                continue;
            }
            if (order.getAuditStatus() != null && order.getAuditStatus() == 1) {
                throw new Exception("已审核的销售订单不能删除，请先反审核");
            }
            // 同步删除子表明细
            String orgId = order.getOrgId();
            String orderNum = order.getOrderNum();
            deleteSalesOrderGoodsByOrderNum(orgId, orderNum);

            SalesOrder up = new SalesOrder();
            up.setId(Integer.valueOf(id));
            up.setDelFlag("2");
            up.setUpdateBy(getUsername());
            up.setUpdateTime(new Date());
            salesOrderService.updateById(up);
        }
        return success();
    }

    @ApiOperation("销售订单详情")
    @PostMapping("/listById")
    public AjaxResult listById(@ApiParam("详情参数: id") @RequestBody Map<String, String> params, HttpServletRequest request) {
        String id = params.get("id");
        SalesOrder order = salesOrderService.getById(id);
        if (order == null) {
            return success(null);
        }
        String orgId = tokenService.getLoginOrgId(request);
        List<SalesOrderGoods> goods = salesOrderGoodsService.list(new QueryWrapper<SalesOrderGoods>()
                .eq("org_id", orgId)
                .eq("order_num", order.getOrderNum())
                .eq("del_flag", "0"));
        enrichGoodsFromMaterial(goods, orgId);
        order.setGoods(goods);
        enrichSalesOrderDisplay(order, orgId);
        return success(order);
    }

    @ApiOperation("销售订单分页")
    @GetMapping("/page")
    public AjaxResult page(@RequestParam("pageNum") Integer pageNum,
                           @RequestParam("pageSize") Integer pageSize,
                           SalesOrder query,
                           HttpServletRequest request) {
        String orgId = tokenService.getLoginOrgId(request);
        QueryWrapper<SalesOrder> wrapper = buildWrapper(query, orgId);
        Page<SalesOrder> page = salesOrderService.page(new Page<SalesOrder>(pageNum, pageSize), wrapper);
        for (SalesOrder row : page.getRecords()) {
            enrichSalesOrderDisplay(row, orgId);
        }
        return success(page);
    }

    @ApiOperation("销售订单列表")
    @PostMapping("/list")
    public AjaxResult list(@RequestBody(required = false) SalesOrder query, HttpServletRequest request) {
        String orgId = tokenService.getLoginOrgId(request);
        if (query == null) {
            query = new SalesOrder();
        }
        if (StringUtils.isBlank(query.getCustomerNum())) {
            query.setCustomerNum(firstNonBlank(
                    request.getParameter("customerNum"),
                    request.getParameter("customer_no"),
                    request.getParameter("customerNo")
            ));
        }
        if (StringUtils.isBlank(query.getEmployeeNum())) {
            query.setEmployeeNum(firstNonBlank(
                    request.getParameter("employeeNum"),
                    request.getParameter("employee_no"),
                    request.getParameter("employeeNo")
            ));
        }
        List<SalesOrder> list = salesOrderService.list(buildWrapper(query, orgId));
        for (SalesOrder row : list) {
            enrichSalesOrderDisplay(row, orgId);
        }
        return success(list);
    }

    @ApiOperation("销售订单导出")
    @PostMapping("/export")
    public void export(HttpServletResponse response, SalesOrder query, HttpServletRequest request) {
        String orgId = tokenService.getLoginOrgId(request);
        List<SalesOrder> list = salesOrderService.list(buildWrapper(query, orgId));
        for (SalesOrder row : list) {
            enrichSalesOrderDisplay(row, orgId);
        }
        ExcelUtil<SalesOrder> util = new ExcelUtil<SalesOrder>(SalesOrder.class);
        util.exportExcel(response, list, "销售订单");
    }

    @ApiOperation("白条分配订单货品明细（销售日期、物流线/市场/客户、产品类型）")
    @PostMapping("/goodsDetailList")
    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("销售日期不能为空");
        }
        String orgId = tokenService.getLoginOrgId(request);
        String lineNum = params.get("lineNum");
        String marketNum = params.get("marketNum");
        String customerNum = params.get("customerNum");
        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(),
                productTypeList
        );
        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("销售订单审核")
    @PostMapping("/audit")
    public AjaxResult audit(@RequestBody Map<String, String> params) throws Exception {
        String id = params.get("id");
        SalesOrder order = getActiveById(id);
        order.setAuditStatus(1);
        order.setUpdateBy(getUsername());
        order.setUpdateTime(new Date());
        return success(salesOrderService.updateById(order));
    }

    @ApiOperation("销售订单反审核")
    @PostMapping("/unaudit")
    public AjaxResult unaudit(@RequestBody Map<String, String> params) throws Exception {
        String id = params.get("id");
        SalesOrder order = getActiveById(id);
        order.setAuditStatus(0);
        order.setUpdateBy(getUsername());
        order.setUpdateTime(new Date());
        return success(salesOrderService.updateById(order));
    }

    @ApiOperation("销售订单打印数据")
    @PostMapping("/print")
    public AjaxResult print(@RequestBody Map<String, String> params, HttpServletRequest request) throws Exception {
        String id = params.get("id");
        SalesOrder order = getActiveById(id);
        String orgId = tokenService.getLoginOrgId(request);
        List<SalesOrderGoods> goods = salesOrderGoodsService.list(new QueryWrapper<SalesOrderGoods>()
                .eq("org_id", orgId)
                .eq("order_num", order.getOrderNum())
                .eq("del_flag", "0"));
        enrichGoodsFromMaterial(goods, orgId);
        order.setGoods(goods);
        enrichSalesOrderDisplay(order, orgId);
        return success(order);
    }

    @ApiOperation("获取新增销售订单页面数据")
    @PostMapping("/getAddPageData")
    public AjaxResult getAddPageData(HttpServletRequest request) throws Exception {
        String orgId = tokenService.getLoginOrgId(request);
        String tomorrow = LocalDate.now().plusDays(1).format(DateTimeFormatter.ISO_LOCAL_DATE);
        Map<String, Object> data = new HashMap<String, Object>();
        data.put("documentDate", tomorrow);
        data.put("saleDate", tomorrow);
        data.put("orderNum", generateSalesOrderNum(orgId));
        return success(data);
    }

    @ApiOperation("按货品编号、客户编号、单据日期查询物料及价格")
    @PostMapping("/getGoodsPriceInfo")
    public AjaxResult getGoodsPriceInfo(@RequestBody Map<String, String> params, HttpServletRequest request) throws Exception {
        String goodsNum = params.get("goodsNum");
        String customerNum = params.get("customerNum");
        String documentDate = params.get("documentDate");
        if (StringUtils.isAnyBlank(goodsNum, customerNum, documentDate)) {
            throw new Exception("货品编号、客户编号、单据日期不能为空");
        }
        String orgId = tokenService.getLoginOrgId(request);
        String safeDate = documentDate.trim().replace("'", "''");

        BaseMaterial material = baseMaterialService.getOne(new QueryWrapper<BaseMaterial>()
                .eq("org_id", orgId)
                .eq("goods_num", goodsNum.trim())
                .eq("del_flag", "0")
                .last("limit 1"));
        if (material == null) {
            throw new Exception("未找到该货品编号对应物料");
        }

        RelCustomerMarket rel = relCustomerMarketService.getOne(new QueryWrapper<RelCustomerMarket>()
                .eq("org_id", orgId)
                .eq("customer_num", customerNum.trim())
                .eq("del_flag", "0")
                .orderByDesc("id")
                .last("limit 1"));
        if (rel == null || StringUtils.isBlank(rel.getMarketNum())) {
            throw new Exception("该客户未维护市场信息");
        }

        BaseMarket market = baseMarketService.getOne(new QueryWrapper<BaseMarket>()
                .eq("org_id", orgId)
                .eq("market_num", rel.getMarketNum())
                .eq("del_flag", "0")
                .last("limit 1"));
        if (market == null || StringUtils.isBlank(market.getPriceType())) {
            throw new Exception("该市场未维护价格类型");
        }

        String priceType = market.getPriceType().trim();
        SalesPrice price = salesPriceService.getOne(new QueryWrapper<SalesPrice>()
                .eq("org_id", orgId)
                .eq("del_flag", "0")
                .eq("price_type", priceType)
                .eq("effective_date", safeDate)
                .orderByDesc("id")
                .last("limit 1"), false);
        SalesPriceItem priceItem = null;
        if (price != null && StringUtils.isNotBlank(price.getPriceNum())) {
            priceItem = salesPriceItemService.getOne(new QueryWrapper<SalesPriceItem>()
                    .eq("org_id", orgId)
                    .eq("price_num", price.getPriceNum())
                    .eq("material_num", goodsNum.trim())
                    .eq("del_flag", "0")
                    .orderByDesc("id")
                    .last("limit 1"), false);
        }

        Map<String, Object> data = new HashMap<String, Object>();
        data.put("goodsNum", material.getGoodsNum());
        data.put("goodsName", material.getGoodsName());
        data.put("goodsSpec", material.getGoodsSpec());
        data.put("baseUnit", material.getUnit());
        data.put("assUnit", material.getAssistantUnit());
        data.put("customerNum", customerNum.trim());
        data.put("marketNum", rel.getMarketNum());
        data.put("priceType", priceType);
        data.put("documentDate", safeDate);
        data.put("priceNum", priceItem == null ? null : priceItem.getPriceNum());
        data.put("unitPrice", priceItem == null || priceItem.getUnitPrice() == null ? BigDecimal.ZERO : priceItem.getUnitPrice());
        data.put("pickupPrice", priceItem == null ? null : priceItem.getPickupPrice());
        return success(data);
    }

    @ApiOperation("物流线-市场-客户三级树（el-tree）")
    @GetMapping("/lineMarketCustomer")
    public AjaxResult lineMarketCustomerTree(HttpServletRequest request) {
        String orgId = tokenService.getLoginOrgId(request);
        QueryWrapper<BaseLine> lineQw = new QueryWrapper<BaseLine>()
                .eq("org_id", orgId)
                .and(w -> w.eq("del_flag", "0").or().isNull("del_flag"))
                .orderByAsc("line_num")
                .orderByAsc("id");
        List<BaseLine> lines = baseLineService.list(lineQw);

        QueryWrapper<BaseMarket> marketQw = new QueryWrapper<BaseMarket>()
                .eq("org_id", orgId)
                .and(w -> w.eq("del_flag", "0").or().isNull("del_flag"))
                .orderByAsc("market_num")
                .orderByAsc("id");
        List<BaseMarket> allMarkets = baseMarketService.list(marketQw);
        Map<String, List<BaseMarket>> marketsByLineNum = groupMarketsByLineNum(allMarkets);

        QueryWrapper<RelCustomerMarket> relQw = new QueryWrapper<RelCustomerMarket>()
                .eq("org_id", orgId)
                .and(w -> w.eq("del_flag", "0").or().isNull("del_flag"))
                .orderByDesc("id");
        List<RelCustomerMarket> allRels = relCustomerMarketService.list(relQw);

        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 = buildCustomerNumToNameMap(customers);

        Map<String, List<RelCustomerMarket>> relsByLineAndMarket = groupRelsByLineAndMarket(allRels);
        return success(buildLineMarketCustomerElTree(lines, marketsByLineNum, relsByLineAndMarket, customerNameMap, null, null, null));
    }

    @ApiOperation("物流线-市场-客户三级树（白条分配），按销售日期和产品类型统计goodsCount")
    @GetMapping("/lineMarketCustomerStats")
    public AjaxResult lineMarketCustomerStats(
            @ApiParam(value = "销售日期(yyyy-MM-dd)", required = true)
            @RequestParam("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("销售日期不能为空");
        }
        List<String> productTypeList = new ArrayList<>();
        productTypeList.add(StringUtils.isBlank(productType) ? "002" : productType.trim());
        String orgId = tokenService.getLoginOrgId(request);

        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>>());
        }

        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();
        }

        String productionDate = params.get("productionDate");
        if (StringUtils.isBlank(productionDate)) {
            throw new Exception("生产日期不能为空");
        }
        List<String> productTypeList = parseCommaSeparatedList(trimOrNull(params.get("productTypes")));
        if (!StringUtils.isNotEmpty(productTypeList)) {
            throw new Exception("产品类型不能为空");
        }

        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>>());
        }

        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();
        }
        String productionDate = params.get("productionDate");
        if (StringUtils.isBlank(productionDate)) {
            throw new Exception("销售日期不能为空");
        }
        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("产品类型不能为空");
        }

        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>>();
        }
        return allMarkets.stream()
                .filter(m -> StringUtils.isNotBlank(m.getLineNum()))
                .collect(Collectors.groupingBy(m -> m.getLineNum().trim(), LinkedHashMap::new, Collectors.toList()));
    }

    /** 客户关系按 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 (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());
            lineNode.put("label", StringUtils.isNotBlank(line.getLineName()) ? line.getLineName() : lineNum);
            lineNode.put("type", "line");
            lineNode.put("lineNum", lineNum);
            lineNode.put("lineName", line.getLineName());
            lineNode.put("goodsCount", lineAssNumCountMap.getOrDefault(lineNum, BigDecimal.ZERO));

            List<Map<String, Object>> marketChildren = new ArrayList<Map<String, Object>>();
            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());
                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());
                marketNode.put("goodsCount", marketAssNumCountMap.getOrDefault(lineNum + "\0" + marketNum, BigDecimal.ZERO));

                String relKey = lineNum + "\0" + marketNum;
                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;
                    }
                    String customerName = StringUtils.isNotBlank(rel.getCustomerName())
                            ? rel.getCustomerName()
                            : 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);
                    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);
                    custNode.put("goodsCount", customerAssNumCountMap.getOrDefault(lineNum + "\0" + marketNum + "\0" + customerNum, BigDecimal.ZERO));
                    customerChildren.add(custNode);
                }
                marketNode.put("children", customerChildren);
                marketChildren.add(marketNode);
            }
            lineNode.put("children", marketChildren);
            tree.add(lineNode);
        }
        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) {
        if (list == null || list.isEmpty()) {
            return Collections.emptyList();
        }
        int size = Math.max(1, batchSize);
        List<List<T>> parts = new ArrayList<List<T>>();
        for (int i = 0; i < list.size(); i += size) {
            parts.add(list.subList(i, Math.min(i + size, list.size())));
        }
        return parts;
    }

    private String firstNonBlank(String... vals) {
        if (vals == null || vals.length == 0) {
            return null;
        }
        for (String v : vals) {
            if (StringUtils.isNotBlank(v)) {
                return v.trim();
            }
        }
        return null;
    }

    private QueryWrapper<SalesOrder> buildWrapper(SalesOrder query, String orgId) {
        QueryWrapper<SalesOrder> wrapper = new QueryWrapper<SalesOrder>()
                .eq("org_id", orgId)
                .eq("del_flag", "0");
        if (query == null) {
            return wrapper;
        }
        if (query.getDocumentDateStart() != null) {
            wrapper.ge("document_date", query.getDocumentDateStart());
        }
        if (query.getDocumentDateEnd() != null) {
            wrapper.le("document_date", query.getDocumentDateEnd());
        }
        if (query.getSaleDateStart() != null) {
            wrapper.ge("sale_date", query.getSaleDateStart());
        }
        if (query.getSaleDateEnd() != null) {
            wrapper.le("sale_date", query.getSaleDateEnd());
        }
        if (StringUtils.isNotBlank(query.getCustomerName())) {
            wrapper.like("customer_name", query.getCustomerName());
        }
        if (StringUtils.isNotBlank(query.getCustomerNum())) {
            wrapper.eq("customer_num", query.getCustomerNum().trim());
        }
        if (StringUtils.isNotBlank(query.getEmployeeNum())) {
            wrapper.eq("employee_num", query.getEmployeeNum().trim());
        }
        if (StringUtils.isNotBlank(query.getLineNum())) {
            wrapper.like("line_num", query.getLineNum());
        }
        if (StringUtils.isNotBlank(query.getMarketNum())) {
            wrapper.like("market_num", query.getMarketNum());
        }
        if (query.getAuditStatus() != null) {
            wrapper.eq("audit_status", query.getAuditStatus());
        }
        wrapper.orderByDesc("id");
        return wrapper;
    }

    private void validateBeforeSave(SalesOrder order, String orgId, boolean isAdd) throws Exception {
        if (order == null) {
            throw new Exception("参数不能为空");
        }
        if (order.getCustomerNum() != null) {
            order.setCustomerNum(order.getCustomerNum().trim());
        }
        if (order.getOrderNum() != null) {
            order.setOrderNum(order.getOrderNum().trim());
        }
        if (StringUtils.isAnyBlank(order.getOrderNum(), order.getCustomerNum())) {
            throw new Exception("单据编号、客户编号不能为空");
        }
        QueryWrapper<SalesOrder> numWrapper = new QueryWrapper<SalesOrder>()
                .eq("order_num", order.getOrderNum())
                .eq("del_flag", "0");
        if (!isAdd && order.getId() != null) {
            numWrapper.ne("id", order.getId());
        }
        if (salesOrderService.count(numWrapper) > 0) {
            throw new Exception("该单据编号已存在");
        }
        if (order.getGoods() == null || order.getGoods().isEmpty()) {
            throw new Exception("货品清单不能为空");
        }
        BaseCustomer customer = customerService.getOne(new QueryWrapper<BaseCustomer>()
                .eq("org_id", orgId)
                .eq("customer_num", order.getCustomerNum())
                .eq("del_flag", "0")
                .last("limit 1"));
        if (customer == null) {
            throw new Exception("客户编号不存在");
        }
        if (StringUtils.isNotBlank(order.getEmployeeNum())) {
            BaseEmployee emp = baseEmployeeService.getOne(new QueryWrapper<BaseEmployee>()
                    .eq("org_id", orgId)
                    .eq("employee_num", order.getEmployeeNum().trim())
                    .eq("del_flag", "0")
                    .last("limit 1"));
            if (emp == null) {
                throw new Exception("业务员编号不存在");
            }
        }
    }

    private void fillCustomerAndMarketInfo(SalesOrder order, String orgId) throws Exception {
        BaseCustomer customer = customerService.getOne(new QueryWrapper<BaseCustomer>()
                .eq("org_id", orgId)
                .eq("customer_num", order.getCustomerNum())
                .eq("del_flag", "0")
                .last("limit 1"));
        if (customer == null) {
            throw new Exception("客户编号不存在");
        }
        order.setCustomerName(customer.getCustomerName());

        /** 仅按客户编号解析市场、物流线（取客户市场维护表中最新一条），不依赖前端传入 */
        RelCustomerMarket rel = relCustomerMarketService.getOne(new QueryWrapper<RelCustomerMarket>()
                .eq("org_id", orgId)
                .eq("customer_num", order.getCustomerNum())
                .eq("del_flag", "0")
                .orderByDesc("id")
                .last("limit 1"));
        if (rel != null) {
            order.setMarketNum(rel.getMarketNum());
            order.setLineNum(rel.getLineNum());
        } else {
            order.setMarketNum(null);
            order.setLineNum(null);
        }
    }

    private void enrichSalesOrderDisplay(SalesOrder order, String orgId) {
        if (order == null) {
            return;
        }
        order.setReceivableCustomer(null);
        order.setEmployeeName(null);
        if (StringUtils.isNotBlank(order.getCustomerNum())) {
            BaseCustomer c = customerService.getOne(new QueryWrapper<BaseCustomer>()
                    .eq("org_id", orgId)
                    .eq("customer_num", order.getCustomerNum())
                    .eq("del_flag", "0")
                    .last("limit 1"));
            if (c != null) {
                order.setReceivableCustomer(c.getReceivableCustomer());
                if (StringUtils.isBlank(order.getCustomerName())) {
                    order.setCustomerName(c.getCustomerName());
                }
            }
        }
        if (StringUtils.isNotBlank(order.getEmployeeNum())) {
            BaseEmployee e = baseEmployeeService.getOne(new QueryWrapper<BaseEmployee>()
                    .eq("org_id", orgId)
                    .eq("employee_num", order.getEmployeeNum().trim())
                    .eq("del_flag", "0")
                    .last("limit 1"));
            if (e != null) {
                order.setEmployeeName(e.getEmployeeName());
            }
        }
    }

    private void enrichGoodsFromMaterial(List<SalesOrderGoods> goods, String orgId) {
        if (goods == null || goods.isEmpty()) {
            return;
        }
        for (SalesOrderGoods g : goods) {
            if (StringUtils.isBlank(g.getGoodsNum())) {
                continue;
            }
            BaseMaterial m = baseMaterialService.getOne(new QueryWrapper<BaseMaterial>()
                    .eq("org_id", orgId)
                    .eq("goods_num", g.getGoodsNum())
                    .eq("del_flag", "0")
                    .last("limit 1"));
            if (m == null) {
                continue;
            }
            if (StringUtils.isBlank(g.getGoodsName())) {
                g.setGoodsName(m.getGoodsName());
            }
            if (StringUtils.isBlank(g.getGoodsSpec())) {
                g.setGoodsSpec(m.getGoodsSpec());
            }
            if (StringUtils.isBlank(g.getBaseUnit())) {
                g.setBaseUnit(m.getUnit());
            }
            if (StringUtils.isBlank(g.getAssUnit())) {
                g.setAssUnit(m.getAssistantUnit());
            }
            if (StringUtils.isBlank(g.getConversionValue())) {
                g.setConversionValue(m.getConversionValue());
            }
        }
    }

    /**
     * 单据编号规则：
     * XSDD + 当前日期后一天的yyyyMMdd + 3位机器标识 + 3位递增序号（不足补零，从001开始）
     */
    private String generateSalesOrderNum(String orgId) throws Exception {
        if (StringUtils.isBlank(orgId)) {
            throw new Exception("orgId不能为空");
        }
        String tomorrowYmd = LocalDate.now().plusDays(1).format(DateTimeFormatter.BASIC_ISO_DATE);
        String machineCode = getMachineCode3Digits();
        String base = "XSDD" + tomorrowYmd + machineCode;

        // 取该 base 前缀下当前最大序号（最后3位），若不存在则从 001 开始
        SalesOrder last = salesOrderService.getOne(new QueryWrapper<SalesOrder>()
                .eq("org_id", orgId)
                .eq("del_flag", "0")
                // 由于订单编号固定前缀格式，这里用 like 匹配前缀即可
                .like("order_num", base)
                .orderByDesc("order_num")
                .last("limit 1"));

        int startSeq = 1;
        if (last != null && StringUtils.isNotBlank(last.getOrderNum()) && last.getOrderNum().length() >= 3) {
            String suffix = last.getOrderNum().substring(last.getOrderNum().length() - 3);
            try {
                startSeq = Integer.parseInt(suffix) + 1;
            } catch (Exception ignored) {
                startSeq = 1;
            }
        }

        for (int seq = startSeq; seq <= 999; seq++) {
            String candidate = base + String.format("%03d", seq);
            long occupied = salesOrderService.count(new QueryWrapper<SalesOrder>()
                    .eq("org_id", orgId)
                    .eq("order_num", candidate)
                    .eq("del_flag", "0"));
            if (occupied == 0) {
                return candidate;
            }
        }
        throw new Exception("无法生成唯一销售订单编号");
    }

    /**
     * 生成 3 位机器标识：优先取网卡 MAC（取前向量 hash），取不到则使用主机名 hash。
     */
    private String getMachineCode3Digits() {
        try {
            byte[] mac = null;
            for (NetworkInterface ni : Collections.list(NetworkInterface.getNetworkInterfaces())) {
                if (ni == null || !ni.isUp() || ni.isLoopback() || ni.isVirtual()) {
                    continue;
                }
                byte[] addr = ni.getHardwareAddress();
                if (addr != null && addr.length > 0) {
                    mac = addr;
                    break;
                }
            }

            int val = 0;
            if (mac != null) {
                for (byte b : mac) {
                    val = (val * 31 + (b & 0xff)) % 1000;
                }
            } else {
                String host = InetAddress.getLocalHost().getHostName();
                val = Math.abs(host.hashCode()) % 1000;
            }
            return String.format("%03d", val);
        } catch (Exception e) {
            // 极端情况下兜底，确保接口不因机器标识失败而报错
            return "001";
        }
    }

    /** 小计=单价×基本计量数量；总计金额=各明细小计之和 */
    private void recalcGoodsSubTotalsAndOrderTotal(SalesOrder order) {
        List<SalesOrderGoods> goods = order.getGoods();
        if (goods == null || goods.isEmpty()) {
            order.setTotalAmount(null);
            return;
        }
        BigDecimal sum = BigDecimal.ZERO;
        boolean any = false;
        for (SalesOrderGoods g : goods) {
            BigDecimal line = null;

            // 优先按 unitPrice * baseNum 计算
            BigDecimal p = parseMoney(g.getUnitPrice());
            BigDecimal q = parseMoney(g.getBaseNum());
            if (p != null && q != null) {
                line = p.multiply(q);
            }

            if (line != null) {
                g.setSubTotal(formatMoney(line));
                sum = sum.add(line);
                any = true;
                continue;
            }

            // unitPrice 缺失时：不要清空，直接沿用已存在的小计 sub_total
            BigDecimal existing = parseMoney(g.getSubTotal());
            if (existing != null) {
                g.setSubTotal(formatMoney(existing));
                sum = sum.add(existing);
                any = true;
            } else {
                g.setSubTotal(null);
            }
        }

        if (any) {
            order.setTotalAmount(formatMoney(sum));
        } else {
            order.setTotalAmount(null);
        }
    }

    private BigDecimal parseMoney(String s) {
        if (StringUtils.isBlank(s)) {
            return null;
        }
        try {
            return new BigDecimal(s.trim().replace(",", ""));
        } catch (Exception e) {
            return null;
        }
    }

    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;
        }
        return n.stripTrailingZeros().toPlainString();
    }

    /** 按单据编号将原明细逻辑删除（修改订单时先标记旧清单 del_flag=2） */
    private void deleteSalesOrderGoodsByOrderNum(String orgId, String orderNum) {
        if (StringUtils.isBlank(orgId) || StringUtils.isBlank(orderNum)) {
            return;
        }
        SalesOrderGoods up = new SalesOrderGoods();
        up.setDelFlag("2");
        up.setUpdateBy(getUsername());
        up.setUpdateTime(new Date());
        salesOrderGoodsService.update(up, new QueryWrapper<SalesOrderGoods>()
                .eq("org_id", orgId)
                .eq("order_num", orderNum)
                .eq("del_flag", "0"));
    }

    private void saveGoods(SalesOrder order, String orgId, String username) {
        List<SalesOrderGoods> goods = order.getGoods();
        if (goods == null) {
            goods = Collections.emptyList();
        }
        Date now = new Date();
        for (SalesOrderGoods g : goods) {
            g.setId(null);
            g.setOrgId(orgId);
            g.setOrderNum(order.getOrderNum());
            g.setDelFlag("0");
            g.setCreateBy(username);
            g.setCreateTime(now);
            g.setUpdateBy(null);
            g.setUpdateTime(null);
            salesOrderGoodsService.save(g);
        }
    }

    private SalesOrder getActiveById(String id) throws Exception {
        SalesOrder order = salesOrderService.getById(id);
        if (order == null || !"0".equals(order.getDelFlag())) {
            throw new Exception("销售订单不存在");
        }
        return order;
    }
}
