Ver código fonte

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

lishiqiang 6 dias atrás
pai
commit
afdf1f8196
28 arquivos alterados com 2427 adições e 45 exclusões
  1. 26 0
      ship-module-resource/ship-module-resource-biz/src/main/java/com/yc/ship/module/resource/dal/mysql/room/ResourceRoomMapper.java
  2. 63 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/report/CabinMixDailyController.java
  3. 47 5
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/report/OpsDailyController.java
  4. 17 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/report/vo/CabinMixDailyReqVO.java
  5. 30 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/report/vo/ConsumptionIncomeDailyReqVO.java
  6. 128 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/report/vo/ConsumptionIncomeDailyRespVO.java
  7. 30 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/report/vo/ConsumptionIncomeVoyageReqVO.java
  8. 131 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/report/vo/ConsumptionIncomeVoyageRespVO.java
  9. 209 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/dataobject/consumption/ConsumptionIncomeDailyDO.java
  10. 209 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/dataobject/consumption/ConsumptionIncomeVoyageDO.java
  11. 20 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/consumption/ConsumptionIncomeDailyMapper.java
  12. 21 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/consumption/ConsumptionIncomeVoyageMapper.java
  13. 5 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/order/TradeDetailBaseMapper.java
  14. 5 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/order/TradeDetailMapper.java
  15. 4 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/order/TradeOrderCountryMapper.java
  16. 4 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/order/TradeOrderFloorMapper.java
  17. 4 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/order/TradeOrderRoomModelMapper.java
  18. 4 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/order/TradeOrderTotalMapper.java
  19. 4 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/order/TradeOrderUserMapper.java
  20. 4 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/order/TradeVisitorMapper.java
  21. 6 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/orderpolicy/OrderPolicyMapper.java
  22. 13 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/report/CabinMixDailyMapper.java
  23. 40 30
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/otc/impl/OtcTradeOrderServiceImpl.java
  24. 17 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/report/CabinMixDailyService.java
  25. 35 5
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/report/OpsDailyService.java
  26. 457 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/report/impl/CabinMixDailyServiceImpl.java
  27. 842 5
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/report/impl/OpsDailyServiceImpl.java
  28. 52 0
      ship-module-trade/ship-module-trade-biz/src/main/resources/mapper/report/CabinMixDailyMapper.xml

+ 26 - 0
ship-module-resource/ship-module-resource-biz/src/main/java/com/yc/ship/module/resource/dal/mysql/room/ResourceRoomMapper.java

@@ -12,6 +12,7 @@ import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 
 import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 
 /**
@@ -71,4 +72,29 @@ public interface ResourceRoomMapper extends BaseMapperX<ResourceRoomDO> {
 
     List<ResourceRoomRespVO> selectAllByShipIdAndFloors(@Param("shipId") String shipId, @Param("voyageId") String voyageId, @Param("floors") String floors);
 
+    /**
+     * 获取省际度假游轮房型分布日报表表头
+     * @param shipId
+     * @return
+     */
+    @Select("SELECT" +
+            " t1.room_model_id AS roomModelId," +
+            " count( t1.id ) AS num," +
+            " t2.sort," +
+            " t2.NAME AS room_model_name," +
+            " t2.short_name AS shortName " +
+            " FROM" +
+            " resource_room t1" +
+            " LEFT JOIN resource_room_model t2 ON t1.room_model_id = t2.id " +
+            " WHERE" +
+            " t1.ship_id = #{shipId} " +
+            " AND t1.deleted = 0 " +
+            " AND t2.deleted = 0 " +
+            " GROUP BY" +
+            " t1.room_model_id" +
+            " ORDER BY" +
+            " CASE WHEN t2.sort = 999 THEN 0 ELSE 1 END ASC," +
+            " t2.sort ASC," +
+            " t2.id ASC")
+    List<Map<String, Object>> getCabinMixDailyTableHeadList(@Param("shipId")Long shipId);
 }

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

@@ -0,0 +1,63 @@
+package com.yc.ship.module.trade.controller.admin.report;
+
+import com.yc.ship.framework.apilog.core.annotation.ApiAccessLog;
+import com.yc.ship.framework.common.pojo.CommonResult;
+import com.yc.ship.module.trade.controller.admin.report.vo.CabinMixDailyReqVO;
+import com.yc.ship.module.trade.controller.admin.report.vo.ConsumptionIncomeDailyReqVO;
+import com.yc.ship.module.trade.controller.admin.report.vo.CruiseOpsDailyRespVO;
+import com.yc.ship.module.trade.service.report.CabinMixDailyService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import static com.yc.ship.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
+import static com.yc.ship.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 省际度假游轮房型分布日报表")
+@RestController
+@RequestMapping("/report/interprovincial/cabin-mix-daily")
+@Validated
+@Slf4j
+public class CabinMixDailyController {
+    @Resource
+    private CabinMixDailyService cabinMixDailyService;
+    // ==================== 省际度假游轮房型分布日报表 ====================
+    /**
+     * 查询省际度假游轮房型分布表头
+     */
+    @GetMapping("/getTableHead")
+    @Operation(summary = "查询省际度假游轮房型分布表头")
+    public CommonResult<List<Map<String, Object>>> getTableHeadList(@RequestParam("shipId") Long shipId) {
+        List<Map<String, Object>> headList = cabinMixDailyService.getTableHeadList(shipId);
+        return success(headList);
+    }
+
+    /**
+     * 查询省际度假游轮房型分布日报表
+     */
+    @GetMapping("/list")
+    @Operation(summary = "查询省际度假游轮房型分布日报表")
+    @ResponseBody
+    public CommonResult<List<Map<String, Object>>> list(@Validated CabinMixDailyReqVO reqVO) {
+        List<Map<String, Object>> list = cabinMixDailyService.getList(reqVO);
+        return success(list);
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出省际度假游轮房型分布日报表 Excel")
+    @ApiAccessLog(operateType = EXPORT)
+    public void exportCabinMixDaily(@Valid CabinMixDailyReqVO reqVO,
+                                                  HttpServletResponse response) throws IOException {
+        cabinMixDailyService.exportCabinMixDaily(reqVO, response);
+    }
+
+}

+ 47 - 5
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/report/OpsDailyController.java

@@ -3,11 +3,7 @@ package com.yc.ship.module.trade.controller.admin.report;
 import com.yc.ship.framework.apilog.core.annotation.ApiAccessLog;
 import com.yc.ship.framework.common.pojo.CommonResult;
 import com.yc.ship.framework.common.pojo.PageResult;
-import com.yc.ship.module.trade.controller.admin.report.vo.CruiseOpsDailyReqVO;
-import com.yc.ship.module.trade.controller.admin.report.vo.CruiseOpsDailyRespVO;
-import com.yc.ship.module.trade.controller.admin.report.vo.YangtzePassengerSummaryPageReqVO;
-import com.yc.ship.module.trade.controller.admin.report.vo.YangtzePassengerSummaryRemarkReqVO;
-import com.yc.ship.module.trade.controller.admin.report.vo.YangtzePassengerSummaryRespVO;
+import com.yc.ship.module.trade.controller.admin.report.vo.*;
 import com.yc.ship.module.trade.service.report.OpsDailyService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -114,5 +110,51 @@ public class OpsDailyController {
         opsDailyService.updateYangtzePassengerSummaryRemark(reqVO);
         return success(true);
     }
+
+    // ==================== 省际度假游轮二消情况航次表 ====================
+
+    /**
+     * 查询省际度假游轮二消情况航次表
+     *
+     * @param reqVO 查询条件
+     * @return 数据列表
+     */
+    @GetMapping("/consumption-income-voyage/list")
+    @Operation(summary = "查询省际度假游轮二消情况航次表")
+    public CommonResult<List<ConsumptionIncomeVoyageRespVO>> getConsumptionIncomeVoyageList(@Valid ConsumptionIncomeVoyageReqVO reqVO) {
+        List<ConsumptionIncomeVoyageRespVO> list = opsDailyService.getConsumptionIncomeVoyageList(reqVO);
+        return success(list);
+    }
+
+    @GetMapping("/consumption-income-voyage/export-excel")
+    @Operation(summary = "导出省际度假游轮二消情况航次表 Excel")
+    @ApiAccessLog(operateType = EXPORT)
+    public void exportConsumptionIncomeVoyageExcel(@Valid ConsumptionIncomeVoyageReqVO reqVO,
+                                                   HttpServletResponse response) throws IOException {
+        opsDailyService.exportConsumptionIncomeVoyageExcel(reqVO, response);
+    }
+
+    // ==================== 省际度假游轮二消情况日报表 ====================
+
+    /**
+     * 查询省际度假游轮二消情况日报表
+     *
+     * @param reqVO 查询条件
+     * @return 数据列表
+     */
+    @GetMapping("/consumption-income-daily/list")
+    @Operation(summary = "查询省际度假游轮二消情况日报表")
+    public CommonResult<List<ConsumptionIncomeDailyRespVO>> getConsumptionIncomeDailyList(@Valid ConsumptionIncomeDailyReqVO reqVO) {
+        List<ConsumptionIncomeDailyRespVO> list = opsDailyService.getConsumptionIncomeDailyList(reqVO);
+        return success(list);
+    }
+
+    @GetMapping("/consumption-income-daily/export-excel")
+    @Operation(summary = "导出省际度假游轮二消情况日报表 Excel")
+    @ApiAccessLog(operateType = EXPORT)
+    public void exportConsumptionIncomeDailyExcel(@Valid ConsumptionIncomeDailyReqVO reqVO,
+                                                  HttpServletResponse response) throws IOException {
+        opsDailyService.exportConsumptionIncomeDailyExcel(reqVO, response);
+    }
 }
 

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

