Przeglądaj źródła

Merge branch 'main' of http://47.98.207.247:3000/lsq/ship-ota-server

# Conflicts:
#	ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/order/impl/TradeOrderRepositoryServiceImpl.java
lishiqiang 3 tygodni temu
rodzic
commit
383064ff13
22 zmienionych plików z 1012 dodań i 18 usunięć
  1. 1 0
      ship-module-product/ship-module-product-biz/src/main/java/com/yc/ship/module/product/controller/admin/voyagestockdetail/VoyageStockDetailController.java
  2. 6 0
      ship-module-product/ship-module-product-biz/src/main/java/com/yc/ship/module/product/controller/admin/voyagestockdetail/vo/VoyageStockDetailNewRespVO.java
  3. 2 0
      ship-module-product/ship-module-product-biz/src/main/java/com/yc/ship/module/product/controller/admin/voyagestockdetail/vo/VoyageStockDetailSaveReqVO.java
  4. 4 0
      ship-module-product/ship-module-product-biz/src/main/java/com/yc/ship/module/product/dal/dataobject/voyagestockdetail/VoyageStockDetailDO.java
  5. 1 0
      ship-module-product/ship-module-product-biz/src/main/java/com/yc/ship/module/product/dal/mysql/voyagestockdistribute/VoyageStockDistributeNewMapper.java
  6. 8 1
      ship-module-product/ship-module-product-biz/src/main/java/com/yc/ship/module/product/service/voyagestockdetail/VoyageStockDetailServiceImpl.java
  7. 30 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/report/KanbanBoardController.java
  8. 23 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/report/vo/VoyageDataSummaryReqVO.java
  9. 108 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/report/vo/VoyageDataSummaryRespVO.java
  10. 27 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/report/VoyageStockBoardMapper.java
  11. 9 2
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/order/impl/TradeOrderRepositoryServiceImpl.java
  12. 17 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/report/VoyageStockBoardService.java
  13. 554 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/report/impl/VoyageStockBoardServiceImpl.java
  14. 11 5
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/utils/IdCardProvinceUtil.java
  15. 112 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/utils/excel/VoyageDataSummaryExportStyleHandler.java
  16. 1 1
      ship-module-trade/ship-module-trade-biz/src/main/resources/mapper/order/TradeOrderMapper.xml
  17. 1 1
      ship-module-trade/ship-module-trade-biz/src/main/resources/mapper/order/TradeVisitorMapper.xml
  18. 2 2
      ship-module-trade/ship-module-trade-biz/src/main/resources/mapper/orderjzdetail/OrderGiftVisitorMapper.xml
  19. 2 2
      ship-module-trade/ship-module-trade-biz/src/main/resources/mapper/orderjzdetail/OrderJzDetailMapper.xml
  20. 4 0
      ship-module-trade/ship-module-trade-biz/src/main/resources/mapper/orderpolicy/OrderPolicyMapper.xml
  21. 85 0
      ship-module-trade/ship-module-trade-biz/src/main/resources/mapper/report/VoyageStockBoardMapper.xml
  22. 4 4
      ship-module-trade/ship-module-trade-biz/src/main/resources/mapper/report/YangtzePassengerSummaryMapper.xml

+ 1 - 0
ship-module-product/ship-module-product-biz/src/main/java/com/yc/ship/module/product/controller/admin/voyagestockdetail/VoyageStockDetailController.java

