昨天 684e4306a08feeae188070f264148e6765da8dcf
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java
@@ -8,15 +8,30 @@
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.account.mapper.sales.AccountInvoiceApplicationMapper;
import com.ruoyi.account.mapper.sales.AccountSalesCollectionMapper;
import com.ruoyi.account.pojo.sales.AccountInvoiceApplication;
import com.ruoyi.account.pojo.sales.AccountSalesCollection;
import com.ruoyi.approve.bean.dto.ApprovalInstanceDto;
import com.ruoyi.approve.mapper.ApprovalTemplateMapper;
import com.ruoyi.approve.pojo.ApprovalInstance;
import com.ruoyi.approve.pojo.ApprovalTemplate;
import com.ruoyi.approve.service.ApprovalInstanceService;
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.mapper.CustomerMapper;
import com.ruoyi.basic.mapper.ProductMapper;
import com.ruoyi.basic.mapper.ProductModelMapper;
import com.ruoyi.basic.mapper.OilDepotMapper;
import com.ruoyi.basic.mapper.TankInfoMapper;
import com.ruoyi.basic.pojo.Customer;
import com.ruoyi.basic.pojo.OilDepot;
import com.ruoyi.basic.pojo.TankInfo;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.common.enums.FileNameType;
import com.ruoyi.common.enums.SaleEnum;
import com.ruoyi.common.enums.SalesQuotationStatusEnum;
import com.ruoyi.common.enums.TypeEnums;
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.EnumUtil;
@@ -26,18 +41,24 @@
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.other.mapper.TempFileMapper;
import com.ruoyi.production.mapper.*;
import com.ruoyi.production.mapper.ProductionProductInputMapper;
import com.ruoyi.production.mapper.ProductionProductMainMapper;
import com.ruoyi.production.mapper.ProductionProductOutputMapper;
import com.ruoyi.production.service.ProductionProductMainService;
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysDeptMapper;
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.purchase.dto.SimpleReturnOrderGroupDto;
import com.ruoyi.purchase.mapper.PurchaseReturnOrderProductsMapper;
import com.ruoyi.quality.mapper.QualityInspectMapper;
import com.ruoyi.sales.dto.*;
import com.ruoyi.sales.mapper.*;
import com.ruoyi.sales.pojo.*;
import com.ruoyi.sales.mapper.CommonFileMapper;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.mapper.ShippingInfoMapper;
import com.ruoyi.sales.pojo.CommonFile;
import com.ruoyi.sales.pojo.SalesLedger;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.sales.pojo.ShippingInfo;
import com.ruoyi.sales.service.ISalesLedgerService;
import com.ruoyi.sales.vo.SalesLedgerVo;
import lombok.RequiredArgsConstructor;
@@ -51,6 +72,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import java.io.InputStream;
import java.lang.reflect.Field;
@@ -78,6 +100,10 @@
    private static final String LOCK_PREFIX = "contract_no_lock:";
    private static final long LOCK_WAIT_TIMEOUT = 10; // 锁等待超时时间(秒)
    private static final long LOCK_EXPIRE_TIME = 30;  // 锁自动过期时间(秒)
    /** 销售类型:卖油(走产品明细) */
    private static final String LEDGER_TYPE_OIL = "卖油";
    /** 销售类型:代储(卖存储位置,走油库+储罐+手工合同金额) */
    private static final String LEDGER_TYPE_DEPOSIT = "代储";
    private final SalesLedgerMapper salesLedgerMapper;
    private final CustomerMapper customerMapper;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
@@ -93,6 +119,12 @@
    private final QualityInspectMapper qualityInspectMapper;
    private final RedisTemplate<String, String> redisTemplate;
    private final FileUtil fileUtil;
    private final AccountInvoiceApplicationMapper accountInvoiceApplicationMapper;
    private final AccountSalesCollectionMapper accountSalesCollectionMapper;
    private final ApprovalTemplateMapper approvalTemplateMapper;
    private final ApprovalInstanceService approvalInstanceService;
    private final OilDepotMapper oilDepotMapper;
    private final TankInfoMapper tankInfoMapper;
    @Autowired
    private SysDeptMapper sysDeptMapper;