@@ -0,0 +1,17 @@
+package com.yc.ship.module.trade.controller.admin.report.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - 省际度假游轮房型分布日报表 Request VO")
+@Data
+@ToString(callSuper = true)
+public class CabinMixDailyReqVO {
+    @Schema(description = "开始日期", example = "2026-01-01")
+    private String startDate;
+    @Schema(description = "结束日期", example = "2026-03-31")
+    private String endDate;
+    @Schema(description = "游轮ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Long shipId;
+}

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

@@ -0,0 +1,30 @@
+package com.yc.ship.module.trade.controller.admin.report.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * 省际度假游轮二消情况日报表 Request VO
+ *
+ * @author auto-generated
+ */
+@Schema(description = "管理后台 - 省际度假游轮二消情况日报表 Request VO")
+@Data
+public class ConsumptionIncomeDailyReqVO {
+
+    @Schema(description = "开始日期", example = "2026-01-01")
+    private String startDate;
+
+    @Schema(description = "结束日期", example = "2026-03-31")
+    private String endDate;
+
+    @Schema(description = "船舶名称", example = "长江一号")
+    private String shipName;
+
+    @Schema(description = "航线", example = "上水")
+    private String route;
+
+    @Schema(description = "航次号", example = "V20260101")
+    private String voyageNo;
+
+}

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

@@ -0,0 +1,128 @@
+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;
+
+/**
+ * 省际度假游轮二消情况日报表 Response VO
+ *
+ * @author auto-generated
+ */
+@Schema(description = "管理后台 - 省际度假游轮二消情况日报表 Response VO")
+@Data
+public class ConsumptionIncomeDailyRespVO {
+
+    @Schema(description = "序号")
+    private Integer index;
+
+    @Schema(description = "统计日期")
+    private String statDate;
+
+    @Schema(description = "统计月份")
+    private String statMonth;
+
+    @Schema(description = "航次ID")
+    private Long voyageId;
+
+    @Schema(description = "航次名称")
+    private String voyageName;
+
+    @Schema(description = "船舶名称")
+    private String shipName;
+
+    @Schema(description = "航线(上水/下水)")
+    private String route;
+
+    @Schema(description = "航次号")
+    private String voyageNo;
+
+    @Schema(description = "前台收入")
+    private BigDecimal frontDeskIncome;
+
+    @Schema(description = "餐厅收入")
+    private BigDecimal restaurantIncome;
+
+    @Schema(description = "酒吧收入-预包装酒水")
+    private BigDecimal barPrepackedWine;
+
+    @Schema(description = "酒吧收入-调制酒水")
+    private BigDecimal barMixedWine;
+
+    @Schema(description = "酒吧收入-KTV")
+    private BigDecimal barKtv;
+
+    @Schema(description = "酒吧收入-麻将")
+    private BigDecimal barMahjong;
+
+    @Schema(description = "酒吧收入-烧烤")
+    private BigDecimal barBarbecue;
+
+    @Schema(description = "酒吧收入-其他")
+    private BigDecimal barOther;
+
+    @Schema(description = "文创收入")
+    private BigDecimal culturalIncome;
+
+    @Schema(description = "客房收入")
+    private BigDecimal roomIncome;
+
+    @Schema(description = "自营收入")
+    private BigDecimal selfIncome;
+
+    @Schema(description = "其他收入")
+    private BigDecimal otherIncome;
+
+    @Schema(description = "联营商-摄影收入")
+    private BigDecimal partnerPhotography;
+
+    @Schema(description = "联营商-商超收入")
+    private BigDecimal partnerSupermarket;
+
+    @Schema(description = "联营商-SPA收入")
+    private BigDecimal partnerSpa;
+
+    @Schema(description = "联营商-医务室收入")
+    private BigDecimal partnerClinic;
+
+    @Schema(description = "收入合计")
+    private BigDecimal totalIncome;
+
+    @Schema(description = "微信/支付宝/刷卡支付")
+    private BigDecimal payOnline;
+
+    @Schema(description = "现金支付")
+    private BigDecimal payCash;
+
+    @Schema(description = "其他支付")
+    private BigDecimal payOther;
+
+    @Schema(description = "应收款")
+    private BigDecimal receivable;
+
+    @Schema(description = "应付摄影款")
+    private BigDecimal payablePhotography;
+
+    @Schema(description = "应付商超款")
+    private BigDecimal payableSupermarket;
+
+    @Schema(description = "应付SPA款")
+    private BigDecimal payableSpa;
+
+    @Schema(description = "应付医务室款")
+    private BigDecimal payableClinic;
+
+    @Schema(description = "联营商应付款合计")
+    private BigDecimal partnerTotalPayable;
+
+    @Schema(description = "长江收入")
+    private BigDecimal changjiangIncome;
+
+    @Schema(description = "是否小计行")
+    private Boolean isSubtotal;
+
+    @Schema(description = "是否累计行")
+    private Boolean isCumulative;
+
+}

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

@@ -0,0 +1,30 @@
+package com.yc.ship.module.trade.controller.admin.report.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * 省际度假游轮二消情况航次表 Request VO
+ *
+ * @author auto-generated
+ */
+@Schema(description = "管理后台 - 省际度假游轮二消情况航次表 Request VO")
+@Data
+public class ConsumptionIncomeVoyageReqVO {
+
+    @Schema(description = "开始日期", example = "2026-01-01")
+    private String startDate;
+
+    @Schema(description = "结束日期", example = "2026-03-31")
+    private String endDate;
+
+    @Schema(description = "船舶名称", example = "长江一号")
+    private String shipName;
+
+    @Schema(description = "航线", example = "上水")
+    private String route;
+
+    @Schema(description = "航次号", example = "V20260101")
+    private String voyageNo;
+
+}

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

@@ -0,0 +1,131 @@
+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;
+
+/**
+ * 省际度假游轮二消情况航次表 Response VO
+ *
+ * @author auto-generated
+ */
+@Schema(description = "管理后台 - 省际度假游轮二消情况航次表 Response VO")
+@Data
+public class ConsumptionIncomeVoyageRespVO {
+
+    @Schema(description = "序号")
+    private Integer index;
+
+    @Schema(description = "航次ID")
+    private Long id;
+
+    @Schema(description = "航次名称")
+    private String voyageName;
+
+    @Schema(description = "船舶名称")
+    private String shipName;
+
+    @Schema(description = "航线(上水/下水)")
+    private String route;
+
+    @Schema(description = "航次号")
+    private String voyageNo;
+
+    @Schema(description = "统计月份")
+    private String statMonth;
+
+    @Schema(description = "指标人数")
+    private BigDecimal targetPeople;
+
+    @Schema(description = "人均金额(元/人)")
+    private BigDecimal avgPerPerson;
+
+    @Schema(description = "前台收入")
+    private BigDecimal frontDeskIncome;
+
+    @Schema(description = "餐厅收入")
+    private BigDecimal restaurantIncome;
+
+    @Schema(description = "酒吧收入-预包装酒水")
+    private BigDecimal barPrepackedWine;
+
+    @Schema(description = "酒吧收入-调制酒水")
+    private BigDecimal barMixedWine;
+
+    @Schema(description = "酒吧收入-KTV")
+    private BigDecimal barKtv;
+
+    @Schema(description = "酒吧收入-麻将")
+    private BigDecimal barMahjong;
+
+    @Schema(description = "酒吧收入-烧烤")
+    private BigDecimal barBarbecue;
+
+    @Schema(description = "酒吧收入-其他")
+    private BigDecimal barOther;
+
+    @Schema(description = "文创收入")
+    private BigDecimal culturalIncome;
+
+    @Schema(description = "客房收入")
+    private BigDecimal roomIncome;
+
+    @Schema(description = "自营收入")
+    private BigDecimal selfIncome;
+
+    @Schema(description = "其他收入")
+    private BigDecimal otherIncome;
+
+    @Schema(description = "联营商-摄影收入")
+    private BigDecimal partnerPhotography;
+
+    @Schema(description = "联营商-商超收入")
+    private BigDecimal partnerSupermarket;
+
+    @Schema(description = "联营商-SPA收入")
+    private BigDecimal partnerSpa;
+
+    @Schema(description = "联营商-医务室收入")
+    private BigDecimal partnerClinic;
+
+    @Schema(description = "收入合计")
+    private BigDecimal totalIncome;
+
+    @Schema(description = "微信/支付宝/刷卡支付")
+    private BigDecimal payOnline;
+
+    @Schema(description = "现金支付")
+    private BigDecimal payCash;
+
+    @Schema(description = "应收款")
+    private BigDecimal receivable;
+
+    @Schema(description = "其他支付")
+    private BigDecimal payOther;
+
+    @Schema(description = "应付摄影款")
+    private BigDecimal payablePhotography;
+
+    @Schema(description = "应付商超款")
+    private BigDecimal payableSupermarket;
+
+    @Schema(description = "应付SPA款")
+    private BigDecimal payableSpa;
+
+    @Schema(description = "应付医务室款")
+    private BigDecimal payableClinic;
+
+    @Schema(description = "联营商应付款合计")
+    private BigDecimal partnerTotalPayable;
+
+    @Schema(description = "长江收入")
+    private BigDecimal changjiangIncome;
+
+    @Schema(description = "是否小计行")
+    private Boolean isSubtotal;
+
+    @Schema(description = "是否累计行")
+    private Boolean isCumulative;
+
+}

+ 209 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/dataobject/consumption/ConsumptionIncomeDailyDO.java

@@ -0,0 +1,209 @@
+package com.yc.ship.module.trade.dal.dataobject.consumption;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.yc.ship.framework.mybatis.core.dataobject.BaseDO;
+import lombok.*;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@TableName("consumption_income_daily")
+@KeySequence("consumption_income_daily_seq")
+@Data
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ConsumptionIncomeDailyDO extends BaseDO {
+
+    @TableId(type = IdType.ASSIGN_ID)
+    private Long id;
+
+    /**
+     * 统计日期(订单创建日期)
+     */
+    private String statDate;
+    /**
+     * 统计月份(如2026-04)
+     */
+    private String statMonth;
+
+    private Long voyageId;
+    /**
+     * 航次名称
+     */
+    private String voyageName;
+
+    /**
+     * 船舶名称
+     */
+    private String shipName;
+
+    /**
+     * 航线(上水/下水)
+     */
+    private String route;
+
+    /**
+     * 航次号
+     */
+    private String voyageNo;
+
+    /**
+     * 前台收入
+     */
+    private BigDecimal frontDeskIncome;
+
+    /**
+     * 餐厅收入
+     */
+    private BigDecimal restaurantIncome;
+
+    /**
+     * 酒吧收入-预包装酒水
+     */
+    private BigDecimal barPrepackedWine;
+
+    /**
+     * 酒吧收入-调制酒水
+     */
+    private BigDecimal barMixedWine;
+
+    /**
+     * 酒吧收入-KTV
+     */
+    private BigDecimal barKtv;
+
+    /**
+     * 酒吧收入-麻将
+     */
+    private BigDecimal barMahjong;
+
+    /**
+     * 酒吧收入-烧烤
+     */
+    private BigDecimal barBarbecue;
+
+    /**
+     * 酒吧收入-其他
+     */
+    private BigDecimal barOther;
+
+    /**
+     * 文创收入
+     */
+    private BigDecimal culturalIncome;
+
+    /**
+     * 客房收入
+     */
+    private BigDecimal roomIncome;
+
+    /**
+     * 自营收入
+     */
+    private BigDecimal selfIncome;
+
+    /**
+     * 其他收入
+     */
+    private BigDecimal otherIncome;
+
+    /**
+     * 联营商-摄影收入
+     */
+    private BigDecimal partnerPhotography;
+
+    /**
+     * 联营商-商超收入
+     */
+    private BigDecimal partnerSupermarket;
+
+    /**
+     * 联营商-SPA收入
+     */
+    private BigDecimal partnerSpa;
+
+    /**
+     * 联营商-医务室收入
+     */
+    private BigDecimal partnerClinic;
+
+    /**
+     * 收入合计
+     */
+    private BigDecimal totalIncome;
+
+    /**
+     * 微信/支付宝/刷卡支付
+     */
+    private BigDecimal payOnline;
+
+    /**
+     * 现金支付
+     */
+    private BigDecimal payCash;
+
+    /**
+     * 其他支付
+     */
+    private BigDecimal payOther;
+
+    /**
+     * 应收款
+     */
+    private BigDecimal receivable;
+
+    /**
+     * 应付摄影款
+     */
+    private BigDecimal payablePhotography;
+
+    /**
+     * 应付商超款
+     */
+    private BigDecimal payableSupermarket;
+
+    /**
+     * 应付SPA款
+     */
+    private BigDecimal payableSpa;
+
+    /**
+     * 应付医务室款
+     */
+    private BigDecimal payableClinic;
+
+    /**
+     * 联营商应付款合计
+     */
+    private BigDecimal partnerTotalPayable;
+
+    /**
+     * 长江收入
+     */
+    private BigDecimal changjiangIncome;
+
+    /**
+     * 同步状态,0未同步,1同步中,2已同步
+     */
+    private String syncStatus;
+
+    /**
+     * 同步开始时间
+     */
+    private LocalDateTime syncBeginTime;
+
+    /**
+     * 同步结束时间
+     */
+    private LocalDateTime syncEndTime;
+
+    /**
+     * 同步ID
+     */
+    private String syncId;
+}

+ 209 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/dataobject/consumption/ConsumptionIncomeVoyageDO.java

@@ -0,0 +1,209 @@
+package com.yc.ship.module.trade.dal.dataobject.consumption;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.yc.ship.framework.mybatis.core.dataobject.BaseDO;
+import lombok.*;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@TableName("consumption_income_voyage")
+@KeySequence("consumption_income_voyage_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ConsumptionIncomeVoyageDO extends BaseDO {
+    @TableId(type = IdType.ASSIGN_ID)
+    private Long id;
+    /**
+     * 航次名称
+     */
+    private String voyageName;
+
+    /**
+     * 船舶名称
+     */
+    private String shipName;
+
+    /**
+     * 航线(上水/下水)
+     */
+    private String route;
+
+    /**
+     * 航次号
+     */
+    private String voyageNo;
+
+    /**
+     * 指标人数
+     */
+    private BigDecimal targetPeople;
+
+    /**
+     * 人均金额(元/人)
+     */
+    private BigDecimal avgPerPerson;
+
+    /**
+     * 前台收入
+     */
+    private BigDecimal frontDeskIncome;
+
+    /**
+     * 餐厅收入
+     */
+    private BigDecimal restaurantIncome;
+
+    /**
+     * 酒吧收入-预包装酒水
+     */
+    private BigDecimal barPrepackedWine;
+
+    /**
+     * 酒吧收入-调制酒水
+     */
+    private BigDecimal barMixedWine;
+
+    /**
+     * 酒吧收入-KTV
+     */
+    private BigDecimal barKtv;
+
+    /**
+     * 酒吧收入-麻将
+     */
+    private BigDecimal barMahjong;
+
+    /**
+     * 酒吧收入-烧烤
+     */
+    private BigDecimal barBarbecue;
+
+    /**
+     * 酒吧收入-其他
+     */
+    private BigDecimal barOther;
+
+    /**
+     * 文创收入
+     */
+    private BigDecimal culturalIncome;
+
+    /**
+     * 客房收入
+     */
+    private BigDecimal roomIncome;
+
+    /**
+     * 自营收入
+     */
+    private BigDecimal selfIncome;
+
+    /**
+     * 其他收入
+     */
+    private BigDecimal otherIncome;
+
+    /**
+     * 联营商-摄影收入
+     */
+    private BigDecimal partnerPhotography;
+
+    /**
+     * 联营商-商超收入
+     */
+    private BigDecimal partnerSupermarket;
+
+    /**
+     * 联营商-SPA收入
+     */
+    private BigDecimal partnerSpa;
+
+    /**
+     * 联营商-医务室收入
+     */
+    private BigDecimal partnerClinic;
+
+    /**
+     * 收入合计
+     */
+    private BigDecimal totalIncome;
+
+    /**
+     * 微信/支付宝/刷卡支付
+     */
+    private BigDecimal payOnline;
+
+    /**
+     * 现金支付
+     */
+    private BigDecimal payCash;
+
+    /**
+     * 应收款
+     */
+    private BigDecimal receivable;
+    /**
+     * 其他
+     */
+    private BigDecimal payOther;
+
+    /**
+     * 应付摄影款
+     */
+    private BigDecimal payablePhotography;
+
+    /**
+     * 应付商超款
+     */
+    private BigDecimal payableSupermarket;
+
+    /**
+     * 应付SPA款
+     */
+    private BigDecimal payableSpa;
+
+    /**
+     * 应付医务室款
+     */
+    private BigDecimal payableClinic;
+
+    /**
+     * 联营商应付款合计
+     */
+    private BigDecimal partnerTotalPayable;
+
+    /**
+     * 长江收入
+     */
+    private BigDecimal changjiangIncome;
+
+    /**
+     * 统计月份(如2026-04)
+     */
+    private String statMonth;
+
+    /**
+     * 同步状态, 0未同步,1同步中,2已同步
+     */
+    private String syncStatus;
+    /**
+     * 同步开始时间
+     */
+    private LocalDateTime syncBeginTime;
+    /**
+     * 同步结束时间
+     */
+    private LocalDateTime syncEndTime;
+    /**
+     * 同步ID, 客户端同步ID
+     */
+    private String syncId;
+
+}

+ 20 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/consumption/ConsumptionIncomeDailyMapper.java

@@ -0,0 +1,20 @@
+package com.yc.ship.module.trade.dal.mysql.consumption;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.yc.ship.framework.mybatis.core.mapper.BaseMapperX;
+import com.yc.ship.module.trade.controller.admin.report.vo.ConsumptionIncomeDailyReqVO;
+import com.yc.ship.module.trade.dal.dataobject.consumption.ConsumptionIncomeDailyDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+@Mapper
+public interface ConsumptionIncomeDailyMapper extends BaseMapperX<ConsumptionIncomeDailyDO> {
+    default List<ConsumptionIncomeDailyDO> selectList(ConsumptionIncomeDailyReqVO reqVO) {
+        return selectList(new LambdaQueryWrapper<ConsumptionIncomeDailyDO>()
+                .between(reqVO.getStartDate() != null && reqVO.getEndDate() != null,
+                        ConsumptionIncomeDailyDO::getStatDate,
+                        reqVO.getStartDate(), reqVO.getEndDate())
+                .orderByAsc(ConsumptionIncomeDailyDO::getStatDate, ConsumptionIncomeDailyDO::getVoyageNo));
+    }
+}

+ 21 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/consumption/ConsumptionIncomeVoyageMapper.java

@@ -0,0 +1,21 @@
+package com.yc.ship.module.trade.dal.mysql.consumption;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.yc.ship.framework.mybatis.core.mapper.BaseMapperX;
+import com.yc.ship.module.trade.controller.admin.report.vo.ConsumptionIncomeVoyageReqVO;
+import com.yc.ship.module.trade.dal.dataobject.consumption.ConsumptionIncomeVoyageDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+@Mapper
+public interface ConsumptionIncomeVoyageMapper extends BaseMapperX<ConsumptionIncomeVoyageDO> {
+    default List<ConsumptionIncomeVoyageDO> selectList(ConsumptionIncomeVoyageReqVO reqVO) {
+        return selectList(new LambdaQueryWrapper<ConsumptionIncomeVoyageDO>()
+                .between(reqVO.getStartDate() != null && reqVO.getEndDate() != null,
+                        ConsumptionIncomeVoyageDO::getStatMonth,
+                        reqVO.getStartDate(), reqVO.getEndDate())
+                .orderByAsc(ConsumptionIncomeVoyageDO::getStatMonth, ConsumptionIncomeVoyageDO::getVoyageNo));
+    }
+
+}

+ 5 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/order/TradeDetailBaseMapper.java

@@ -8,6 +8,7 @@ import com.yc.ship.module.trade.dal.dataobject.order.TradeDetailDO;
 import com.yc.ship.module.trade.service.order.bo.TradeOrderDetailBaseBO;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Update;
 
 import java.time.LocalDateTime;
 import java.util.List;
@@ -53,4 +54,8 @@ public interface TradeDetailBaseMapper extends BaseMapperX<TradeDetailBaseDO> {
 
     @TenantIgnore
     void deleteAllByOrderId(@Param("orderId") Long orderId);
+
+    @Update("UPDATE trade_detail_base SET deleted = #{deleted},update_time = now() WHERE id = #{id}")
+    @TenantIgnore
+    void updateDeletedById(@Param("id") Long id, @Param("deleted")  int deleted);
 }

+ 5 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/order/TradeDetailMapper.java

@@ -18,6 +18,7 @@ import com.yc.ship.module.trade.dal.dataobject.order.TradeDetailDO;
 import com.yc.ship.module.trade.service.order.bo.TradeDetailBO;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Update;
 import org.springframework.cache.annotation.Cacheable;
 
 import java.math.BigDecimal;
@@ -204,4 +205,8 @@ public interface TradeDetailMapper extends BaseMapperX<TradeDetailDO> {
 
     @TenantIgnore
     void deleteAllByOrderId(@Param("orderId") Long orderId);
+
+    @Update("UPDATE trade_detail SET deleted = #{deleted},update_time = now() WHERE id = #{id}")
+    @TenantIgnore
+    void updateDeletedById(@Param("id") Long id, @Param("deleted")  int deleted);
 }

+ 4 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/order/TradeOrderCountryMapper.java

@@ -36,4 +36,8 @@ public interface TradeOrderCountryMapper extends BaseMapperX<TradeOrderCountryDO
     @Delete("delete from trade_order_country where order_id = #{orderId}")
     @TenantIgnore
     void deleteAllByOrderId(Long orderId);
+
+    @Update("UPDATE trade_order_country SET deleted = #{deleted},update_time = now() WHERE id = #{id}")
+    @TenantIgnore
+    void updateDeletedById(@Param("id") Long id, @Param("deleted")  int deleted);
 }