@@ -202,6 +202,7 @@ public class VoyageStockDetailController {
         sumDetailNewResp.setLockNum(filterList.stream().map(VoyageStockDetailNewRespVO::getLockNum).filter(java.util.Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
         sumDetailNewResp.setOtherNum(filterList.stream().map(VoyageStockDetailNewRespVO::getOtherNum).filter(java.util.Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
         sumDetailNewResp.setAssignRoomNum(filterList.stream().map(VoyageStockDetailNewRespVO::getAssignRoomNum).filter(java.util.Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
+        sumDetailNewResp.setAssignSurplusNum(filterList.stream().map(VoyageStockDetailNewRespVO::getAssignSurplusNum).filter(java.util.Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
         list.add(detailNewRespVO);
         list.add(sumDetailNewResp);
         list.stream().forEach(detail -> {

+ 6 - 0
ship-module-product/ship-module-product-biz/src/main/java/com/yc/ship/module/product/controller/admin/voyagestockdetail/vo/VoyageStockDetailNewRespVO.java

@@ -96,4 +96,10 @@ public class VoyageStockDetailNewRespVO {
      * 指定房间数
      */
     private BigDecimal assignRoomNum;
+    /**
+     * 指定剩余房间数
+     */
+    private BigDecimal assignSurplusNum;
+
+    private String remark;
 }

+ 2 - 0
ship-module-product/ship-module-product-biz/src/main/java/com/yc/ship/module/product/controller/admin/voyagestockdetail/vo/VoyageStockDetailSaveReqVO.java

@@ -56,6 +56,8 @@ public class VoyageStockDetailSaveReqVO {
     @Schema(description = "共享房间数")
     @ExcelProperty("共享房间数")
     private BigDecimal shareNum;
+    @Schema(description = "备注")
+    private String remark;
 
 
 }

+ 4 - 0
ship-module-product/ship-module-product-biz/src/main/java/com/yc/ship/module/product/dal/dataobject/voyagestockdetail/VoyageStockDetailDO.java

@@ -93,4 +93,8 @@ public class VoyageStockDetailDO extends TenantBaseDO {
      */
     private BigDecimal shareNum;
 
+    /**
+     * 备注
+     */
+    private String remark;
 }

+ 1 - 0
ship-module-product/ship-module-product-biz/src/main/java/com/yc/ship/module/product/dal/mysql/voyagestockdistribute/VoyageStockDistributeNewMapper.java

@@ -64,6 +64,7 @@ public interface VoyageStockDistributeNewMapper extends BaseMapperX<VoyageStockD
     @Select("SELECT" +
             "    SUM(p.book_num + p.num) AS totalNum," +
             "    p.room_model_id as roomModelId," +
+            "    p.num," +
             "    p.floor as floor " +
             "FROM " +
             "    product_voyage_stock_distribute_new p " +

+ 8 - 1
ship-module-product/ship-module-product-biz/src/main/java/com/yc/ship/module/product/service/voyagestockdetail/VoyageStockDetailServiceImpl.java

@@ -222,17 +222,24 @@ public class VoyageStockDetailServiceImpl implements VoyageStockDetailService {
             } else {
                 item.setRoomNum(BigDecimal.ZERO);
             }
-            // 获取指定房间数
+            // 获取指定房间数和剩余房间数
             Map<String, Object> assignRoomData = assignRoomNumMap.get(key);
             if (assignRoomData != null) {
                 Object assignRoomNum = assignRoomData.get("totalNum");
+                Object assignSurplusNum= assignRoomData.get("num");
                 if (assignRoomNum != null) {
                     item.setAssignRoomNum(new BigDecimal(assignRoomNum.toString()));
                 } else {
                     item.setAssignRoomNum(BigDecimal.ZERO);
                 }
+                if(assignSurplusNum!=null){
+                    item.setAssignSurplusNum(new BigDecimal(assignSurplusNum.toString()));
+                }else{
+                    item.setAssignSurplusNum(BigDecimal.ZERO);
+                }
             } else {
                 item.setAssignRoomNum(BigDecimal.ZERO);
+                item.setAssignSurplusNum(BigDecimal.ZERO);
             }
             // 设置留位、锁位、其他数量
             Map<String, Object> lockLeaveData = lockLeaveMap.get(key);

+ 30 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/report/KanbanBoardController.java

@@ -221,4 +221,34 @@ public class KanbanBoardController {
                                                       HttpServletResponse response) throws IOException {
         voyageStockBoardService.exportFinancialRevenueDashboardExcel(reqVO, response);
     }
+
+    // ==================== 航次数据情况汇总接口 ====================
+
+    /**
+     * 查询航次数据情况汇总
+     *
+     * @param reqVO 查询条件
+     * @return 航次数据情况汇总(多航次合并为一个结果)
+     */
+    @GetMapping("/voyageDataSummary")
+    @Operation(summary = "查询航次数据情况汇总")
+    public CommonResult<VoyageDataSummaryRespVO> getVoyageDataSummary(@Valid VoyageDataSummaryReqVO reqVO) {
+        VoyageDataSummaryRespVO vo = voyageStockBoardService.getVoyageDataSummary(reqVO);
+        return success(vo);
+    }
+
+    /**
+     * 导出航次数据情况汇总 Excel
+     *
+     * @param reqVO    查询条件
+     * @param response HTTP响应
+     * @throws IOException IO异常
+     */
+    @GetMapping("/voyageDataSummary/export-excel")
+    @Operation(summary = "导出航次数据情况汇总 Excel")
+    @ApiAccessLog(operateType = EXPORT)
+    public void exportVoyageDataSummaryExcel(@Valid VoyageDataSummaryReqVO reqVO,
+                                              HttpServletResponse response) throws IOException {
+        voyageStockBoardService.exportVoyageDataSummaryExcel(reqVO, response);
+    }
 }

+ 23 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/report/vo/VoyageDataSummaryReqVO.java

@@ -0,0 +1,23 @@
+package com.yc.ship.module.trade.controller.admin.report.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+/**
+ * 航次数据情况汇总 Request VO
+ */
+@Schema(description = "管理后台 - 航次数据情况汇总 Request VO")
+@Data
+public class VoyageDataSummaryReqVO {
+
+    @Schema(description = "游轮ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "游轮ID不能为空")
+    private Long shipId;
+
+    @Schema(description = "航次ID列表", example = "[1,2,3]")
+    private List<Long> voyageIds;
+
+}

+ 108 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/report/vo/VoyageDataSummaryRespVO.java

@@ -0,0 +1,108 @@
+package com.yc.ship.module.trade.controller.admin.report.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 航次数据情况汇总 Response VO
+ */
+@Schema(description = "管理后台 - 航次数据情况汇总 Response VO")
+@Data
+public class VoyageDataSummaryRespVO {
+
+    @Schema(description = "航次ID")
+    private Long voyageId;
+
+    @Schema(description = "航次名称")
+    private String voyageName;
+
+    @Schema(description = "游轮名称")
+    private String shipName;
+
+    @Schema(description = "航向(上水/下水)")
+    private String direction;
+
+    @Schema(description = "总人数")
+    private Integer totalPeople;
+
+    @Schema(description = "总房间数")
+    private BigDecimal totalRooms;
+
+    @Schema(description = "男性人数")
+    private Integer maleCount;
+
+    @Schema(description = "女性人数")
+    private Integer femaleCount;
+
+    @Schema(description = "男女比例,如 44%:54%")
+    private String maleFemaleRatio;
+
+    @Schema(description = "境内人数")
+    private Integer domesticCount;
+
+    @Schema(description = "境外人数")
+    private Integer overseasCount;
+
+    @Schema(description = "境内外比例,如 46%:54%")
+    private String domesticOverseasRatio;
+
+    @Schema(description = "房型预定列表")
+    private List<DimensionItemVO> roomTypeList;
+
+    @Schema(description = "年龄区间列表")
+    private List<DimensionItemVO> ageGroupList;
+
+    @Schema(description = "境内省份列表")
+    private List<DimensionItemVO> domesticProvinceList;
+
+    @Schema(description = "境外国家/区域列表")
+    private List<DimensionItemVO> internationalRegionList;
+
+    @Schema(description = "团队人数")
+    private Integer teamCount;
+
+    @Schema(description = "散客人数")
+    private Integer individualCount;
+
+    @Schema(description = "团散占比,如 44%:39%")
+    private String teamIndividualRatio;
+
+    @Schema(description = "航次信息列表(多航次时用于展示)")
+    private List<VoyageInfoVO> voyageInfoList;
+
+    /**
+     * 航次信息 VO
+     */
+    @Schema(description = "航次信息")
+    @Data
+    public static class VoyageInfoVO {
+        @Schema(description = "航次ID")
+        private Long voyageId;
+        @Schema(description = "航次名称")
+        private String voyageName;
+        @Schema(description = "航向")
+        private String direction;
+    }
+
+    /**
+     * 统计维度单项 VO
+     */
+    @Schema(description = "统计维度单项")
+    @Data
+    public static class DimensionItemVO {
+
+        @Schema(description = "名称")
+        private String name;
+
+        @Schema(description = "数量")
+        private Integer count;
+
+        @Schema(description = "占比,如 59.31%")
+        private String ratio;
+
+    }
+
+}

+ 27 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/report/VoyageStockBoardMapper.java

@@ -116,4 +116,31 @@ public interface VoyageStockBoardMapper {
      * @return 代理商付款列表
      */
     List<FinancialRevenueDashboardRespVO.AgentPaymentItemVO> selectPartialPaidAgentList(@Param("vo") FinancialRevenueDashboardReqVO reqVO);
+
+    /**
+     * 查询航次数据汇总所需的游客详细信息
+     *
+     * @param shipId    游轮ID
+     * @param voyageIds 航次ID列表
+     * @return 游客详细信息列表
+     */
+    List<Map<String, Object>> selectVoyageDataSummaryVisitorList(@Param("shipId") Long shipId, @Param("voyageIds") List<Long> voyageIds);
+
+    /**
+     * 查询航次数据汇总所需的房型信息
+     *
+     * @param shipId    游轮ID
+     * @param voyageIds 航次ID列表
+     * @return 房型信息列表
+     */
+    List<Map<String, Object>> selectVoyageDataSummaryRoomList(@Param("shipId") Long shipId, @Param("voyageIds") List<Long> voyageIds);
+
+    /**
+     * 查询航次基本信息
+     *
+     * @param shipId    游轮ID
+     * @param voyageIds 航次ID列表
+     * @return 航次基本信息列表
+     */
+    List<Map<String, Object>> selectVoyageBaseInfoList(@Param("shipId") Long shipId, @Param("voyageIds") List<Long> voyageIds);
 }

+ 9 - 2
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/order/impl/TradeOrderRepositoryServiceImpl.java

@@ -795,8 +795,15 @@ public class TradeOrderRepositoryServiceImpl implements TradeOrderRepositoryServ
         Page<TradeDetailDO> page = new Page<>(1, 1);
         LambdaQueryWrapper<TradeDetailDO> queryWrapper = new LambdaQueryWrapper<>();
         queryWrapper.eq(TradeDetailDO::getOrderId, orderId);
-        List<TradeDetailDO> list = tradeDetailMapper.selectPage(page, queryWrapper).getRecords();
-        return list!= null && !list.isEmpty() ? list.get(0) : null;
+        List<TradeDetailDO> records = tradeDetailMapper.selectPage(page, queryWrapper).getRecords();
+        if(CollUtil.isNotEmpty( records)){
+            return records.get(0);
+        }else {
+            TradeDetailDO tradeDetailDO = new TradeDetailDO();
+            tradeDetailDO.setProductId(0L);
+            tradeDetailDO.setProductName("订单:"+orderId+"定金支付");
+            return tradeDetailDO;
+        }
     }
 
     @Override

+ 17 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/report/VoyageStockBoardService.java

@@ -119,4 +119,21 @@ public interface VoyageStockBoardService {
      * @throws IOException IO异常
      */
     void exportFinancialRevenueDashboardExcel(@Valid FinancialRevenueDashboardReqVO reqVO, HttpServletResponse response) throws IOException;
+
+    /**
+     * 查询航次数据情况汇总(多航次合并为一个结果)
+     *
+     * @param reqVO 查询条件
+     * @return 航次数据情况汇总
+     */
+    VoyageDataSummaryRespVO getVoyageDataSummary(@Valid VoyageDataSummaryReqVO reqVO);
+
+    /**
+     * 导出航次数据情况汇总 Excel
+     *
+     * @param reqVO    查询条件
+     * @param response HTTP响应
+     * @throws IOException IO异常
+     */
+    void exportVoyageDataSummaryExcel(@Valid VoyageDataSummaryReqVO reqVO, HttpServletResponse response) throws IOException;
 }

+ 554 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/report/impl/VoyageStockBoardServiceImpl.java

@@ -20,6 +20,7 @@ import com.yc.ship.module.trade.dal.mysql.report.VoyageStockBoardMapper;
 import com.yc.ship.module.trade.service.report.VoyageStockBoardService;
 import com.yc.ship.module.trade.utils.IdCardProvinceUtil;
 import com.yc.ship.module.trade.utils.excel.AllVoyageStockBoardExportStyleHandler;
+import com.yc.ship.module.trade.utils.excel.VoyageDataSummaryExportStyleHandler;
 import com.yc.ship.module.trade.utils.excel.VoyageStockBoardExportStyleHandler;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -1405,4 +1406,557 @@ public class VoyageStockBoardServiceImpl implements VoyageStockBoardService {
 
         return result;
     }
+
+    // ==================== 航次数据情况汇总(多航次合并为一个结果) ====================
+
+    @Override
+    public VoyageDataSummaryRespVO getVoyageDataSummary(VoyageDataSummaryReqVO reqVO) {
+        // 1. 查询基础数据(不分航次,一次性查所有)
+        List<Map<String, Object>> visitorList = voyageStockBoardMapper.selectVoyageDataSummaryVisitorList(
+                reqVO.getShipId(), reqVO.getVoyageIds());
+        List<Map<String, Object>> roomList = voyageStockBoardMapper.selectVoyageDataSummaryRoomList(
+                reqVO.getShipId(), reqVO.getVoyageIds());
+        List<Map<String, Object>> voyageBaseList = voyageStockBoardMapper.selectVoyageBaseInfoList(reqVO.getShipId(), reqVO.getVoyageIds());
+
+        if (CollUtil.isEmpty(voyageBaseList)) {
+            return buildEmptyVoyageDataSummaryVO(reqVO.getShipId());
+        }
+
+        // 2. 构建合并后的汇总VO
+        return buildMergedVoyageDataSummaryVO(voyageBaseList, visitorList, roomList);
+    }
+
+    @Override
+    public void exportVoyageDataSummaryExcel(VoyageDataSummaryReqVO reqVO, HttpServletResponse response) throws IOException {
+        VoyageDataSummaryRespVO vo = getVoyageDataSummary(reqVO);
+
+        if (vo == null || vo.getTotalPeople() == null || vo.getTotalPeople() == 0) {
+            ExcelUtils.exportEmpty(response, "航次数据情况汇总");
+            return;
+        }
+
+        // 生成文件名和标题
+        String fileName = buildExportFileName(vo);
+        String title = buildExportTitle(vo);
+
+        // 构建动态表头和数据
+        List<List<String>> head = buildVoyageDataSummaryHeaders(vo);
+        List<List<Object>> exportData = buildVoyageDataSummaryExportData(vo);
+        int dataRowCount = exportData.size();
+
+        response.addHeader("Content-Disposition",
+                "attachment;filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()));
+        response.setContentType("application/vnd.ms-excel;charset=UTF-8");
+
+        EasyExcel.write(response.getOutputStream())
+                .head(head)
+                .relativeHeadRowIndex(1)
+                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
+                .registerWriteHandler(new VoyageDataSummaryExportStyleHandler(title, dataRowCount))
+                .registerConverter(new LongStringConverter())
+                .sheet("各航次情况子表")
+                .doWrite(exportData);
+    }
+
+    /**
+     * 构建空结果VO
+     */
+    private VoyageDataSummaryRespVO buildEmptyVoyageDataSummaryVO(Long shipId) {
+        VoyageDataSummaryRespVO vo = new VoyageDataSummaryRespVO();
+        vo.setShipName("");
+        vo.setVoyageName("");
+        vo.setDirection("");
+        vo.setTotalPeople(0);
+        vo.setTotalRooms(BigDecimal.ZERO);
+        vo.setMaleCount(0);
+        vo.setFemaleCount(0);
+        vo.setMaleFemaleRatio("0%:0%");
+        vo.setDomesticCount(0);
+        vo.setOverseasCount(0);
+        vo.setDomesticOverseasRatio("0%:0%");
+        vo.setTeamCount(0);
+        vo.setIndividualCount(0);
+        vo.setTeamIndividualRatio("0%:0%");
+        vo.setRoomTypeList(Collections.emptyList());
+        vo.setAgeGroupList(Collections.emptyList());
+        vo.setDomesticProvinceList(Collections.emptyList());
+        vo.setInternationalRegionList(Collections.emptyList());
+        vo.setVoyageInfoList(Collections.emptyList());
+        return vo;
+    }
+
+    /**
+     * 构建合并后的航次数据汇总 VO(多航次合并为一个结果)
+     */
+    private VoyageDataSummaryRespVO buildMergedVoyageDataSummaryVO(List<Map<String, Object>> voyageBaseList,
+                                                                    List<Map<String, Object>> visitorList,
+                                                                    List<Map<String, Object>> roomList) {
+        VoyageDataSummaryRespVO vo = new VoyageDataSummaryRespVO();
+
+        // 游轮名称(所有航次相同)
+        String shipName = voyageBaseList.get(0).get("shipName") != null
+                ? String.valueOf(voyageBaseList.get(0).get("shipName")) : "";
+        vo.setShipName(shipName);
+
+        // 构建航次信息列表
+        List<VoyageDataSummaryRespVO.VoyageInfoVO> voyageInfoList = new ArrayList<>();
+        StringBuilder voyageNameSb = new StringBuilder();
+        StringBuilder directionSb = new StringBuilder();
+        Set<String> directionSet = new LinkedHashSet<>();
+
+        for (int i = 0; i < voyageBaseList.size(); i++) {
+            Map<String, Object> base = voyageBaseList.get(i);
+            Long voyageId = Long.valueOf(String.valueOf(base.get("voyageId")));
+            String vName = formatVoyageName(base);
+            String dir = base.get("directionLabel") != null ? String.valueOf(base.get("directionLabel")) : "";
+
+            VoyageDataSummaryRespVO.VoyageInfoVO info = new VoyageDataSummaryRespVO.VoyageInfoVO();
+            info.setVoyageId(voyageId);
+            info.setVoyageName(vName);
+            info.setDirection(dir);
+            voyageInfoList.add(info);
+
+            if (i > 0) {
+                voyageNameSb.append("、");
+            }
+            voyageNameSb.append(vName);
+            if (StrUtil.isNotBlank(dir)) {
+                directionSet.add(dir);
+            }
+        }
+        vo.setVoyageInfoList(voyageInfoList);
+        vo.setVoyageName(voyageNameSb.toString());
+
+        // 方向:单航次直接显示,多航次用顿号连接
+        if (directionSet.size() == 1) {
+            vo.setDirection(directionSet.iterator().next());
+        } else {
+            vo.setDirection(String.join("、", directionSet));
+        }
+
+        // ========== 合并统计计算 ==========
+
+        // 总房间数(按房型名称合并)
+        Map<String, BigDecimal> roomTypeCountMap = new HashMap<>();
+        for (Map<String, Object> room : roomList) {
+            String roomModelName = room.get("roomModelName") != null ? String.valueOf(room.get("roomModelName")) : "";
+            BigDecimal roomNum = new BigDecimal(String.valueOf(room.get("roomNum")));
+            if (StrUtil.isNotBlank(roomModelName) && roomNum != null && roomNum.compareTo(BigDecimal.ZERO) > 0) {
+                roomTypeCountMap.merge(roomModelName, roomNum, BigDecimal::add);
+            }
+        }
+        BigDecimal totalRooms = BigDecimal.valueOf(roomTypeCountMap.values().stream().mapToDouble(BigDecimal::doubleValue).sum());
+        vo.setTotalRooms(totalRooms);
+
+        // 总人数
+        int totalPeople = visitorList.size();
+        vo.setTotalPeople(totalPeople);
+
+        // 性别统计
+        int maleCount = 0;
+        int femaleCount = 0;
+        for (Map<String, Object> v : visitorList) {
+            Integer gender = parseIntValue(v.get("gender"));
+            if (gender != null) {
+                if (gender == 1) maleCount++;
+                else if (gender == 0) femaleCount++;
+            }
+        }
+        vo.setMaleCount(maleCount);
+        vo.setFemaleCount(femaleCount);
+        int genderTotal = maleCount + femaleCount;
+        vo.setMaleFemaleRatio(genderTotal > 0
+                ? calcRatio(maleCount, genderTotal) + ":" + calcRatio(femaleCount, genderTotal)
+                : "0%:0%");
+
+        // 境内外统计(合并所有航次)
+        int domesticCount = 0;
+        int overseasCount = 0;
+        Map<String, Integer> provinceCountMap = new HashMap<>();
+        Map<String, Integer> countryCountMap = new HashMap<>();
+
+        for (Map<String, Object> v : visitorList) {
+            String nationalityName = v.get("nationalityName") != null ? String.valueOf(v.get("nationalityName")) : "";
+            String credentialNo = v.get("credentialNo") != null ? String.valueOf(v.get("credentialNo")) : "";
+            Integer credentialType = parseIntValue(v.get("credentialType"));
+
+            String province = IdCardProvinceUtil.getProvinceName(credentialType, credentialNo, nationalityName);
+
+            if (StrUtil.isNotBlank(province)) {
+                domesticCount++;
+                provinceCountMap.merge(province, 1, Integer::sum);
+            } else {
+                String country = StrUtil.isNotBlank(nationalityName) ? nationalityName : "其他";
+                overseasCount++;
+                countryCountMap.merge(country, 1, Integer::sum);
+            }
+        }
+        vo.setDomesticCount(domesticCount);
+        vo.setOverseasCount(overseasCount);
+        int totalDomesticOverseas = domesticCount + overseasCount;
+        vo.setDomesticOverseasRatio(totalDomesticOverseas > 0
+                ? calcRatio(domesticCount, totalDomesticOverseas) + ":" + calcRatio(overseasCount, totalDomesticOverseas)
+                : "0%:0%");
+
+        // 房型统计(合并后重新计算占比)
+        List<VoyageDataSummaryRespVO.DimensionItemVO> roomTypeList = roomTypeCountMap.entrySet().stream()
+                .sorted(Map.Entry.<String, BigDecimal>comparingByValue().reversed())
+                .map(entry -> {
+                    VoyageDataSummaryRespVO.DimensionItemVO item = new VoyageDataSummaryRespVO.DimensionItemVO();
+                    item.setName(entry.getKey());
+                    item.setCount(entry.getValue().intValue());
+                    item.setRatio(calcRatio(entry.getValue().intValue(), totalRooms.intValue()));
+                    return item;
+                })
+                .collect(Collectors.toList());
+        vo.setRoomTypeList(roomTypeList);
+
+        // 年龄区间统计(合并所有航次)
+        List<VoyageDataSummaryRespVO.DimensionItemVO> ageGroupList = analyzeAgeGroupsMerged(visitorList);
+        vo.setAgeGroupList(ageGroupList);
+
+        // 境内省份(合并后重新计算占比)
+        int finalDomesticCount = domesticCount;
+        List<VoyageDataSummaryRespVO.DimensionItemVO> domesticProvinceList = provinceCountMap.entrySet().stream()
+                .sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
+                .map(entry -> {
+                    VoyageDataSummaryRespVO.DimensionItemVO item = new VoyageDataSummaryRespVO.DimensionItemVO();
+                    item.setName(entry.getKey());
+                    item.setCount(entry.getValue());
+                    item.setRatio(calcRatio(entry.getValue(), finalDomesticCount));
+                    return item;
+                })
+                .collect(Collectors.toList());
+        vo.setDomesticProvinceList(domesticProvinceList);
+
+        // 境外国家(合并后重新计算占比)
+        int finalOverseasCount = overseasCount;
+        List<VoyageDataSummaryRespVO.DimensionItemVO> internationalRegionList = countryCountMap.entrySet().stream()
+                .sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
+                .map(entry -> {
+                    VoyageDataSummaryRespVO.DimensionItemVO item = new VoyageDataSummaryRespVO.DimensionItemVO();
+                    item.setName(entry.getKey());
+                    item.setCount(entry.getValue());
+                    item.setRatio(calcRatio(entry.getValue(), finalOverseasCount));
+                    return item;
+                })
+                .collect(Collectors.toList());
+        vo.setInternationalRegionList(internationalRegionList);
+
+        // 团散统计:按订单游客数判断,>=10人为团,<10人为散
+        Map<String, Long> orderVisitorCount = visitorList.stream()
+                .collect(Collectors.groupingBy(
+                        v -> String.valueOf(v.get("orderId")),
+                        Collectors.counting()
+                ));
+
+        int teamCount = 0;
+        int individualCount = 0;
+        for (Long visitorCount : orderVisitorCount.values()) {
+            if (visitorCount >= 10) {
+                teamCount += visitorCount;
+            } else {
+                individualCount += visitorCount;
+            }
+        }
+        vo.setTeamCount(teamCount);
+        vo.setIndividualCount(individualCount);
+        int teamIndividualTotal = teamCount + individualCount;
+        vo.setTeamIndividualRatio(teamIndividualTotal > 0
+                ? calcRatio(teamCount, teamIndividualTotal) + ":" + calcRatio(individualCount, teamIndividualTotal)
+                : "0%:0%");
+
+        return vo;
+    }
+
+    /**
+     * 解析整数值(兼容Number和String)
+     */
+    private Integer parseIntValue(Object obj) {
+        if (obj == null) return null;
+        if (obj instanceof Number) return ((Number) obj).intValue();
+        try {
+            return Integer.parseInt(String.valueOf(obj));
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    /**
+     * 格式化航次名称(如 0418航次)
+     */
+    private String formatVoyageName(Map<String, Object> base) {
+        Object startTimeObj = base.get("startTime");
+        if (startTimeObj == null) return "";
+        String startTimeStr = String.valueOf(startTimeObj);
+        try {
+            if (startTimeStr.length() >= 10) {
+                String monthDay = startTimeStr.substring(5, 7) + startTimeStr.substring(8, 10);
+                return monthDay + "航次";
+            }
+        } catch (Exception ignored) {}
+        return startTimeStr;
+    }
+
+    /**
+     * 年龄段分组统计(合并版)
+     */
+    private List<VoyageDataSummaryRespVO.DimensionItemVO> analyzeAgeGroupsMerged(List<Map<String, Object>> visitorList) {
+        String[] ageGroups = {"12岁以下", "12岁-18岁", "18岁-30岁", "30岁-45岁", "45岁-60岁", "60岁以上"};
+        Map<String, Integer> ageGroupCountMap = new LinkedHashMap<>();
+        for (String group : ageGroups) {
+            ageGroupCountMap.put(group, 0);
+        }
+
+        int totalWithAge = 0;
+        for (Map<String, Object> v : visitorList) {
+            Object ageObj = v.get("age");
+            if (ageObj == null) continue;
+            int age;
+            try {
+                age = Integer.parseInt(String.valueOf(ageObj));
+            } catch (Exception e) {
+                continue;
+            }
+            totalWithAge++;
+            String group = getAgeGroupLabel(age);
+            ageGroupCountMap.merge(group, 1, Integer::sum);
+        }
+
+        List<VoyageDataSummaryRespVO.DimensionItemVO> result = new ArrayList<>();
+        for (String group : ageGroups) {
+            Integer count = ageGroupCountMap.getOrDefault(group, 0);
+            VoyageDataSummaryRespVO.DimensionItemVO item = new VoyageDataSummaryRespVO.DimensionItemVO();
+            item.setName(group);
+            item.setCount(count);
+            item.setRatio(calcRatio(count, totalWithAge));
+            result.add(item);
+        }
+        return result;
+    }
+
+    /**
+     * 获取年龄段标签
+     */
+    private String getAgeGroupLabel(int age) {
+        if (age < 12) return "12岁以下";
+        else if (age < 18) return "12岁-18岁";
+        else if (age < 30) return "18岁-30岁";
+        else if (age < 45) return "30岁-45岁";
+        else if (age < 60) return "45岁-60岁";
+        else return "60岁以上";
+    }
+
+    /**
+     * 生成导出文件名
+     * 单航次:长江行·揽月 0418航次(下水)数据情况.xls
+     * 多航次:长江行·揽月 各航次数据情况.xls
+     */
+    private String buildExportFileName(VoyageDataSummaryRespVO vo) {
+        String shipName = vo.getShipName() != null ? vo.getShipName() : "";
+        List<VoyageDataSummaryRespVO.VoyageInfoVO> infoList = vo.getVoyageInfoList();
+        if (CollUtil.isEmpty(infoList) || infoList.size() == 1) {
+            String voyageName = vo.getVoyageName() != null ? vo.getVoyageName() : "";
+            String direction = vo.getDirection() != null ? vo.getDirection() : "";
+            return shipName + " " + voyageName + "(" + direction + ")数据情况.xls";
+        }
+        return shipName + " 各航次数据情况.xls";
+    }
+
+    /**
+     * 构建导出标题
+     */
+    private String buildExportTitle(VoyageDataSummaryRespVO vo) {
+        String shipName = vo.getShipName() != null ? vo.getShipName() : "";
+        List<VoyageDataSummaryRespVO.VoyageInfoVO> infoList = vo.getVoyageInfoList();
+        if (CollUtil.isEmpty(infoList) || infoList.size() == 1) {
+            String voyageName = vo.getVoyageName() != null ? vo.getVoyageName() : "";
+            String direction = vo.getDirection() != null ? vo.getDirection() : "";
+            return shipName + " " + voyageName + "(" + direction + ")数据情况";
+        }
+        return shipName + " 各航次数据情况";
+    }
+
+    /**
+     * 构建航次数据汇总导出表头(2层多级表头,共18列)
+     */
+    private List<List<String>> buildVoyageDataSummaryHeaders(VoyageDataSummaryRespVO vo) {
+        List<List<String>> head = new ArrayList<>();
+
+        // 航次(1列)
+        head.add(Arrays.asList("航次", "航次"));
+
+        // 数据汇总(2列)
+        head.add(Arrays.asList("数据汇总", ""));
+        head.add(Arrays.asList("数据汇总", ""));
+
+        // 预定间数及占比(3列)
+        head.add(Arrays.asList("预定间数及占比", "房型"));
+        head.add(Arrays.asList("预定间数及占比", "间数"));
+        head.add(Arrays.asList("预定间数及占比", "占比"));
+
+        // 年龄区间(3列)
+        head.add(Arrays.asList("年龄区间", "年龄"));
+        head.add(Arrays.asList("年龄区间", "人数"));
+        head.add(Arrays.asList("年龄区间", "占比"));
+
+        // 境内(3列),显示人数/占比
+        String domesticHeader = "境内";
+        if (vo.getDomesticCount() != null && vo.getOverseasCount() != null) {
+            int total = vo.getDomesticCount() + vo.getOverseasCount();
+            if (total > 0) {
+                domesticHeader = "境内(" + vo.getDomesticCount() + "/" + calcRatio(vo.getDomesticCount(), total) + ")";
+            }
+        }
+        head.add(Arrays.asList(domesticHeader, "省份"));
+        head.add(Arrays.asList(domesticHeader, "人数"));
+        head.add(Arrays.asList(domesticHeader, "占比"));
+
+        // 境外(3列),显示人数/占比
+        String overseasHeader = "境外";
+        if (vo.getDomesticCount() != null && vo.getOverseasCount() != null) {
+            int total = vo.getDomesticCount() + vo.getOverseasCount();
+            if (total > 0) {
+                overseasHeader = "境外(" + vo.getOverseasCount() + "/" + calcRatio(vo.getOverseasCount(), total) + ")";
+            }
+        }
+        head.add(Arrays.asList(overseasHeader, "国家/区域"));
+        head.add(Arrays.asList(overseasHeader, "人数"));
+        head.add(Arrays.asList(overseasHeader, "占比"));
+
+        // 团散占比(3列)
+        head.add(Arrays.asList("团散占比", "团队人数"));
+        head.add(Arrays.asList("团散占比", "散客人数"));
+        head.add(Arrays.asList("团散占比", "占比"));
+
+        return head;
+    }
+
+    /**
+     * 构建航次数据汇总导出数据(单个VO)
+     */
+    private List<List<Object>> buildVoyageDataSummaryExportData(VoyageDataSummaryRespVO vo) {
+        List<List<Object>> result = new ArrayList<>();
+
+        List<VoyageDataSummaryRespVO.DimensionItemVO> roomList = vo.getRoomTypeList() != null ? vo.getRoomTypeList() : Collections.emptyList();
+        List<VoyageDataSummaryRespVO.DimensionItemVO> ageList = vo.getAgeGroupList() != null ? vo.getAgeGroupList() : Collections.emptyList();
+        List<VoyageDataSummaryRespVO.DimensionItemVO> domesticList = vo.getDomesticProvinceList() != null ? vo.getDomesticProvinceList() : Collections.emptyList();
+        List<VoyageDataSummaryRespVO.DimensionItemVO> internationalList = vo.getInternationalRegionList() != null ? vo.getInternationalRegionList() : Collections.emptyList();
+
+        // 数据汇总固定4行
+        String[] summaryLabels = {"总人数", "总房间数", "男女比例", "境内外比例"};
+        Object[] summaryValues = {vo.getTotalPeople(), vo.getTotalRooms(), vo.getMaleFemaleRatio(), vo.getDomesticOverseasRatio()};
+
+        // 航次列显示内容:多航次时展示所有航次
+        String voyageCellContent = buildVoyageCellContent(vo);
+
+        int maxRows = Math.max(4, Math.max(roomList.size(),
+                Math.max(ageList.size(),
+                        Math.max(domesticList.size(), internationalList.size()))));
+
+        for (int i = 0; i < maxRows; i++) {
+            List<Object> row = new ArrayList<>();
+
+            // 航次(第1行显示,后续为空,由WriteHandler合并)
+            if (i == 0) {
+                row.add(voyageCellContent);
+            } else {
+                row.add("");
+            }
+
+            // 数据汇总(标签和数值)
+            if (i < 4) {
+                row.add(summaryLabels[i]);
+                row.add(summaryValues[i]);
+            } else {
+                row.add("");
+                row.add("");
+            }
+
+            // 房型
+            if (i < roomList.size()) {
+                row.add(roomList.get(i).getName());
+                row.add(roomList.get(i).getCount());
+                row.add(roomList.get(i).getRatio());
+            } else {
+                row.add("");
+                row.add("");
+                row.add("");
+            }
+
+            // 年龄
+            if (i < ageList.size()) {
+                row.add(ageList.get(i).getName());
+                row.add(ageList.get(i).getCount());
+                row.add(ageList.get(i).getRatio());
+            } else {
+                row.add("");
+                row.add("");
+                row.add("");
+            }
+
+            // 境内
+            if (i < domesticList.size()) {
+                row.add(domesticList.get(i).getName());
+                row.add(domesticList.get(i).getCount());
+                row.add(domesticList.get(i).getRatio());
+            } else {
+                row.add("");
+                row.add("");
+                row.add("");
+            }
+
+            // 境外
+            if (i < internationalList.size()) {
+                row.add(internationalList.get(i).getName());
+                row.add(internationalList.get(i).getCount());
+                row.add(internationalList.get(i).getRatio());
+            } else {
+                row.add("");
+                row.add("");
+                row.add("");
+            }
+
+            // 团散占比(只有第1行)
+            if (i == 0) {
+                row.add(vo.getTeamCount());
+                row.add(vo.getIndividualCount());
+                row.add(vo.getTeamIndividualRatio());
+            } else {
+                row.add("");
+                row.add("");
+                row.add("");
+            }
+
+            result.add(row);
+        }
+
+        return result;
+    }
+
+    /**
+     * 构建航次列的显示内容
+     * 单航次:0418航次\n(下水)
+     * 多航次:0418(下水)\n0422(上水)\n...
+     */
+    private String buildVoyageCellContent(VoyageDataSummaryRespVO vo) {
+        List<VoyageDataSummaryRespVO.VoyageInfoVO> infoList = vo.getVoyageInfoList();
+        if (CollUtil.isEmpty(infoList)) {
+            return vo.getVoyageName() + "\n(" + vo.getDirection() + ")";
+        }
+        if (infoList.size() == 1) {
+            return infoList.get(0).getVoyageName() + "\n(" + infoList.get(0).getDirection() + ")";
+        }
+        // 多航次:每行一个航次
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < infoList.size(); i++) {
+            VoyageDataSummaryRespVO.VoyageInfoVO info = infoList.get(i);
+            if (i > 0) sb.append("\n");
+            sb.append(info.getVoyageName());
+            if (StrUtil.isNotBlank(info.getDirection())) {
+                sb.append("(").append(info.getDirection()).append(")");
+            }
+        }
+        return sb.toString();
+    }
 }

+ 11 - 5
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/utils/IdCardProvinceUtil.java

@@ -237,22 +237,28 @@ public class IdCardProvinceUtil {
      * @return 省份名称,无法解析时返回空字符串
      */
     public static String getProvinceName(Integer credentialType, String credentialNo, String  nationalityName) {
-        if (credentialType == null) {
-            return "";
-        }
         // 台湾通行证、台胞证 → 直接返回"台湾"
-        if (credentialType == CREDENTIAL_TYPE_TAIWAN_PASS
+       /* if (credentialType == CREDENTIAL_TYPE_TAIWAN_PASS
                 || credentialType == CREDENTIAL_TYPE_TAIWAN_COMPATRIOT_PERMIT) {
             return "台湾";
+        }*/
+
+        if ("中国台湾".equals(nationalityName)) {
+            return "台湾";
         }
 
-        if (nationalityName.contains("香港")) {
+        if ("中国香港".equals(nationalityName)) {
             return "香港";
         }
 
         if (nationalityName.contains("澳门")) {
             return "澳门";
         }
+
+        if (credentialType == null) {
+            return "";
+        }
+
         // 非可解析的证件类型,直接返回空
         if (!PARSEABLE_CREDENTIAL_TYPES.contains(credentialType)  && !"中国".equals(nationalityName)) {
             return "";

+ 112 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/utils/excel/VoyageDataSummaryExportStyleHandler.java

@@ -0,0 +1,112 @@
+package com.yc.ship.module.trade.utils.excel;
+
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.excel.write.handler.CellWriteHandler;
+import com.alibaba.excel.write.handler.SheetWriteHandler;
+import com.alibaba.excel.write.handler.context.CellWriteHandlerContext;
+import com.alibaba.excel.write.handler.context.SheetWriteHandlerContext;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.ss.util.CellRangeAddress;
+import org.apache.poi.xssf.usermodel.XSSFCellStyle;
+import org.apache.poi.xssf.usermodel.XSSFColor;
+
+/**
+ * 航次数据情况汇总 导出样式处理器
+ * 处理顶部标题行、表头样式、航次列与数据汇总列的合并
+ */
+public class VoyageDataSummaryExportStyleHandler implements SheetWriteHandler, CellWriteHandler {
+
+    private static final int TOTAL_COLUMNS = 18;
+
+    private final String title;
+    private final int dataRowCount;
+
+    private CellStyle headerStyle;
+    private CellStyle titleStyle;
+
+    public VoyageDataSummaryExportStyleHandler(String title, int dataRowCount) {
+        this.title = title;
+        this.dataRowCount = dataRowCount;
+    }
+
+    @Override
+    public void afterSheetCreate(SheetWriteHandlerContext context) {
+        Sheet sheet = context.getWriteSheetHolder().getSheet();
+        Workbook workbook = sheet.getWorkbook();
+
+        // 第0行:大标题(合并所有列)
+        if (StrUtil.isNotBlank(title)) {
+            Row titleRow = sheet.createRow(0);
+            Cell titleCell = titleRow.createCell(0);
+            titleCell.setCellValue(title);
+            titleCell.setCellStyle(createTitleStyle(workbook));
+            // 合并标题行,跨所有18列
+            sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, TOTAL_COLUMNS - 1));
+            titleRow.setHeightInPoints(28);
+        }
+
+        // 数据从第3行开始(0=标题, 1-2=表头, 3+=数据)
+        int dataStartRow = 3;
+
+        if (dataRowCount > 1) {
+            // 合并航次列(第0列)
+            sheet.addMergedRegion(new CellRangeAddress(dataStartRow, dataStartRow + dataRowCount - 1, 0, 0));
+
+            // 合并数据汇总列的4个标签单元格(第1列)
+            // 数据汇总有4行:总人数、总房间数、男女比例、境内外比例
+            int summaryRows = Math.min(4, dataRowCount);
+            if (summaryRows > 1) {
+                // 数据汇总标签列(第1列)不需要合并,每行显示不同标签
+                // 但数据汇总值列(第2列)也不需要合并
+            }
+        }
+    }
+
+    @Override
+    public void afterCellDispose(CellWriteHandlerContext context) {
+        if (Boolean.TRUE.equals(context.getHead())) {
+            Cell cell = context.getCell();
+            cell.setCellStyle(createHeaderStyle(cell.getSheet().getWorkbook()));
+        }
+    }
+
+    private CellStyle createTitleStyle(Workbook workbook) {
+        if (titleStyle == null) {
+            titleStyle = workbook.createCellStyle();
+            titleStyle.setAlignment(HorizontalAlignment.CENTER);
+            titleStyle.setVerticalAlignment(VerticalAlignment.CENTER);
+            titleStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+            if (titleStyle instanceof XSSFCellStyle) {
+                XSSFColor color = new XSSFColor(new byte[]{(byte) 0xD6, (byte) 0xE3, (byte) 0xF8}, null);
+                ((XSSFCellStyle) titleStyle).setFillForegroundColor(color);
+            }
+            Font font = workbook.createFont();
+            font.setBold(true);
+            font.setFontHeightInPoints((short) 12);
+            titleStyle.setFont(font);
+        }
+        return titleStyle;
+    }
+
+    private CellStyle createHeaderStyle(Workbook workbook) {
+        if (headerStyle == null) {
+            headerStyle = workbook.createCellStyle();
+            headerStyle.setAlignment(HorizontalAlignment.CENTER);
+            headerStyle.setVerticalAlignment(VerticalAlignment.CENTER);
+            headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+            headerStyle.setBorderTop(BorderStyle.THIN);
+            headerStyle.setBorderBottom(BorderStyle.THIN);
+            headerStyle.setBorderLeft(BorderStyle.THIN);
+            headerStyle.setBorderRight(BorderStyle.THIN);
+            if (headerStyle instanceof XSSFCellStyle) {
+                XSSFColor color = new XSSFColor(new byte[]{(byte) 0xB4, (byte) 0xC7, (byte) 0xE7}, null);
+                ((XSSFCellStyle) headerStyle).setFillForegroundColor(color);
+            }
+            Font font = workbook.createFont();
+            font.setBold(true);
+            font.setFontHeightInPoints((short) 10);
+            headerStyle.setFont(font);
+        }
+        return headerStyle;
+    }
+}

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

@@ -285,7 +285,7 @@
             LEFT JOIN (
             SELECT order_id, SUM(pay_amount) AS actual_amount
             FROM trade_order_pay
-            WHERE pay_status = 1
+            WHERE pay_status = 1 or order_type = 2
             GROUP BY order_id
             ) topay ON td.id = topay.order_id
         WHERE

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

@@ -268,7 +268,7 @@
             CASE WHEN tjz.is_jz = 1 THEN '是' ELSE '否' END AS jz,
             DATE_FORMAT(pv.start_time, '%Y.%m.%d') AS travelDate,
             tor.pay_amount AS amount,
-            top.pay_amount AS payAmount,
+            tor.real_pay_amount AS payAmount,
             tor.deposi,
             tv.room_index_id AS roomIndexId,
             torm.room_model_name AS roomType,

+ 2 - 2
ship-module-trade/ship-module-trade-biz/src/main/resources/mapper/orderjzdetail/OrderGiftVisitorMapper.xml

@@ -36,7 +36,7 @@
         INNER JOIN resource_route r ON pv.route_id = r.id
         LEFT JOIN resource_room_model rm ON v.room_model_id = rm.id
         LEFT JOIN trade_order_jz_dispatch dj ON gfv.dispatch_id = dj.id AND dj.source_type = 2
-        WHERE v.deleted = 0 AND o.deleted = 0 AND pv.deleted = 0
+        WHERE v.deleted = 0 AND o.deleted = 0 AND pv.deleted = 0 AND o.order_status IN (15, 14, 13, 10, 12, 9, 8, 7, 6, 5, 4, 3, 1, 0)
         AND o.voyage_id = #{vo.voyageId}
         <if test="vo.idCard != null and vo.idCard != ''">
             AND v.credential_no LIKE CONCAT('%', #{vo.idCard}, '%')
@@ -99,7 +99,7 @@
         INNER JOIN resource_route r ON pv.route_id = r.id
         LEFT JOIN resource_room_model rm ON v.room_model_id = rm.id
         LEFT JOIN trade_order_jz_dispatch dj ON gfv.dispatch_id = dj.id AND dj.source_type = 2
-        WHERE  o.voyage_id = #{vo.voyageId}
+        WHERE  o.voyage_id = #{vo.voyageId} AND o.order_status IN (15, 14, 13, 10, 12, 9, 8, 7, 6, 5, 4, 3, 1, 0)
         <if test="vo.idCard != null and vo.idCard != ''">
             AND v.credential_no LIKE CONCAT('%', #{vo.idCard}, '%')
         </if>

+ 2 - 2
ship-module-trade/ship-module-trade-biz/src/main/resources/mapper/orderjzdetail/OrderJzDetailMapper.xml

@@ -23,7 +23,7 @@
         left join resource_room_model rm on u.room_model_id = rm.id
         left join trade_order_jz_dispatch dj on d.dispatch_id = dj.id
         inner join trade_detail td on td.order_id = o.id and td.product_id != 2034458675435925505
-        where d.deleted = 0 and o.voyage_id = #{vo.voyageId}
+        where d.deleted = 0 and o.voyage_id = #{vo.voyageId} and o.order_status IN (15, 14, 13, 10, 12, 9, 8, 7, 6, 5, 4, 3, 1, 0)
         <if test="vo.idCard != null">
             and d.id_card like concat('%', #{vo.idCard}, '%')
         </if>
@@ -62,7 +62,7 @@
         left join resource_room_model rm on u.room_model_id = rm.id
         left join trade_order_jz_dispatch dj on d.dispatch_id = dj.id
         inner join trade_detail td on td.order_id = o.id and td.product_id != 2034458675435925505
-        where d.deleted = 0 and o.voyage_id = #{vo.voyageId}
+        where d.deleted = 0 and o.voyage_id = #{vo.voyageId} and o.order_status IN (15, 14, 13, 10, 12, 9, 8, 7, 6, 5, 4, 3, 1, 0)
         <if test="vo.idCard != null and vo.idCard != ''">
             and d.id_card like concat('%', #{vo.idCard}, '%')
         </if>

+ 4 - 0
ship-module-trade/ship-module-trade-biz/src/main/resources/mapper/orderpolicy/OrderPolicyMapper.xml

@@ -20,4 +20,8 @@
         GROUP BY
             policy_id
     </select>
+
+    <delete id="deleteAllByOrderId">
+        delete from trade_order_policy where order_id=#{orderId}
+    </delete>
 </mapper>

+ 85 - 0
ship-module-trade/ship-module-trade-biz/src/main/resources/mapper/report/VoyageStockBoardMapper.xml

@@ -365,4 +365,89 @@
         ORDER BY amount DESC
     </select>
 
+    <!-- ==================== 航次数据情况汇总 ==================== -->
+
+    <!-- 查询航次数据汇总所需的游客详细信息 -->
+    <select id="selectVoyageDataSummaryVisitorList" resultType="java.util.HashMap">
+        SELECT
+            tv.id as visitorId,
+            tv.gender,
+            tv.age,
+            tv.credential_type as credentialType,
+            tv.credential_no as credentialNo,
+            tv.nationality,
+            a.name as nationalityName,
+            tor.id as orderId,
+            tor.voyage_id as voyageId
+        FROM trade_visitor tv
+        INNER JOIN trade_order tor ON tv.order_id = tor.id AND tor.deleted = 0
+        LEFT JOIN area a ON tv.nationality = a.id
+        LEFT JOIN product_voyage pv ON tor.voyage_id = pv.id
+        WHERE tv.deleted = 0
+        AND tor.order_status IN (15, 14, 13, 10, 12, 9, 8, 7, 6, 5, 4, 3, 1, 0)
+        <if test="shipId != null">
+            AND pv.ship_id = #{shipId}
+        </if>
+        <if test="voyageIds != null and voyageIds.size() > 0">
+            AND tor.voyage_id IN
+            <foreach collection="voyageIds" item="item" separator="," open="(" close=")">
+                #{item}
+            </foreach>
+        </if>
+    </select>
+
+    <!-- 查询航次数据汇总所需的房型信息 -->
+    <select id="selectVoyageDataSummaryRoomList" resultType="java.util.HashMap">
+        SELECT
+            tor.voyage_id as voyageId,
+            torm.room_model_name as roomModelName,
+            SUM(COALESCE(torm.use_room_num, 0)) as roomNum
+        FROM trade_order_room_model torm
+        INNER JOIN trade_order tor ON torm.order_id = tor.id AND tor.deleted = 0
+        WHERE torm.deleted = 0
+        AND tor.order_status IN (15, 14, 13, 10, 12, 9, 8, 7, 6, 5, 4, 3, 1, 0)
+        AND torm.room_model_name IS NOT NULL
+        AND torm.room_model_name != ''
+        <if test="shipId != null">
+            AND tor.ship_id = #{shipId}
+        </if>
+        <if test="voyageIds != null and voyageIds.size() > 0">
+            AND tor.voyage_id IN
+            <foreach collection="voyageIds" item="item" separator="," open="(" close=")">
+                #{item}
+            </foreach>
+        </if>
+        GROUP BY tor.voyage_id, torm.room_model_name
+        ORDER BY SUM(COALESCE(torm.use_room_num, 0)) DESC
+    </select>
+
+    <!-- 查询航次基本信息 -->
+    <select id="selectVoyageBaseInfoList" resultType="java.util.HashMap">
+        SELECT
+            pv.id as voyageId,
+            pv.name as voyageName,
+            pv.start_time as startTime,
+            rs.name as shipName,
+            rr.direction as direction,
+            CASE rr.direction
+                WHEN 1 THEN '上水'
+                WHEN 2 THEN '下水'
+                ELSE '未知'
+            END as directionLabel
+        FROM product_voyage pv
+        INNER JOIN resource_ship rs ON pv.ship_id = rs.id AND rs.deleted = 0
+        INNER JOIN resource_route rr ON pv.route_id = rr.id AND rr.deleted = 0
+        WHERE pv.deleted = 0
+        <if test="shipId != null">
+            AND pv.ship_id = #{shipId}
+        </if>
+        <if test="voyageIds != null and voyageIds.size() > 0">
+            AND pv.id IN
+            <foreach collection="voyageIds" item="item" separator="," open="(" close=")">
+                #{item}
+            </foreach>
+        </if>
+        ORDER BY pv.start_time ASC
+    </select>
+
 </mapper>

+ 4 - 4
ship-module-trade/ship-module-trade-biz/src/main/resources/mapper/report/YangtzePassengerSummaryMapper.xml

@@ -25,7 +25,7 @@
             COALESCE(visitor_stats.totalPassengers, 0) AS totalPassengers,
             COALESCE(free_stats.ticketedPassengers, 0) AS ticketedPassengers,
             COALESCE(free_stats.freePassengers, 0) AS freePassengers,
-            COALESCE(free_stats.estimatedPassengers, 0) AS estimatedPassengers,
+            COALESCE(room_stats.reservedRooms * 2 + visitor_stats.totalPassengers, 0) AS estimatedPassengers,
             COALESCE(finance_stats.receivableAmount, 0) AS receivableAmount,
             COALESCE(finance_stats.receivedAmount, 0) AS receivedAmount,
             COALESCE(finance_stats.receivableAmount, 0) - COALESCE(finance_stats.receivedAmount, 0) AS unreceivedAmount,
@@ -67,7 +67,7 @@
                 SUM(CASE WHEN tv.type IN ('with', 'leader') THEN 1 ELSE 0 END) AS companionLeaderCount
             FROM trade_order o
             INNER JOIN trade_visitor tv ON o.id = tv.order_id AND tv.deleted = 0
-        WHERE o.deleted = 0 AND o.order_status IN (15, 13, 10, 12, 9, 8, 7, 6, 5, 4, 3, 1, 0)
+        WHERE o.deleted = 0 AND o.order_status IN (15, 14, 13, 10, 12, 9, 8, 7, 6, 5, 4, 3, 1, 0)
             GROUP BY o.voyage_id
         ) visitor_stats ON v.id = visitor_stats.voyage_id
 
@@ -125,7 +125,7 @@
                 WHERE deleted = 0
                 GROUP BY order_id
             ) visitor_cnt ON o.id = visitor_cnt.order_id
-            WHERE o.deleted = 0 AND o.order_status IN (15, 13, 10, 12, 9, 8, 7, 6, 5, 4, 3, 1, 0)
+            WHERE o.deleted = 0 AND o.order_status IN (15, 14, 13, 10, 12, 9, 8, 7, 6, 5, 4, 3, 1, 0)
             GROUP BY o.voyage_id
         ) team_stats ON v.id = team_stats.voyage_id
 
@@ -136,7 +136,7 @@
                 SUM(CASE WHEN tv.nationality != '1' AND tv.nationality IS NOT NULL AND tv.nationality != '' THEN 1 ELSE 0 END) AS overseasCount
             FROM trade_order o
             INNER JOIN trade_visitor tv ON o.id = tv.order_id AND tv.deleted = 0
-            WHERE o.deleted = 0 AND o.order_status IN (15, 13, 10, 12, 9, 8, 7, 6, 5, 4, 3, 1, 0)
+            WHERE o.deleted = 0 AND o.order_status IN (15, 14, 13, 10, 12, 9, 8, 7, 6, 5, 4, 3, 1, 0)
             GROUP BY o.voyage_id
         ) nationality_stats ON v.id = nationality_stats.voyage_id
         WHERE v.deleted = 0