Sfoglia il codice sorgente

新增消费习惯分析看板接口

jincheng 2 settimane fa
parent
commit
b82a27615e
15 ha cambiato i file con 505 aggiunte e 6 eliminazioni
  1. 5 0
      ship-module-product/ship-module-product-biz/src/main/java/com/yc/ship/module/product/dal/dataobject/voyage/VoyageDO.java
  2. 30 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/report/KanbanBoardController.java
  3. 16 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/report/OpsDailyController.java
  4. 27 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/report/vo/ConsumptionProfileDashboardReqVO.java
  5. 66 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/report/vo/ConsumptionProfileDashboardRespVO.java
  6. 45 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/report/vo/ConsumptionProfileExportVO.java
  7. 52 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/report/vo/ConsumptionProfileRawVO.java
  8. 22 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/report/vo/YangtzePassengerSummaryRemarkReqVO.java
  9. 13 4
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/report/VoyageStockBoardMapper.java
  10. 8 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/report/OpsDailyService.java
  11. 17 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/report/VoyageStockBoardService.java
  12. 21 1
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/report/impl/OpsDailyServiceImpl.java
  13. 113 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/report/impl/VoyageStockBoardServiceImpl.java
  14. 69 0
      ship-module-trade/ship-module-trade-biz/src/main/resources/mapper/report/VoyageStockBoardMapper.xml
  15. 1 1
      ship-module-trade/ship-module-trade-biz/src/main/resources/mapper/report/YangtzePassengerSummaryMapper.xml

+ 5 - 0
ship-module-product/ship-module-product-biz/src/main/java/com/yc/ship/module/product/dal/dataobject/voyage/VoyageDO.java

@@ -140,4 +140,9 @@ public class VoyageDO extends TenantBaseDO {
 
 
     private Integer isLock;
+
+    /**
+     * 备注
+     */
+    private String remark;
 }

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

@@ -161,4 +161,34 @@ public class KanbanBoardController {
                                             HttpServletResponse response) throws IOException {
         voyageStockBoardService.exportAllVoyageStockBoardExcel(reqVO, response);
     }
+
+
+
+    /**
+     * 查询消费习惯看板数据
+     *
+     * @param reqVO 查询条件
+     * @return 消费习惯看板数据
+     */
+    @GetMapping("/consumptionProfileDashboard")
+    @Operation(summary = "查询消费习惯看板数据")
+    public CommonResult<ConsumptionProfileDashboardRespVO> getConsumptionProfileDashboard(@Valid ConsumptionProfileDashboardReqVO reqVO) {
+        ConsumptionProfileDashboardRespVO respVO = voyageStockBoardService.getConsumptionProfileDashboard(reqVO);
+        return success(respVO);
+    }
+
+    /**
+     * 导出消费习惯看板 Excel
+     *
+     * @param reqVO    查询条件
+     * @param response HTTP响应
+     * @throws IOException IO异常
+     */
+    @GetMapping("/consumptionProfileDashboard/export-excel")
+    @Operation(summary = "导出消费习惯看板 Excel")
+    @ApiAccessLog(operateType = EXPORT)
+    public void exportConsumptionProfileDashboardExcel(@Valid ConsumptionProfileDashboardReqVO reqVO,
+                                                       HttpServletResponse response) throws IOException {
+        voyageStockBoardService.exportConsumptionProfileDashboardExcel(reqVO, response);
+    }
 }

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

@@ -6,6 +6,7 @@ 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.service.report.OpsDailyService;
 import io.swagger.v3.oas.annotations.Operation;
@@ -13,6 +14,8 @@ 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.GetMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
@@ -98,5 +101,18 @@ public class OpsDailyController {
                                                      HttpServletResponse response) throws IOException {
         opsDailyService.exportYangtzePassengerSummaryExcel(pageReqVO, response);
     }