+ 4 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/order/TradeOrderFloorMapper.java

@@ -22,4 +22,8 @@ public interface TradeOrderFloorMapper extends BaseMapperX<TradeOrderFloorDO> {
     @Delete("delete from trade_order_floor where order_id = #{orderId}")
     @TenantIgnore
     void deleteAllByOrderId(@Param("orderId") Long orderId);
+
+    @Update("UPDATE trade_order_floor SET deleted = #{deleted},update_time = now() WHERE id = #{id}")
+    @TenantIgnore
+    void updateDeletedById(@Param("id") Long id, @Param("deleted")  int deleted);
 }

+ 4 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/order/TradeOrderRoomModelMapper.java

@@ -55,4 +55,8 @@ public interface TradeOrderRoomModelMapper extends BaseMapperX<TradeOrderRoomMod
 
     @TenantIgnore
     void deleteAllByOrderId(@Param("orderId") Long orderId);
+
+    @Update("UPDATE trade_order_room_model SET deleted = #{deleted},update_time = now() WHERE id = #{id}")
+    @TenantIgnore
+    void updateDeletedById(@Param("id") Long id, @Param("deleted")  int deleted);
 }

+ 4 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/order/TradeOrderTotalMapper.java

@@ -37,4 +37,8 @@ public interface TradeOrderTotalMapper extends BaseMapperX<TradeOrderTotalDO> {
     @Delete("delete from trade_order_total where old_order_id = #{orderId}")
     @TenantIgnore
     void deleteAllByOrderId(@Param("orderId") Long orderId);
+
+    @Update("UPDATE trade_order_total SET deleted = #{deleted},update_time = now() WHERE id = #{id}")
+    @TenantIgnore
+    void updateDeletedById(@Param("id") Long id, @Param("deleted")  int deleted);
 }

+ 4 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/order/TradeOrderUserMapper.java

@@ -23,4 +23,8 @@ public interface TradeOrderUserMapper extends BaseMapperX<TradeOrderUserDO> {
     @Delete("delete from trade_order_user where order_id = #{orderId}")
     @TenantIgnore
     void deleteAllByOrderId(@Param("orderId") Long orderId);
+
+    @Update("UPDATE trade_order_user SET deleted = #{deleted},update_time = now() WHERE id = #{id}")
+    @TenantIgnore
+    void updateDeletedById(@Param("id") Long id, @Param("deleted")  int deleted);
 }

+ 4 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/order/TradeVisitorMapper.java

@@ -128,4 +128,8 @@ public interface TradeVisitorMapper extends BaseMapperX<TradeVisitorDO> {
 
     @TenantIgnore
     void deleteAllByOrderId(@Param("orderId") Long orderId);
+
+    @Update("UPDATE trade_visitor SET deleted = #{deleted},update_time = now() WHERE id = #{id}")
+    @TenantIgnore
+    void updateDeletedById(@Param("id") Long id, @Param("deleted")  int deleted);
 }

+ 6 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/orderpolicy/OrderPolicyMapper.java

@@ -5,11 +5,13 @@ import java.util.*;
 import com.yc.ship.framework.common.pojo.PageResult;
 import com.yc.ship.framework.mybatis.core.query.LambdaQueryWrapperX;
 import com.yc.ship.framework.mybatis.core.mapper.BaseMapperX;
+import com.yc.ship.framework.tenant.core.aop.TenantIgnore;
 import com.yc.ship.module.trade.controller.admin.order.vo.order.OrderTotalPolicyVO;
 import com.yc.ship.module.trade.dal.dataobject.orderpolicy.OrderPolicyDO;
 import org.apache.ibatis.annotations.Mapper;
 import com.yc.ship.module.trade.controller.admin.orderpolicy.vo.*;
 import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Update;
 
 /**
  * 订单使用的优惠政策 Mapper
@@ -48,4 +50,8 @@ public interface OrderPolicyMapper extends BaseMapperX<OrderPolicyDO> {
     List<OrderTotalPolicyVO> selectOrderPolicyStatsByOrderIds(@Param("orderIds") List<Long> orderIds);
 
     void deleteAllByOrderId(@Param("orderId")Long orderId);
+
+    @Update("UPDATE trade_order_policy SET deleted = #{deleted},update_time = now() WHERE id = #{id}")
+    @TenantIgnore
+    void updateDeletedById(@Param("id") Long id, @Param("deleted")  int deleted);
 }

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

@@ -0,0 +1,13 @@
+package com.yc.ship.module.trade.dal.mysql.report;
+
+import com.yc.ship.module.trade.controller.admin.report.vo.CabinMixDailyReqVO;
+import com.yc.ship.module.trade.controller.admin.report.vo.CruiseOpsDailyRespVO;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+@Mapper
+public interface CabinMixDailyMapper {
+    List<Map<String,Object>> getList(@Param("vo") CabinMixDailyReqVO reqVO, @Param("roomColumns") String roomColumns);}

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

@@ -699,8 +699,9 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
     @Transactional(rollbackFor = Exception.class)
     public void rollbackOrderUserDataVersion(Long orderId){
 
-        tradeOrderUserMapper.deleteAllByOrderId(orderId);
         //首先把旧数据删除
+        tradeOrderUserMapper.delete(TradeOrderUserDO::getOrderId,orderId);
+
         // ====================== 【关键1】加锁查询:锁住当前版本行 ======================
         // FOR UPDATE 行锁 → 并发请求会排队,绝对安全
 
@@ -721,8 +722,9 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
             TradeOrderUserDO a = new TradeOrderUserDO();
             BeanUtils.copyProperties(lastVersion, a);
             a.setSyncStatus("0");
-            int updateMain = tradeOrderUserMapper.insert(a);
-            if (!(updateMain ==1)) {
+            tradeOrderUserMapper.updateDeletedById(dataId,0);
+            boolean updateMain = tradeOrderUserMapper.insertOrUpdate(a);
+            if (!updateMain) {
                 throw new RuntimeException("主表回退失败");
             }
 
@@ -756,8 +758,8 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
     @Transactional(rollbackFor = Exception.class)
     public void rollbackOrderDetailDataVersion(Long orderId){
         //首先把旧数据删除
-        tradeDetailMapper.deleteAllByOrderId(orderId);
-
+        tradeDetailMapper.delete(TradeDetailDO::getOrderId,orderId);
+        
         // ====================== 【关键1】加锁查询:锁住当前版本行 ======================
         // FOR UPDATE 行锁 → 并发请求会排队,绝对安全
 
@@ -778,8 +780,9 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
             TradeDetailDO a = new TradeDetailDO();
             BeanUtils.copyProperties(lastVersion, a);
             a.setSyncStatus("0");
-            int updateMain = tradeDetailMapper.insert(a);
-            if (!(updateMain ==1)) {
+            tradeDetailMapper.updateDeletedById(dataId,0);
+            boolean updateMain = tradeDetailMapper.insertOrUpdate(a);
+            if (!updateMain) {
                 throw new RuntimeException("主表回退失败");
             }
 
@@ -813,8 +816,8 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
     @Transactional(rollbackFor = Exception.class)
     public void rollbackOrderDetailBaseDataVersion(Long orderId){
         //首先把旧数据删除
-        tradeDetailBaseMapper.deleteAllByOrderId(orderId);
-
+        tradeDetailBaseMapper.delete(TradeDetailBaseDO::getOrderId,orderId);
+        
         // ====================== 【关键1】加锁查询:锁住当前版本行 ======================
         // FOR UPDATE 行锁 → 并发请求会排队,绝对安全
 
@@ -835,8 +838,9 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
             TradeDetailBaseDO a = new TradeDetailBaseDO();
             BeanUtils.copyProperties(lastVersion, a);
 //            a.setSyncStatus("0");
-            int updateMain = tradeDetailBaseMapper.insert(a);
-            if (!(updateMain ==1)) {
+            tradeDetailBaseMapper.updateDeletedById(dataId,0);
+            boolean updateMain = tradeDetailBaseMapper.insertOrUpdate(a);
+            if (!updateMain) {
                 throw new RuntimeException("主表回退失败");
             }
 
@@ -870,7 +874,7 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
     @Transactional(rollbackFor = Exception.class)
     public void rollbackOrderTotalDataVersion(Long orderId){
         //首先把旧数据删除
-        tradeOrderTotalMapper.deleteAllByOrderId(orderId);
+        tradeOrderTotalMapper.delete(TradeOrderTotalDO::getOldOrderId,orderId);
 
         // ====================== 【关键1】加锁查询:锁住当前版本行 ======================
         // FOR UPDATE 行锁 → 并发请求会排队,绝对安全
@@ -892,8 +896,9 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
             TradeOrderTotalDO a = new TradeOrderTotalDO();
             BeanUtils.copyProperties(lastVersion, a);
             a.setSyncStatus("0");
-            int updateMain = tradeOrderTotalMapper.insert(a);
-            if (!(updateMain ==1)) {
+            tradeOrderTotalMapper.updateDeletedById(dataId,0);
+            boolean updateMain = tradeOrderTotalMapper.insertOrUpdate(a);
+            if (!updateMain) {
                 throw new RuntimeException("主表回退失败");
             }
             // ====================== 3. 查询【再上一版】 ======================
@@ -923,7 +928,7 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
     @Transactional(rollbackFor = Exception.class)
     public void rollbackOrderCountryDataVersion(Long orderId){
         //首先把旧数据删除
-        tradeOrderCountryMapper.deleteAllByOrderId(orderId);
+        tradeOrderCountryMapper.delete(TradeOrderCountryDO::getOrderId,orderId);
 
         // ====================== 【关键1】加锁查询:锁住当前版本行 ======================
         // FOR UPDATE 行锁 → 并发请求会排队,绝对安全
@@ -945,8 +950,9 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
             TradeOrderCountryDO a = new TradeOrderCountryDO();
             BeanUtils.copyProperties(lastVersion, a);
             a.setSyncStatus("0");
-            int updateMain = tradeOrderCountryMapper.insert(a);
-            if (!(updateMain ==1)) {
+            tradeOrderCountryMapper.updateDeletedById(dataId,0);
+            boolean updateMain = tradeOrderCountryMapper.insertOrUpdate(a);
+            if (!updateMain) {
                 throw new RuntimeException("主表回退失败");
             }
 
@@ -979,8 +985,8 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
 
     @Transactional(rollbackFor = Exception.class)
     public void rollbackOrderRoomModelDataVersion(Long orderId){
-        //首先把旧数据删除
-        tradeOrderRoomModelMapper.deleteAllByOrderId(orderId);
+        //首先把旧数据删除,26-6-8修改:作逻辑删除,然后回滚时手动设置deleted=0去insertOrUpdate
+        tradeOrderRoomModelMapper.delete(TradeOrderRoomModelDO::getOrderId,orderId);
 
         // ====================== 【关键1】加锁查询:锁住当前版本行 ======================
         // FOR UPDATE 行锁 → 并发请求会排队,绝对安全
@@ -1002,8 +1008,9 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
             TradeOrderRoomModelDO a = new TradeOrderRoomModelDO();
             BeanUtils.copyProperties(lastVersion, a);
             a.setSyncStatus("0");
-            int updateMain = tradeOrderRoomModelMapper.insert(a);
-            if (!(updateMain ==1)) {
+            tradeOrderRoomModelMapper.updateDeletedById(dataId,0);
+            boolean updateMain = tradeOrderRoomModelMapper.insertOrUpdate(a);
+            if (!updateMain) {
                 throw new RuntimeException("主表回退失败");
             }
 
@@ -1037,7 +1044,7 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
     @Transactional(rollbackFor = Exception.class)
     public void rollbackOrderVisitorDataVersion(Long orderId){
         //首先把旧数据删除
-        tradeVisitorMapper.deleteAllByOrderId(orderId);
+        tradeVisitorMapper.delete(TradeVisitorDO::getOrderId,orderId);
 
         // ====================== 【关键1】加锁查询:锁住当前版本行 ======================
         // FOR UPDATE 行锁 → 并发请求会排队,绝对安全
@@ -1059,8 +1066,9 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
             TradeVisitorDO a = new TradeVisitorDO();
             BeanUtils.copyProperties(lastVersion, a);
             a.setSyncStatus("0");
-            int updateMain = tradeVisitorMapper.insert(a);
-            if (!(updateMain ==1)) {
+            tradeVisitorMapper.updateDeletedById(dataId,0);
+            boolean updateMain = tradeVisitorMapper.insertOrUpdate(a);
+            if (!updateMain) {
                 throw new RuntimeException("主表回退失败");
             }
 
@@ -1094,7 +1102,7 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
     @Transactional(rollbackFor = Exception.class)
     public void rollbackOrderFloorDataVersion(Long orderId){
         //首先把旧数据删除
-        tradeOrderFloorMapper.deleteAllByOrderId(orderId);
+        tradeOrderFloorMapper.delete(TradeOrderFloorDO::getOrderId,orderId);
 
         // ====================== 【关键1】加锁查询:锁住当前版本行 ======================
         // FOR UPDATE 行锁 → 并发请求会排队,绝对安全
@@ -1116,8 +1124,9 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
             TradeOrderFloorDO a = new TradeOrderFloorDO();
             BeanUtils.copyProperties(lastVersion, a);
             a.setSyncStatus("0");
-            int updateMain = tradeOrderFloorMapper.insert(a);
-            if (!(updateMain ==1)) {
+            tradeOrderFloorMapper.updateDeletedById(dataId,0);
+            boolean updateMain = tradeOrderFloorMapper.insertOrUpdate(a);
+            if (!updateMain) {
                 throw new RuntimeException("主表回退失败");
             }
 
@@ -1151,7 +1160,7 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
     @Transactional(rollbackFor = Exception.class)
     public void rollbackOrderPolicyDataVersion(Long orderId){
         //首先把旧数据删除
-        orderPolicyMapper.deleteAllByOrderId(orderId);
+        orderPolicyMapper.delete(OrderPolicyDO::getOrderId,orderId);
 
         // ====================== 【关键1】加锁查询:锁住当前版本行 ======================
         // FOR UPDATE 行锁 → 并发请求会排队,绝对安全
@@ -1172,8 +1181,9 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
             // ====================== 2. 主表回退(直接把上版本插进去) ======================
             OrderPolicyDO a = new OrderPolicyDO();
             BeanUtils.copyProperties(lastVersion, a);
-            int updateMain = orderPolicyMapper.insert(a);
-            if (!(updateMain ==1)) {
+            orderPolicyMapper.updateDeletedById(dataId,0);
+            boolean updateMain = orderPolicyMapper.insertOrUpdate(a);
+            if (!updateMain) {
                 throw new RuntimeException("主表回退失败");
             }
 

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

@@ -0,0 +1,17 @@
+package com.yc.ship.module.trade.service.report;
+
+import com.yc.ship.module.trade.controller.admin.report.vo.CabinMixDailyReqVO;
+
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+public interface CabinMixDailyService {
+    List<Map<String, Object>> getTableHeadList(Long shipId);
+
+    List<Map<String, Object>> getList(CabinMixDailyReqVO reqVO);
+
+    void exportCabinMixDaily(@Valid CabinMixDailyReqVO reqVO, HttpServletResponse response)throws IOException;
+}

+ 35 - 5
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/report/OpsDailyService.java

@@ -1,11 +1,7 @@
 package com.yc.ship.module.trade.service.report;
 
 import com.yc.ship.framework.common.pojo.PageResult;
-import com.yc.ship.module.trade.controller.admin.report.vo.CruiseOpsDailyReqVO;
-import com.yc.ship.module.trade.controller.admin.report.vo.CruiseOpsDailyRespVO;
-import com.yc.ship.module.trade.controller.admin.report.vo.YangtzePassengerSummaryPageReqVO;
-import com.yc.ship.module.trade.controller.admin.report.vo.YangtzePassengerSummaryRemarkReqVO;
-import com.yc.ship.module.trade.controller.admin.report.vo.YangtzePassengerSummaryRespVO;
+import com.yc.ship.module.trade.controller.admin.report.vo.*;
 
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
@@ -60,4 +56,38 @@ public interface OpsDailyService {
      */
     void updateYangtzePassengerSummaryRemark(YangtzePassengerSummaryRemarkReqVO reqVO);
 
