sen
2 天以前 7ed2a032d0724e68aec8af940f2ce0023a9f0eb7
tms/src/main/java/com/ruoyi/tms/service/impl/TmsArBillServiceImpl.java
@@ -1,12 +1,28 @@
package com.ruoyi.tms.service.impl;
import java.util.List;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.DictUtils;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.tms.domain.*;
import com.ruoyi.tms.mapper.*;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.Resource;
import java.io.File;
import java.io.FileOutputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collectors;
import com.ruoyi.tms.domain.TmsArBillItem;
import com.ruoyi.tms.mapper.TmsArBillItemMapper;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.stereotype.Service;
import org.springframework.scheduling.annotation.Async;
@@ -18,10 +34,19 @@
import com.ruoyi.common.enums.DataSourceType;
import com.ruoyi.common.core.service.BaseService;
import com.ruoyi.tms.mapper.TmsArBillMapper;
import com.ruoyi.tms.domain.TmsArBill;
import com.ruoyi.tms.service.ITmsArBillService;
import com.ruoyi.common.core.text.Convert;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
/**
 * 应收账单Service业务层处理
@@ -39,7 +64,27 @@
    @Resource
    private TmsArBillItemMapper tmsArBillItemMapper;
    @Resource
    private TmsReceivableFeeMapper tmsReceivableFeeMapper;
    @Resource
    private TmsReceivableFeeItemMapper tmsReceivableFeeItemMapper;
    @Resource
    private TmsTripMapper tmsTripMapper;
    @Autowired
    private ISysConfigService sysConfigService;
    @Autowired
    private RedisCache redisCache;
    @Resource
    private TmsDispatchOrderMapper tmsDispatchOrderMapper;
    @Autowired
    private RestTemplate restTemplate;
    @Value("${custom.cwxtApi.url}")
    private String url;
    /**
     * 查询应收账单
@@ -52,7 +97,7 @@
    public TmsArBill selectTmsArBillById(Integer id)
    {
        TmsArBill tmsArBill = tmsArBillMapper.selectTmsArBillById(id);
        tmsArBill.setItems(tmsArBillItemMapper.selectTmsArBillItemList(new TmsArBillItem(){{setBillId( id);}}));
        tmsArBill.setItems(tmsArBillItemMapper.selectTmsArBillItemList(new TmsArBillItem(){{setBillId( id);setStatus(0);}}));
        return tmsArBill;
    }
@@ -112,6 +157,31 @@
    {
        tmsArBill.setCreateTime(DateUtils.getNowDate());
        return tmsArBillMapper.insertTmsArBill(tmsArBill);
    }
    @Override
    public AjaxResult cancelArBill(Integer id) {
        TmsArBillItem billItem = tmsArBillItemMapper.selectTmsArBillItemById(id);
        if(billItem == null){
            return AjaxResult.warn("数据不存在");
        }
        if(billItem.getStatus() == 1){
            return AjaxResult.warn("该数据已作废");
        }
        billItem.setStatus(1);
        tmsArBillItemMapper.updateTmsArBillItem(billItem);
        tmsReceivableFeeMapper.update(new LambdaUpdateWrapper<TmsReceivableFee>()
                .set(TmsReceivableFee::getStatus,0)
                .in(TmsReceivableFee::getId, billItem.getArFeeId())
        );
        tmsArBillMapper.update(new LambdaUpdateWrapper<TmsArBill>()
                .setSql("settle_amount = settle_amount - " + billItem.getEstimateAmount())
                .setSql("actual_settlement_amount = actual_settlement_amount - " + billItem.getEstimateAmount())
                .eq(TmsArBill::getId, billItem.getBillId())
        );
        return AjaxResult.success();
    }
    /**
@@ -187,4 +257,1042 @@
    {
        return tmsArBillMapper.deleteTmsArBillById(id);
    }
    /**
     * 手动推送应收账单到外部系统
     *
     * @param id 应收账单ID
     * @return 结果
     */
    @Override
    public void manualPushToExternalSystem(Integer id) {
        TmsArBill tmsArBill = tmsArBillMapper.selectTmsArBillById(id);
        if (tmsArBill == null) {
            throw new RuntimeException("应收账单不存在");
        }
        // 查询关联的应收费用列表
        List<TmsReceivableFee> tmsReceivableFees = tmsReceivableFeeMapper.selectList(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TmsReceivableFee>()
                .eq(TmsReceivableFee::getBillRelationId, id)
        );
        // 为每个应收费用加载明细
        for (TmsReceivableFee fee : tmsReceivableFees) {
            List<TmsReceivableFeeItem> items = tmsReceivableFeeItemMapper.selectTmsReceivableFeeItemList(new TmsReceivableFeeItem() {
                {
                    setHeadId(fee.getId());
                }
            });
            fee.setItems(items);
        }
        // 异步推送
        AsyncTaskExecutor executor = new SimpleAsyncTaskExecutor();
        executor.execute(() -> pushToExternalSystem(tmsArBill, tmsReceivableFees));
    }
    @Override
    public void cancelPushToExternalSystem(Integer id) {
        TmsArBill tmsArBill = tmsArBillMapper.selectTmsArBillById(id);
        if (tmsArBill == null) {
            throw new RuntimeException("应收账单不存在");
        }
        // 异步推送作废请求
        AsyncTaskExecutor executor = new SimpleAsyncTaskExecutor();
        executor.execute(() -> pushCancelToExternalSystem(tmsArBill));
    }
    /**
     * 向外部系统推送应收数据作废
     * @param tmsArBill 应收账单
     */
    @Async
    protected void pushCancelToExternalSystem(TmsArBill tmsArBill) {
        java.util.Map<String, Object> requestBody = new java.util.HashMap<>();
        try {
    ;
            // 构建请求体
            String apiUrl = url+"/cancelBill";
            // 构建请求体,只需要sourceSystemId
            requestBody.put("sourceSystemId", tmsArBill.getSourceSystemId());
            // 设置HTTP头
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(requestBody), headers);
            // 发送API请求
            ResponseEntity<String> response = restTemplate.exchange(apiUrl, HttpMethod.POST, entity, String.class);
            logger.info("推送应收数据作废到外部系统成功,响应: {}", response.getBody());
            // 更新推送状态为成功
            tmsArBill.setStatus(3); // 设置账单状态为作废
            tmsArBillMapper.updateTmsArBill(tmsArBill);
            // 重置关联的应收费用状态为待确认
            tmsReceivableFeeMapper.update(new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<TmsReceivableFee>()
                    .set(TmsReceivableFee::getStatus, 0)
                    .set(TmsReceivableFee::getBillRelationId, null)
                    .set(TmsReceivableFee::getBillRelationNo, null)
                    .eq(TmsReceivableFee::getBillRelationId, tmsArBill.getId())
            );
            logger.info("重置应收费用状态成功,账单ID: {}", tmsArBill.getId());
        } catch (Exception e) {
            logger.error("推送应收数据作废到外部系统失败,账单ID: {}, 客户: {}",
                tmsArBill.getId(), tmsArBill.getCustomerName(), e);
            logger.debug("推送失败的请求数据: {}", JSON.toJSONString(requestBody));
            // 更新推送状态为失败
            tmsArBill.setPushStatus(3);
            tmsArBill.setPushTime(DateUtils.getNowDate());
            tmsArBillMapper.updateTmsArBill(tmsArBill);
        }
    }
    /**
     * 更新推送状态
     *
     * @param id 应收账单ID
     * @param pushStatus 推送状态
     * @return 结果
     */
    @Override
    public int updatePushStatus(Integer id, Integer pushStatus) {
        TmsArBill tmsArBill = new TmsArBill();
        tmsArBill.setId(id);
        tmsArBill.setPushStatus(pushStatus);
        tmsArBill.setPushTime(DateUtils.getNowDate());
        return tmsArBillMapper.updateTmsArBill(tmsArBill);
    }
    /**
     * 向外部系统推送数据
     * @param tmsArBill 应收账单
     * @param tmsReceivableFees 应收费用列表
     */
    @Async
    protected void pushToExternalSystem(TmsArBill tmsArBill, List<TmsReceivableFee> tmsReceivableFees) {
        java.util.Map<String, Object> requestBody = new java.util.HashMap<>();
        try {
            // 更新推送状态为推送中
            tmsArBill.setPushStatus(1);
            tmsArBill.setPushTime(DateUtils.getNowDate());
            tmsArBillMapper.updateTmsArBill(tmsArBill);
            // 构建请求体
            String apiUrl = url+"/addBill";
            // 构建bill部分
            java.util.Map<String, Object> billMap = new java.util.HashMap<>();
            billMap.put("billName", tmsArBill.getBillName());
            billMap.put("customerName", tmsArBill.getCustomerName());
            billMap.put("payee", "");
            billMap.put("responsiblePerson", "");
            billMap.put("responsibleLeader", "");
            billMap.put("settlementMethod", "");
            billMap.put("businessType", "");
            billMap.put("promotionRequirement", "");
            billMap.put("isInternalSettlement", "0");
            billMap.put("internalSettlementUnit", "");
            billMap.put("documentCount", tmsReceivableFees.size());
            billMap.put("totalAmount", tmsArBill.getSettleAmount());
            billMap.put("currency", "RMB");
            billMap.put("discountAmount", 0.00);
            billMap.put("receivedAmount", 0.00);
            billMap.put("pendingAmount", tmsArBill.getSettleAmount());
            billMap.put("exchangeRate", tmsArBill.getSettleRate());
            billMap.put("cnyAmount", tmsArBill.getSettleAmount());
            billMap.put("periodType", "");
            billMap.put("businessStartDate", "");
            billMap.put("businessEndDate", "");
            billMap.put("billingStartDate", "");
            billMap.put("billingEndDate", "");
            billMap.put("billGenerateDate", "");
            billMap.put("billSendDate", "");
            billMap.put("billDueDate", "");
            billMap.put("settlementCategory", "");
            billMap.put("settlementPeriod", "");
            billMap.put("status", "0");
            billMap.put("remark", "");
            billMap.put("sourceSystemId", tmsArBill.getId());
            // 构建fees部分
            List<java.util.Map<String, Object>> feesList = new java.util.ArrayList<>();
            for (int i = 0; i < tmsReceivableFees.size(); i++) {
                TmsReceivableFee fee = tmsReceivableFees.get(i);
                java.util.Map<String, Object> feeMap = new java.util.HashMap<>();
                feeMap.put("serialNumber", String.format("%03d", i + 1));
                feeMap.put("relatedBillNo", "");
                feeMap.put("sourceSystem", "TMS");
                feeMap.put("businessSector", "0");
                feeMap.put("documentType", "0");
                feeMap.put("documentNo", fee.getDispatchNo() != null ? fee.getDispatchNo() : "");
                feeMap.put("isInternalSettlement", "0");
                feeMap.put("internalSettlementUnit", "");
                feeMap.put("customerName", tmsArBill.getCustomerName());
                feeMap.put("projectName", fee.getProjectName() != null ? fee.getProjectName() : "");
                feeMap.put("businessTime", fee.getDispatchConfirmTime());
                feeMap.put("receivableConfirmTime", fee.getDispatchConfirmTime());
                feeMap.put("receivableAmount", fee.getReceivableRMBAmount().add(fee.getReceivableHKBAmount()));
                // 构建receivableAmountStr
                BigDecimal rmbAmount = fee.getReceivableRMBAmount();
                BigDecimal hkbAmount = fee.getReceivableHKBAmount();
                StringBuilder amountStr = new StringBuilder();
                if (rmbAmount.compareTo(BigDecimal.ZERO) > 0) {
                    amountStr.append(rmbAmount).append("人民币");
                }
                if (hkbAmount.compareTo(BigDecimal.ZERO) > 0) {
                    if (amountStr.length() > 0) {
                        amountStr.append(" ");
                    }
                    amountStr.append(hkbAmount).append("港币");
                }
                feeMap.put("receivableAmountStr", amountStr.toString());
                feeMap.put("status", "1");
                feeMap.put("remark", "");
                // 构建feeDetails部分
                List<java.util.Map<String, Object>> feeDetailsList = new java.util.ArrayList<>();
                List<TmsReceivableFeeItem> items = fee.getItems();
                for (int j = 0; j < items.size(); j++) {
                    TmsReceivableFeeItem item = items.get(j);
                    java.util.Map<String, Object> feeDetailMap = new java.util.HashMap<>();
                    feeDetailMap.put("serialNumber", String.format("%03d", j + 1));
                    feeDetailMap.put("feeType", item.getFeeType());
                    feeDetailMap.put("feeName", item.getFeeName());
                    feeDetailMap.put("billingUnit", "次");
                    feeDetailMap.put("unitPrice", item.getRegisterAmount());
                    feeDetailMap.put("billingQuantity", item.getRegisterAmount());
                    feeDetailMap.put("billingAmount", item.getRegisterAmount());
                    feeDetailMap.put("actualAmount", item.getRegisterAmount());
                    feeDetailMap.put("currency", item.getCurrency());
                    feeDetailMap.put("feeRegTime", item.getRegisterTime());
                    feeDetailMap.put("remark", "");
                    feeDetailsList.add(feeDetailMap);
                }
                feeMap.put("feeDetails", feeDetailsList);
                feesList.add(feeMap);
            }
            // 构建完整请求体
            requestBody.put("bill", billMap);
            requestBody.put("fees", feesList);
            // 设置HTTP头
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(requestBody), headers);
            // 发送API请求
            ResponseEntity<String> response = restTemplate.exchange(apiUrl, HttpMethod.POST, entity, String.class);
            logger.info("推送数据到外部系统成功,响应: {}", response.getBody());
            // 解析响应,获取sourceSystemId
            try {
                JSONObject result = JSONObject.parseObject(response.getBody());
                String sourceSystemId = result.getString("sourceSystemId");
                if (sourceSystemId != null) {
                    tmsArBill.setSourceSystemId(Integer.parseInt(sourceSystemId));
                }
            } catch (Exception e) {
                logger.error("解析外部系统响应失败: {}", e.getMessage());
            }
            // 更新推送状态为成功
            tmsArBill.setPushStatus(2);
            tmsArBill.setPushTime(DateUtils.getNowDate());
            tmsArBillMapper.updateTmsArBill(tmsArBill);
        } catch (Exception e) {
            logger.error("推送数据到外部系统失败,账单ID: {}, 客户: {}",
                tmsArBill.getId(), tmsArBill.getCustomerName(), e);
            logger.debug("推送失败的请求数据: {}", JSON.toJSONString(requestBody));
            // 更新推送状态为失败
            tmsArBill.setPushStatus(3);
            tmsArBill.setPushTime(DateUtils.getNowDate());
            tmsArBillMapper.updateTmsArBill(tmsArBill);
        }
    }
    /**
     * 导出对账单一式多联格式
     *
     * @param tmsArBill 应收账单
     * @param exportKey 导出功能的唯一标识
     */
    @DataSource(DataSourceType.SLAVE)
    @Async
    @Override
    public void exportArBillFormat(TmsArBill tmsArBill, String exportKey) {
        // 设置当前任务为“下载中”状态
        com.ruoyi.common.utils.file.DownloadExportUtil.deleteDownloadFile(redisCache, exportKey, "0");
        try {
            // 生成文件名
            String fileName = com.ruoyi.common.utils.poi.ExcelUtil.encodeFileName("arBillFormatData");
            // 执行导出
            fileName = exportArBillFormatData(fileName, tmsArBill);
            // 设置下载完成状态
            com.ruoyi.common.utils.file.DownloadExportUtil.setDownloadFile(redisCache, exportKey, fileName);
            logger.info("Export completed for key: {}, file: {}", exportKey, fileName);
        } catch (Exception e) {
            logger.error("Export failed for key: {}, error: {}", exportKey, e.getMessage(), e);
            com.ruoyi.common.utils.file.DownloadExportUtil.deleteDownloadFile(redisCache, exportKey, "1"); // 设置失败状态
            throw e;
        }
    }
    /**
     * 导出对账单一式多联数据
     *
     * @param fileName 文件名
     * @param tmsArBill 应收账单
     * @return 导出后的文件名
     */
    protected String exportArBillFormatData(String fileName, TmsArBill tmsArBill) {
        try (SXSSFWorkbook workbook = new SXSSFWorkbook(1000)) {
            // 创建工作表
            SXSSFSheet sheet = workbook.createSheet("对账单");
            // 创建样式
            Map<String, CellStyle> styles = createArBillStyles(workbook);
            // 查询数据
            TmsArBill bill = selectTmsArBillById(tmsArBill.getId());
            // 获取港币兑人民币汇率
            String rateStr = sysConfigService.selectConfigByKey("sys.hk.rmb.rate");
            BigDecimal exchangeRate = new BigDecimal(rateStr);
            // 收集所有费用名称和对应的货币,用于动态生成列
            Map<String, String> feeCurrencyMap = new HashMap<>();
                for (TmsArBillItem item : bill.getItems()) {
                    // 应收费用ID
                    Integer arFeeId = item.getArFeeId();
                    // 应收费用明细
                    List<TmsReceivableFeeItem> tmsReceivableFeeItems = tmsReceivableFeeItemMapper.selectTmsReceivableFeeItemList(new TmsReceivableFeeItem() {{
                        setHeadId(arFeeId);
                    }});
                    // 从应收费用明细中收集费用名称和货币
                    for (TmsReceivableFeeItem feeItem : tmsReceivableFeeItems) {
                        feeCurrencyMap.put(feeItem.getFeeName(), feeItem.getCurrency());
                    }
                }
            // 将费用名称转换为列表,并按要求排序:运费放在最前面,杂费放在最后面
            List<String> feeNameList = new ArrayList<>();
            List<String> otherFees = new ArrayList<>();
            for (String feeName : feeCurrencyMap.keySet()) {
                if ("运费".equals(feeName)) {
                    feeNameList.add(feeName);
                } else if ("杂费".equals(feeName)) {
                    otherFees.add(feeName);
                } else {
                    otherFees.add(feeName);
                }
            }
            // 添加其他费用
            feeNameList.addAll(otherFees.stream().filter(fee -> !"杂费".equals(fee)).collect(Collectors.toList()));
            // 添加杂费到最后
            if (feeCurrencyMap.containsKey("杂费")) {
                feeNameList.add("杂费");
            }
            // 基础列数(序号、装货日期、装货点、卸货点、车牌、型号)
            int baseColumns = 6;
            // 备注列位置
            int remarkColumn = baseColumns + feeNameList.size();
            // 设置列宽
            sheet.setColumnWidth(0, 5000);  // 序号
            sheet.setColumnWidth(1, 5000);  // 装货日期
            sheet.setColumnWidth(2, 8000);  // 装货点
            sheet.setColumnWidth(3, 8000);  // 卸货点
            sheet.setColumnWidth(4, 5000);  // 车牌
            sheet.setColumnWidth(5, 5000);  // 型号
            // 设置费用列宽
            for (int i = 0; i < feeNameList.size(); i++) {
                sheet.setColumnWidth(baseColumns + i, 6000);
            }
            sheet.setColumnWidth(remarkColumn, 10000); // 备注
            // 标题区域
            createTitleArea(sheet, styles,bill);
            // 表头
            createDynamicTableHeader(sheet, styles, feeNameList, feeCurrencyMap, baseColumns, remarkColumn);
            // 数据区域
            int startRow = 5;
            // 费用合计映射
            Map<String, BigDecimal> feeTotals = new HashMap<>();
            for (String feeName : feeNameList) {
                feeTotals.put(feeName, BigDecimal.ZERO);
            }
            int rowIndex = 0;
                for (TmsArBillItem item : bill.getItems()) {
                    // 查询对应的应收费用
                    TmsReceivableFee fee = tmsReceivableFeeMapper.selectTmsReceivableFeeById(item.getArFeeId());
                    if (fee != null) {
                        Row row = sheet.createRow(startRow + rowIndex);
                        // 序号
                        Cell cell0 = row.createCell(0);
                        cell0.setCellValue(rowIndex + 1);
                        cell0.setCellStyle(styles.get("data"));
                        // 查询调度单信息
                        TmsDispatchOrder dispatchOrder = null;
                        if (fee.getDispatchId() != null) {
                            dispatchOrder = tmsDispatchOrderMapper.selectTmsDispatchOrderById(fee.getDispatchId());
                        }
                        // 装货日期
                        Cell cell1 = row.createCell(1);
                        if (dispatchOrder != null && dispatchOrder.getOrderTime() !=null) {
                            cell1.setCellValue(DateUtils.parseDateToStr("yyyy-MM-dd", dispatchOrder.getOrderTime()));
                        }
                        cell1.setCellStyle(styles.get("data"));
                        // 装货点
                        Cell cell2 = row.createCell(2);
                        if (dispatchOrder != null && dispatchOrder.getShipperRegionLabel() != null) {
                            cell2.setCellValue(dispatchOrder.getShipperRegionLabel());
                        }
                        cell2.setCellStyle(styles.get("data"));
                        // 卸货点
                        Cell cell3 = row.createCell(3);
                        if (dispatchOrder != null && dispatchOrder.getReceiverRegionLabel() != null) {
                            cell3.setCellValue(dispatchOrder.getReceiverRegionLabel());
                        }
                        cell3.setCellStyle(styles.get("data"));
                        // 车牌
                        Cell cell4 = row.createCell(4);
                        if (dispatchOrder != null && dispatchOrder.getLicensePlate() != null) {
                            cell4.setCellValue(dispatchOrder.getLicensePlate());
                        }
                        cell4.setCellStyle(styles.get("data"));
                        // 型号
                        Cell cell5 = row.createCell(5);
                        if (dispatchOrder != null && dispatchOrder.getActualVehicleType() != null) {
                            // 使用字典转换车辆型号
                            String vehicleType = dispatchOrder.getActualVehicleType();
                            String vehicleTypeLabel = DictUtils.getDictLabel("vehicle_type", vehicleType);
                            cell5.setCellValue(StringUtils.isNotEmpty(vehicleTypeLabel) ? vehicleTypeLabel : vehicleType);
                        }
                        cell5.setCellStyle(styles.get("data"));
                        // 构建费用名称到金额的映射(港币转人民币)
                        Map<String, BigDecimal> feeMap = new HashMap<>();
                        // 查询应收费用明细
                        List<TmsReceivableFeeItem> tmsReceivableFeeItems = tmsReceivableFeeItemMapper.selectTmsReceivableFeeItemList(new TmsReceivableFeeItem() {{
                            setHeadId(fee.getId());
                        }});
                        // 处理应收费用明细
                        for (TmsReceivableFeeItem feeItem : tmsReceivableFeeItems) {
                            // 保持原始金额,不进行币种转换
                            feeMap.put(feeItem.getFeeName(), feeItem.getRegisterAmount());
                        }
                        // 填充费用列
                        for (int j = 0; j < feeNameList.size(); j++) {
                            String feeName = feeNameList.get(j);
                            Cell cell = row.createCell(baseColumns + j);
                            BigDecimal feeAmount = feeMap.getOrDefault(feeName, BigDecimal.ZERO);
                            cell.setCellValue(feeAmount.doubleValue());
                            cell.setCellStyle(styles.get("data"));
                            // 累计合计
                            feeTotals.put(feeName, feeTotals.get(feeName).add(feeAmount));
                        }
                        // 备注 - 为空
                        Cell remarkCell = row.createCell(remarkColumn);
                        remarkCell.setCellValue("");
                        remarkCell.setCellStyle(styles.get("data"));
                        rowIndex++;
                    }
                }
            // 小计行
            int subTotalRow = startRow + rowIndex;
            Row subTotal = sheet.createRow(subTotalRow);
            // 为所有列创建单元格
            for (int i = 0; i <= remarkColumn; i++) {
                Cell cell = subTotal.createCell(i);
                if (i == 0) {
                    cell.setCellValue("小计");
                }
                cell.setCellStyle(styles.get("total"));
            }
            sheet.addMergedRegion(new CellRangeAddress(subTotalRow, subTotalRow, 0, 5));
            // 填充费用小计
            for (int j = 0; j < feeNameList.size(); j++) {
                String feeName = feeNameList.get(j);
                Cell cell = subTotal.getCell(baseColumns + j);
                cell.setCellValue(feeTotals.get(feeName).doubleValue());
            }
            // 合计行
            int totalRow = subTotalRow + 1;
            Row total = sheet.createRow(totalRow);
            // 为所有列创建单元格
            for (int i = 0; i <= remarkColumn; i++) {
                Cell cell = total.createCell(i);
                if (i == 0) {
                    cell.setCellValue("合计(RMB)");
                }
                // 合计行的备注设置汇率
                if (i == remarkColumn) {
                    cell.setCellValue("汇率: " + exchangeRate);
                }
                cell.setCellStyle(styles.get("total"));
            }
            sheet.addMergedRegion(new CellRangeAddress(totalRow, totalRow, 0, 5));
            // 计算所有费用的合计(转换为人民币)
            BigDecimal grandTotal = BigDecimal.ZERO;
            for (String feeName : feeTotals.keySet()) {
                BigDecimal amount = feeTotals.get(feeName);
                // 获取该费用的币种
                String currency = feeCurrencyMap.get(feeName);
                // 如果是港币,转换为人民币
                if ("HKD".equals(currency) || "港币".equals(currency)) {
                    amount = amount.multiply(exchangeRate).setScale(2, RoundingMode.HALF_UP);
                }
                grandTotal = grandTotal.add(amount);
            }
            // 填充费用合计(所有费用的总和)
            for (int j = 0; j < feeNameList.size(); j++) {
                Cell cell = total.getCell(baseColumns + j);
                if (j == 0) {
                    cell.setCellValue(grandTotal.doubleValue());
                }
            }
            // 合并费用列单元格
            if (feeNameList.size() > 1) {
                sheet.addMergedRegion(new CellRangeAddress(totalRow, totalRow, baseColumns, baseColumns + feeNameList.size() - 1));
            }
            // 备注说明区域
            createNotesArea(sheet, styles, totalRow + 1, grandTotal);
            // 账户信息和签字盖章区域(合并成一个大格)
            createAccountAndSignatureArea(sheet, styles, totalRow + 4, bill);
            // 保存文件
            String path = RuoYiConfig.getDownloadPath() + fileName;
            File file = new File(path);
            File parentFile = file.getParentFile();
            if (!parentFile.exists()) {
                parentFile.mkdirs();
            }
            try (FileOutputStream fos = new FileOutputStream(file)) {
                workbook.write(fos);
            }
            return fileName;
        } catch (Exception e) {
            logger.error("导出对账单一式多联格式失败", e);
            throw new RuntimeException("导出失败,请检查数据", e);
        }
    }
    /**
     * 创建对账单一式多联样式
     *
     * @param workbook 工作簿
     * @return 样式映射
     */
    private Map<String, CellStyle> createArBillStyles(SXSSFWorkbook workbook) {
        Map<String, CellStyle> styles = new HashMap<>();
        // 标题样式
        CellStyle titleStyle = workbook.createCellStyle();
        titleStyle.setAlignment(HorizontalAlignment.CENTER);
        titleStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        titleStyle.setBorderTop(BorderStyle.THIN);
        titleStyle.setBorderBottom(BorderStyle.THIN);
        titleStyle.setBorderLeft(BorderStyle.THIN);
        titleStyle.setBorderRight(BorderStyle.THIN);
        Font titleFont = workbook.createFont();
        titleFont.setFontName("微软雅黑");
        titleFont.setFontHeightInPoints((short) 16);
        titleFont.setBold(true);
        titleStyle.setFont(titleFont);
        styles.put("title", titleStyle);
        // 表头样式
        CellStyle headerStyle = workbook.createCellStyle();
        headerStyle.setAlignment(HorizontalAlignment.CENTER);
        headerStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        headerStyle.setBorderTop(BorderStyle.THIN);
        headerStyle.setBorderBottom(BorderStyle.THIN);
        headerStyle.setBorderLeft(BorderStyle.THIN);
        headerStyle.setBorderRight(BorderStyle.THIN);
        Font headerFont = workbook.createFont();
        headerFont.setFontName("微软雅黑");
        headerFont.setFontHeightInPoints((short) 11);
        headerFont.setBold(true);
        headerStyle.setFont(headerFont);
        styles.put("header", headerStyle);
        // 数据样式
        CellStyle dataStyle = workbook.createCellStyle();
        dataStyle.setAlignment(HorizontalAlignment.CENTER);
        dataStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        dataStyle.setBorderTop(BorderStyle.THIN);
        dataStyle.setBorderBottom(BorderStyle.THIN);
        dataStyle.setBorderLeft(BorderStyle.THIN);
        dataStyle.setBorderRight(BorderStyle.THIN);
        Font dataFont = workbook.createFont();
        dataFont.setFontName("微软雅黑");
        dataFont.setFontHeightInPoints((short) 11);
        dataStyle.setFont(dataFont);
        styles.put("data", dataStyle);
        // 合计样式
        CellStyle totalStyle = workbook.createCellStyle();
        totalStyle.setAlignment(HorizontalAlignment.CENTER);
        totalStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        totalStyle.setBorderTop(BorderStyle.THIN);
        totalStyle.setBorderBottom(BorderStyle.THIN);
        totalStyle.setBorderLeft(BorderStyle.THIN);
        totalStyle.setBorderRight(BorderStyle.THIN);
        Font totalFont = workbook.createFont();
        totalFont.setFontName("微软雅黑");
        totalFont.setFontHeightInPoints((short) 11);
        totalFont.setBold(true);
        totalStyle.setFont(totalFont);
        styles.put("total", totalStyle);
        // 文本样式
        CellStyle textStyle = workbook.createCellStyle();
        textStyle.setAlignment(HorizontalAlignment.LEFT);
        textStyle.setVerticalAlignment(VerticalAlignment.TOP);
        textStyle.setBorderTop(BorderStyle.THIN);
        textStyle.setBorderBottom(BorderStyle.THIN);
        textStyle.setBorderLeft(BorderStyle.THIN);
        textStyle.setBorderRight(BorderStyle.THIN);
        Font textFont = workbook.createFont();
        textFont.setFontName("微软雅黑");
        textFont.setFontHeightInPoints((short) 11);
        textStyle.setFont(textFont);
        styles.put("text", textStyle);
        // 文本样式
        CellStyle textStyle2 = workbook.createCellStyle();
        textStyle2.setAlignment(HorizontalAlignment.LEFT);
        textStyle2.setVerticalAlignment(VerticalAlignment.TOP);
        textStyle2.setBorderTop(BorderStyle.THIN);
        textStyle2.setBorderBottom(BorderStyle.THIN);
        textStyle2.setBorderLeft(BorderStyle.THIN);
        Font textFont2 = workbook.createFont();
        textFont2.setFontName("微软雅黑");
        textFont2.setFontHeightInPoints((short) 11);
        textStyle2.setFont(textFont2);
        styles.put("text2", textStyle2);
        return styles;
    }
    /**
     * 创建标题区域
     *
     * @param sheet 工作表
     * @param styles 样式映射
     */
    private void createTitleArea(SXSSFSheet sheet, Map<String, CellStyle> styles, TmsArBill tmsArBill) {
        // 标题行(无边框,居中)
        Row titleRow = sheet.createRow(0);
        titleRow.setHeightInPoints(30);
        for (int i = 0; i <= 9; i++) {
            Cell cell = titleRow.createCell(i);
            if (i == 0) {
                cell.setCellValue("珠海市汇畅交通投资有限公司");
                // 居中显示
                CellStyle centerStyle = sheet.getWorkbook().createCellStyle();
                centerStyle.setAlignment(HorizontalAlignment.CENTER);
                centerStyle.setVerticalAlignment(VerticalAlignment.CENTER);
                cell.setCellStyle(centerStyle);
            }
            // 不设置边框样式
        }
        sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 9));
        // 对账单行 - 居中显示(无边框)
        Row billRow = sheet.createRow(1);
        billRow.setHeightInPoints(20);
        for (int i = 0; i <= 9; i++) {
            Cell cell = billRow.createCell(i);
            if (i == 0) {
                cell.setCellValue(tmsArBill.getBillName());
                // 居中显示
                CellStyle centerStyle = sheet.getWorkbook().createCellStyle();
                centerStyle.setAlignment(HorizontalAlignment.CENTER);
                centerStyle.setVerticalAlignment(VerticalAlignment.CENTER);
                cell.setCellStyle(centerStyle);
            }
            // 不设置边框样式
        }
        sheet.addMergedRegion(new CellRangeAddress(1, 1, 0, 9));
        // FROM行(在TO上面,无边框)
        Row fromRow = sheet.createRow(2);
        fromRow.setHeightInPoints(20);
        for (int i = 0; i <= 9; i++) {
            Cell cell = fromRow.createCell(i);
            if (i == 0) {
                cell.setCellValue("FROM:珠海市汇畅交通投资有限公司");
            }
            // 不设置边框样式
        }
        // TO行(无边框)
        Row toRow = sheet.createRow(3);
        toRow.setHeightInPoints(20);
        for (int i = 0; i <= 9; i++) {
            Cell cell = toRow.createCell(i);
            if (i == 0) {
                cell.setCellValue("TO: "+tmsArBill.getCustomerName());
            }
            // 不设置边框样式
        }
    }
    /**
     * 创建动态表格表头
     *
     * @param sheet 工作表
     * @param styles 样式映射
     * @param feeNameList 费用名称列表
     * @param feeCurrencyMap 费用名称到货币的映射
     * @param baseColumns 基础列数
     * @param remarkColumn 备注列位置
     */
    private void createDynamicTableHeader(SXSSFSheet sheet, Map<String, CellStyle> styles, List<String> feeNameList, Map<String, String> feeCurrencyMap, int baseColumns, int remarkColumn) {
        Row headerRow = sheet.createRow(4);
        headerRow.setHeightInPoints(25);
        // 基础列
        String[] baseHeaders = {"序号", "装货日期", "装货点", "卸货点", "车牌", "型号"};
        for (int i = 0; i < baseHeaders.length; i++) {
            Cell cell = headerRow.createCell(i);
            cell.setCellValue(baseHeaders[i]);
            cell.setCellStyle(styles.get("header"));
        }
        // 费用列
        for (int i = 0; i < feeNameList.size(); i++) {
            Cell cell = headerRow.createCell(baseColumns + i);
            String feeName = feeNameList.get(i);
            String currency = feeCurrencyMap.get(feeName);
            cell.setCellValue(feeName + "(" + currency + ")");
            cell.setCellStyle(styles.get("header"));
        }
        // 备注列
        Cell remarkCell = headerRow.createCell(remarkColumn);
        remarkCell.setCellValue("备注");
        remarkCell.setCellStyle(styles.get("header"));
    }
    /**
     * 将金额转换为中文大写
     *
     * @param amount 金额
     * @return 中文大写金额
     */
    private String convertToChineseUppercase(BigDecimal amount) {
        if (amount == null) {
            return "零元整";
        }
        String[] digits = {"零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"};
        String[] units = {"", "拾", "佰", "仟"};
        String[] bigUnits = {"", "万", "亿"};
        String amountStr = amount.setScale(2, RoundingMode.HALF_UP).toString();
        String integerPart = amountStr.split("\\.")[0];
        String decimalPart = amountStr.split("\\.")[1];
        StringBuilder result = new StringBuilder();
        // 处理整数部分
        int length = integerPart.length();
        for (int i = 0; i < length; i++) {
            int digit = Integer.parseInt(String.valueOf(integerPart.charAt(i)));
            int unitIndex = (length - i - 1) % 4;
            int bigUnitIndex = (length - i - 1) / 4;
            if (digit != 0) {
                result.append(digits[digit]).append(units[unitIndex]).append(bigUnits[bigUnitIndex]);
            } else {
                // 避免连续的零
                if (i > 0 && Integer.parseInt(String.valueOf(integerPart.charAt(i - 1))) != 0) {
                    result.append(digits[digit]);
                }
                // 只在非末尾位置添加大单位
                if (unitIndex == 0 && bigUnitIndex > 0 && i < length - 1) {
                    result.append(bigUnits[bigUnitIndex]);
                }
            }
        }
        result.append("元");
        // 处理小数部分
        int jiao = Integer.parseInt(String.valueOf(decimalPart.charAt(0)));
        int fen = Integer.parseInt(String.valueOf(decimalPart.charAt(1)));
        if (jiao == 0 && fen == 0) {
            result.append("整");
        } else if (jiao == 0) {
            result.append("零").append(digits[fen]).append("分");
        } else if (fen == 0) {
            result.append(digits[jiao]).append("角整");
        } else {
            result.append(digits[jiao]).append("角").append(digits[fen]).append("分");
        }
        return result.toString();
    }
    /**
     * 创建备注说明区域
     *
     * @param sheet 工作表
     * @param styles 样式映射
     * @param startRow 起始行
     * @param grandTotal 总金额
     */
    private void createNotesArea(SXSSFSheet sheet, Map<String, CellStyle> styles, int startRow, BigDecimal grandTotal) {
        // 创建无边界样式
        CellStyle noBorderStyle = sheet.getWorkbook().createCellStyle();
        // 创建两行
        for (int rowIdx = 0; rowIdx < 2; rowIdx++) {
            Row row = sheet.createRow(startRow + rowIdx);
            for (int i = 0; i <= 9; i++) {
                Cell cell = row.createCell(i);
                if (i == 0) {
                    // 第一列设置带边框的样式
                    cell.setCellStyle(styles.get("text2"));
                } else {
                    // 其他列设置无边界样式
                    cell.setCellStyle(noBorderStyle);
                }
            }
        }
        // 第一行备注
        Row firstRow = sheet.getRow(startRow);
        Cell cell1 = firstRow.getCell(0);
        String chineseAmount = convertToChineseUppercase(grandTotal);
        cell1.setCellValue("1.依据合同相关规定,贵司需向我司支付¥" + grandTotal.setScale(2, RoundingMode.HALF_UP) + "(大写金额:" + chineseAmount + ")的费用,请贵司及时安排支付。");
        // 第二行备注
        Row secondRow = sheet.getRow(startRow + 1);
        Cell cell2 = secondRow.getCell(0);
        cell2.setCellValue("2.传真件与原件起同等法律效力。");
    }
    /**
     * 创建账户信息和签字盖章区域
     *
     * @param sheet    工作表
     * @param styles   样式映射
     * @param startRow 起始行
     * @param bill
     */
    private void createAccountAndSignatureArea(SXSSFSheet sheet, Map<String, CellStyle> styles, int startRow, TmsArBill bill) {
        // 计算结束行(账户信息4行 + 签字盖章6行)
        int endRow = startRow + 9;
        // 创建所有行,设置行高
        for (int rowIdx = 0; rowIdx <= 9; rowIdx++) {
            Row row = sheet.createRow(startRow + rowIdx);
            row.setHeightInPoints(20);
            for (int i = 0; i <= 9; i++) {
                Cell cell = row.createCell(i);
                // 不设置边框样式,只显示数据
            }
        }
        // 账户信息区域(左上角)
        Row infoRow1 = sheet.getRow(startRow);
        Cell infoCell1 = infoRow1.getCell(0);
        infoCell1.setCellValue("烦请核对确认并转至我司如下帐号:");
        Row infoRow2 = sheet.getRow(startRow + 1);
        Cell infoCell2 = infoRow2.getCell(0);
        infoCell2.setCellValue("开户银行:中国农业发展银行珠海市分行");
        Row infoRow3 = sheet.getRow(startRow + 2);
        Cell infoCell3 = infoRow3.getCell(0);
        infoCell3.setCellValue("账号:20344990100000422001");
        Row infoRow4 = sheet.getRow(startRow + 3);
        Cell infoCell4 = infoRow4.getCell(0);
        infoCell4.setCellValue("户名:珠海市汇畅交通投资有限公司");
        // 付款单位(左下角)
        Row payerRow = sheet.getRow(startRow + 4);
        Cell payerCell = payerRow.getCell(0);
        payerCell.setCellValue("付款单位(甲方):" + bill.getCustomerName());
        // 收款单位(右下角)
        Row payeeRow = sheet.getRow(startRow + 4);
        Cell payeeCell = payeeRow.getCell(6);
        payeeCell.setCellValue("收款单位(乙方):珠海市汇畅交通投资有限公司");
        // 制表人员
        Row creatorRow = sheet.getRow(startRow + 5);
        Cell creatorCell1 = creatorRow.getCell(0);
        creatorCell1.setCellValue("制表人员:");
        Cell creatorCell2 = creatorRow.getCell(6);
        creatorCell2.setCellValue("制表人员:");
        // 审核人员
        Row checkerRow = sheet.getRow(startRow + 6);
        Cell checkerCell1 = checkerRow.getCell(0);
        checkerCell1.setCellValue("审核人员:");
        Cell checkerCell2 = checkerRow.getCell(6);
        checkerCell2.setCellValue("审核人员:");
        // 复核人员
        Row reviewerRow = sheet.getRow(startRow + 7);
        Cell reviewerCell1 = reviewerRow.getCell(0);
        reviewerCell1.setCellValue("复核人员:");
        Cell reviewerCell2 = reviewerRow.getCell(6);
        reviewerCell2.setCellValue("复核人员:");
        // 盖章
        Row stampRow = sheet.getRow(startRow + 8);
        Cell stampCell1 = stampRow.getCell(0);
        stampCell1.setCellValue("盖章:");
        Cell stampCell2 = stampRow.getCell(6);
        stampCell2.setCellValue("盖章:");
        // 日期
        Row dateRow = sheet.getRow(startRow + 9);
        Cell dateCell1 = dateRow.getCell(0);
        dateCell1.setCellValue("日期:2026年  月  日");
        Cell dateCell2 = dateRow.getCell(6);
        dateCell2.setCellValue("日期:2026年  月  日");
    }
    /**
     * 创建签字盖章区域
     *
     * @param sheet    工作表
     * @param styles   样式映射
     * @param startRow 起始行
     * @param bill
     */
    private void createSignatureArea(SXSSFSheet sheet, Map<String, CellStyle> styles, int startRow, TmsArBill bill) {
        // 付款单位(甲方)和收款单位(乙方)行
        Row payerRow = sheet.createRow(startRow);
        for (int i = 0; i <= 9; i++) {
            Cell cell = payerRow.createCell(i);
            if (i == 0) {
                cell.setCellValue("付款单位(甲方):"+bill.getCustomerName());
            } else if (i == 6) {
                cell.setCellValue("收款单位(乙方):珠海市汇畅交通投资有限公司");
            }
            cell.setCellStyle(styles.get("text"));
        }
        // 制表人员行
        Row creatorRow = sheet.createRow(startRow + 1);
        for (int i = 0; i <= 9; i++) {
            Cell cell = creatorRow.createCell(i);
            if (i == 0) {
                cell.setCellValue("制表人员:");
            } else if (i == 6) {
                cell.setCellValue("制表人员:");
            }
            cell.setCellStyle(styles.get("text"));
        }
        // 审核人员行
        Row checkerRow = sheet.createRow(startRow + 2);
        for (int i = 0; i <= 9; i++) {
            Cell cell = checkerRow.createCell(i);
            if (i == 0) {
                cell.setCellValue("审核人员:");
            } else if (i == 6) {
                cell.setCellValue("审核人员:");
            }
            cell.setCellStyle(styles.get("text"));
        }
        // 复核人员行
        Row reviewerRow = sheet.createRow(startRow + 3);
        for (int i = 0; i <= 9; i++) {
            Cell cell = reviewerRow.createCell(i);
            if (i == 0) {
                cell.setCellValue("复核人员:");
            } else if (i == 6) {
                cell.setCellValue("复核人员:");
            }
            cell.setCellStyle(styles.get("text"));
        }
        // 盖章行
        Row stampRow = sheet.createRow(startRow + 4);
        for (int i = 0; i <= 9; i++) {
            Cell cell = stampRow.createCell(i);
            if (i == 0) {
                cell.setCellValue("盖章:");
            } else if (i == 6) {
                cell.setCellValue("盖章:");
            }
            cell.setCellStyle(styles.get("text"));
        }
        // 日期行
        Row dateRow = sheet.createRow(startRow + 5);
        for (int i = 0; i <= 9; i++) {
            Cell cell = dateRow.createCell(i);
            if (i == 0) {
                cell.setCellValue("日期:2026年  月  日");
            } else if (i == 6) {
                cell.setCellValue("日期:2026年  月  日");
            }
            cell.setCellStyle(styles.get("text"));
        }
    }
}