+
+    /**
+     * 更新长江行游轮游客数据统计备注
+     *
+     * @param reqVO 备注更新请求
+     * @return 成功
+     */
+    @PutMapping("/yangtze-passenger-summary/remark")
+    @Operation(summary = "更新长江行游轮游客数据统计备注")
+    public CommonResult<Boolean> updateYangtzePassengerSummaryRemark(@Valid @RequestBody YangtzePassengerSummaryRemarkReqVO reqVO) {
+        opsDailyService.updateYangtzePassengerSummaryRemark(reqVO);
+        return success(true);
+    }
 }
 

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

@@ -0,0 +1,27 @@
+package com.yc.ship.module.trade.controller.admin.report.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 消费画像看板请求 VO
+ */
+@Schema(description = "管理后台 - 消费画像看板请求")
+@Data
+public class ConsumptionProfileDashboardReqVO {
+
+    @Schema(description = "游轮ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "游轮ID不能为空")
+    private Long shipId;
+
+    @Schema(description = "开始月份(格式:yyyy-MM)", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-01")
+    @NotNull(message = "开始月份不能为空")
+    private String startMonth;
+
+    @Schema(description = "结束月份(格式:yyyy-MM)", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-12")
+    @NotNull(message = "结束月份不能为空")
+    private String endMonth;
+
+}

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

@@ -0,0 +1,66 @@
+package com.yc.ship.module.trade.controller.admin.report.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 消费画像看板响应 VO
+ */
+@Schema(description = "管理后台 - 消费画像看板响应")
+@Data
+public class ConsumptionProfileDashboardRespVO {
+
+    @Schema(description = "消费画像数据列表")
+    private List<ConsumptionProfileItemVO> list;
+
+    /**
+     * 消费画像单项 VO
+     */
+    @Schema(description = "消费画像单项")
+    @Data
+    public static class ConsumptionProfileItemVO {
+
+        @Schema(description = "航次ID")
+        private Long voyageId;
+
+        @Schema(description = "航次名称")
+        private String voyageName;
+
+        @Schema(description = "总订单数")
+        private Integer totalOrders;
+
+        @Schema(description = "平均预订提前天数")
+        private String avgBookAdvanceDays;
+
+        @Schema(description = "0-3天订单占比")
+        private String ratio0to3Days;
+
+        @Schema(description = "4-7天订单占比")
+        private String ratio4to7Days;
+
+        @Schema(description = "8-15天订单占比")
+        private String ratio8to15Days;
+
+        @Schema(description = "16-30天订单占比")
+        private String ratio16to30Days;
+
+        @Schema(description = "30天以上占比")
+        private String ratioOver30Days;
+
+        @Schema(description = "非套房占比")
+        private String nonSuiteRatio;
+
+        @Schema(description = "套房占比")
+        private String suiteRatio;
+
+        @Schema(description = "增值服务购买率")
+        private String valueAddedPurchaseRate;
+
+        @Schema(description = "人均增值消费金额")
+        private String avgValueAddedConsumption;
+
+    }
+
+}

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

@@ -0,0 +1,45 @@
+package com.yc.ship.module.trade.controller.admin.report.vo;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
+
+/**
+ * 消费画像看板导出 VO
+ */
+@Data
+public class ConsumptionProfileExportVO {
+
+    @ExcelProperty("航次")
+    private String voyageName;
+
+    @ExcelProperty("平均预订提前天数")
+    private String avgBookAdvanceDays;
+
+    @ExcelProperty("0-3天订单占比")
+    private String ratio0to3Days;
+
+    @ExcelProperty("4-7天订单占比")
+    private String ratio4to7Days;
+
+    @ExcelProperty("8-15天订单占比")
+    private String ratio8to15Days;
+
+    @ExcelProperty("16-30天订单占比")
+    private String ratio16to30Days;
+
+    @ExcelProperty("30天以上占比")
+    private String ratioOver30Days;
+
+    @ExcelProperty("非套房占比")
+    private String nonSuiteRatio;
+
+    @ExcelProperty("套房占比")
+    private String suiteRatio;
+
+    @ExcelProperty("增值服务购买率")
+    private String valueAddedPurchaseRate;
+
+    @ExcelProperty("人均增值消费金额")
+    private String avgValueAddedConsumption;
+
+}

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

@@ -0,0 +1,52 @@
+package com.yc.ship.module.trade.controller.admin.report.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 消费画像看板原始查询数据 VO(内部使用)
+ */
+@Data
+public class ConsumptionProfileRawVO {
+
+    /** 航次ID */
+    private Long voyageId;
+
+    /** 航次名称 */
+    private String voyageName;
+
+    /** 总订单数 */
+    private Integer totalOrders;
+
+    /** 平均预订提前天数 */
+    private BigDecimal avgAdvanceDays;
+
+    /** 0-3天订单数 */
+    private Integer orders0to3;
+
+    /** 4-7天订单数 */
+    private Integer orders4to7;
+
+    /** 8-15天订单数 */
+    private Integer orders8to15;
+
+    /** 16-30天订单数 */
+    private Integer orders16to30;
+
+    /** 30天以上订单数 */
+    private Integer ordersOver30;
+
+    /** 套房订单数 */
+    private Integer suiteOrders;
+
+    /** 增值服务订单数 */
+    private Integer vaOrders;
+
+    /** 增值服务总金额 */
+    private BigDecimal vaTotalAmount;
+
+    /** 总人数 */
+    private Integer totalPeople;
+
+}

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

@@ -0,0 +1,22 @@
+package com.yc.ship.module.trade.controller.admin.report.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 长江行游轮游客数据及营收统计 - 更新备注 Request VO
+ */
+@Schema(description = "管理后台 - 长江行游轮游客数据统计备注更新 Request VO")
+@Data
+public class YangtzePassengerSummaryRemarkReqVO {
+
+    @Schema(description = "航次ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @NotNull(message = "航次ID不能为空")
+    private Long voyageId;
+
+    @Schema(description = "备注", example = "重点关注")
+    private String remark;
+
+}

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

@@ -2,10 +2,7 @@ package com.yc.ship.module.trade.dal.mysql.report;
 
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.yc.ship.module.trade.controller.admin.report.vo.AllVoyageStockBoardReqVO;
-import com.yc.ship.module.trade.controller.admin.report.vo.AllVoyageStockBoardRespVO;
-import com.yc.ship.module.trade.controller.admin.report.vo.VoyageStockBoardItemVO;
-import com.yc.ship.module.trade.controller.admin.report.vo.VoyageStockBoardRespVO;
+import com.yc.ship.module.trade.controller.admin.report.vo.*;
 import com.yc.ship.module.trade.dal.dataobject.order.TradeVisitorDO;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
@@ -83,4 +80,16 @@ public interface VoyageStockBoardMapper {
      * @return
      */
     List<Map<String, Object>> selectVoyageStockNumGruopByRoomModelIdAndFloor(Long voyageId);
+
+    /**
+     * 查询消费习惯看板数据
+     *
+     * @param shipId    游轮ID
+     * @param startDate 开始日期
+     * @param endDate   结束日期
+     * @return 消费画像原始数据列表
+     */
+    List<ConsumptionProfileRawVO> selectConsumptionProfileList(@Param("shipId") Long shipId,
+                                                               @Param("startDate") String startDate,
+                                                               @Param("endDate") String endDate);
 }

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

@@ -4,6 +4,7 @@ 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 javax.servlet.http.HttpServletResponse;
@@ -52,4 +53,11 @@ public interface OpsDailyService {
     void exportYangtzePassengerSummaryExcel(YangtzePassengerSummaryPageReqVO pageReqVO,
                                             HttpServletResponse response) throws IOException;
 
+    /**
+     * 更新长江行游轮游客数据统计备注
+     *
+     * @param reqVO 备注更新请求
+     */
+    void updateYangtzePassengerSummaryRemark(YangtzePassengerSummaryRemarkReqVO reqVO);
+
 }

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

@@ -85,4 +85,21 @@ public interface VoyageStockBoardService {
      * @throws IOException IO异常
      */
     void exportVisitorAgeDashboardExcel(@Valid VisitorSourceDashboardReqVO reqVO, HttpServletResponse response) throws IOException;
+
+    /**
+     * 查询消费习惯看板数据
+     *
+     * @param reqVO 查询条件
+     * @return 消费习惯看板数据
+     */
+    ConsumptionProfileDashboardRespVO getConsumptionProfileDashboard(@Valid ConsumptionProfileDashboardReqVO reqVO);
+
+    /**
+     * 导出消费习惯看板 Excel
+     *
+     * @param reqVO    查询条件
+     * @param response HTTP响应
+     * @throws IOException IO异常
+     */
+    void exportConsumptionProfileDashboardExcel(@Valid ConsumptionProfileDashboardReqVO reqVO, HttpServletResponse response) throws IOException;
 }

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

@@ -10,9 +10,12 @@ import com.yc.ship.framework.common.pojo.PageResult;
 import com.yc.ship.framework.excel.core.util.ExcelUtils;
 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.dal.mysql.report.OpsDailyMapper;
 import com.yc.ship.module.trade.dal.mysql.report.YangtzePassengerSummaryMapper;
@@ -37,7 +40,7 @@ import java.util.stream.Collectors;
  * <p>
  * 数据结构:日常数据行 + 月度小计(数据+同比) + 累计行(数据+同比)
  *
- * @author system
+ * @author jincheng
  */
 @Service
 @Slf4j
@@ -519,6 +522,9 @@ public class OpsDailyServiceImpl implements OpsDailyService {
     @Resource
     private YangtzePassengerSummaryMapper yangtzePassengerSummaryMapper;
 
+    @Resource
+    private VoyageMapper voyageMapper;
+
     @Override
     public PageResult<YangtzePassengerSummaryRespVO> getYangtzePassengerSummaryPage(YangtzePassengerSummaryPageReqVO pageReqVO) {
         IPage<YangtzePassengerSummaryRespVO> page = yangtzePassengerSummaryMapper.selectYangtzePassengerSummaryPage(
@@ -736,5 +742,19 @@ public class OpsDailyServiceImpl implements OpsDailyService {
         return amount.setScale(2, RoundingMode.HALF_UP).toPlainString();
     }
 
+    @Override
+    public void updateYangtzePassengerSummaryRemark(YangtzePassengerSummaryRemarkReqVO reqVO) {
+        // 校验航次存在
+        VoyageDO voyageDO = voyageMapper.selectById(reqVO.getVoyageId());
+        if (voyageDO == null) {
+            throw new IllegalArgumentException("航次不存在");
+        }
+        // 更新备注
+        VoyageDO updateDO = new VoyageDO();
+        updateDO.setId(reqVO.getVoyageId());
+        updateDO.setRemark(reqVO.getRemark());
+        voyageMapper.updateById(updateDO);
+    }
+
 
 }

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

@@ -1104,4 +1104,117 @@ public class VoyageStockBoardServiceImpl implements VoyageStockBoardService {
 
         return list;
     }
+
+    // ==================== 消费习惯看板 ====================
+
+    @Override
+    public ConsumptionProfileDashboardRespVO getConsumptionProfileDashboard(ConsumptionProfileDashboardReqVO reqVO) {
+        // 1. 解析月份范围为日期
+        String startDate = reqVO.getStartMonth() + "-01";
+        java.time.YearMonth endYearMonth = java.time.YearMonth.parse(reqVO.getEndMonth(),
+                java.time.format.DateTimeFormatter.ofPattern("yyyy-MM"));
+        String endDate = endYearMonth.atEndOfMonth().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd"));
+
+        // 2. 查询原始数据
+        List<ConsumptionProfileRawVO> rawList = voyageStockBoardMapper.selectConsumptionProfileList(
+                reqVO.getShipId(), startDate, endDate);
+
+        // 3. 转换并计算
+        ConsumptionProfileDashboardRespVO respVO = new ConsumptionProfileDashboardRespVO();
+        List<ConsumptionProfileDashboardRespVO.ConsumptionProfileItemVO> list = new ArrayList<>();
+
+        if (CollUtil.isEmpty(rawList)) {
+            respVO.setList(list);
+            return respVO;
+        }
+
+        for (ConsumptionProfileRawVO raw : rawList) {
+            ConsumptionProfileDashboardRespVO.ConsumptionProfileItemVO item =
+                    new ConsumptionProfileDashboardRespVO.ConsumptionProfileItemVO();
+            item.setVoyageId(raw.getVoyageId());
+            item.setVoyageName(raw.getVoyageName());
+
+            int totalOrders = raw.getTotalOrders() != null ? raw.getTotalOrders() : 0;
+            item.setTotalOrders(totalOrders);
+
+            // 平均预订提前天数
+            item.setAvgBookAdvanceDays(formatDecimalOne(raw.getAvgAdvanceDays()));
+
+            // 各区间订单占比
+            item.setRatio0to3Days(calcRatio(raw.getOrders0to3(), totalOrders));
+            item.setRatio4to7Days(calcRatio(raw.getOrders4to7(), totalOrders));
+            item.setRatio8to15Days(calcRatio(raw.getOrders8to15(), totalOrders));
+            item.setRatio16to30Days(calcRatio(raw.getOrders16to30(), totalOrders));
+            item.setRatioOver30Days(calcRatio(raw.getOrdersOver30(), totalOrders));
+
+            // 套房占比(有套房的订单 / 总订单)
+            int suiteOrders = raw.getSuiteOrders() != null ? raw.getSuiteOrders() : 0;
+            item.setSuiteRatio(calcRatio(suiteOrders, totalOrders));
+            item.setNonSuiteRatio(calcRatio(totalOrders - suiteOrders, totalOrders));
+
+            // 增值服务购买率
+            int vaOrders = raw.getVaOrders() != null ? raw.getVaOrders() : 0;
+            item.setValueAddedPurchaseRate(calcRatio(vaOrders, totalOrders));
+
+            // 人均增值消费金额
+            BigDecimal vaTotalAmount = safeDecimal(raw.getVaTotalAmount());
+            int totalPeople = raw.getTotalPeople() != null ? raw.getTotalPeople() : 0;
+            if (totalPeople > 0 && vaTotalAmount.compareTo(BigDecimal.ZERO) > 0) {
+                BigDecimal avgVa = vaTotalAmount.divide(BigDecimal.valueOf(totalPeople), 2, RoundingMode.HALF_UP);
+                item.setAvgValueAddedConsumption(avgVa.stripTrailingZeros().toPlainString());
+            } else {
+                item.setAvgValueAddedConsumption("0.00");
+            }
+
+            list.add(item);
+        }
+
+        respVO.setList(list);
+        return respVO;
+    }
+
+    @Override
+    public void exportConsumptionProfileDashboardExcel(ConsumptionProfileDashboardReqVO reqVO, HttpServletResponse response) throws IOException {
+        ConsumptionProfileDashboardRespVO data = getConsumptionProfileDashboard(reqVO);
+        List<ConsumptionProfileDashboardRespVO.ConsumptionProfileItemVO> list = data.getList();
+
+        if (CollUtil.isEmpty(list)) {
+            ExcelUtils.exportEmpty(response, "消费习惯看板");
+            return;
+        }
+
+        List<ConsumptionProfileExportVO> exportList = new ArrayList<>();
+        for (ConsumptionProfileDashboardRespVO.ConsumptionProfileItemVO item : list) {
+            ConsumptionProfileExportVO vo = new ConsumptionProfileExportVO();
+            vo.setVoyageName(item.getVoyageName());
+            vo.setAvgBookAdvanceDays(item.getAvgBookAdvanceDays());
+            vo.setRatio0to3Days(item.getRatio0to3Days());
+            vo.setRatio4to7Days(item.getRatio4to7Days());
+            vo.setRatio8to15Days(item.getRatio8to15Days());
+            vo.setRatio16to30Days(item.getRatio16to30Days());
+            vo.setRatioOver30Days(item.getRatioOver30Days());
+            vo.setNonSuiteRatio(item.getNonSuiteRatio());
+            vo.setSuiteRatio(item.getSuiteRatio());
+            vo.setValueAddedPurchaseRate(item.getValueAddedPurchaseRate());
+            vo.setAvgValueAddedConsumption(item.getAvgValueAddedConsumption());
+            exportList.add(vo);
+        }
+
+        response.addHeader("Content-Disposition",
+                "attachment;filename=" + URLEncoder.encode("消费习惯看板.xlsx", StandardCharsets.UTF_8.name()));
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");
+
+        EasyExcel.write(response.getOutputStream(), ConsumptionProfileExportVO.class)
+                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
+                .sheet("消费习惯数据")
+                .doWrite(exportList);
+    }
+
+    /**
+     * 格式化数字(保留1位小数)
+     */
+    private String formatDecimalOne(BigDecimal val) {
+        if (val == null) return "0.0";
+        return val.setScale(1, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString();
+    }
 }

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

@@ -216,4 +216,73 @@
             td.voyage_id, torm.floor, torm.room_model_id order by torm.room_model_id ,torm.floor asc;
     </select>
 
+
+    <select id="selectConsumptionProfileList"
+            resultType="com.yc.ship.module.trade.controller.admin.report.vo.ConsumptionProfileRawVO">
+        SELECT
+            pv.id as voyageId,
+            pv.name as voyageName,
+            COALESCE(o.totalOrders, 0) as totalOrders,
+            COALESCE(o.avgAdvanceDays, 0) as avgAdvanceDays,
+            COALESCE(o.orders0to3, 0) as orders0to3,
+            COALESCE(o.orders4to7, 0) as orders4to7,
+            COALESCE(o.orders8to15, 0) as orders8to15,
+            COALESCE(o.orders16to30, 0) as orders16to30,
+            COALESCE(o.ordersOver30, 0) as ordersOver30,
+            COALESCE(s.suiteOrders, 0) as suiteOrders,
+            COALESCE(v.vaOrders, 0) as vaOrders,
+            COALESCE(v.vaTotalAmount, 0) as vaTotalAmount,
+            COALESCE(p.totalPeople, 0) as totalPeople
+        FROM product_voyage pv
+                 LEFT JOIN (
+            SELECT
+                tor.voyage_id,
+                COUNT(*) as totalOrders,
+                AVG(GREATEST(DATEDIFF(DATE(pv2.start_time), DATE(tor.create_time)), 0)) as avgAdvanceDays,
+                SUM(CASE WHEN GREATEST(DATEDIFF(DATE(pv2.start_time), DATE(tor.create_time)), 0) BETWEEN 0 AND 3 THEN 1 ELSE 0 END) as orders0to3,
+                SUM(CASE WHEN GREATEST(DATEDIFF(DATE(pv2.start_time), DATE(tor.create_time)), 0) BETWEEN 4 AND 7 THEN 1 ELSE 0 END) as orders4to7,
+                SUM(CASE WHEN GREATEST(DATEDIFF(DATE(pv2.start_time), DATE(tor.create_time)), 0) BETWEEN 8 AND 15 THEN 1 ELSE 0 END) as orders8to15,
+                SUM(CASE WHEN GREATEST(DATEDIFF(DATE(pv2.start_time), DATE(tor.create_time)), 0) BETWEEN 16 AND 30 THEN 1 ELSE 0 END) as orders16to30,
+                SUM(CASE WHEN GREATEST(DATEDIFF(DATE(pv2.start_time), DATE(tor.create_time)), 0) > 30 THEN 1 ELSE 0 END) as ordersOver30
+            FROM trade_order tor
+                     INNER JOIN product_voyage pv2 ON tor.voyage_id = pv2.id AND pv2.deleted = 0
+            WHERE tor.deleted = 0 AND tor.order_status IN (15, 14, 13, 10, 12, 9, 8, 7, 6, 5, 4, 3, 1, 0)
+            GROUP BY tor.voyage_id
+        ) o ON pv.id = o.voyage_id
+                 LEFT JOIN (
+            SELECT
+                tor.voyage_id,
+                COUNT(DISTINCT tor.id) as suiteOrders
+            FROM trade_order tor
+                     INNER JOIN trade_order_room_model torm ON tor.id = torm.order_id AND torm.deleted = 0
+            WHERE tor.deleted = 0 AND tor.order_status IN (15, 14, 13, 10, 12, 9, 8, 7, 6, 5, 4, 3, 1, 0)
+              AND torm.room_model_name LIKE CONCAT('%', '套房', '%')
+            GROUP BY tor.voyage_id
+        ) s ON pv.id = s.voyage_id
+                 LEFT JOIN (
+            SELECT
+                tor.voyage_id,
+                COUNT(DISTINCT CASE WHEN td.product_id != 0 THEN tor.id END) as vaOrders,
+                SUM(CASE WHEN td.product_id != 0 THEN COALESCE(td.actual_price, 0) ELSE 0 END) as vaTotalAmount
+            FROM trade_order tor
+                     LEFT JOIN trade_detail td ON tor.id = td.order_id AND td.deleted = 0
+            WHERE tor.deleted = 0 AND tor.order_status IN (15, 14, 13, 10, 12, 9, 8, 7, 6, 5, 4, 3, 1, 0)
+            GROUP BY tor.voyage_id
+        ) v ON pv.id = v.voyage_id
+                 LEFT JOIN (
+            SELECT
+                tor.voyage_id,
+                COUNT(tv.id) as totalPeople
+            FROM trade_order tor
+                     INNER JOIN trade_visitor tv ON tor.id = tv.order_id AND tv.deleted = 0
+            WHERE tor.deleted = 0 AND tor.order_status IN (15, 14, 13, 10, 12, 9, 8, 7, 6, 5, 4, 3, 1, 0)
+            GROUP BY tor.voyage_id
+        ) p ON pv.id = p.voyage_id
+        WHERE pv.ship_id = #{shipId}
+          AND pv.deleted = 0
+          AND pv.start_time &gt;= #{startDate}
+          AND pv.start_time &lt; DATE_ADD(#{endDate}, INTERVAL 1 DAY)
+        ORDER BY pv.start_time
+    </select>
+
 </mapper>

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

@@ -17,6 +17,7 @@
             s.name AS shipName,
             CONCAT(DATE_FORMAT(v.boarding_time, '%Y-%m-%d'), ' ', CASE WHEN r.direction = 1 THEN '上水' ELSE '下水' END) AS voyageInfo,
             GREATEST(DATEDIFF(DATE(v.boarding_time), CURDATE()), 0) AS countdown,
+            v.remark AS remark,
             528 AS capacity,
             COALESCE(room_stats.totalRooms, 0) AS totalRooms,
             COALESCE(room_stats.paidRooms, 0) AS paidRooms,
@@ -25,7 +26,6 @@
             COALESCE(free_stats.ticketedPassengers, 0) AS ticketedPassengers,
             COALESCE(free_stats.freePassengers, 0) AS freePassengers,
             COALESCE(free_stats.estimatedPassengers, 0) AS estimatedPassengers,
-            '' AS remark,
             COALESCE(finance_stats.receivableAmount, 0) AS receivableAmount,
             COALESCE(finance_stats.receivedAmount, 0) AS receivedAmount,
             COALESCE(finance_stats.receivableAmount, 0) - COALESCE(finance_stats.receivedAmount, 0) AS unreceivedAmount,