+    /**
+     * 查询省际度假游轮二消情况航次表
+     *
+     * @param reqVO 查询条件
+     * @return 数据列表
+     */
+    List<ConsumptionIncomeVoyageRespVO> getConsumptionIncomeVoyageList(ConsumptionIncomeVoyageReqVO reqVO);
+
+    /**
+     * 导出省际度假游轮二消情况航次表 Excel
+     *
+     * @param reqVO    查询条件
+     * @param response HTTP响应
+     * @throws IOException IO异常
+     */
+    void exportConsumptionIncomeVoyageExcel(ConsumptionIncomeVoyageReqVO reqVO, HttpServletResponse response) throws IOException;
+
+    /**
+     * 查询省际度假游轮二消情况日报表
+     *
+     * @param reqVO 查询条件
+     * @return 数据列表
+     */
+    List<ConsumptionIncomeDailyRespVO> getConsumptionIncomeDailyList(ConsumptionIncomeDailyReqVO reqVO);
+
+    /**
+     * 导出省际度假游轮二消情况日报表 Excel
+     *
+     * @param reqVO    查询条件
+     * @param response HTTP响应
+     * @throws IOException IO异常
+     */
+    void exportConsumptionIncomeDailyExcel(ConsumptionIncomeDailyReqVO reqVO, HttpServletResponse response) throws IOException;
+
 }

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

