jinch 1 settimana fa
parent
commit
cf2bc2e000

+ 57 - 4
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/order/vo/order/TouristExportVisitorVO.java

@@ -3,6 +3,8 @@ package com.yc.ship.module.trade.controller.admin.order.vo.order;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
+import java.math.BigDecimal;
+
 /**
  * 游客名单导出 - 游客信息VO
  */
@@ -10,11 +12,53 @@ import lombok.Data;
 @Data
 public class TouristExportVisitorVO {
 
+    // ==================== 订单详情 ====================
+    @Schema(description = "代理商")
+    private String sourceName;
+
+    @Schema(description = "订单号")
+    private String orderNo;
+
+    @Schema(description = "团号")
+    private String groupNo;
+
+    @Schema(description = "航向")
+    private String direction;
+
+    @Schema(description = "航期")
+    private String travelDate;
+
+    @Schema(description = "应收款")
+    private BigDecimal amount;
+
+    @Schema(description = "实收款")
+    private BigDecimal payAmount;
+
+    @Schema(description = "定金")
+    private BigDecimal deposi;
+
+    @Schema(description = "支付状态(1=已支付,其他=未支付)")
+    private Integer payStatus;
+
+    // ==================== 房间详情 ====================
+    @Schema(description = "房间索引ID(用于合并)")
+    private String roomIndexId;
+
+    @Schema(description = "序号(按订单分组)")
+    private Integer roomIndex;
+
+    @Schema(description = "房型")
+    private String roomType;
+
+    @Schema(description = "入住类型(房型描述)")
+    private String roomDescription;
+
+    // ==================== 游客详情 ====================
     @Schema(description = "游客姓名")
     private String name;
 
-    @Schema(description = "国籍名称")
-    private String nationalityName;
+    @Schema(description = "性别")
+    private Integer gender;
 
     @Schema(description = "证件类型")
     private Integer credentialType;
@@ -22,8 +66,17 @@ public class TouristExportVisitorVO {
     @Schema(description = "证件号")
     private String credentialNo;
 
-    @Schema(description = "生日")
-    private String birthday;
+    @Schema(description = "游客类型")
+    private String visitorType;
+
+    @Schema(description = "国籍名称")
+    private String nationalityName;
+
+    @Schema(description = "增值服务")
+    private String valueAddedService;
+
+    @Schema(description = "优惠政策")
+    private String policyName;
 
     @Schema(description = "备注")
     private String remark;

+ 224 - 38
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/otc/impl/OtcTradeOrderServiceImpl.java

@@ -2668,39 +2668,90 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
         return CommonResult.success("修改成功");
     }
 
+
+
+    private String getPersonTypeAll(String type) {
+        String des;
+        switch (type) {
+            case "leader":
+                des = "领队";
+                break;
+            case "with":
+                des = "陪同";
+                break;
+            case "childTake":
+            case "childPlus":
+            case "childNonTake":
+                des = "儿童";
+                break;
+            case "babyTake":
+            case "babyPlus":
+            case "babyNonTake":
+                des = "婴儿";
+                break;
+            case "adultTake":
+            case "adultPlus":
+            default:
+                des = "成人";
+                break;
+        }
+        return des;
+    }
+
+
+    /**
+     * 导出游客名单列表(按订单和房间分组)
+     *
+     * 功能说明:
+     * 1. 查询订单基础信息(船名、航期、航向)
+     * 2. 查询游客列表(包含订单、房间、游客详细信息)
+     * 3. 使用Excel模板进行数据填充
+     * 4. 按订单和房间自动合并单元格
+     *
+     * 导出结构:
+     * - 订单详情(8列):代理商、订单号、团号、航向、航期、应收款、实收款、定金
+     * - 房间详情(3列):序号、房型、入住类型
+     * - 游客详情(8列):姓名、性别、证件类型、证件号、游客类型、国籍、增值服务、备注
+     *
+     * @param reqVO 查询条件
+     * @return 导出的Excel文件
+     */
     @Override
     public File exportTouristList(TradeOrderPageReqVO reqVO) {
         // 1. 查询订单基础信息(船名、航期、航向)
         Map<String, Object> baseInfo = tradeOrderMapper.selectTouristExportBase(reqVO);
 
-        // 2. 查询游客列表
+        // 2. 查询游客列表(包含订单、房间、游客详细信息)
         List<TouristExportVisitorVO> visitorList = tradeVisitorMapper.selectTouristExportVisitor(reqVO);
 
-        // 3. 加载模板
+        // 3. 加载Excel模板
         InputStream template = getClass().getClassLoader().getResourceAsStream("templates/tourist_template.xlsx");
-        String fileName =  String.valueOf(System.currentTimeMillis());
+        String fileName = String.valueOf(System.currentTimeMillis());
         String tmpFile = "/tmp/" + fileName + "_tourist.xlsx";
         //String tmpFile = "D:/tmp/" + fileName + "_tourist.xlsx";
 
-        // 先不合并,直接导出
+        // 4. 准备ExcelWriter,注册样式处理器和合并策略
+        //    - ExcelStyleHandler: 设置单元格样式
+        //    - TouristListMergeStrategy: 按订单和房间自动合并单元格
         ExcelWriter excelWriter = EasyExcel.write(tmpFile).withTemplate(template).build();
-        WriteSheet writeSheet = EasyExcelFactory.writerSheet().registerWriteHandler(new ExcelStyleHandler(9)).build();
+        WriteSheet writeSheet = EasyExcelFactory.writerSheet()
+                .registerWriteHandler(new ExcelStyleHandler(visitorList))
+               /* .registerWriteHandler(new TouristListMergeStrategy(visitorList))*/
+                .build();
         FillConfig fillConfig = FillConfig.builder().forceNewRow(true).autoStyle(true).build();
 
-        // 4. 准备基础信息数据
+        // 5. 准备基础信息数据(填充到Excel表头区域)
         Map<String, Object> baseData = new HashMap<>();
 
         // 船名
         baseData.put("shipName", baseInfo != null && baseInfo.get("shipName") != null ? baseInfo.get("shipName") : "");
 
-        // 航期
-        // 航期
+        // 航期(格式化:yyyy.M.d)
         baseData.put("travelDate", baseInfo != null && baseInfo.get("travelDate") != null
                 ? ((LocalDateTime) baseInfo.get("travelDate")).format(DateTimeFormatter.ofPattern("yyyy.M.d"))
                 : "");
 
-
-        // 航向
+        // 航向(1=宜昌-重庆,其他=重庆-宜昌)
         Integer direction = baseInfo != null && baseInfo.get("direction") != null ? (Integer) baseInfo.get("direction") : 0;
         baseData.put("direction", direction != null && direction == 1 ? "宜昌-重庆" : "重庆-宜昌");
 
@@ -2709,6 +2760,7 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
         StringBuilder countryStat = new StringBuilder();
 
         if (visitorList != null && !visitorList.isEmpty()) {
+            // 按国籍分组统计人数
             Map<String, Long> countryMap = visitorList.stream()
                     .filter(item -> ObjectUtil.isNotEmpty(item) && StringUtils.isNotEmpty(item.getNationalityName()))
                     .collect(Collectors.groupingBy(
@@ -2716,6 +2768,7 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
                             Collectors.counting()
                     ));
 
+            // 拼接国籍统计字符串(格式:数量/国籍)
             for (Map.Entry<String, Long> entry : countryMap.entrySet()) {
                 if (countryStat.length() > 0) {
                     countryStat.append(" ");
@@ -2726,41 +2779,174 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
         baseData.put("totalCount", totalCount);
         baseData.put("countryStat", countryStat.toString());
 
-        // 5. 准备游客明细列表
+        // 6. 准备游客明细列表数据(按订单→房间→游客三级分组填充)
         List<Map<String, Object>> touristData = new ArrayList<>();
 
-        if (visitorList != null) {
-            for (int i = 0; i < visitorList.size(); i++) {
-                TouristExportVisitorVO visitor = visitorList.get(i);
-                Map<String, Object> item = new HashMap<>();
-                if (ObjectUtil.isEmpty(visitor)) {
-                    continue;
+        if (visitorList != null && !visitorList.isEmpty()) {
+            // 按订单号分组(使用 LinkedHashMap 保持 visitorList 的原始顺序)
+            Map<String, List<TouristExportVisitorVO>> orderGroupMap = visitorList.stream()
+                    .collect(Collectors.groupingBy(
+                            TouristExportVisitorVO::getOrderNo,
+                            LinkedHashMap::new,
+                            Collectors.toList()
+                    ));
+
+            // 按订单号计算 payAmount 总和
+            Map<String, BigDecimal> orderPayAmountMap = visitorList.stream()
+                    .collect(Collectors.groupingBy(
+                            TouristExportVisitorVO::getOrderNo,
+                            LinkedHashMap::new,
+                            Collectors.reducing(
+                                    BigDecimal.ZERO,
+                                    v -> v.getPayAmount() != null ? v.getPayAmount() : BigDecimal.ZERO,
+                                    BigDecimal::add
+                            )
+                    ));
+
+            // 房间序号从1开始累加
+            int roomIndex = 1;
+            for (List<TouristExportVisitorVO> orderVisitors : orderGroupMap.values()) {
+                // 按房间索引分组(同一房间的游客合并显示房间详情)
+                // 使用 LinkedHashMap 保持房间在 visitorList 中的出现顺序
+                Map<String, List<TouristExportVisitorVO>> roomGroupMap = orderVisitors.stream()
+                        .collect(Collectors.groupingBy(
+                                v -> v.getRoomIndexId() != null ? v.getRoomIndexId() : "",
+                                LinkedHashMap::new,
+                                Collectors.toList()
+                        ));
+
+
+                for (List<TouristExportVisitorVO> roomVisitors : roomGroupMap.values()) {
+                    // 准备房间入住类型描述(如:2个成人/1个儿童)
+                    String roomDescription = prepareRoomDescription(roomVisitors);
+
+                    // 为该房间的每个游客创建一行数据
+                    for (TouristExportVisitorVO visitor : roomVisitors) {
+                        Map<String, Object> item = new HashMap<>();
+
+                        // 订单详情(8列):同一订单的游客合并显示这些列
+                        item.put("sourceName", StringUtils.isEmpty(visitor.getSourceName()) ? "" : visitor.getSourceName()); // 代理商名称
+                        item.put("orderNo", StringUtils.isEmpty(visitor.getOrderNo()) ? "" : visitor.getOrderNo()); // 订单号
+                        item.put("groupNo", StringUtils.isEmpty(visitor.getGroupNo()) ? "" : visitor.getGroupNo()); // 团号
+                        item.put("direction", StringUtils.isEmpty(visitor.getDirection()) ? "" : visitor.getDirection()); // 航向(宜昌-重庆/重庆-宜昌)
+                        item.put("travelDate", StringUtils.isEmpty(visitor.getTravelDate()) ? "" : visitor.getTravelDate()); // 航期(格式:yyyy.M.d)
+                        item.put("amount", visitor.getAmount() != null ? visitor.getAmount() : BigDecimal.ZERO); // 应收款
+
+                        // 实收款计算逻辑:根据支付状态判断
+                        //Integer payStatus = visitor.getPayStatus(); // 支付状态
+                        //BigDecimal payAmount = visitor.getPayAmount() != null ? visitor.getPayAmount() : BigDecimal.ZERO; // 实际金额
+                        BigDecimal deposi = visitor.getDeposi() != null ? visitor.getDeposi() : BigDecimal.ZERO; // 定金
+                        BigDecimal applyPayAmount  = BigDecimal.ZERO;  // 实际收款
+
+                        item.put("payAmount", orderPayAmountMap.getOrDefault(visitor.getOrderNo(), BigDecimal.ZERO)); // 实收款
+
+                        item.put("deposi", deposi); // 定金
+
+                        // 房间详情(3列):同一房间的游客合并显示这些列
+                        item.put("roomIndex", roomIndex); // 序号(按订单内的房间顺序)
+                        item.put("roomType", StringUtils.isEmpty(visitor.getRoomType()) ? "" : visitor.getRoomType()); // 房型(如:豪华标准间)
+                        item.put("roomDescription", roomDescription); // 入住类型(如:2个成人/1个儿童)
+
+                        // 游客详情(8列):每个游客独立显示
+                        item.put("name", StringUtils.isEmpty(visitor.getName()) ? "" : visitor.getName()); // 游客姓名
+                        item.put("gender", visitor.getGender() != null ? (visitor.getGender() == 1 ? "男" : "女") : ""); // 性别(1=男,其他=女)
+                        item.put("credentialType", DictFrameworkUtils.getDictDataLabel(DictTypeConstants.VISITOR_CREDENTIAL_TYPE, visitor.getCredentialType())); // 证件类型(身份证/护照等)
+                        item.put("credentialNo", StringUtils.isEmpty(visitor.getCredentialNo()) ? "" : visitor.getCredentialNo()); // 证件号
+                        item.put("visitorType", StringUtils.isEmpty(visitor.getVisitorType()) ? "" : getPersonTypeAll(visitor.getVisitorType())); // 游客类型(成人/儿童/婴儿)
+                        item.put("nationalityName", StringUtils.isEmpty(visitor.getNationalityName()) ? "" : visitor.getNationalityName()); // 国籍(如:中国、美国)
+                        item.put("valueAddedService", StringUtils.isEmpty(visitor.getValueAddedService()) ? "" : formatPolicyName(visitor.getValueAddedService())); // 增值服务(如:接送站、保险等)
+                        item.put("policyName", ""); // 优惠政策(如:早鸟优惠、团立减等)
+                        item.put("remark", StringUtils.isEmpty(visitor.getRemark()) ? "" : visitor.getRemark()); // 备注信息
+
+                        touristData.add(item);
+                    }
+                    roomIndex++;
                 }
-                // 序号
-                item.put("xh", i + 1);
-                // 姓名
-                item.put("name", StringUtils.isEmpty(visitor.getName()) ? "" : visitor.getName());
-                // 国籍
-                item.put("nationalityName", StringUtils.isEmpty(visitor.getNationalityName()) ? "" : visitor.getNationalityName());
-                // 证件类型
-                item.put("credentialType", DictFrameworkUtils.getDictDataLabel(DictTypeConstants.VISITOR_CREDENTIAL_TYPE, visitor.getCredentialType()));
-                // 证件号
-                item.put("credentialNo", StringUtils.isEmpty(visitor.getCredentialNo()) ? "" : visitor.getCredentialNo());
-                // 出生年月
-                item.put("birthday", StringUtils.isEmpty(visitor.getBirthday()) ? "" : visitor.getBirthday());
-                // 备注
-                item.put("remark", StringUtils.isEmpty(visitor.getRemark()) ? "" : visitor.getRemark());
-
-                touristData.add(item);
-            }
-        }
-
-        // 6. 填充数据
-        excelWriter.fill(new FillWrapper("visitor", touristData),fillConfig, writeSheet);
+            }
+        }
+
+        // 7. 填充数据到Excel模板
+        //    先填充游客明细列表数据,再填充基础信息
+        excelWriter.fill(new FillWrapper("visitor", touristData), fillConfig, writeSheet);
         excelWriter.fill(baseData, writeSheet);
         excelWriter.finish();
 
         return new File(tmpFile);
     }
 
+    /**
+     * 准备房间入住类型描述
+     *
+     * 功能说明:
+     * 统计房间内各类型游客的数量,生成入住类型描述字符串
+     *
+     * 示例:
+     * - 2个成人 → "2个成人"
+     * - 1个成人/1个儿童 → "1个成人/1个儿童"
+     * - 2个成人/1个儿童/1个婴儿 → "2个成人/1个儿童/1个婴儿"
+     *
+     * @param roomVisitors 同一房间的游客列表
+     * @return 入住类型描述字符串
+     */
+    private String prepareRoomDescription(List<TouristExportVisitorVO> roomVisitors) {
+        if (roomVisitors == null || roomVisitors.isEmpty()) {
+            return "";
+        }
+
+        // 按游客类型统计数量
+        Map<String, Long> typeCountMap = roomVisitors.stream()
+                .filter(v -> StringUtils.isNotEmpty(v.getVisitorType()))
+                .collect(Collectors.groupingBy(TouristExportVisitorVO::getVisitorType, Collectors.counting()));
+
+        StringBuilder sb = new StringBuilder();
+        for (Map.Entry<String, Long> entry : typeCountMap.entrySet()) {
+            if (sb.length() > 0) {
+                sb.append("/");
+            }
+            sb.append(entry.getValue()).append("个").append(getPersonTypeDes1(entry.getKey()));
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * 格式化优惠政策名称
+     *
+     * 功能说明:
+     * 将优惠政策字符串按 "、" 分割后,添加序号并用逗号连接
+     *
+     * 示例:
+     * - 输入:null 或 "" → 输出:""
+     * - 输入:"升舱礼遇2升3" → 输出:"①升舱礼遇2升3"
+     * - 输入:"升舱礼遇2升3、3升4、5升6" → 输出:"①升舱礼遇2升3"
+     *
+     * @param policyName 原始优惠政策字符串
+     * @return 格式化后的优惠政策字符串
+     */
+    private String formatPolicyName(String policyName) {
+        if (StringUtils.isEmpty(policyName)) {
+            return "";
+        }
+
+        // 按 "," 分割
+        String[] policies = policyName.split(",");
+        if (policies.length == 0) {
+            return "";
+        }
+
+        // 序号字符:①②③④⑤⑥⑦⑧⑨⑩...
+        String[] numberSymbols = {"①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨", "⑩", "⑪", "⑫", "⑬", "⑭", "⑮"};
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < policies.length; i++) {
+            if (sb.length() > 0) {
+                sb.append("\n");
+            }
+            // 如果序号超过预定义范围,使用普通数字编号
+            String prefix = i < numberSymbols.length ? numberSymbols[i] : (i + 1) + ".";
+            sb.append(prefix).append(policies[i].trim());
+        }
+
+        return sb.toString();
+    }
 }

+ 177 - 3
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/utils/excel/ExcelStyleHandler.java

@@ -1,16 +1,20 @@
 package com.yc.ship.module.trade.utils.excel;
 
+
 import com.alibaba.excel.write.handler.CellWriteHandler;
 import com.alibaba.excel.write.handler.context.CellWriteHandlerContext;
+import com.yc.ship.module.trade.controller.admin.order.vo.order.TouristExportVisitorVO;
 import org.apache.poi.ss.usermodel.*;
 import org.apache.poi.ss.util.CellRangeAddress;
+import org.apache.poi.ss.util.RegionUtil;
 
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
 public class ExcelStyleHandler implements CellWriteHandler {
-    private final int templateRowIndex;
+    private int templateRowIndex;
+
     private final Map<Integer, CellStyle> templateStyles = new HashMap<>();
 
     // 保存模板行的合并信息:列索引 -> [起始列, 结束列]
@@ -18,12 +22,35 @@ public class ExcelStyleHandler implements CellWriteHandler {
 
     private boolean templateProcessed = false;
 
+    /** 游客列表数据 */
+    private List<TouristExportVisitorVO> visitorList;
+
+    /** 数据起始行索引 */
+    private static final int DATA_START_ROW = 8;
+
+    /** 记录已处理的单元格数量 */
+    private int processedCellCount = 0;
+
+    /** 总数据行数 */
+    private int totalRows = 0;
+
+    /** 每行数据列数(根据实际Excel模板列数调整) */
+    private static final int TOTAL_COLUMNS = 18;
+
+    /** 是否已经执行过合并 */
+    private boolean mergeCompleted = false;
+
     public ExcelStyleHandler(int templateRowIndex) {
         this.templateRowIndex = templateRowIndex;
     }
 
-    @Override
-    public void afterCellDispose(CellWriteHandlerContext context) {
+    public ExcelStyleHandler(List<TouristExportVisitorVO> visitorList) {
+        this.visitorList = visitorList;
+        this.totalRows = visitorList != null ? visitorList.size() : 0;
+    }
+
+
+    public void afterCellDispose1(CellWriteHandlerContext context) {
         if (Boolean.TRUE.equals(context.getHead())) {
             return;
         }
@@ -151,4 +178,151 @@ public class ExcelStyleHandler implements CellWriteHandler {
             }
         }
     }
+
+    /**
+     * 单元格写入完成后执行合并
+     * 此方法在每个单元格写入完成后检查是否是最后一个单元格,如果是则统一处理合并
+     *
+     * @param context 单元格写入上下文
+     */
+    @Override
+    public void afterCellDispose(CellWriteHandlerContext context) {
+        // 只处理游客列表场景
+        if (visitorList == null || visitorList.isEmpty()) {
+            return;
+        }
+
+        // 跳过表头行
+        if (Boolean.TRUE.equals(context.getHead())) {
+            return;
+        }
+
+        // 统计处理的单元格数量
+        processedCellCount++;
+
+        System.out.println("processedCellCount: " + processedCellCount);
+        // 当最后一个单元格写入完成后,执行合并
+        int totalCells = totalRows * TOTAL_COLUMNS;
+        if (!mergeCompleted && processedCellCount >= totalCells) {
+            mergeCompleted = true;
+            performMerge(context.getWriteSheetHolder().getSheet());
+        }
+    }
+
+    /**
+     * 执行合并操作
+     * 此方法在数据写入完成后统一处理合并,避免逐行合并时的磁盘刷新问题
+     *
+     * 合并规则:
+     * 1. 第0列(代理商):按 sourceName 合并
+     * 2. 第1-7列(订单信息):按 orderNo 合并
+     * 3. 第8-10列(房间信息):按 roomIndexId 合并(同一订单内)
+     * 4. 横向不需要合并(按列单独合并)
+     *
+     * @param sheet Excel工作表
+     */
+    private void performMerge(Sheet sheet) {
+        // 遍历游客列表,从第二行开始与上一行比较
+        for (int i = 1; i < visitorList.size(); i++) {
+            TouristExportVisitorVO currentVisitor = visitorList.get(i);
+            TouristExportVisitorVO preVisitor = visitorList.get(i - 1);
+
+            int currentRowIndex = DATA_START_ROW + i;
+            int preRowIndex = DATA_START_ROW + i - 1;
+
+            // ===== 第0列(代理商):按 sourceName 合并 =====
+            if (currentVisitor.getSourceName() != null && preVisitor.getSourceName() != null
+                    && currentVisitor.getSourceName().equals(preVisitor.getSourceName())) {
+                mergeCell(sheet, currentRowIndex, preRowIndex, 1, 1);
+            }
+
+            // ===== 第1-7列(订单信息):按 orderNo 合并 =====
+            // 同一订单的所有游客合并显示订单信息
+            if (currentVisitor.getOrderNo() != null
+                    && currentVisitor.getOrderNo().equals(preVisitor.getOrderNo())) {
+                // 订单号列(第1列)
+                mergeCell(sheet, currentRowIndex, preRowIndex, 2, 2);
+                // 团号列(第2列)
+                mergeCell(sheet, currentRowIndex, preRowIndex, 3, 3);
+                // 航向列(第3列)
+                mergeCell(sheet, currentRowIndex, preRowIndex, 4, 4);
+                // 航期列(第4列)
+                mergeCell(sheet, currentRowIndex, preRowIndex, 5, 5);
+                // 应收款列(第5列)
+                mergeCell(sheet, currentRowIndex, preRowIndex, 6, 6);
+                // 实收款列(第6列)
+                mergeCell(sheet, currentRowIndex, preRowIndex, 7, 7);
+                // 定金列(第7列)
+                mergeCell(sheet, currentRowIndex, preRowIndex, 8, 8);
+            }
+
+            // ===== 第8-10列(房间信息):按 roomIndexId 合并(同一订单内) =====
+            String currentRoomIndex = currentVisitor.getRoomIndexId() != null ? currentVisitor.getRoomIndexId() : "";
+            String preRoomIndex = preVisitor.getRoomIndexId() != null ? preVisitor.getRoomIndexId() : "";
+            if (currentRoomIndex.equals(preRoomIndex)
+                    && currentVisitor.getOrderNo() != null
+                    && currentVisitor.getOrderNo().equals(preVisitor.getOrderNo())) {
+                // 序号列(第8列)
+                mergeCell(sheet, currentRowIndex, preRowIndex, 9, 9);
+                // 房型列(第9列)
+                mergeCell(sheet, currentRowIndex, preRowIndex, 10, 10);
+                // 入住类型列(第10列)
+                mergeCell(sheet, currentRowIndex, preRowIndex, 11, 11);
+            }
+        }
+    }
+
+    /**
+     * 合并单元格并设置边框
+     *
+     * 功能说明:
+     * - 如果目标单元格已存在合并区域,则扩展合并区域
+     * - 如果目标单元格未合并,则创建新的合并区域
+     * - 设置合并单元格的边框样式
+     *
+     * @param sheet Excel工作表
+     * @param currentRow 当前行索引
+     * @param preRow 上一行索引
+     * @param startCol 起始列索引
+     * @param endCol 结束列索引
+     */
+    private void mergeCell(Sheet sheet, int currentRow, int preRow, int startCol, int endCol) {
+        List<CellRangeAddress> mergedRegions = sheet.getMergedRegions();
+
+        // 检查上一行是否已存在合并区域
+        for (int i = 0; i < mergedRegions.size(); i++) {
+            CellRangeAddress region = mergedRegions.get(i);
+            // 如果合并区域包含上一行的指定列
+            if (region.getFirstRow() <= preRow && region.getLastRow() >= preRow
+                    && region.getFirstColumn() == startCol && region.getLastColumn() == endCol) {
+                // 扩展合并区域
+                sheet.removeMergedRegion(i);
+                CellRangeAddress newRegion = new CellRangeAddress(region.getFirstRow(), currentRow, startCol, endCol);
+                sheet.addMergedRegion(newRegion);
+                // setBorder(newRegion, sheet);
+                return;
+            }
+        }
+
+        // 如果没有找到已存在的合并区域,创建新的合并区域
+        CellRangeAddress newRegion = new CellRangeAddress(preRow, currentRow, startCol, endCol);
+        sheet.addMergedRegion(newRegion);
+        // setBorder(newRegion, sheet);
+    }
+
+    /**
+     * 设置合并单元格边框
+     *
+     * 功能说明:
+     * 为合并区域设置细边框样式,使合并后的单元格显示边框
+     *
+     * @param cra 合并区域对象
+     * @param sheet Excel工作表
+     */
+    private void setBorder(CellRangeAddress cra, Sheet sheet) {
+        RegionUtil.setBorderBottom(BorderStyle.THIN, cra, sheet);
+        RegionUtil.setBorderLeft(BorderStyle.THIN, cra, sheet);
+        RegionUtil.setBorderRight(BorderStyle.THIN, cra, sheet);
+        RegionUtil.setBorderTop(BorderStyle.THIN, cra, sheet);
+    }
 }

+ 2 - 2
ship-module-trade/ship-module-trade-biz/src/main/resources/mapper/order/TradeOrderMapper.xml

@@ -2429,10 +2429,10 @@
             </foreach>
         </if>
         <if test="vo.contactName != null and vo.contactName != ''">
-            AND tor.contact_name LIKE CONCAT('%', #{vo.contactName}, '%')
+            AND tor.link_man LIKE CONCAT('%', #{vo.contactName}, '%')
         </if>
         <if test="vo.mobile != null and vo.mobile != ''">
-            AND tor.mobile = #{vo.mobile}
+            AND tor.link_mobile = #{vo.mobile}
         </if>
         LIMIT 1
     </select>

+ 28 - 11
ship-module-trade/ship-module-trade-biz/src/main/resources/mapper/order/TradeVisitorMapper.xml

@@ -248,19 +248,35 @@
     <!-- 查询游客名单导出游客列表 -->
     <select id="selectTouristExportVisitor" resultType="com.yc.ship.module.trade.controller.admin.order.vo.order.TouristExportVisitorVO">
         SELECT
-        tv.name,
-        a.name as nationalityName,
-        tv.credential_type as credentialType,
-        tv.credential_no as credentialNo,
-        tv.birthday,
-        tor.remark
+            od.name AS sourceName,
+            tor.order_no AS orderNo,
+            tor.group_no AS groupNo,
+            CASE WHEN rr.direction = 1 THEN '上水' ELSE '下水' END AS direction,
+            DATE_FORMAT(pv.start_time, '%Y.%m.%d') AS travelDate,
+            tor.pay_amount AS amount,
+            top.pay_amount AS payAmount,
+            tor.deposi,
+            tv.room_index_id AS roomIndexId,
+            torm.room_model_name AS roomType,
+            tv.name,
+            tv.gender,
+            tv.credential_type AS credentialType,
+            tv.credential_no AS credentialNo,
+            tv.type AS visitorType,
+            a.name AS nationalityName,
+            GROUP_CONCAT(ps.product_name) AS valueAddedService,
+            tor.remark
         FROM trade_visitor tv
         INNER JOIN trade_order tor ON tv.order_id = tor.id AND tor.deleted = 0
-        INNER JOIN trade_detail td ON tv.detail_id = td.id AND td.deleted = 0 AND td.product_type = 0
+        INNER JOIN trade_detail td ON tv.id = td.visitor_id AND td.deleted = 0
         LEFT JOIN area a ON tv.nationality = a.id
         LEFT JOIN product_voyage pv ON tor.voyage_id = pv.id
-        LEFT JOIN ota_distributor od on od.id = tor.source_id
+        LEFT JOIN resource_route rr ON pv.route_id = rr.id
+        LEFT JOIN ota_distributor od ON tor.source_id = od.id
         LEFT JOIN trade_order_room_model torm ON tv.room_index_id = torm.room_index_id AND torm.deleted = 0
+        left JOIN product_spu ps on ps.id = td.product_id
+        left join trade_order_pay top ON top.order_id = tor.id and top.deleted = 0 and top.pay_status = 1
+
         WHERE tv.deleted = 0
 
             <if test="vo.orderNo != null and vo.orderNo != ''">
@@ -318,11 +334,12 @@
             </foreach>
         </if>
         <if test="vo.contactName != null and vo.contactName != ''">
-            AND tor.contact_name LIKE CONCAT('%', #{vo.contactName}, '%')
+            AND tor.link_man LIKE CONCAT('%', #{vo.contactName}, '%')
         </if>
         <if test="vo.mobile != null and vo.mobile != ''">
-            AND tor.mobile = #{vo.mobile}
+            AND tor.link_mobile = #{vo.mobile}
         </if>
-        ORDER BY tor.order_no ASC, tv.id ASC
+        group by tv.id
+        ORDER BY od.id ASC,tor.order_no ASC, tv.room_index_id ASC
     </select>
 </mapper>

BIN
ship-module-trade/ship-module-trade-biz/src/main/resources/templates/tourist_template.xlsx