@@ -106,14 +138,13 @@
    @Autowired
    private ProductionProductMainService productionProductMainService;
    @Autowired
    private PurchaseReturnOrderProductsMapper purchaseReturnOrderProductsMapper;
    ;
    @Autowired
    private SysUserMapper sysUserMapper;
    @Override
    public List<SalesLedger> selectSalesLedgerList(SalesLedgerDto salesLedgerDto) {
        return salesLedgerMapper.selectSalesLedgerList(salesLedgerDto);
        List<SalesLedger> list = salesLedgerMapper.selectSalesLedgerList(salesLedgerDto);
        fillTrustFund(list);
        return list;
    }
    public List<SalesLedgerProduct> getSalesLedgerProductListByRelateId(Long relateId, SaleEnum type) {
@@ -133,17 +164,11 @@
        productWrapper.eq(SalesLedgerProduct::getType, type.getCode());
        List<SalesLedgerProduct> salesLedgerProducts = salesLedgerProductMapper.selectList(productWrapper);
        if (type.equals(SaleEnum.PURCHASE)) {
            // 查询退货信息
            List<Long> productIds = salesLedgerProducts.stream().map(SalesLedgerProduct::getProductModelId).collect(Collectors.toList());
            List<SimpleReturnOrderGroupDto> groupListByProductIds = new ArrayList<>();
            if(CollectionUtils.isNotEmpty(productIds)){
                groupListByProductIds = purchaseReturnOrderProductsMapper.getReturnOrderGroupListByProductIds(productIds);
            }
            Map<Long, BigDecimal> returnOrderGroupDtoMap = groupListByProductIds.stream().collect(Collectors.toMap(SimpleReturnOrderGroupDto::getProductModelId, SimpleReturnOrderGroupDto::getSumReturnQuantity));
            salesLedgerProducts.forEach(item -> {
                BigDecimal returnQuality = returnOrderGroupDtoMap.getOrDefault(item.getProductModelId(), BigDecimal.ZERO);
                item.setReturnQuality(returnQuality);
                item.setAvailableQuality(item.getQuantity().subtract(returnQuality));
                BigDecimal returnQuantity = item.getReturnQuantity() == null ? BigDecimal.ZERO : item.getReturnQuantity();
                item.setReturnQuantity(returnQuantity);
                BigDecimal quantity = item.getQuantity() == null ? BigDecimal.ZERO : item.getQuantity();
                item.setAvailableQuality(quantity.subtract(returnQuantity));
            });
        }
        return salesLedgerProducts;
@@ -263,29 +288,73 @@
    @Override
    public List<MonthlyAmountDto> getAmountHalfYear(Integer type) {
        LocalDate now = LocalDate.now();
        List<MonthlyAmountDto> result = new ArrayList<>();
        for (int i = 5; i >= 0; i--) {
            YearMonth yearMonth = YearMonth.from(now.minusMonths(i));
            LocalDateTime startTime = yearMonth.atDay(1).atStartOfDay();
            LocalDateTime endTime = yearMonth.atEndOfMonth().atTime(23, 59, 59);
            LocalDate startTime = yearMonth.atDay(1);
            LocalDate endTime = yearMonth.atEndOfMonth();
            MonthlyAmountDto dto = new MonthlyAmountDto();
            dto.setMonth(yearMonth.format(DateTimeFormatter.ofPattern("yyyy-MM")));
            dto.setReceiptAmount(BigDecimal.ZERO);
            dto.setInvoiceAmount(BigDecimal.ZERO);
            //回款金额
            List<AccountSalesCollection> accountSalesCollections = accountSalesCollectionMapper.selectList(new LambdaQueryWrapper<AccountSalesCollection>()
                    .between(AccountSalesCollection::getCollectionDate, startTime, endTime));
            BigDecimal totalIncome = Optional.of(
                    accountSalesCollections.stream()
                            .map(AccountSalesCollection::getCollectionAmount)
                            .filter(Objects::nonNull)
                            .reduce(BigDecimal.ZERO, BigDecimal::add)
            ).orElse(BigDecimal.ZERO);
            dto.setReceiptAmount(totalIncome);
            //开票金额
            List<AccountInvoiceApplication> accountInvoiceApplications = accountInvoiceApplicationMapper.selectList(new LambdaQueryWrapper<AccountInvoiceApplication>()
                            .eq(AccountInvoiceApplication::getStatus,1)
                    .between(AccountInvoiceApplication::getApplyDate, startTime, endTime));
            BigDecimal totalInvoiceAmount = Optional.of(
                    accountInvoiceApplications.stream()
                            .map(AccountInvoiceApplication::getInvoiceAmount)
                            .filter(Objects::nonNull)
                            .reduce(BigDecimal.ZERO, BigDecimal::add)
            ).orElse(BigDecimal.ZERO);
            dto.setInvoiceAmount(totalInvoiceAmount);
            result.add(dto);
        }
        return result;
    }
    @Override
    public IPage<SalesLedgerVo> selectSalesLedgerListPage(Page page, SalesLedgerDto salesLedgerDto) {
        return salesLedgerMapper.selectSalesLedgerListPage(page, salesLedgerDto);
        IPage<SalesLedgerVo> result = salesLedgerMapper.selectSalesLedgerListPage(page, salesLedgerDto);
        fillTrustFund(result.getRecords());
        return result;
    }
    /**
     * 回填客户的信托基金,并判断本笔合同金额是否已超出。
     * 超出与否由后端算好返回,前端只负责标红
     */
    private void fillTrustFund(List<? extends SalesLedger> records) {
        if (CollectionUtils.isEmpty(records)) {
            return;
        }
        List<Long> customerIds = records.stream()
                .map(SalesLedger::getCustomerId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        if (customerIds.isEmpty()) {
            return;
        }
        Map<Long, BigDecimal> trustFundMap = customerMapper.selectBatchIds(customerIds).stream()
                .filter(item -> item.getTrustFund() != null)
                .collect(Collectors.toMap(Customer::getId, Customer::getTrustFund, (existing, replacement) -> existing));
        records.forEach(ledger -> {
            BigDecimal trustFund = trustFundMap.get(ledger.getCustomerId());
            ledger.setTrustFund(trustFund);
            ledger.setExceedsTrustFund(trustFund != null && ledger.getContractAmount() != null
                    && ledger.getContractAmount().compareTo(trustFund) > 0);
        });
    }
    @Override
@@ -302,9 +371,17 @@
            if (CollectionUtils.isEmpty(salesLedgerImportDtoList)) return AjaxResult.error("销售台账数据为空!");
            List<SalesLedgerImportDto> salesLedgerProductImportDtoList = stringListMap.get("销售产品数据");
            if (CollectionUtils.isEmpty(salesLedgerProductImportDtoList)) return AjaxResult.error("销售产品数据为空!");
            // 客户数据
            List<Customer> customers = customerMapper.selectList(new LambdaQueryWrapper<Customer>().in(Customer::getCustomerName,
                    salesLedgerImportDtoList.stream().map(SalesLedgerImportDto::getCustomerName).collect(Collectors.toList())));
            // 客户数据 - 参考 listPage 查询私海客户(type = 0)
            // type = 0(私海客户)或者 type = 1(公海客户)且已被分配,并且是自己领用、自己创建或者共享给自己的客户
            Long loginUserId = loginUser.getUser().getUserId();
            List<Customer> customers = customerMapper.selectList(new QueryWrapper<Customer>()
                    .in("customer_name", salesLedgerImportDtoList.stream()
                            .map(SalesLedgerImportDto::getCustomerName).collect(Collectors.toList()))
                    .and(wrapper -> wrapper.eq("type", 0)
                            .or(wrapper2 -> wrapper2.eq("type", 1).eq("is_assigned", 1)))
                    .and(wrapper -> wrapper.eq("usage_user", loginUserId)
                            .or(wrapper2 -> wrapper2.eq("create_user", loginUserId)
                                    .or(wrapper3 -> wrapper3.exists("select 1 from customer_user cu where cu.customer_id = customer.id and cu.user_id = " + loginUserId)))));
//            // 规格型号数据
//            List<ProductModel> productModels = productModelMapper.selectList(new LambdaQueryWrapper<ProductModel>().in(ProductModel::getModel,
//                    salesLedgerProductImportDtoList.stream().map(SalesLedgerImportDto::getSpecificationModel).collect(Collectors.toList())));
@@ -322,21 +399,26 @@
                if (salesLedger1 != null) {
                    continue;
                }
                // 判断业务员是否存在
                SysUser salesman = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>()
                        .eq(SysUser::getNickName, salesLedgerImportDto.getSalesman()));
                if (salesman == null) {
                    throw new RuntimeException("业务员:" + salesLedgerImportDto.getSalesman() + "不存在!");
                }
                SalesLedger salesLedger = new SalesLedger();
                BeanUtils.copyProperties(salesLedgerImportDto, salesLedger);
                salesLedger.setExecutionDate(DateUtils.toLocalDate(salesLedgerImportDto.getExecutionDate()));
                salesLedger.setDeliveryDate(DateUtils.toLocalDate(salesLedgerImportDto.getDeliveryDate()));
                // 通过客户名称查询客户ID,客户合同号
                salesLedger.setCustomerId(customers.stream()
                Optional<Customer> customerOptional = customers.stream()
                        .filter(customer -> customer.getCustomerName().equals(salesLedger.getCustomerName()))
                        .findFirst()
                        .map(Customer::getId)
                        .orElse(null));
                salesLedger.setCustomerContractNo(customers.stream()
                        .filter(customer -> customer.getCustomerName().equals(salesLedger.getCustomerName()))
                        .findFirst()
                        .map(Customer::getTaxpayerIdentificationNumber)
                        .orElse(null));
                        .findFirst();
                if (customerOptional.isEmpty()) {
                    throw new RuntimeException("客户:" + salesLedger.getCustomerName() + "不存在!或者非私海用户");
                }
                salesLedger.setCustomerId(customerOptional.get().getId());
                salesLedger.setCustomerContractNo(customerOptional.get().getTaxpayerIdentificationNumber());
                Long aLong = sysUsers.stream()
                        .filter(sysUser -> sysUser.getNickName().equals(salesLedger.getEntryPerson()))
                        .findFirst()
@@ -354,6 +436,7 @@
                salesLedger.setContractAmount(salesLedgerProductImportDtos.stream()
                        .map(SalesLedgerProductImportDto::getTaxInclusiveTotalPrice)
                        .reduce(BigDecimal.ZERO, BigDecimal::add));
                salesLedger.setNetContractAmount(salesLedger.getContractAmount());
                salesLedgerMapper.insert(salesLedger);
@@ -364,13 +447,16 @@
                    salesLedgerProduct.setType(1);
                    // 计算不含税总价
                    salesLedgerProduct.setTaxExclusiveTotalPrice(salesLedgerProduct.getTaxInclusiveTotalPrice().divide(new BigDecimal(1).add(salesLedgerProduct.getTaxRate().divide(new BigDecimal(100))), 2, RoundingMode.HALF_UP));
                    list.stream()
                    // 校验产品规格是否存在
                    Optional<Map<String, Object>> productModelOptional = list.stream()
                            .filter(map -> Objects.equals(map.get("productName"), salesLedgerProduct.getProductCategory()) && Objects.equals(map.get("model"), salesLedgerProduct.getSpecificationModel()))
                            .findFirst()
                            .ifPresent(map -> {
                                salesLedgerProduct.setProductModelId(Long.parseLong(map.get("modelId").toString()));
                                salesLedgerProduct.setProductId(Long.parseLong(map.get("id").toString()));
                            });
                            .findFirst();
                    if (productModelOptional.isEmpty()) {
                        throw new RuntimeException("产品大类:" + salesLedgerProduct.getProductCategory() + ",规格型号:" + salesLedgerProduct.getSpecificationModel() + "不存在!");
                    }
                    Map<String, Object> productModelMap = productModelOptional.get();
                    salesLedgerProduct.setProductModelId(Long.parseLong(productModelMap.get("modelId").toString()));
                    salesLedgerProduct.setProductId(Long.parseLong(productModelMap.get("id").toString()));
//                    salesLedgerProduct.setProductId(productList.stream()
//                            .filter(product -> product.getProductName().equals(salesLedgerProduct.getProductCategory()))
//                            .findFirst()
@@ -501,6 +587,19 @@
        // 删除附件表
        commonFileService.deleteByBusinessIds(idList, FileNameType.SALE.getValue());
        // 删除台账对应的审批实例(同一台账可能反复提交/重审,会有多条)
        List<ApprovalInstance> approvalInstances = approvalInstanceService.list(
                new LambdaQueryWrapper<ApprovalInstance>()
                        .in(ApprovalInstance::getBusinessId, idList)
                        .eq(ApprovalInstance::getBusinessType, TypeEnums.SALES_LEDGER_APPROVAL.getCode())
                        .eq(ApprovalInstance::getDeleted, 0));
        if (CollectionUtils.isNotEmpty(approvalInstances)) {
            approvalInstanceService.delete(approvalInstances.stream()
                    .map(ApprovalInstance::getId)
                    .filter(Objects::nonNull)
                    .collect(Collectors.toList()));
        }
        // 删除生产管控数据
        //查询生产报工id
        ArrayList<Long> mainIdList = productionProductMainService.listMain(idList);
@@ -526,18 +625,35 @@
        SalesLedger salesLedger = convertToEntity(salesLedgerDto);
        salesLedger.setCustomerName(customer.getCustomerName());
        salesLedger.setTenantId(customer.getTenantId());
        applyLedgerType(salesLedger, salesLedgerDto);
        // 录入人:如果未传则使用当前登录用户
        if (StringUtils.isEmpty(salesLedger.getEntryPerson())) {
            salesLedger.setEntryPerson(String.valueOf(SecurityUtils.getUserId()));
        }
        // 新增和修改都要重新走审批
        salesLedger.setApprovalStatus(SalesQuotationStatusEnum.PENDING.getCode());
        // 3. 新增或更新主表
        if (salesLedger.getId() == null) {
            String contractNo = generateSalesContractNo();
            String contractNo = salesLedger.getSalesContractNo();
            if (StringUtils.isEmpty(contractNo)) {
                contractNo = generateSalesContractNo(salesLedgerDto.getEntryDate());
            }
            salesLedger.setSalesContractNo(contractNo);
            salesLedgerMapper.insert(salesLedger);
        } else {
            // 改单会重新走一遍审批:先作废此前未结束的审批实例,事务提交前还会重新发起
            approvalInstanceService.lambdaUpdate()
                    .set(ApprovalInstance::getStatus, "REJECTED")
                    .eq(ApprovalInstance::getBusinessId, salesLedger.getId())
                    .eq(ApprovalInstance::getBusinessType, TypeEnums.SALES_LEDGER_APPROVAL.getCode())
                    .update();
            salesLedgerMapper.updateById(salesLedger);
        }
        // 4. 处理子表数据
        List<SalesLedgerProduct> productList = salesLedgerDto.getProductData();
        // 4. 处理子表数据。代储没有产品明细,合同金额以手工填写的为准,不能被子表汇总覆盖
        List<SalesLedgerProduct> productList = LEDGER_TYPE_OIL.equals(salesLedger.getLedgerType())
                ? salesLedgerDto.getProductData() : null;
        if (productList != null && !productList.isEmpty()) {
            handleSalesLedgerProducts(salesLedger.getId(), productList, EnumUtil.fromCode(SaleEnum.class, salesLedgerDto.getType()));
            updateMainContractAmount(
@@ -549,10 +665,112 @@
            );
        }
        // 5. 保存销售台账附件
        // 5. 兜底:确保实际合同金额有初始值等于合同金额
        if (salesLedger.getNetContractAmount() == null) {
            SalesLedger dbLedger = salesLedgerMapper.selectById(salesLedger.getId());
            if (dbLedger != null && dbLedger.getNetContractAmount() == null && dbLedger.getContractAmount() != null) {
                SalesLedger updateEntity = new SalesLedger();
                updateEntity.setId(salesLedger.getId());
                updateEntity.setNetContractAmount(dbLedger.getContractAmount());
                salesLedgerMapper.updateById(updateEntity);
            }
        }
        // 6. 保存销售台账附件
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.SALES_LEDGER, salesLedger.getId(), salesLedgerDto.getStorageBlobDTOs());
        // 7. 发起审批。合同金额要取落库后的值(卖油的金额由第 4 步的产品明细汇总写回)
        SalesLedger savedLedger = salesLedgerMapper.selectById(salesLedger.getId());
        startSalesLedgerApproval(savedLedger, customer.getTrustFund());
        return 1;
    }
    /**
     * 校验销售类型并补齐代储的油库/储罐信息。
     * 卖油:合同金额只能由产品明细汇总产生;代储:没有明细,必须选油库、储罐并手工填合同金额
     */
    private void applyLedgerType(SalesLedger salesLedger, SalesLedgerDto salesLedgerDto) {
        String ledgerType = salesLedgerDto.getLedgerType();
        if (StringUtils.isEmpty(ledgerType)) {
            throw new BaseException("销售类型不能为空");
        }
        if (LEDGER_TYPE_OIL.equals(ledgerType)) {
            salesLedger.setOilDepotId(null);
            salesLedger.setOilDepotName(null);
            salesLedger.setTankId(null);
            salesLedger.setTankNo(null);
            // updateById 会跳过 null,对修改是空操作;新增时与改造前一致(本就为 null)
            salesLedger.setContractAmount(null);
            return;
        }
        if (!LEDGER_TYPE_DEPOSIT.equals(ledgerType)) {
            throw new BaseException("销售类型不合法:" + ledgerType);
        }
        if (salesLedgerDto.getOilDepotId() == null) {
            throw new BaseException("代储必须选择油库");
        }
        if (salesLedgerDto.getTankId() == null) {
            throw new BaseException("代储必须选择储罐");
        }
        if (salesLedgerDto.getContractAmount() == null
                || salesLedgerDto.getContractAmount().compareTo(BigDecimal.ZERO) <= 0) {
            throw new BaseException("代储必须填写合同金额");
        }
        OilDepot oilDepot = oilDepotMapper.selectById(salesLedgerDto.getOilDepotId());
        if (oilDepot == null) {
            throw new BaseException("油库不存在:" + salesLedgerDto.getOilDepotId());
        }
        TankInfo tankInfo = tankInfoMapper.selectById(salesLedgerDto.getTankId());
        if (tankInfo == null) {
            throw new BaseException("储罐不存在:" + salesLedgerDto.getTankId());
        }
        // 名称以基础数据为准,不采信前端传值
        salesLedger.setOilDepotName(oilDepot.getDepotName());
        salesLedger.setTankNo(tankInfo.getTankNo());
    }
    /**
     * 发起销售台账审批,走系统已有的 OA 审批流程。
     * 合同金额超出客户信托基金时,审批标题带上提醒字眼
     */
    private void startSalesLedgerApproval(SalesLedger ledger, BigDecimal trustFund) {
        if (ledger == null) {
            return;
        }
        ApprovalTemplate approvalTemplate = approvalTemplateMapper.selectOne(
                new LambdaQueryWrapper<ApprovalTemplate>()
                        .eq(ApprovalTemplate::getBusinessType, TypeEnums.SALES_LEDGER_APPROVAL.getCode())
                        .eq(ApprovalTemplate::getDeleted, 0)
                        .orderByDesc(ApprovalTemplate::getId)
                        .last("LIMIT 1")
        );
        if (approvalTemplate == null) {
            throw new RuntimeException("请先配置销售台账审批模板");
        }
        boolean exceedsTrustFund = trustFund != null && ledger.getContractAmount() != null
                && ledger.getContractAmount().compareTo(trustFund) > 0;
        String title = ledger.getSalesContractNo() + "审批";
        if (exceedsTrustFund) {
            title += "(合同金额超出客户信托基金,请重点审核)";
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        ApprovalInstanceDto approvalInstance = new ApprovalInstanceDto();
        approvalInstance.setTemplateId(approvalTemplate.getId());
        approvalInstance.setTemplateName(approvalTemplate.getTemplateName());
        approvalInstance.setBusinessId(ledger.getId());
        approvalInstance.setBusinessType(TypeEnums.SALES_LEDGER_APPROVAL.getCode());
        approvalInstance.setCurrentLevel(1);
        approvalInstance.setTitle(title);
        approvalInstance.setApplicantId(loginUser.getUserId());
        approvalInstance.setApplicantName(loginUser.getNickName());
        approvalInstance.setApplyTime(LocalDateTime.now());
        try {
            approvalInstanceService.add(approvalInstance);
        } catch (Exception e) {
            log.error("SalesLedgerServiceImpl approve error for salesContractNo: {}", ledger.getSalesContractNo(), e);
            throw new RuntimeException("审批失败: " + e.getMessage(), e);
        }
    }
    @Override
@@ -590,8 +808,8 @@
    }
    @Transactional(readOnly = true)
    public String generateSalesContractNo() {
        LocalDate currentDate = LocalDate.now();
    public String generateSalesContractNo(Date entryDate) {
        LocalDate currentDate = entryDate != null ? DateUtils.toLocalDate(entryDate) : LocalDate.now();
        String datePart = currentDate.format(DateTimeFormatter.BASIC_ISO_DATE);
        String lockKey = LOCK_PREFIX + datePart;
        String lockValue = Thread.currentThread().getId() + "-" + System.nanoTime(); // 唯一标识锁持有者
@@ -687,6 +905,15 @@
            amountField.setAccessible(true);
            amountField.set(entity, totalAmount);
            // 实际合同金额初始值等于合同金额
            try {
                Field netAmountField = mainEntityClass.getDeclaredField("netContractAmount");
                netAmountField.setAccessible(true);
                netAmountField.set(entity, totalAmount);
            } catch (NoSuchFieldException ignored) {
                // 子类可能没有该字段,忽略
            }
            mainMapper.updateById(entity);
        } catch (Exception e) {
            throw new RuntimeException("动态更新主表金额失败", e);