@@ -0,0 +1,457 @@
+package com.yc.ship.module.trade.service.report.impl;
+
+import com.alibaba.excel.EasyExcel;
+import com.alibaba.excel.converters.longconverter.LongStringConverter;
+import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
+import com.yc.ship.module.resource.dal.mysql.room.ResourceRoomMapper;
+import com.yc.ship.module.resource.helper.DateHelper;
+import com.yc.ship.module.trade.controller.admin.report.vo.CabinMixDailyReqVO;
+import com.yc.ship.module.trade.controller.admin.report.vo.ConsumptionIncomeDailyReqVO;
+import com.yc.ship.module.trade.dal.mysql.report.CabinMixDailyMapper;
+import com.yc.ship.module.trade.service.report.CabinMixDailyService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+@Slf4j
+public class CabinMixDailyServiceImpl implements CabinMixDailyService {
+    @Resource
+    private ResourceRoomMapper resourceRoomMapper;
+    @Resource
+    private CabinMixDailyMapper cabinMixDailyMapper;
+
+    @Override
+    public List<Map<String, Object>> getTableHeadList(Long shipId) {
+        List<Map<String, Object>> list = resourceRoomMapper.getCabinMixDailyTableHeadList(shipId);
+        return list;
+    }
+
+    @Override
+    public List<Map<String, Object>> getList(CabinMixDailyReqVO reqVO) {
+        if (reqVO.getShipId() == null) {
+            return Collections.emptyList();
+        }
+
+        List<Map<String, Object>> roomTypeList = resourceRoomMapper.getCabinMixDailyTableHeadList(reqVO.getShipId());
+        if (roomTypeList == null || roomTypeList.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        List<Map<String,Object>> currentDataList = cabinMixDailyMapper.getList(reqVO, buildRoomColumns(roomTypeList));
+
+        if (currentDataList == null || currentDataList.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        List<Map<String, Object>> lastYearDataList = queryLastYearData(reqVO, roomTypeList);
+
+        return processCabinMixData(currentDataList, lastYearDataList, roomTypeList);
+    }
+
+
+    private List<Map<String, Object>> queryLastYearData(CabinMixDailyReqVO reqVO, List<Map<String, Object>> roomTypeList) {
+        try {
+            String startDate = reqVO.getStartDate();
+            String endDate = reqVO.getEndDate();
+
+            if (startDate == null || startDate.isEmpty()) {
+                return new ArrayList<>();
+            }
+
+            Date startD = DateHelper.parseDate(startDate);
+            Date endD = endDate != null && !endDate.isEmpty() ? DateHelper.parseDate(endDate) : null;
+
+            CabinMixDailyReqVO lastYearReq = new CabinMixDailyReqVO();
+            lastYearReq.setShipId(reqVO.getShipId());
+            lastYearReq.setStartDate(DateHelper.getAppointYearStartDate(startD));
+
+            if (endD != null) {
+                Calendar cal = Calendar.getInstance();
+                cal.setTime(startD);
+                int daysDiff = (int) ((endD.getTime() - startD.getTime()) / (1000 * 60 * 60 * 24));
+                cal.add(Calendar.YEAR, -1);
+                cal.add(Calendar.DATE, daysDiff);
+                lastYearReq.setEndDate(DateHelper.format(cal.getTime(), "yyyy-MM-dd"));
+            } else {
+                lastYearReq.setEndDate(DateHelper.format(new Date(), "yyyy-MM-dd"));
+            }
+
+            return cabinMixDailyMapper.getList(lastYearReq, buildRoomColumns(roomTypeList));
+        } catch (Exception e) {
+            log.warn("[queryLastYearData] 查询去年同期数据失败: {}", e.getMessage());
+            return new ArrayList<>();
+        }
+    }
+
+    private String buildRoomColumns(List<Map<String, Object>> roomTypeList) {
+        StringBuilder columns = new StringBuilder();
+        for (int i = 0; i < roomTypeList.size(); i++) {
+            Map<String, Object> roomType = roomTypeList.get(i);
+            String roomModelName = roomType.get("room_model_name").toString();
+            Long roomModelId = Long.valueOf(roomType.get("roomModelId").toString());
+
+            String columnSql;
+            if (roomModelName.contains("内舱")) {
+                columnSql = String.format(
+                        "COALESCE(voyage_visitor.total_visitor / 2, 0) AS `%s`",
+                        roomModelName
+                );
+            } else {
+                columnSql = String.format(
+                        "COALESCE(SUM(CASE WHEN torm.room_model_id = %s THEN torm.use_room_num ELSE 0 END), 0) AS `%s`",
+                        roomModelId, roomModelName
+                );
+            }
+
+            columns.append(columnSql);
+
+            if (i < roomTypeList.size() - 1) {
+                columns.append(", ");
+            }
+        }
+        return columns.toString();
+    }
+
+    private List<Map<String, Object>> processCabinMixData(
+            List<Map<String, Object>> currentDataList,
+            List<Map<String, Object>> lastYearDataList,
+            List<Map<String, Object>> roomTypeList) {
+
+        List<Map<String, Object>> result = new ArrayList<>();
+
+        Map<String, List<Map<String, Object>>> currentMonthMap = currentDataList.stream()
+                .collect(Collectors.groupingBy(
+                        item -> String.valueOf(item.get("month")),
+                        TreeMap::new, Collectors.toList()));
+
+        Map<String, List<Map<String, Object>>> lastYearMonthMap = lastYearDataList.stream()
+                .collect(Collectors.groupingBy(
+                        item -> String.valueOf(item.get("month")),
+                        TreeMap::new, Collectors.toList()));
+
+        int index = 1;
+        Map<String, Double> cumulativeRoomCountMap = new LinkedHashMap<>();
+        Map<String, Double> lastYearCumulativeMap = new LinkedHashMap<>();
+        boolean isFirstMonth = true;
+
+        List<String> roomModelNames = roomTypeList.stream()
+                .map(room -> room.get("room_model_name").toString())
+                .collect(Collectors.toList());
+
+        for (Map.Entry<String, List<Map<String, Object>>> entry : currentMonthMap.entrySet()) {
+            String month = entry.getKey();
+            List<Map<String, Object>> dailyItems = entry.getValue();
+
+            for (Map<String, Object> data : dailyItems) {
+                Map<String, Object> row = createBasicRow(data, index++, roomModelNames);
+                row.put("month", formatMonth(month));
+                row.put("isSubtotal", false);
+                row.put("isCumulative", false);
+                result.add(row);
+
+                accumulateSingleRoomCount(cumulativeRoomCountMap, data, roomModelNames);
+            }
+
+            Map<String, Double> monthRoomCountMap = calculateMonthRoomCount(dailyItems, roomModelNames);
+            double totalRooms = monthRoomCountMap.values().stream().mapToDouble(Double::doubleValue).sum();
+
+            Map<String, Object> subtotalDataVO = createSummaryRow(
+                    month + "月小计", "", "", "数据", monthRoomCountMap, totalRooms, true, false);
+            subtotalDataVO.put("index", "");
+            result.add(subtotalDataVO);
+
+            String lastYearMonth = getMonthMinusOneYear(month);
+            List<Map<String, Object>> lastYearMonthData = lastYearMonthMap.getOrDefault(lastYearMonth, new ArrayList<>());
+            Map<String, Double> lastYearMonthRoomCountMap = calculateMonthRoomCount(lastYearMonthData, roomModelNames);
+            Map<String, Object> subtotalYoyVO = createYoyRow(monthRoomCountMap, lastYearMonthRoomCountMap, roomModelNames,true, false);
+            result.add(subtotalYoyVO);
+
+            accumulateListRoomCount(lastYearCumulativeMap, lastYearMonthData, roomModelNames);
+
+            if (!isFirstMonth) {
+                double cumulativeTotal = cumulativeRoomCountMap.values().stream().mapToDouble(Double::doubleValue).sum();
+
+                Map<String, Object> cumulativeDataVO = createSummaryRow(
+                        "累计", "", "", "数据", cumulativeRoomCountMap, cumulativeTotal, false, true);
+                cumulativeDataVO.put("index", "");
+                result.add(cumulativeDataVO);
+
+                Map<String, Object> cumulativeYoyVO = createYoyRow(cumulativeRoomCountMap, lastYearCumulativeMap, roomModelNames,false, true);
+                result.add(cumulativeYoyVO);
+            }
+
+            isFirstMonth = false;
+        }
+
+        return result;
+    }
+
+    private String getMonthMinusOneYear(String month) {
+        if (month == null || month.length() < 7) {
+            return "";
+        }
+        try {
+            int year = Integer.parseInt(month.substring(0, 4));
+            String rest = month.substring(4);
+            return (year - 1) + rest;
+        } catch (NumberFormatException e) {
+            return "";
+        }
+    }
+
+    private String formatMonth(String month) {
+        if (month == null || month.length() < 5) {
+            return month;
+        }
+        return month.substring(5);
+    }
+
+    private Map<String, Object> createBasicRow(Map<String, Object> data, int index, List<String> roomModelNames) {
+        Map<String, Object> row = new LinkedHashMap<>();
+        row.put("index", index);
+        row.put("date", String.valueOf(data.get("date")));
+        row.put("ship", String.valueOf(data.get("ship")));
+        row.put("route", String.valueOf(data.get("route")));
+        row.put("voyageNo", String.valueOf(data.get("voyageNo")));
+
+        for (String roomModelName : roomModelNames) {
+            Double roomNum = data.get(roomModelName) != null ?
+                    ((Number) data.get(roomModelName)).doubleValue() : 0.0;
+            row.put(roomModelName, roomNum);
+        }
+
+        Object totalRooms = data.get("totalRooms");
+        if (totalRooms != null) {
+            row.put("totalRooms", ((Number) totalRooms).doubleValue());
+        }
+
+        return row;
+    }
+
+    private Map<String, Double> calculateMonthRoomCount(List<Map<String, Object>> dataList, List<String> roomModelNames) {
+        Map<String, Double> roomCountMap = new LinkedHashMap<>();
+
+        for (String roomModelName : roomModelNames) {
+            double sum = dataList.stream()
+                    .mapToDouble(data -> {
+                        Object value = data.get(roomModelName);
+                        return value != null ? ((Number) value).doubleValue() : 0.0;
+                    })
+                    .sum();
+            roomCountMap.put(roomModelName, sum);
+        }
+
+        return roomCountMap;
+    }
+
+    private void accumulateSingleRoomCount(Map<String, Double> cumulativeMap, Map<String, Object> data, List<String> roomModelNames) {
+        for (String roomModelName : roomModelNames) {
+            Double roomNum = data.get(roomModelName) != null ?
+                    ((Number) data.get(roomModelName)).doubleValue() : 0.0;
+            cumulativeMap.merge(roomModelName, roomNum, Double::sum);
+        }
+    }
+
+    private void accumulateListRoomCount(Map<String, Double> cumulativeMap, List<Map<String, Object>> dataList, List<String> roomModelNames) {
+        for (String roomModelName : roomModelNames) {
+            double sum = dataList.stream()
+                    .mapToDouble(data -> {
+                        Object value = data.get(roomModelName);
+                        return value != null ? ((Number) value).doubleValue() : 0.0;
+                    })
+                    .sum();
+            cumulativeMap.merge(roomModelName, sum, Double::sum);
+        }
+    }
+
+    private Map<String, Object> createSummaryRow(
+            String month, String date, String ship, String voyageNo,
+            Map<String, Double> roomCountMap, double totalRooms,
+            boolean isSubtotal, boolean isCumulative) {
+
+        Map<String, Object> row = new LinkedHashMap<>();
+        row.put("month", month);
+        row.put("date", date);
+        row.put("ship", ship);
+        row.put("route", "");
+        row.put("voyageNo", voyageNo);
+
+        for (Map.Entry<String, Double> entry : roomCountMap.entrySet()) {
+            row.put(entry.getKey(), entry.getValue());
+        }
+
+        row.put("totalRooms", totalRooms);
+        row.put("isSubtotal", isSubtotal);
+        row.put("isCumulative", isCumulative);
+
+        return row;
+    }
+
+    private Map<String, Object> createYoyRow(
+            Map<String, Double> currentMap,
+            Map<String, Double> lastYearMap,
+            List<String> roomModelNames,boolean isSubtotal, boolean isCumulative) {
+
+        Map<String, Object> yoyRow = new LinkedHashMap<>();
+        yoyRow.put("month", "");
+        yoyRow.put("date", "");
+        yoyRow.put("ship", "");
+        yoyRow.put("route", "");
+        yoyRow.put("voyageNo", "同比");
+        yoyRow.put("isSubtotal", isSubtotal);
+        yoyRow.put("isCumulative", isCumulative);
+
+        for (String roomModelName : roomModelNames) {
+            Double currentVal = currentMap.getOrDefault(roomModelName, 0.0);
+            Double lastYearVal = lastYearMap.getOrDefault(roomModelName, 0.0);
+            String yoyPercent = calcYoyPercent(currentVal, lastYearVal);
+            yoyRow.put(roomModelName, yoyPercent);
+        }
+
+        double currentTotal = currentMap.values().stream().mapToDouble(Double::doubleValue).sum();
+        double lastYearTotal = lastYearMap.values().stream().mapToDouble(Double::doubleValue).sum();
+        yoyRow.put("totalRooms", calcYoyPercent(currentTotal, lastYearTotal));
+
+        return yoyRow;
+    }
+
+    private String calcYoyPercent(Number current, Number last) {
+        if (current == null || last == null || last.doubleValue() == 0) {
+            return "-";
+        }
+        double diff = current.doubleValue() - last.doubleValue();
+        double percent = (diff / last.doubleValue()) * 100;
+        if (percent >= 0) {
+            return String.format("+%.1f%%", percent);
+        } else {
+            return String.format("%.1f%%", percent);
+        }
+    }
+
+    @Override
+    public void exportCabinMixDaily(@Valid CabinMixDailyReqVO reqVO, HttpServletResponse response) throws IOException {
+        if (reqVO.getShipId() == null) {
+            throw new IllegalArgumentException("请选择游轮");
+        }
+
+        List<Map<String, Object>> roomTypeList = resourceRoomMapper.getCabinMixDailyTableHeadList(reqVO.getShipId());
+        if (roomTypeList == null || roomTypeList.isEmpty()) {
+            throw new IllegalArgumentException("该游轮暂无房型配置");
+        }
+
+        List<Map<String, Object>> dataList = getList(reqVO);
+        if (dataList == null || dataList.isEmpty()) {
+            throw new IllegalArgumentException("暂无数据可导出");
+        }
+
+        List<List<String>> headers = buildExportHeaders(roomTypeList);
+        List<List<Object>> exportData = transformExportData(dataList, roomTypeList);
+
+        EasyExcel.write(response.getOutputStream())
+                .head(headers)
+                .autoCloseStream(false)
+                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
+                .registerConverter(new LongStringConverter())
+                .sheet("省际度假游轮房型分布日报")
+                .doWrite(exportData);
+
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");
+        response.addHeader("Content-Disposition",
+                "attachment;filename=" + URLEncoder.encode("省际度假游轮房型分布日报表.xlsx", StandardCharsets.UTF_8.name()));
+    }
+
+    private List<List<String>> buildExportHeaders(List<Map<String, Object>> roomTypeList) {
+        List<List<String>> headers = new ArrayList<>();
+
+        headers.add(new ArrayList<>(Arrays.asList("序号")));
+        headers.add(new ArrayList<>(Arrays.asList("月份")));
+        headers.add(new ArrayList<>(Arrays.asList("日期")));
+        headers.add(new ArrayList<>(Arrays.asList("船舶")));
+        headers.add(new ArrayList<>(Arrays.asList("航线")));
+        headers.add(new ArrayList<>(Arrays.asList("航次号")));
+
+        for (Map<String, Object> roomType : roomTypeList) {
+            String roomModelName = roomType.get("room_model_name").toString();
+            headers.add(new ArrayList<>(Arrays.asList(roomModelName)));
+        }
+
+        headers.add(new ArrayList<>(Arrays.asList("合计")));
+
+        return headers;
+    }
+
+    private List<List<Object>> transformExportData(List<Map<String, Object>> dataList, List<Map<String, Object>> roomTypeList) {
+        List<List<Object>> result = new ArrayList<>(dataList.size());
+        List<String> roomModelNames = roomTypeList.stream()
+                .map(room -> room.get("room_model_name").toString())
+                .collect(Collectors.toList());
+
+        for (Map<String, Object> row : dataList) {
+            List<Object> rowData = new ArrayList<>();
+
+            boolean isYoy = "同比".equals(row.get("voyageNo"));
+            boolean isSubtotalOrCumulative = Boolean.TRUE.equals(row.get("isSubtotal"))
+                    || Boolean.TRUE.equals(row.get("isCumulative"));
+
+            if (isYoy) {
+                rowData.add("");
+                rowData.add("");
+                rowData.add("");
+                rowData.add("");
+                rowData.add("");
+                rowData.add("同比");
+
+                for (String roomModelName : roomModelNames) {
+                    rowData.add(row.get(roomModelName) != null ? row.get(roomModelName) : "-");
+                }
+                rowData.add(row.get("totalRooms") != null ? row.get("totalRooms") : "-");
+            } else {
+                rowData.add(row.get("index") != null ? String.valueOf(row.get("index")) : "");
+                rowData.add(row.get("month") != null ? row.get("month") : "");
+                rowData.add(row.get("date") != null ? row.get("date") : "");
+                rowData.add(row.get("ship") != null ? row.get("ship") : "");
+                rowData.add(row.get("route") != null ? row.get("route") : "");
+                rowData.add(row.get("voyageNo") != null ? row.get("voyageNo") : "");
+
+                for (String roomModelName : roomModelNames) {
+                    Object value = row.get(roomModelName);
+                    if (value != null) {
+                        double num = ((Number) value).doubleValue();
+                        if (num == (int) num) {
+                            rowData.add(String.valueOf((int) num));
+                        } else {
+                            rowData.add(String.format("%.1f", num));
+                        }
+                    } else {
+                        rowData.add("-");
+                    }
+                }
+
+                Object totalRooms = row.get("totalRooms");
+                if (totalRooms != null) {
+                    double num = ((Number) totalRooms).doubleValue();
+                    if (num == (int) num) {
+                        rowData.add(String.valueOf((int) num));
+                    } else {
+                        rowData.add(String.format("%.1f", num));
+                    }
+                } else {
+                    rowData.add("-");
+                }
+            }
+
+            result.add(rowData);
+        }
+
+        return result;
+    }
+}

+ 842 - 5
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/report/impl/OpsDailyServiceImpl.java

@@ -4,25 +4,28 @@ import cn.hutool.core.collection.CollUtil;
 import com.alibaba.excel.EasyExcel;
 import com.alibaba.excel.converters.longconverter.LongStringConverter;
 import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.yc.ship.framework.common.pojo.PageResult;
 import com.yc.ship.framework.excel.core.util.ExcelUtils;
+import com.yc.ship.framework.tenant.core.aop.TenantIgnore;
 import com.yc.ship.module.resource.helper.DateHelper;
 import com.yc.ship.module.resource.helper.MathHelper;
 import com.yc.ship.module.product.dal.dataobject.voyage.VoyageDO;
 import com.yc.ship.module.product.dal.mysql.voyage.VoyageMapper;
-import com.yc.ship.module.trade.controller.admin.report.vo.CruiseOpsDailyReqVO;
-import com.yc.ship.module.trade.controller.admin.report.vo.CruiseOpsDailyRespVO;
-import com.yc.ship.module.trade.controller.admin.report.vo.YangtzePassengerSummaryPageReqVO;
-import com.yc.ship.module.trade.controller.admin.report.vo.YangtzePassengerSummaryRemarkReqVO;
-import com.yc.ship.module.trade.controller.admin.report.vo.YangtzePassengerSummaryRespVO;
+import com.yc.ship.module.trade.controller.admin.report.vo.*;
+import com.yc.ship.module.trade.dal.dataobject.consumption.ConsumptionIncomeDailyDO;
+import com.yc.ship.module.trade.dal.dataobject.consumption.ConsumptionIncomeVoyageDO;
+import com.yc.ship.module.trade.dal.mysql.consumption.ConsumptionIncomeDailyMapper;
+import com.yc.ship.module.trade.dal.mysql.consumption.ConsumptionIncomeVoyageMapper;
 import com.yc.ship.module.trade.dal.mysql.report.OpsDailyMapper;
 import com.yc.ship.module.trade.dal.mysql.report.YangtzePassengerSummaryMapper;
 import com.yc.ship.module.trade.service.report.OpsDailyService;
 import com.yc.ship.module.trade.utils.excel.CruiseOpsDailyExportStyleHandler;
 import com.yc.ship.module.trade.utils.excel.YangtzePassengerSummaryExportStyleHandler;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
@@ -49,6 +52,11 @@ public class OpsDailyServiceImpl implements OpsDailyService {
 
     @Resource
     private OpsDailyMapper cruiseOpsDailyMapper;
+    @Resource
+    private ConsumptionIncomeVoyageMapper consumptionIncomeVoyageMapper;
+    @Resource
+    private ConsumptionIncomeDailyMapper consumptionIncomeDailyMapper;
+
 
     @Override
     public List<CruiseOpsDailyRespVO> getCruiseOpsDailyList(CruiseOpsDailyReqVO reqVO) {
@@ -818,5 +826,834 @@ public class OpsDailyServiceImpl implements OpsDailyService {
         voyageMapper.updateById(updateDO);
     }
 
+    @Override
+    @TenantIgnore
+    public List<ConsumptionIncomeVoyageRespVO> getConsumptionIncomeVoyageList(ConsumptionIncomeVoyageReqVO reqVO) {
+        log.info("查询省际度假游轮二消情况航次表任务开始时间{}", LocalDateTime.now());
+
+        List<ConsumptionIncomeVoyageDO> doList = consumptionIncomeVoyageMapper.selectList(reqVO);
+        log.info("查询省际度假游轮二消情况航次表任务结束时间{}", LocalDateTime.now());
+
+        if (CollUtil.isEmpty(doList)) {
+            return new ArrayList<>();
+        }
+
+        List<ConsumptionIncomeVoyageRespVO> respList = convertToList(doList);
+
+        return buildReportWithSubtotalAndCumulative(respList);
+    }
+
+    private List<ConsumptionIncomeVoyageRespVO> convertToList(List<ConsumptionIncomeVoyageDO> doList) {
+        List<ConsumptionIncomeVoyageRespVO> respList = new ArrayList<>();
+        int index = 1;
+        for (ConsumptionIncomeVoyageDO item : doList) {
+            ConsumptionIncomeVoyageRespVO respVO = new ConsumptionIncomeVoyageRespVO();
+            BeanUtils.copyProperties(item, respVO);
+            respVO.setIndex(index++);
+            respVO.setRoute(item.getRoute());
+            respList.add(respVO);
+        }
+        return respList;
+    }
+
+    private List<ConsumptionIncomeVoyageRespVO> buildReportWithSubtotalAndCumulative(
+            List<ConsumptionIncomeVoyageRespVO> currentDataList) {
+
+        List<ConsumptionIncomeVoyageRespVO> result = new ArrayList<>();
+
+        Map<String, List<ConsumptionIncomeVoyageRespVO>> currentMonthMap = currentDataList.stream()
+                .collect(Collectors.groupingBy(
+                        item -> item.getStatMonth() != null ? item.getStatMonth() : "",
+                        TreeMap::new, Collectors.toList()));
+
+        int globalIdx = 1;
+        List<ConsumptionIncomeVoyageRespVO> allDailyDataForCumulative = new ArrayList<>();
+
+        int monthIndex = 0;
+        for (Map.Entry<String, List<ConsumptionIncomeVoyageRespVO>> entry : currentMonthMap.entrySet()) {
+            String month = entry.getKey();
+            List<ConsumptionIncomeVoyageRespVO> dailyItems = entry.getValue();
+
+            for (ConsumptionIncomeVoyageRespVO item : dailyItems) {
+                item.setIndex(globalIdx++);
+            }
+
+            result.addAll(dailyItems);
+            allDailyDataForCumulative.addAll(dailyItems);
+
+            ConsumptionIncomeVoyageRespVO subtotalData = buildConsumptionSubtotalRow(dailyItems, month);
+            result.add(subtotalData);
+
+            ConsumptionIncomeVoyageRespVO lastYearSubtotal = buildConsumptionSubtotalRow(new ArrayList<>(), "");
+            ConsumptionIncomeVoyageRespVO subtotalYoy = buildConsumptionYoyRow(subtotalData, lastYearSubtotal);
+            result.add(subtotalYoy);
+
+            if (monthIndex > 0) {
+                ConsumptionIncomeVoyageRespVO cumData = buildConsumptionSubtotalRow(allDailyDataForCumulative, "累计");
+                result.add(cumData);
+
+                ConsumptionIncomeVoyageRespVO lastYearCumData = buildConsumptionSubtotalRow(new ArrayList<>(), "");
+                ConsumptionIncomeVoyageRespVO cumYoy = buildConsumptionYoyRow(cumData, lastYearCumData);
+                result.add(cumYoy);
+            }
+
+            monthIndex++;
+        }
+
+        return result;
+    }
+
+    private ConsumptionIncomeVoyageRespVO buildConsumptionSubtotalRow(List<ConsumptionIncomeVoyageRespVO> items, String monthLabel) {
+        ConsumptionIncomeVoyageRespVO row = new ConsumptionIncomeVoyageRespVO();
+
+        if ("累计".equals(monthLabel)) {
+            row.setVoyageName("累计");
+        } else if (!"累计".equals(monthLabel) && monthLabel.contains("小计")) {
+            row.setVoyageName(monthLabel);
+        } else {
+            row.setVoyageName(monthLabel + "小计");
+        }
+        row.setRoute("");
+        row.setVoyageNo("数据");
+        row.setIsSubtotal(!"累计".equals(row.getVoyageName()));
+        row.setIsCumulative("累计".equals(row.getVoyageName()));
+
+        if (CollUtil.isEmpty(items)) {
+            return row;
+        }
+
+        BigDecimal targetPeopleSum = BigDecimal.ZERO;
+        BigDecimal frontDeskSum = BigDecimal.ZERO;
+        BigDecimal restaurantSum = BigDecimal.ZERO;
+        BigDecimal barPrepackedSum = BigDecimal.ZERO;
+        BigDecimal barMixedSum = BigDecimal.ZERO;
+        BigDecimal barKtvSum = BigDecimal.ZERO;
+        BigDecimal barMahjongSum = BigDecimal.ZERO;
+        BigDecimal barBarbecueSum = BigDecimal.ZERO;
+        BigDecimal barOtherSum = BigDecimal.ZERO;
+        BigDecimal culturalSum = BigDecimal.ZERO;
+        BigDecimal roomSum = BigDecimal.ZERO;
+        BigDecimal selfSum = BigDecimal.ZERO;
+        BigDecimal otherSum = BigDecimal.ZERO;
+        BigDecimal partnerPhotoSum = BigDecimal.ZERO;
+        BigDecimal partnerSuperSum = BigDecimal.ZERO;
+        BigDecimal partnerSpaSum = BigDecimal.ZERO;
+        BigDecimal partnerClinicSum = BigDecimal.ZERO;
+        BigDecimal totalIncomeSum = BigDecimal.ZERO;
+        BigDecimal payOnlineSum = BigDecimal.ZERO;
+        BigDecimal payCashSum = BigDecimal.ZERO;
+        BigDecimal receivableSum = BigDecimal.ZERO;
+        BigDecimal payOtherSum = BigDecimal.ZERO;
+        BigDecimal payablePhotoSum = BigDecimal.ZERO;
+        BigDecimal payableSuperSum = BigDecimal.ZERO;
+        BigDecimal payableSpaSum = BigDecimal.ZERO;
+        BigDecimal payableClinicSum = BigDecimal.ZERO;
+        BigDecimal partnerTotalPayableSum = BigDecimal.ZERO;
+        BigDecimal changjiangSum = BigDecimal.ZERO;
+
+        for (ConsumptionIncomeVoyageRespVO item : items) {
+            targetPeopleSum = targetPeopleSum.add(safeBigDecimal(item.getTargetPeople()));
+            frontDeskSum = frontDeskSum.add(safeBigDecimal(item.getFrontDeskIncome()));
+            restaurantSum = restaurantSum.add(safeBigDecimal(item.getRestaurantIncome()));
+            barPrepackedSum = barPrepackedSum.add(safeBigDecimal(item.getBarPrepackedWine()));
+            barMixedSum = barMixedSum.add(safeBigDecimal(item.getBarMixedWine()));
+            barKtvSum = barKtvSum.add(safeBigDecimal(item.getBarKtv()));
+            barMahjongSum = barMahjongSum.add(safeBigDecimal(item.getBarMahjong()));
+            barBarbecueSum = barBarbecueSum.add(safeBigDecimal(item.getBarBarbecue()));
+            barOtherSum = barOtherSum.add(safeBigDecimal(item.getBarOther()));
+            culturalSum = culturalSum.add(safeBigDecimal(item.getCulturalIncome()));
+            roomSum = roomSum.add(safeBigDecimal(item.getRoomIncome()));
+            selfSum = selfSum.add(safeBigDecimal(item.getSelfIncome()));
+            otherSum = otherSum.add(safeBigDecimal(item.getOtherIncome()));
+            partnerPhotoSum = partnerPhotoSum.add(safeBigDecimal(item.getPartnerPhotography()));
+            partnerSuperSum = partnerSuperSum.add(safeBigDecimal(item.getPartnerSupermarket()));
+            partnerSpaSum = partnerSpaSum.add(safeBigDecimal(item.getPartnerSpa()));
+            partnerClinicSum = partnerClinicSum.add(safeBigDecimal(item.getPartnerClinic()));
+            totalIncomeSum = totalIncomeSum.add(safeBigDecimal(item.getTotalIncome()));
+            payOnlineSum = payOnlineSum.add(safeBigDecimal(item.getPayOnline()));
+            payCashSum = payCashSum.add(safeBigDecimal(item.getPayCash()));
+            receivableSum = receivableSum.add(safeBigDecimal(item.getReceivable()));
+            payOtherSum = payOtherSum.add(safeBigDecimal(item.getPayOther()));
+            payablePhotoSum = payablePhotoSum.add(safeBigDecimal(item.getPayablePhotography()));
+            payableSuperSum = payableSuperSum.add(safeBigDecimal(item.getPayableSupermarket()));
+            payableSpaSum = payableSpaSum.add(safeBigDecimal(item.getPayableSpa()));
+            payableClinicSum = payableClinicSum.add(safeBigDecimal(item.getPayableClinic()));
+            partnerTotalPayableSum = partnerTotalPayableSum.add(safeBigDecimal(item.getPartnerTotalPayable()));
+            changjiangSum = changjiangSum.add(safeBigDecimal(item.getChangjiangIncome()));
+        }
+
+        row.setTargetPeople(targetPeopleSum);
+
+        if (targetPeopleSum.compareTo(BigDecimal.ZERO) > 0) {
+            row.setAvgPerPerson(totalIncomeSum.divide(targetPeopleSum, 2, RoundingMode.HALF_UP));
+        } else {
+            row.setAvgPerPerson(BigDecimal.ZERO);
+        }
+
+        row.setFrontDeskIncome(frontDeskSum);
+        row.setRestaurantIncome(restaurantSum);
+        row.setBarPrepackedWine(barPrepackedSum);
+        row.setBarMixedWine(barMixedSum);
+        row.setBarKtv(barKtvSum);
+        row.setBarMahjong(barMahjongSum);
+        row.setBarBarbecue(barBarbecueSum);
+        row.setBarOther(barOtherSum);
+        row.setCulturalIncome(culturalSum);
+        row.setRoomIncome(roomSum);
+        row.setSelfIncome(selfSum);
+        row.setOtherIncome(otherSum);
+        row.setPartnerPhotography(partnerPhotoSum);
+        row.setPartnerSupermarket(partnerSuperSum);
+        row.setPartnerSpa(partnerSpaSum);
+        row.setPartnerClinic(partnerClinicSum);
+        row.setTotalIncome(totalIncomeSum);
+        row.setPayOnline(payOnlineSum);
+        row.setPayCash(payCashSum);
+        row.setReceivable(receivableSum);
+        row.setPayOther(payOtherSum);
+        row.setPayablePhotography(payablePhotoSum);
+        row.setPayableSupermarket(payableSuperSum);
+        row.setPayableSpa(payableSpaSum);
+        row.setPayableClinic(payableClinicSum);
+        row.setPartnerTotalPayable(partnerTotalPayableSum);
+        row.setChangjiangIncome(changjiangSum);
+
+        return row;
+    }
+
+    private ConsumptionIncomeVoyageRespVO buildConsumptionYoyRow(ConsumptionIncomeVoyageRespVO currentRow, ConsumptionIncomeVoyageRespVO lastYearRow) {
+        ConsumptionIncomeVoyageRespVO yoyRow = new ConsumptionIncomeVoyageRespVO();
+        yoyRow.setIndex(null);
+        yoyRow.setVoyageName("");
+        yoyRow.setVoyageNo("同比");
+        yoyRow.setIsSubtotal(currentRow.getIsSubtotal() != null && currentRow.getIsSubtotal());
+        yoyRow.setIsCumulative(currentRow.getIsCumulative() != null && currentRow.getIsCumulative());
+
+        yoyRow.setFrontDeskIncome(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getFrontDeskIncome()), safeBigDecimal(lastYearRow.getFrontDeskIncome())));
+        yoyRow.setRestaurantIncome(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getRestaurantIncome()), safeBigDecimal(lastYearRow.getRestaurantIncome())));
+        yoyRow.setBarPrepackedWine(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getBarPrepackedWine()), safeBigDecimal(lastYearRow.getBarPrepackedWine())));
+        yoyRow.setBarMixedWine(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getBarMixedWine()), safeBigDecimal(lastYearRow.getBarMixedWine())));
+        yoyRow.setBarKtv(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getBarKtv()), safeBigDecimal(lastYearRow.getBarKtv())));
+        yoyRow.setBarMahjong(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getBarMahjong()), safeBigDecimal(lastYearRow.getBarMahjong())));
+        yoyRow.setBarBarbecue(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getBarBarbecue()), safeBigDecimal(lastYearRow.getBarBarbecue())));
+        yoyRow.setBarOther(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getBarOther()), safeBigDecimal(lastYearRow.getBarOther())));
+        yoyRow.setCulturalIncome(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getCulturalIncome()), safeBigDecimal(lastYearRow.getCulturalIncome())));
+        yoyRow.setRoomIncome(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getRoomIncome()), safeBigDecimal(lastYearRow.getRoomIncome())));
+        yoyRow.setSelfIncome(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getSelfIncome()), safeBigDecimal(lastYearRow.getSelfIncome())));
+        yoyRow.setOtherIncome(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getOtherIncome()), safeBigDecimal(lastYearRow.getOtherIncome())));
+        yoyRow.setPartnerPhotography(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getPartnerPhotography()), safeBigDecimal(lastYearRow.getPartnerPhotography())));
+        yoyRow.setPartnerSupermarket(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getPartnerSupermarket()), safeBigDecimal(lastYearRow.getPartnerSupermarket())));
+        yoyRow.setPartnerSpa(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getPartnerSpa()), safeBigDecimal(lastYearRow.getPartnerSpa())));
+        yoyRow.setPartnerClinic(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getPartnerClinic()), safeBigDecimal(lastYearRow.getPartnerClinic())));
+        yoyRow.setTotalIncome(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getTotalIncome()), safeBigDecimal(lastYearRow.getTotalIncome())));
+        yoyRow.setPayOnline(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getPayOnline()), safeBigDecimal(lastYearRow.getPayOnline())));
+        yoyRow.setPayCash(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getPayCash()), safeBigDecimal(lastYearRow.getPayCash())));
+        yoyRow.setReceivable(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getReceivable()), safeBigDecimal(lastYearRow.getReceivable())));
+        yoyRow.setPayOther(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getPayOther()), safeBigDecimal(lastYearRow.getPayOther())));
+        yoyRow.setPayablePhotography(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getPayablePhotography()), safeBigDecimal(lastYearRow.getPayablePhotography())));
+        yoyRow.setPayableSupermarket(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getPayableSupermarket()), safeBigDecimal(lastYearRow.getPayableSupermarket())));
+        yoyRow.setPayableSpa(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getPayableSpa()), safeBigDecimal(lastYearRow.getPayableSpa())));
+        yoyRow.setPayableClinic(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getPayableClinic()), safeBigDecimal(lastYearRow.getPayableClinic())));
+        yoyRow.setPartnerTotalPayable(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getPartnerTotalPayable()), safeBigDecimal(lastYearRow.getPartnerTotalPayable())));
+        yoyRow.setChangjiangIncome(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getChangjiangIncome()), safeBigDecimal(lastYearRow.getChangjiangIncome())));
+
+        return yoyRow;
+    }
+
+    private BigDecimal calcYoyPercentBigDecimal(BigDecimal current, BigDecimal last) {
+        if (current == null || last == null || last.compareTo(BigDecimal.ZERO) == 0) {
+            return null;
+        }
+        try {
+            BigDecimal diff = current.subtract(last);
+            BigDecimal percent = diff.divide(last, 4, RoundingMode.HALF_UP).multiply(new BigDecimal("100"));
+            return percent.setScale(2, RoundingMode.HALF_UP);
+        } catch (Exception e) {
+            log.debug("calcYoyPercentBigDecimal error: {},{}", current, last);
+            return null;
+        }
+    }
+
+    @Override
+    @TenantIgnore
+    public void exportConsumptionIncomeVoyageExcel(ConsumptionIncomeVoyageReqVO reqVO, HttpServletResponse response) throws IOException {
+        List<ConsumptionIncomeVoyageRespVO> dataList = getConsumptionIncomeVoyageList(reqVO);
+        if (CollUtil.isEmpty(dataList)) {
+            ExcelUtils.exportEmpty(response, "省际度假游轮二消情况航次表");
+            return;
+        }
+
+        List<List<String>> head = buildConsumptionIncomeVoyageHeaders();
+        List<List<Object>> exportData = transformConsumptionIncomeVoyageExportData(dataList);
+
+        EasyExcel.write(response.getOutputStream())
+                .head(head)
+                .autoCloseStream(false)
+                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
+                .registerConverter(new LongStringConverter())
+                .sheet("省际度假游轮二消情况航次表")
+                .doWrite(exportData);
+
+        response.addHeader("Content-Disposition",
+                "attachment;filename=" + URLEncoder.encode("省际度假游轮二消情况航次表.xls", StandardCharsets.UTF_8.name()));
+        response.setContentType("application/vnd.ms-excel;charset=UTF-8");
+    }
+
+    private List<List<String>> buildConsumptionIncomeVoyageHeaders() {
+        List<List<String>> head = new ArrayList<>();
+
+        head.add(new ArrayList<>(Arrays.asList("序号")));
+        head.add(new ArrayList<>(Arrays.asList("航次")));
+        head.add(new ArrayList<>(Arrays.asList("船舶")));
+        head.add(new ArrayList<>(Arrays.asList("航线")));
+        head.add(new ArrayList<>(Arrays.asList("航次号")));
+        head.add(new ArrayList<>(Arrays.asList("指标人数")));
+        head.add(new ArrayList<>(Arrays.asList("人均金额(元/人)")));
+        head.add(new ArrayList<>(Arrays.asList("前台收入")));
+        head.add(new ArrayList<>(Arrays.asList("餐厅收入")));
+        head.add(new ArrayList<>(Arrays.asList("酒吧","预包装酒水")));
+        head.add(new ArrayList<>(Arrays.asList("酒吧","调制酒水")));
+        head.add(new ArrayList<>(Arrays.asList("酒吧","KTV")));
+        head.add(new ArrayList<>(Arrays.asList("酒吧","麻将")));
+        head.add(new ArrayList<>(Arrays.asList("酒吧","烧烤")));
+        head.add(new ArrayList<>(Arrays.asList("酒吧","其他")));
+        head.add(new ArrayList<>(Arrays.asList("文创收入")));
+        head.add(new ArrayList<>(Arrays.asList("客房收入")));
+        head.add(new ArrayList<>(Arrays.asList("自营收入")));
+        head.add(new ArrayList<>(Arrays.asList("其他收入")));
+        head.add(new ArrayList<>(Arrays.asList("联营商", "摄影收入")));
+        head.add(new ArrayList<>(Arrays.asList("联营商", "商超收入")));
+        head.add(new ArrayList<>(Arrays.asList("联营商", "SPA收入")));
+        head.add(new ArrayList<>(Arrays.asList("联营商", "医务室收入")));
+
+        head.add(new ArrayList<>(Arrays.asList("收入合计")));
+        head.add(new ArrayList<>(Arrays.asList("微信/支付宝/刷卡支付")));
+        head.add(new ArrayList<>(Arrays.asList("现金支付")));
+        head.add(new ArrayList<>(Arrays.asList("应收款")));
+        head.add(new ArrayList<>(Arrays.asList("其他支付")));
+        head.add(new ArrayList<>(Arrays.asList("应付摄影款")));
+        head.add(new ArrayList<>(Arrays.asList("应付商超款")));
+        head.add(new ArrayList<>(Arrays.asList("应付SPA款")));
+        head.add(new ArrayList<>(Arrays.asList("应付医务室款")));
+        head.add(new ArrayList<>(Arrays.asList("联营商应付款合计")));
+        head.add(new ArrayList<>(Arrays.asList("长江收入")));
+
+        return head;
+    }
+
+    private List<List<Object>> transformConsumptionIncomeVoyageExportData(List<ConsumptionIncomeVoyageRespVO> dataList) {
+        List<List<Object>> result = new ArrayList<>(dataList.size());
+        for (ConsumptionIncomeVoyageRespVO row : dataList) {
+            List<Object> rowData = new ArrayList<>(40);
+            boolean isYoy = "同比".equals(row.getVoyageNo());
+
+            if (isYoy) {
+                rowData.add("");
+                rowData.add("");
+                rowData.add("");
+                rowData.add("");
+                rowData.add("同比");
+                rowData.add(formatYoyBigDecimal(row.getTargetPeople()));
+                rowData.add(formatYoyBigDecimal(row.getAvgPerPerson()));
+                rowData.add(formatYoyBigDecimal(row.getFrontDeskIncome()));
+                rowData.add(formatYoyBigDecimal(row.getRestaurantIncome()));
+                rowData.add(formatYoyBigDecimal(row.getBarPrepackedWine()));
+                rowData.add(formatYoyBigDecimal(row.getBarMixedWine()));
+                rowData.add(formatYoyBigDecimal(row.getBarKtv()));
+                rowData.add(formatYoyBigDecimal(row.getBarMahjong()));
+                rowData.add(formatYoyBigDecimal(row.getBarBarbecue()));
+                rowData.add(formatYoyBigDecimal(row.getBarOther()));
+                rowData.add(formatYoyBigDecimal(row.getCulturalIncome()));
+                rowData.add(formatYoyBigDecimal(row.getRoomIncome()));
+                rowData.add(formatYoyBigDecimal(row.getSelfIncome()));
+                rowData.add(formatYoyBigDecimal(row.getOtherIncome()));
+                rowData.add(formatYoyBigDecimal(row.getPartnerPhotography()));
+                rowData.add(formatYoyBigDecimal(row.getPartnerSupermarket()));
+                rowData.add(formatYoyBigDecimal(row.getPartnerSpa()));
+                rowData.add(formatYoyBigDecimal(row.getPartnerClinic()));
+                rowData.add(formatYoyBigDecimal(row.getTotalIncome()));
+                rowData.add(formatYoyBigDecimal(row.getPayOnline()));
+                rowData.add(formatYoyBigDecimal(row.getPayCash()));
+                rowData.add(formatYoyBigDecimal(row.getReceivable()));
+                rowData.add(formatYoyBigDecimal(row.getPayOther()));
+                rowData.add(formatYoyBigDecimal(row.getPayablePhotography()));
+                rowData.add(formatYoyBigDecimal(row.getPayableSupermarket()));
+                rowData.add(formatYoyBigDecimal(row.getPayableSpa()));
+                rowData.add(formatYoyBigDecimal(row.getPayableClinic()));
+                rowData.add(formatYoyBigDecimal(row.getPartnerTotalPayable()));
+                rowData.add(formatYoyBigDecimal(row.getChangjiangIncome()));
+            } else {
+                rowData.add(row.getIndex() != null ? String.valueOf(row.getIndex()) : "");
+                rowData.add(row.getVoyageName() != null ? row.getVoyageName() : "");
+                rowData.add(row.getShipName() != null ? row.getShipName() : "");
+                rowData.add(row.getRoute() != null ? row.getRoute() : "");
+                rowData.add(row.getVoyageNo() != null ? row.getVoyageNo() : "");
+                rowData.add(formatBigDecimal(row.getTargetPeople()));
+                rowData.add(formatBigDecimal(row.getAvgPerPerson()));
+                rowData.add(formatBigDecimal(row.getFrontDeskIncome()));
+                rowData.add(formatBigDecimal(row.getRestaurantIncome()));
+                rowData.add(formatBigDecimal(row.getBarPrepackedWine()));
+                rowData.add(formatBigDecimal(row.getBarMixedWine()));
+                rowData.add(formatBigDecimal(row.getBarKtv()));
+                rowData.add(formatBigDecimal(row.getBarMahjong()));
+                rowData.add(formatBigDecimal(row.getBarBarbecue()));
+                rowData.add(formatBigDecimal(row.getBarOther()));
+                rowData.add(formatBigDecimal(row.getCulturalIncome()));
+                rowData.add(formatBigDecimal(row.getRoomIncome()));
+                rowData.add(formatBigDecimal(row.getSelfIncome()));
+                rowData.add(formatBigDecimal(row.getOtherIncome()));
+                rowData.add(formatBigDecimal(row.getPartnerPhotography()));
+                rowData.add(formatBigDecimal(row.getPartnerSupermarket()));
+                rowData.add(formatBigDecimal(row.getPartnerSpa()));
+                rowData.add(formatBigDecimal(row.getPartnerClinic()));
+                rowData.add(formatBigDecimal(row.getTotalIncome()));
+                rowData.add(formatBigDecimal(row.getPayOnline()));
+                rowData.add(formatBigDecimal(row.getPayCash()));
+                rowData.add(formatBigDecimal(row.getReceivable()));
+                rowData.add(formatBigDecimal(row.getPayOther()));
+                rowData.add(formatBigDecimal(row.getPayablePhotography()));
+                rowData.add(formatBigDecimal(row.getPayableSupermarket()));
+                rowData.add(formatBigDecimal(row.getPayableSpa()));
+                rowData.add(formatBigDecimal(row.getPayableClinic()));
+                rowData.add(formatBigDecimal(row.getPartnerTotalPayable()));
+                rowData.add(formatBigDecimal(row.getChangjiangIncome()));
+            }
+            result.add(rowData);
+        }
+        return result;
+    }
+
+
+    private String formatBigDecimal(BigDecimal value) {
+        if (value == null) {
+            return "";
+        }
+        return value.setScale(2, RoundingMode.HALF_UP).toPlainString();
+    }
+
+    private String formatYoyBigDecimal(BigDecimal value) {
+        if (value == null) {
+            return "-";
+        }
+        double num = value.doubleValue();
+        if (num > 0) {
+            return "↑ +" + value.setScale(2, RoundingMode.HALF_UP).toPlainString() + "%";
+        } else if (num < 0) {
+            return "↓ " + value.setScale(2, RoundingMode.HALF_UP).toPlainString() + "%";
+        } else {
+            return "— 0.00%";
+        }
+    }
+
+    @Override
+    @TenantIgnore
+    public List<ConsumptionIncomeDailyRespVO> getConsumptionIncomeDailyList(ConsumptionIncomeDailyReqVO reqVO) {
+        log.info("查询省际度假游轮二消情况日报表任务开始时间{}", LocalDateTime.now());
+
+        List<ConsumptionIncomeDailyDO> doList = consumptionIncomeDailyMapper.selectList(reqVO);
+        log.info("查询省际度假游轮二消情况日报表任务结束时间{}", LocalDateTime.now());
+
+        if (CollUtil.isEmpty(doList)) {
+            return new ArrayList<>();
+        }
+
+        List<ConsumptionIncomeDailyRespVO> respList = convertDailyToList(doList);
+
+        return buildDailyReportWithSubtotalAndCumulative(respList);
+    }
+
+    private List<ConsumptionIncomeDailyRespVO> convertDailyToList(List<ConsumptionIncomeDailyDO> doList) {
+        List<ConsumptionIncomeDailyRespVO> respList = new ArrayList<>();
+        int index = 1;
+        for (ConsumptionIncomeDailyDO item : doList) {
+            ConsumptionIncomeDailyRespVO respVO = new ConsumptionIncomeDailyRespVO();
+            BeanUtils.copyProperties(item, respVO);
+            respVO.setIndex(index++);
+            respVO.setRoute(item.getRoute());
+            respList.add(respVO);
+        }
+        return respList;
+    }
+
+    private List<ConsumptionIncomeDailyRespVO> buildDailyReportWithSubtotalAndCumulative(
+            List<ConsumptionIncomeDailyRespVO> currentDataList) {
+
+        List<ConsumptionIncomeDailyRespVO> result = new ArrayList<>();
+
+        Map<String, List<ConsumptionIncomeDailyRespVO>> currentMonthMap = currentDataList.stream()
+                .collect(Collectors.groupingBy(
+                        item -> item.getStatMonth() != null ? item.getStatMonth() : "",
+                        TreeMap::new, Collectors.toList()));
+
+        int globalIdx = 1;
+        List<ConsumptionIncomeDailyRespVO> allDailyDataForCumulative = new ArrayList<>();
+
+        int monthIndex = 0;
+        for (Map.Entry<String, List<ConsumptionIncomeDailyRespVO>> entry : currentMonthMap.entrySet()) {
+            String month = entry.getKey();
+            List<ConsumptionIncomeDailyRespVO> dailyItems = entry.getValue();
+
+            for (ConsumptionIncomeDailyRespVO item : dailyItems) {
+                item.setIndex(globalIdx++);
+            }
+
+            result.addAll(dailyItems);
+            allDailyDataForCumulative.addAll(dailyItems);
+
+            ConsumptionIncomeDailyRespVO subtotalData = buildDailySubtotalRow(dailyItems, month);
+            result.add(subtotalData);
+
+            ConsumptionIncomeDailyRespVO lastYearSubtotal = buildDailySubtotalRow(new ArrayList<>(), "");
+            ConsumptionIncomeDailyRespVO subtotalYoy = buildDailyYoyRow(subtotalData, lastYearSubtotal);
+            result.add(subtotalYoy);
+
+            if (monthIndex > 0) {
+                ConsumptionIncomeDailyRespVO cumData = buildDailySubtotalRow(allDailyDataForCumulative, "累计");
+                result.add(cumData);
+
+                ConsumptionIncomeDailyRespVO lastYearCumData = buildDailySubtotalRow(new ArrayList<>(), "");
+                ConsumptionIncomeDailyRespVO cumYoy = buildDailyYoyRow(cumData, lastYearCumData);
+                result.add(cumYoy);
+            }
+
+            monthIndex++;
+        }
+
+        return result;
+    }
+
+    private ConsumptionIncomeDailyRespVO buildDailySubtotalRow(List<ConsumptionIncomeDailyRespVO> items, String monthLabel) {
+        ConsumptionIncomeDailyRespVO row = new ConsumptionIncomeDailyRespVO();
+
+        if ("累计".equals(monthLabel)) {
+            row.setVoyageName("累计");
+        } else if (!"累计".equals(monthLabel) && monthLabel.contains("小计")) {
+            row.setVoyageName(monthLabel);
+        } else {
+            row.setVoyageName(monthLabel + "小计");
+        }
+        row.setVoyageNo("数据");
+        row.setIsSubtotal(!"累计".equals(row.getVoyageName()));
+        row.setIsCumulative("累计".equals(row.getVoyageName()));
+
+        if (CollUtil.isEmpty(items)) {
+            return row;
+        }
+
+        BigDecimal frontDeskSum = BigDecimal.ZERO;
+        BigDecimal restaurantSum = BigDecimal.ZERO;
+        BigDecimal barPrepackedSum = BigDecimal.ZERO;
+        BigDecimal barMixedSum = BigDecimal.ZERO;
+        BigDecimal barKtvSum = BigDecimal.ZERO;
+        BigDecimal barMahjongSum = BigDecimal.ZERO;
+        BigDecimal barBarbecueSum = BigDecimal.ZERO;
+        BigDecimal barOtherSum = BigDecimal.ZERO;
+        BigDecimal culturalSum = BigDecimal.ZERO;
+        BigDecimal roomSum = BigDecimal.ZERO;
+        BigDecimal selfSum = BigDecimal.ZERO;
+        BigDecimal otherSum = BigDecimal.ZERO;
+        BigDecimal partnerPhotoSum = BigDecimal.ZERO;
+        BigDecimal partnerSuperSum = BigDecimal.ZERO;
+        BigDecimal partnerSpaSum = BigDecimal.ZERO;
+        BigDecimal partnerClinicSum = BigDecimal.ZERO;
+        BigDecimal totalIncomeSum = BigDecimal.ZERO;
+        BigDecimal payOnlineSum = BigDecimal.ZERO;
+        BigDecimal payCashSum = BigDecimal.ZERO;
+        BigDecimal payOtherSum = BigDecimal.ZERO;
+        BigDecimal receivableSum = BigDecimal.ZERO;
+        BigDecimal payablePhotoSum = BigDecimal.ZERO;
+        BigDecimal payableSuperSum = BigDecimal.ZERO;
+        BigDecimal payableSpaSum = BigDecimal.ZERO;
+        BigDecimal payableClinicSum = BigDecimal.ZERO;
+        BigDecimal partnerTotalPayableSum = BigDecimal.ZERO;
+        BigDecimal changjiangSum = BigDecimal.ZERO;
+
+        for (ConsumptionIncomeDailyRespVO item : items) {
+            frontDeskSum = frontDeskSum.add(safeBigDecimal(item.getFrontDeskIncome()));
+            restaurantSum = restaurantSum.add(safeBigDecimal(item.getRestaurantIncome()));
+            barPrepackedSum = barPrepackedSum.add(safeBigDecimal(item.getBarPrepackedWine()));
+            barMixedSum = barMixedSum.add(safeBigDecimal(item.getBarMixedWine()));
+            barKtvSum = barKtvSum.add(safeBigDecimal(item.getBarKtv()));
+            barMahjongSum = barMahjongSum.add(safeBigDecimal(item.getBarMahjong()));
+            barBarbecueSum = barBarbecueSum.add(safeBigDecimal(item.getBarBarbecue()));
+            barOtherSum = barOtherSum.add(safeBigDecimal(item.getBarOther()));
+            culturalSum = culturalSum.add(safeBigDecimal(item.getCulturalIncome()));
+            roomSum = roomSum.add(safeBigDecimal(item.getRoomIncome()));
+            selfSum = selfSum.add(safeBigDecimal(item.getSelfIncome()));
+            otherSum = otherSum.add(safeBigDecimal(item.getOtherIncome()));
+            partnerPhotoSum = partnerPhotoSum.add(safeBigDecimal(item.getPartnerPhotography()));
+            partnerSuperSum = partnerSuperSum.add(safeBigDecimal(item.getPartnerSupermarket()));
+            partnerSpaSum = partnerSpaSum.add(safeBigDecimal(item.getPartnerSpa()));
+            partnerClinicSum = partnerClinicSum.add(safeBigDecimal(item.getPartnerClinic()));
+            totalIncomeSum = totalIncomeSum.add(safeBigDecimal(item.getTotalIncome()));
+            payOnlineSum = payOnlineSum.add(safeBigDecimal(item.getPayOnline()));
+            payCashSum = payCashSum.add(safeBigDecimal(item.getPayCash()));
+            payOtherSum = payOtherSum.add(safeBigDecimal(item.getPayOther()));
+            receivableSum = receivableSum.add(safeBigDecimal(item.getReceivable()));
+            payablePhotoSum = payablePhotoSum.add(safeBigDecimal(item.getPayablePhotography()));
+            payableSuperSum = payableSuperSum.add(safeBigDecimal(item.getPayableSupermarket()));
+            payableSpaSum = payableSpaSum.add(safeBigDecimal(item.getPayableSpa()));
+            payableClinicSum = payableClinicSum.add(safeBigDecimal(item.getPayableClinic()));
+            partnerTotalPayableSum = partnerTotalPayableSum.add(safeBigDecimal(item.getPartnerTotalPayable()));
+            changjiangSum = changjiangSum.add(safeBigDecimal(item.getChangjiangIncome()));
+        }
+
+        row.setFrontDeskIncome(frontDeskSum);
+        row.setRestaurantIncome(restaurantSum);
+        row.setBarPrepackedWine(barPrepackedSum);
+        row.setBarMixedWine(barMixedSum);
+        row.setBarKtv(barKtvSum);
+        row.setBarMahjong(barMahjongSum);
+        row.setBarBarbecue(barBarbecueSum);
+        row.setBarOther(barOtherSum);
+        row.setCulturalIncome(culturalSum);
+        row.setRoomIncome(roomSum);
+        row.setSelfIncome(selfSum);
+        row.setOtherIncome(otherSum);
+        row.setPartnerPhotography(partnerPhotoSum);
+        row.setPartnerSupermarket(partnerSuperSum);
+        row.setPartnerSpa(partnerSpaSum);
+        row.setPartnerClinic(partnerClinicSum);
+        row.setTotalIncome(totalIncomeSum);
+        row.setPayOnline(payOnlineSum);
+        row.setPayCash(payCashSum);
+        row.setPayOther(payOtherSum);
+        row.setReceivable(receivableSum);
+        row.setPayablePhotography(payablePhotoSum);
+        row.setPayableSupermarket(payableSuperSum);
+        row.setPayableSpa(payableSpaSum);
+        row.setPayableClinic(payableClinicSum);
+        row.setPartnerTotalPayable(partnerTotalPayableSum);
+        row.setChangjiangIncome(changjiangSum);
+
+        return row;
+    }
+
+    private ConsumptionIncomeDailyRespVO buildDailyYoyRow(ConsumptionIncomeDailyRespVO currentRow, ConsumptionIncomeDailyRespVO lastYearRow) {
+        ConsumptionIncomeDailyRespVO yoyRow = new ConsumptionIncomeDailyRespVO();
+        yoyRow.setIndex(null);
+        yoyRow.setVoyageName("");
+        yoyRow.setVoyageNo("同比");
+        yoyRow.setIsSubtotal(currentRow.getIsSubtotal() != null && currentRow.getIsSubtotal());
+        yoyRow.setIsCumulative(currentRow.getIsCumulative() != null && currentRow.getIsCumulative());
+
+        yoyRow.setFrontDeskIncome(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getFrontDeskIncome()), safeBigDecimal(lastYearRow.getFrontDeskIncome())));
+        yoyRow.setRestaurantIncome(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getRestaurantIncome()), safeBigDecimal(lastYearRow.getRestaurantIncome())));
+        yoyRow.setBarPrepackedWine(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getBarPrepackedWine()), safeBigDecimal(lastYearRow.getBarPrepackedWine())));
+        yoyRow.setBarMixedWine(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getBarMixedWine()), safeBigDecimal(lastYearRow.getBarMixedWine())));
+        yoyRow.setBarKtv(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getBarKtv()), safeBigDecimal(lastYearRow.getBarKtv())));
+        yoyRow.setBarMahjong(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getBarMahjong()), safeBigDecimal(lastYearRow.getBarMahjong())));
+        yoyRow.setBarBarbecue(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getBarBarbecue()), safeBigDecimal(lastYearRow.getBarBarbecue())));
+        yoyRow.setBarOther(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getBarOther()), safeBigDecimal(lastYearRow.getBarOther())));
+        yoyRow.setCulturalIncome(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getCulturalIncome()), safeBigDecimal(lastYearRow.getCulturalIncome())));
+        yoyRow.setRoomIncome(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getRoomIncome()), safeBigDecimal(lastYearRow.getRoomIncome())));
+        yoyRow.setSelfIncome(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getSelfIncome()), safeBigDecimal(lastYearRow.getSelfIncome())));
+        yoyRow.setOtherIncome(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getOtherIncome()), safeBigDecimal(lastYearRow.getOtherIncome())));
+        yoyRow.setPartnerPhotography(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getPartnerPhotography()), safeBigDecimal(lastYearRow.getPartnerPhotography())));
+        yoyRow.setPartnerSupermarket(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getPartnerSupermarket()), safeBigDecimal(lastYearRow.getPartnerSupermarket())));
+        yoyRow.setPartnerSpa(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getPartnerSpa()), safeBigDecimal(lastYearRow.getPartnerSpa())));
+        yoyRow.setPartnerClinic(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getPartnerClinic()), safeBigDecimal(lastYearRow.getPartnerClinic())));
+        yoyRow.setTotalIncome(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getTotalIncome()), safeBigDecimal(lastYearRow.getTotalIncome())));
+        yoyRow.setPayOnline(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getPayOnline()), safeBigDecimal(lastYearRow.getPayOnline())));
+        yoyRow.setPayCash(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getPayCash()), safeBigDecimal(lastYearRow.getPayCash())));
+        yoyRow.setPayOther(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getPayOther()), safeBigDecimal(lastYearRow.getPayOther())));
+        yoyRow.setReceivable(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getReceivable()), safeBigDecimal(lastYearRow.getReceivable())));
+        yoyRow.setPayablePhotography(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getPayablePhotography()), safeBigDecimal(lastYearRow.getPayablePhotography())));
+        yoyRow.setPayableSupermarket(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getPayableSupermarket()), safeBigDecimal(lastYearRow.getPayableSupermarket())));
+        yoyRow.setPayableSpa(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getPayableSpa()), safeBigDecimal(lastYearRow.getPayableSpa())));
+        yoyRow.setPayableClinic(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getPayableClinic()), safeBigDecimal(lastYearRow.getPayableClinic())));
+        yoyRow.setPartnerTotalPayable(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getPartnerTotalPayable()), safeBigDecimal(lastYearRow.getPartnerTotalPayable())));
+        yoyRow.setChangjiangIncome(calcYoyPercentBigDecimal(
+                safeBigDecimal(currentRow.getChangjiangIncome()), safeBigDecimal(lastYearRow.getChangjiangIncome())));
+
+        return yoyRow;
+    }
+
+    @Override
+    @TenantIgnore
+    public void exportConsumptionIncomeDailyExcel(ConsumptionIncomeDailyReqVO reqVO, HttpServletResponse response) throws IOException {
+        List<ConsumptionIncomeDailyRespVO> dataList = getConsumptionIncomeDailyList(reqVO);
+        if (CollUtil.isEmpty(dataList)) {
+            ExcelUtils.exportEmpty(response, "省际度假游轮二消情况日报表");
+            return;
+        }
+
+        List<List<String>> head = buildConsumptionIncomeDailyHeaders();
+        List<List<Object>> exportData = transformConsumptionIncomeDailyExportData(dataList);
+
+        EasyExcel.write(response.getOutputStream())
+                .head(head)
+                .autoCloseStream(false)
+                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
+                .registerConverter(new LongStringConverter())
+                .sheet("省际度假游轮二消情况日报表")
+                .doWrite(exportData);
+
+        response.addHeader("Content-Disposition",
+                "attachment;filename=" + URLEncoder.encode("省际度假游轮二消情况日报表.xls", StandardCharsets.UTF_8.name()));
+        response.setContentType("application/vnd.ms-excel;charset=UTF-8");
+    }
+
+    private List<List<String>> buildConsumptionIncomeDailyHeaders() {
+        List<List<String>> head = new ArrayList<>();
+
+        head.add(new ArrayList<>(Arrays.asList("序号")));
+        head.add(new ArrayList<>(Arrays.asList("统计")));
+        head.add(new ArrayList<>(Arrays.asList("船舶")));
+        head.add(new ArrayList<>(Arrays.asList("航线")));
+        head.add(new ArrayList<>(Arrays.asList("航次号")));
+        head.add(new ArrayList<>(Arrays.asList("日期")));
+        head.add(new ArrayList<>(Arrays.asList("前台")));
+        head.add(new ArrayList<>(Arrays.asList("餐厅")));
+        head.add(new ArrayList<>(Arrays.asList("酒吧","预包装酒水")));
+        head.add(new ArrayList<>(Arrays.asList("酒吧","调制酒水")));
+        head.add(new ArrayList<>(Arrays.asList("酒吧","KTV")));
+        head.add(new ArrayList<>(Arrays.asList("酒吧","麻将")));
+        head.add(new ArrayList<>(Arrays.asList("酒吧","烧烤")));
+        head.add(new ArrayList<>(Arrays.asList("酒吧","其他")));
+        head.add(new ArrayList<>(Arrays.asList("文创收入")));
+        head.add(new ArrayList<>(Arrays.asList("客房收入")));
+        head.add(new ArrayList<>(Arrays.asList("自营收入")));
+        head.add(new ArrayList<>(Arrays.asList("其他收入")));
+        head.add(new ArrayList<>(Arrays.asList("联营商", "摄影收入")));
+        head.add(new ArrayList<>(Arrays.asList("联营商", "商超收入")));
+        head.add(new ArrayList<>(Arrays.asList("联营商", "SPA收入")));
+        head.add(new ArrayList<>(Arrays.asList("联营商", "医务室收入")));
+
+        head.add(new ArrayList<>(Arrays.asList("收入合计")));
+        head.add(new ArrayList<>(Arrays.asList("应付","摄影款")));
+        head.add(new ArrayList<>(Arrays.asList("应付","商超款")));
+        head.add(new ArrayList<>(Arrays.asList("应付","SPA款")));
+        head.add(new ArrayList<>(Arrays.asList("应付","医务室款")));
+        head.add(new ArrayList<>(Arrays.asList("应付","联营商应付款合计")));
+        head.add(new ArrayList<>(Arrays.asList("长江收入")));
+
+        return head;
+    }
+
+    private List<List<Object>> transformConsumptionIncomeDailyExportData(List<ConsumptionIncomeDailyRespVO> dataList) {
+        List<List<Object>> result = new ArrayList<>(dataList.size());
+        for (ConsumptionIncomeDailyRespVO row : dataList) {
+            List<Object> rowData = new ArrayList<>(40);
+            boolean isYoy = "同比".equals(row.getVoyageNo());
+
+            if (isYoy) {
+                rowData.add("");
+                rowData.add("");
+                rowData.add("");
+                rowData.add("");
+                rowData.add("同比");
+                rowData.add("");
+                rowData.add(formatYoyBigDecimal(row.getFrontDeskIncome()));
+                rowData.add(formatYoyBigDecimal(row.getRestaurantIncome()));
+                rowData.add(formatYoyBigDecimal(row.getBarPrepackedWine()));
+                rowData.add(formatYoyBigDecimal(row.getBarMixedWine()));
+                rowData.add(formatYoyBigDecimal(row.getBarKtv()));
+                rowData.add(formatYoyBigDecimal(row.getBarMahjong()));
+                rowData.add(formatYoyBigDecimal(row.getBarBarbecue()));
+                rowData.add(formatYoyBigDecimal(row.getBarOther()));
+                rowData.add(formatYoyBigDecimal(row.getCulturalIncome()));
+                rowData.add(formatYoyBigDecimal(row.getRoomIncome()));
+                rowData.add(formatYoyBigDecimal(row.getSelfIncome()));
+                rowData.add(formatYoyBigDecimal(row.getOtherIncome()));
+                rowData.add(formatYoyBigDecimal(row.getPartnerPhotography()));
+                rowData.add(formatYoyBigDecimal(row.getPartnerSupermarket()));
+                rowData.add(formatYoyBigDecimal(row.getPartnerSpa()));
+                rowData.add(formatYoyBigDecimal(row.getPartnerClinic()));
+                rowData.add(formatYoyBigDecimal(row.getTotalIncome()));
+                rowData.add(formatYoyBigDecimal(row.getPayablePhotography()));
+                rowData.add(formatYoyBigDecimal(row.getPayableSupermarket()));
+                rowData.add(formatYoyBigDecimal(row.getPayableSpa()));
+                rowData.add(formatYoyBigDecimal(row.getPayableClinic()));
+                rowData.add(formatYoyBigDecimal(row.getPartnerTotalPayable()));
+                rowData.add(formatYoyBigDecimal(row.getChangjiangIncome()));
+            } else {
+                rowData.add(row.getIndex() != null ? String.valueOf(row.getIndex()) : "");
+                rowData.add(row.getVoyageName() != null ? row.getVoyageName() : "");
+                rowData.add(row.getShipName() != null ? row.getShipName() : "");
+                rowData.add(row.getRoute() != null ? row.getRoute() : "");
+                rowData.add(row.getVoyageNo() != null ? row.getVoyageNo() : "");
+                rowData.add(row.getStatDate() != null ? row.getStatDate() : "");
+                rowData.add(formatBigDecimal(row.getFrontDeskIncome()));
+                rowData.add(formatBigDecimal(row.getRestaurantIncome()));
+                rowData.add(formatBigDecimal(row.getBarPrepackedWine()));
+                rowData.add(formatBigDecimal(row.getBarMixedWine()));
+                rowData.add(formatBigDecimal(row.getBarKtv()));
+                rowData.add(formatBigDecimal(row.getBarMahjong()));
+                rowData.add(formatBigDecimal(row.getBarBarbecue()));
+                rowData.add(formatBigDecimal(row.getBarOther()));
+                rowData.add(formatBigDecimal(row.getCulturalIncome()));
+                rowData.add(formatBigDecimal(row.getRoomIncome()));
+                rowData.add(formatBigDecimal(row.getSelfIncome()));
+                rowData.add(formatBigDecimal(row.getOtherIncome()));
+                rowData.add(formatBigDecimal(row.getPartnerPhotography()));
+                rowData.add(formatBigDecimal(row.getPartnerSupermarket()));
+                rowData.add(formatBigDecimal(row.getPartnerSpa()));
+                rowData.add(formatBigDecimal(row.getPartnerClinic()));
+                rowData.add(formatBigDecimal(row.getTotalIncome()));
+                rowData.add(formatBigDecimal(row.getPayablePhotography()));
+                rowData.add(formatBigDecimal(row.getPayableSupermarket()));
+                rowData.add(formatBigDecimal(row.getPayableSpa()));
+                rowData.add(formatBigDecimal(row.getPayableClinic()));
+                rowData.add(formatBigDecimal(row.getPartnerTotalPayable()));
+                rowData.add(formatBigDecimal(row.getChangjiangIncome()));
+            }
+            result.add(rowData);
+        }
+        return result;
+    }
 
 }

+ 52 - 0
ship-module-trade/ship-module-trade-biz/src/main/resources/mapper/report/CabinMixDailyMapper.xml

@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.yc.ship.module.trade.dal.mysql.report.CabinMixDailyMapper">
+    <select id="getList" resultType="java.util.Map">
+        SELECT
+        DATE_FORMAT(v.boarding_time, '%Y-%m') AS `month`,
+        DATE_FORMAT(v.boarding_time, '%d') AS `date`,
+        s.name AS ship,
+        r.name AS route,
+        v.name AS voyageNo,
+        td.voyage_id,
+        ${roomColumns},
+        COALESCE(SUM(torm.use_room_num), 0) + COALESCE ( voyage_visitor.total_visitor / 2, 0 ) AS totalRooms
+        FROM
+        trade_order td
+        LEFT JOIN trade_order_room_model torm ON torm.deleted = 0 AND td.id = torm.order_id
+        LEFT JOIN product_voyage v ON td.voyage_id = v.id AND v.deleted = 0
+        INNER JOIN resource_ship s ON v.ship_id = s.id AND s.deleted = 0
+        INNER JOIN resource_route r ON v.route_id = r.id AND r.deleted = 0
+        LEFT JOIN (
+        SELECT
+        tod.voyage_id,
+        COUNT(tv.id) AS total_visitor
+        FROM trade_visitor tv
+        LEFT JOIN trade_order tod
+        ON tv.order_id = tod.id AND tod.deleted = 0
+        WHERE tv.type IN ('with','leader') AND tv.deleted = 0
+        GROUP BY tod.voyage_id
+        ) voyage_visitor ON voyage_visitor.voyage_id = v.id
+        WHERE
+        td.deleted = 0
+        AND td.order_status > 0
+        <if test="vo.shipId != null">
+            AND v.ship_id = #{vo.shipId}
+        </if>
+        <if test="vo.startDate != null and vo.startDate != ''">
+            AND v.boarding_time &gt;= #{vo.startDate}
+        </if>
+        <if test="vo.endDate != null and vo.endDate != ''">
+            AND v.boarding_time &lt; DATE_ADD(#{vo.endDate}, INTERVAL 1 DAY)
+        </if>
+        GROUP BY
+        td.voyage_id,
+        v.boarding_time,
+        s.name,
+        r.name,
+        v.name
+        ORDER BY
+        v.boarding_time
+    </select>
+
+</mapper>