|
@@ -0,0 +1,511 @@
|
|
|
|
|
+package com.yc.ship.module.trade.service.report.impl;
|
|
|
|
|
+
|
|
|
|
|
+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.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.trade.controller.admin.report.vo.CruiseOpsDailyReqVO;
|
|
|
|
|
+import com.yc.ship.module.trade.controller.admin.report.vo.CruiseOpsDailyRespVO;
|
|
|
|
|
+import com.yc.ship.module.trade.dal.mysql.report.OpsDailyMapper;
|
|
|
|
|
+import com.yc.ship.module.trade.service.report.OpsDailyService;
|
|
|
|
|
+import com.yc.ship.module.trade.utils.excel.CruiseOpsDailyExportStyleHandler;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+
|
|
|
|
|
+import javax.annotation.Resource;
|
|
|
|
|
+import javax.servlet.http.HttpServletResponse;
|
|
|
|
|
+import java.io.IOException;
|
|
|
|
|
+import java.math.BigDecimal;
|
|
|
|
|
+import java.math.RoundingMode;
|
|
|
|
|
+import java.net.URLEncoder;
|
|
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
|
|
+import java.util.*;
|
|
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 省际度假游轮经营情况日报 Service 实现类
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 数据结构:日常数据行 + 月度小计(数据+同比) + 累计行(数据+同比)
|
|
|
|
|
+ *
|
|
|
|
|
+ * @author system
|
|
|
|
|
+ */
|
|
|
|
|
+@Service
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+public class OpsDailyServiceImpl implements OpsDailyService {
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private OpsDailyMapper cruiseOpsDailyMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public List<CruiseOpsDailyRespVO> getCruiseOpsDailyList(CruiseOpsDailyReqVO reqVO) {
|
|
|
|
|
+ // 1. 查询本期原始数据
|
|
|
|
|
+ List<CruiseOpsDailyRespVO> currentDataList = cruiseOpsDailyMapper.selectCruiseOpsDailyList(reqVO);
|
|
|
|
|
+ if (CollUtil.isEmpty(currentDataList)) {
|
|
|
|
|
+ return new ArrayList<>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 计算本期数据的派生字段(载客率、用房率、收入合计)
|
|
|
|
|
+ calculateDerivedFields(currentDataList);
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 计算去年同期范围并查询去年数据
|
|
|
|
|
+ String startDate = reqVO.getStartDate();
|
|
|
|
|
+ String endDate = reqVO.getEndDate();
|
|
|
|
|
+ CruiseOpsDailyReqVO lastYearReq = new CruiseOpsDailyReqVO();
|
|
|
|
|
+ try {
|
|
|
|
|
+ Date startD = DateHelper.parseDate(startDate);
|
|
|
|
|
+ Date endD = endDate != null ? DateHelper.parseDate(endDate) : null;
|
|
|
|
|
+ // 去年同期
|
|
|
|
|
+ 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"));
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("[getCruiseOpsDailyList] 解析日期失败, 跳过同比计算: {}", e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<CruiseOpsDailyRespVO> lastYearDataList = new ArrayList<>();
|
|
|
|
|
+ if (lastYearReq.getStartDate() != null && lastYearReq.getEndDate() != null) {
|
|
|
|
|
+ lastYearDataList = cruiseOpsDailyMapper.selectCruiseOpsDailyList(lastYearReq);
|
|
|
|
|
+ calculateDerivedFields(lastYearDataList);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 构建完整报表(含小计、累计、同比)
|
|
|
|
|
+ return buildReportWithYoy(currentDataList, lastYearDataList);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void exportCruiseOpsDailyExcel(CruiseOpsDailyReqVO reqVO, HttpServletResponse response) throws IOException {
|
|
|
|
|
+ List<CruiseOpsDailyRespVO> dataList = getCruiseOpsDailyList(reqVO);
|
|
|
|
|
+ if (CollUtil.isEmpty(dataList)) {
|
|
|
|
|
+ ExcelUtils.exportEmpty(response, "省际度假游轮经营情况日报表");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 构建表头(与前端页面15列一致)
|
|
|
|
|
+ List<List<String>> head = buildExportHeaders();
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 转换数据:同比行的同比值写入对应数据列
|
|
|
|
|
+ List<List<Object>> exportData = transformExportData(dataList);
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 使用 EasyExcel 写出,注册自定义样式处理器
|
|
|
|
|
+ CruiseOpsDailyExportStyleHandler styleHandler = new CruiseOpsDailyExportStyleHandler(dataList);
|
|
|
|
|
+ EasyExcel.write(response.getOutputStream())
|
|
|
|
|
+ .head(head)
|
|
|
|
|
+ .autoCloseStream(false)
|
|
|
|
|
+ .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
|
|
|
|
|
+ .registerWriteHandler(styleHandler)
|
|
|
|
|
+ .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");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 导出相关私有方法 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 构建导出表头(与前端页面15列一一对应)
|
|
|
|
|
+ */
|
|
|
|
|
+ private List<List<String>> buildExportHeaders() {
|
|
|
|
|
+ String[] headers = {"序号", "月份", "日期", "船舶", "航线", "航次号",
|
|
|
|
|
+ "客容量", "载客量", "载客率", "房总数", "用房数", "用房率",
|
|
|
|
|
+ "船票收入", "二消收入", "收入合计"};
|
|
|
|
|
+ List<List<String>> head = new ArrayList<>(headers.length);
|
|
|
|
|
+ for (String h : headers) {
|
|
|
|
|
+ head.add(Collections.singletonList(h));
|
|
|
|
|
+ }
|
|
|
|
|
+ return head;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 将报表数据转为 EasyExcel 导出格式(List<List<Object>>)
|
|
|
|
|
+ * 同比行:同比值写入对应数据列位置(与前端页面展示一致)
|
|
|
|
|
+ * 小计/累计数据行:日期/船舶/航线列留空(合并到月份列)
|
|
|
|
|
+ */
|
|
|
|
|
+ private List<List<Object>> transformExportData(List<CruiseOpsDailyRespVO> dataList) {
|
|
|
|
|
+ List<List<Object>> result = new ArrayList<>(dataList.size());
|
|
|
|
|
+ for (CruiseOpsDailyRespVO row : dataList) {
|
|
|
|
|
+ List<Object> rowData = new ArrayList<>(15);
|
|
|
|
|
+ boolean isYoy = "同比".equals(row.getVoyageNo());
|
|
|
|
|
+
|
|
|
|
|
+ if (isYoy) {
|
|
|
|
|
+ // 同比行:序号~航线列留空(合并到上一行),航次号显示"同比"
|
|
|
|
|
+ rowData.add(""); // 序号 - 合并
|
|
|
|
|
+ rowData.add(""); // 月份 - 合并
|
|
|
|
|
+ rowData.add(""); // 日期 - 合并
|
|
|
|
|
+ rowData.add(""); // 船舶 - 合并
|
|
|
|
|
+ rowData.add(""); // 航线 - 合并
|
|
|
|
|
+ rowData.add("同比");
|
|
|
|
|
+ rowData.add(formatYoyForExport(row.getPassengerCapacityYoy()));
|
|
|
|
|
+ rowData.add(formatYoyForExport(row.getPassengerCountYoy()));
|
|
|
|
|
+ rowData.add(formatYoyForExport(row.getPassengerRateYoy()));
|
|
|
|
|
+ rowData.add(formatYoyForExport(row.getTotalRoomsYoy()));
|
|
|
|
|
+ rowData.add(formatYoyForExport(row.getUsedRoomsYoy()));
|
|
|
|
|
+ rowData.add(formatYoyForExport(row.getRoomRateYoy()));
|
|
|
|
|
+ rowData.add(formatYoyForExport(row.getTicketIncomeYoy()));
|
|
|
|
|
+ rowData.add(formatYoyForExport(row.getSecondIncomeYoy()));
|
|
|
|
|
+ rowData.add(formatYoyForExport(row.getTotalIncomeYoy()));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 普通行 / 小计数据行 / 累计数据行
|
|
|
|
|
+ rowData.add(row.getIndex() != null ? String.valueOf(row.getIndex()) : "");
|
|
|
|
|
+ rowData.add(row.getMonth() != null ? row.getMonth() : "");
|
|
|
|
|
+ rowData.add(row.getDate() != null ? row.getDate() : "");
|
|
|
|
|
+ rowData.add(row.getShip() != null ? row.getShip() : "");
|
|
|
|
|
+ rowData.add(row.getRoute() != null ? row.getRoute() : "");
|
|
|
|
|
+ rowData.add(row.getVoyageNo() != null ? row.getVoyageNo() : "");
|
|
|
|
|
+ rowData.add(formatNumber(row.getPassengerCapacity()));
|
|
|
|
|
+ rowData.add(formatNumber(row.getPassengerCount()));
|
|
|
|
|
+ rowData.add(row.getPassengerRate() != null ? row.getPassengerRate() : "");
|
|
|
|
|
+ rowData.add(formatNumber(row.getTotalRooms()));
|
|
|
|
|
+ rowData.add(row.getUsedRooms() != null ? row.getUsedRooms() : "");
|
|
|
|
|
+ rowData.add(row.getRoomRate() != null ? row.getRoomRate() : "");
|
|
|
|
|
+ rowData.add(formatMoneyForExport(row.getTicketIncome()));
|
|
|
|
|
+ rowData.add(formatMoneyForExport(row.getSecondIncome()));
|
|
|
|
|
+ rowData.add(formatMoneyForExport(row.getTotalIncome()));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ result.add(rowData);
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 格式化同比值为导出显示格式(带箭头标识)
|
|
|
|
|
+ * 正值: ↑ +15.5% 负值: ↓ -12.3% 持平: — 0.0% 无数据: -
|
|
|
|
|
+ */
|
|
|
|
|
+ private String formatYoyForExport(String yoyValue) {
|
|
|
|
|
+ if (yoyValue == null || "-".equals(yoyValue)) {
|
|
|
|
|
+ return "-";
|
|
|
|
|
+ }
|
|
|
|
|
+ String numStr = yoyValue.replace("%", "");
|
|
|
|
|
+ try {
|
|
|
|
|
+ double num = Double.parseDouble(numStr);
|
|
|
|
|
+ if (num > 0) {
|
|
|
|
|
+ String prefix = yoyValue.startsWith("+") ? "↑ " : "↑ +";
|
|
|
|
|
+ return prefix + yoyValue;
|
|
|
|
|
+ } else if (num < 0) {
|
|
|
|
|
+ return "↓ " + yoyValue;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return "— " + yoyValue;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
|
|
+ return yoyValue;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 格式化金额为导出显示(与前端 formatMoney 一致)
|
|
|
|
|
+ */
|
|
|
|
|
+ private String formatMoneyForExport(BigDecimal amount) {
|
|
|
|
|
+ if (amount == null) {
|
|
|
|
|
+ return "-";
|
|
|
|
|
+ }
|
|
|
|
|
+ return "¥" + amount.setScale(2, RoundingMode.HALF_UP).toPlainString();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 格式化整数为字符串
|
|
|
|
|
+ */
|
|
|
|
|
+ private String formatNumber(Integer val) {
|
|
|
|
|
+ return val != null ? String.valueOf(val) : "";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 私有方法 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 计算派生字段(载客率、用房率、收入合计)
|
|
|
|
|
+ */
|
|
|
|
|
+ private void calculateDerivedFields(List<CruiseOpsDailyRespVO> dataList) {
|
|
|
|
|
+ for (CruiseOpsDailyRespVO item : dataList) {
|
|
|
|
|
+ // 载客率 = 载客量 / 客容量 * 100
|
|
|
|
|
+ if (item.getPassengerCapacity() != null && item.getPassengerCapacity() > 0
|
|
|
|
|
+ && item.getPassengerCount() != null) {
|
|
|
|
|
+ double rate = (double) item.getPassengerCount() / item.getPassengerCapacity() * 100;
|
|
|
|
|
+ item.setPassengerRate(String.format("%.1f%%", rate));
|
|
|
|
|
+ }
|
|
|
|
|
+ // 用房率 = 用房数 / 房总数 * 100
|
|
|
|
|
+ if (item.getTotalRooms() != null && item.getTotalRooms() > 0
|
|
|
|
|
+ && item.getUsedRooms() != null) {
|
|
|
|
|
+ double usedRoomsVal = safeParseInt(item.getUsedRooms());
|
|
|
|
|
+ double rate = usedRoomsVal / item.getTotalRooms() * 100;
|
|
|
|
|
+ item.setRoomRate(String.format("%.1f%%", rate));
|
|
|
|
|
+ }
|
|
|
|
|
+ // 收入合计 = 船票收入 + 二消收入
|
|
|
|
|
+ BigDecimal ticket = item.getTicketIncome() != null ? item.getTicketIncome() : BigDecimal.ZERO;
|
|
|
|
|
+ BigDecimal second = item.getSecondIncome() != null ? item.getSecondIncome() : BigDecimal.ZERO;
|
|
|
|
|
+ item.setTotalIncome(ticket.add(second));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 构建完整报表数据结构:
|
|
|
|
|
+ * - 日常数据行(带序号)
|
|
|
|
|
+ * - 月度小计数据行 + 月度小计同比行(小计同比=当月小计 vs 去年同月小计)
|
|
|
|
|
+ * - 累计数据行 + 累计同比行(累计同比=累计本期 vs 累计去年同期,从第二个月起)
|
|
|
|
|
+ *
|
|
|
|
|
+ * 去年同期月份通过年份减1来匹配(如 2026-01 → 2025-01),
|
|
|
|
|
+ * 不再使用位置索引匹配,保证数据对应准确。
|
|
|
|
|
+ */
|
|
|
|
|
+ private List<CruiseOpsDailyRespVO> buildReportWithYoy(
|
|
|
|
|
+ List<CruiseOpsDailyRespVO> currentDataList,
|
|
|
|
|
+ List<CruiseOpsDailyRespVO> lastYearDataList) {
|
|
|
|
|
+
|
|
|
|
|
+ List<CruiseOpsDailyRespVO> result = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 本期按月份分组(TreeMap保证有序)
|
|
|
|
|
+ Map<String, List<CruiseOpsDailyRespVO>> currentMonthMap = currentDataList.stream()
|
|
|
|
|
+ .collect(Collectors.groupingBy(
|
|
|
|
|
+ item -> item.getMonth() != null ? item.getMonth() : "",
|
|
|
|
|
+ TreeMap::new, Collectors.toList()));
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 去年同期按月份分组(同样用TreeMap保持顺序)
|
|
|
|
|
+ Map<String, List<CruiseOpsDailyRespVO>> lastYearMonthMap = lastYearDataList.stream()
|
|
|
|
|
+ .collect(Collectors.groupingBy(
|
|
|
|
|
+ item -> item.getMonth() != null ? item.getMonth() : "",
|
|
|
|
|
+ TreeMap::new, Collectors.toList()));
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 全局序号和累计容器
|
|
|
|
|
+ int globalIdx = 1;
|
|
|
|
|
+ List<CruiseOpsDailyRespVO> allDailyDataForCumulative = new ArrayList<>();
|
|
|
|
|
+ List<CruiseOpsDailyRespVO> allLastYearDailyForCumulative = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 遍历每个月份(通过年份-1匹配去年同期月份)
|
|
|
|
|
+ int monthIndex = 0;
|
|
|
|
|
+ for (Map.Entry<String, List<CruiseOpsDailyRespVO>> entry : currentMonthMap.entrySet()) {
|
|
|
|
|
+ String month = entry.getKey();
|
|
|
|
|
+ List<CruiseOpsDailyRespVO> dailyItems = entry.getValue();
|
|
|
|
|
+
|
|
|
|
|
+ // --- 4.1 设置序号 ---
|
|
|
|
|
+ for (CruiseOpsDailyRespVO item : dailyItems) {
|
|
|
|
|
+ item.setIndex(globalIdx++);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // --- 4.2 加入结果和累计容器 ---
|
|
|
|
|
+ result.addAll(dailyItems);
|
|
|
|
|
+ allDailyDataForCumulative.addAll(dailyItems);
|
|
|
|
|
+
|
|
|
|
|
+ // --- 4.3 月度小计数据行 ---
|
|
|
|
|
+ CruiseOpsDailyRespVO subtotalData = buildSubtotalRow(dailyItems, month);
|
|
|
|
|
+ result.add(subtotalData);
|
|
|
|
|
+
|
|
|
|
|
+ // --- 4.4 月度小计同比行(通过月份key转换匹配去年同期)---
|
|
|
|
|
+ String lastYearMonth = getMonthMinusOneYear(month);
|
|
|
|
|
+ List<CruiseOpsDailyRespVO> lastYearMonthData = lastYearMonthMap.getOrDefault(
|
|
|
|
|
+ lastYearMonth, new ArrayList<>());
|
|
|
|
|
+ CruiseOpsDailyRespVO lastYearSubtotal = buildSubtotalRow(lastYearMonthData, "");
|
|
|
|
|
+ CruiseOpsDailyRespVO subtotalYoy = buildYoyRow(subtotalData, lastYearSubtotal);
|
|
|
|
|
+ result.add(subtotalYoy);
|
|
|
|
|
+
|
|
|
|
|
+ // 累加去年同期月度数据(用于累计同比计算)
|
|
|
|
|
+ allLastYearDailyForCumulative.addAll(lastYearMonthData);
|
|
|
|
|
+
|
|
|
|
|
+ // --- 4.5 累计数据行 + 累计同比行(从第二个月起,累计同比=累计本期 vs 累计去年同期) ---
|
|
|
|
|
+ if (monthIndex > 0) {
|
|
|
|
|
+ // 累计数据行
|
|
|
|
|
+ CruiseOpsDailyRespVO cumData = buildSubtotalRow(allDailyDataForCumulative, "累计");
|
|
|
|
|
+ result.add(cumData);
|
|
|
|
|
+
|
|
|
|
|
+ // 累计同比行:用累计本期 vs 累计去年同期 重新计算同比
|
|
|
|
|
+ CruiseOpsDailyRespVO lastYearCumData = buildSubtotalRow(allLastYearDailyForCumulative, "");
|
|
|
|
|
+ CruiseOpsDailyRespVO cumYoy = buildYoyRow(cumData, lastYearCumData);
|
|
|
|
|
+ result.add(cumYoy);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ monthIndex++;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 将月份字符串减去一年(如 2026-01 → 2025-01)
|
|
|
|
|
+ */
|
|
|
|
|
+ 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 CruiseOpsDailyRespVO buildSubtotalRow(List<CruiseOpsDailyRespVO> items, String monthLabel) {
|
|
|
|
|
+ CruiseOpsDailyRespVO row = new CruiseOpsDailyRespVO();
|
|
|
|
|
+
|
|
|
|
|
+ if ("累计".equals(monthLabel)) {
|
|
|
|
|
+ row.setMonth("累计");
|
|
|
|
|
+ } else if (!"累计".equals(monthLabel) && monthLabel.contains("小计")) {
|
|
|
|
|
+ row.setMonth(monthLabel);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 正常月度小计
|
|
|
|
|
+ row.setMonth(monthLabel + "小计");
|
|
|
|
|
+ }
|
|
|
|
|
+ row.setDate("");
|
|
|
|
|
+ row.setShip("");
|
|
|
|
|
+ row.setRoute("");
|
|
|
|
|
+ row.setVoyageNo("数据");
|
|
|
|
|
+ row.setIsSubtotal(!"累计".equals(row.getMonth()));
|
|
|
|
|
+ row.setIsCumulative("累计".equals(row.getMonth()));
|
|
|
|
|
+
|
|
|
|
|
+ if (CollUtil.isEmpty(items)) {
|
|
|
|
|
+ return row;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 数值汇总
|
|
|
|
|
+ int passengerCapacitySum = 0;
|
|
|
|
|
+ int passengerCountSum = 0;
|
|
|
|
|
+ int totalRoomsSum = 0;
|
|
|
|
|
+ int usedRoomsSum = 0;
|
|
|
|
|
+ BigDecimal ticketIncomeSum = BigDecimal.ZERO;
|
|
|
|
|
+ BigDecimal secondIncomeSum = BigDecimal.ZERO;
|
|
|
|
|
+
|
|
|
|
|
+ for (CruiseOpsDailyRespVO item : items) {
|
|
|
|
|
+ passengerCapacitySum += safeInt(item.getPassengerCapacity());
|
|
|
|
|
+ passengerCountSum += safeInt(item.getPassengerCount());
|
|
|
|
|
+ totalRoomsSum += safeInt(item.getTotalRooms());
|
|
|
|
|
+ usedRoomsSum += safeParseInt(item.getUsedRooms());
|
|
|
|
|
+ ticketIncomeSum = ticketIncomeSum.add(safeBigDecimal(item.getTicketIncome()));
|
|
|
|
|
+ secondIncomeSum = secondIncomeSum.add(safeBigDecimal(item.getSecondIncome()));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ row.setPassengerCapacity(passengerCapacitySum);
|
|
|
|
|
+ row.setPassengerCount(passengerCountSum);
|
|
|
|
|
+ row.setTotalRooms(totalRoomsSum);
|
|
|
|
|
+ row.setUsedRooms(String.valueOf(usedRoomsSum));
|
|
|
|
|
+ row.setTicketIncome(ticketIncomeSum);
|
|
|
|
|
+ row.setSecondIncome(secondIncomeSum);
|
|
|
|
|
+
|
|
|
|
|
+ // 派生字段重新计算
|
|
|
|
|
+ if (passengerCapacitySum > 0) {
|
|
|
|
|
+ double rate = (double) passengerCountSum / passengerCapacitySum * 100;
|
|
|
|
|
+ row.setPassengerRate(String.format("%.1f%%", rate));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (totalRoomsSum > 0) {
|
|
|
|
|
+ double rate = (double) usedRoomsSum / totalRoomsSum * 100;
|
|
|
|
|
+ row.setRoomRate(String.format("%.1f%%", rate));
|
|
|
|
|
+ }
|
|
|
|
|
+ row.setTotalIncome(ticketIncomeSum.add(secondIncomeSum));
|
|
|
|
|
+
|
|
|
|
|
+ return row;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 构建同比行(对比本期汇总 vs 往期汇总)
|
|
|
|
|
+ * 仅保留同比率数据,原始数值字段不填充(前端同比行只展示同比率)
|
|
|
|
|
+ */
|
|
|
|
|
+ private CruiseOpsDailyRespVO buildYoyRow(CruiseOpsDailyRespVO currentRow, CruiseOpsDailyRespVO lastYearRow) {
|
|
|
|
|
+ CruiseOpsDailyRespVO yoyRow = new CruiseOpsDailyRespVO();
|
|
|
|
|
+ yoyRow.setIndex(null);
|
|
|
|
|
+ yoyRow.setMonth(""); // 与上一行(小计/累计)的月份合并
|
|
|
|
|
+ yoyRow.setDate("");
|
|
|
|
|
+ yoyRow.setShip("");
|
|
|
|
|
+ yoyRow.setRoute("");
|
|
|
|
|
+ yoyRow.setVoyageNo("同比");
|
|
|
|
|
+ yoyRow.setIsSubtotal(currentRow.getIsSubtotal() != null && currentRow.getIsSubtotal());
|
|
|
|
|
+ yoyRow.setIsCumulative(currentRow.getIsCumulative() != null && currentRow.getIsCumulative());
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 计算各指标的同比率 =====
|
|
|
|
|
+ yoyRow.setPassengerCapacityYoy(calcYoyPercent(
|
|
|
|
|
+ safeInt(currentRow.getPassengerCapacity()), safeInt(lastYearRow.getPassengerCapacity())));
|
|
|
|
|
+ yoyRow.setPassengerCountYoy(calcYoyPercent(
|
|
|
|
|
+ safeInt(currentRow.getPassengerCount()), safeInt(lastYearRow.getPassengerCount())));
|
|
|
|
|
+
|
|
|
|
|
+ // 载客率同比(百分点差异)
|
|
|
|
|
+ yoyRow.setPassengerRateYoy(calcRateDiff(currentRow.getPassengerRate(), lastYearRow.getPassengerRate()));
|
|
|
|
|
+
|
|
|
|
|
+ yoyRow.setTotalRoomsYoy(calcYoyPercent(
|
|
|
|
|
+ safeInt(currentRow.getTotalRooms()), safeInt(lastYearRow.getTotalRooms())));
|
|
|
|
|
+ yoyRow.setUsedRoomsYoy(calcYoyPercent(
|
|
|
|
|
+ safeParseInt(currentRow.getUsedRooms()), safeParseInt(lastYearRow.getUsedRooms())));
|
|
|
|
|
+
|
|
|
|
|
+ // 用房率同比(百分点差异)
|
|
|
|
|
+ yoyRow.setRoomRateYoy(calcRateDiff(currentRow.getRoomRate(), lastYearRow.getRoomRate()));
|
|
|
|
|
+
|
|
|
|
|
+ // 收入同比率
|
|
|
|
|
+ yoyRow.setTicketIncomeYoy(calcYoyPercent(
|
|
|
|
|
+ safeBigDecimal(currentRow.getTicketIncome()), safeBigDecimal(lastYearRow.getTicketIncome())));
|
|
|
|
|
+ yoyRow.setSecondIncomeYoy(calcYoyPercent(
|
|
|
|
|
+ safeBigDecimal(currentRow.getSecondIncome()), safeBigDecimal(lastYearRow.getSecondIncome())));
|
|
|
|
|
+ yoyRow.setTotalIncomeYoy(calcYoyPercent(
|
|
|
|
|
+ safeBigDecimal(currentRow.getTotalIncome()), safeBigDecimal(lastYearRow.getTotalIncome())));
|
|
|
|
|
+
|
|
|
|
|
+ return yoyRow;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 计算同比百分比 (本期-往期)/往期*100
|
|
|
|
|
+ */
|
|
|
|
|
+ private String calcYoyPercent(Number current, Number last) {
|
|
|
|
|
+ if (current == null || last == null || last.doubleValue() == 0) {
|
|
|
|
|
+ return "-";
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ return MathHelper.getNumberYOY(current, last, 1) + "%";
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.debug("calcYoyPercent error: {},{}", current, last);
|
|
|
|
|
+ return "-";
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 计算比率百分点差异(如 84.0% - 82.0% = +2.0)
|
|
|
|
|
+ */
|
|
|
|
|
+ private String calcRateDiff(String currentRate, String lastRate) {
|
|
|
|
|
+ if (currentRate == null || lastRate == null || !currentRate.contains("%") || !lastRate.contains("%")) {
|
|
|
|
|
+ return "-";
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ double currentVal = Double.parseDouble(currentRate.replace("%", ""));
|
|
|
|
|
+ double lastVal = Double.parseDouble(lastRate.replace("%", ""));
|
|
|
|
|
+ double diff = currentVal - lastVal;
|
|
|
|
|
+ return (diff >= 0 ? "+" : "") + String.format("%.1f", diff);
|
|
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
|
|
+ return "-";
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private int safeInt(Integer val) {
|
|
|
|
|
+ return val != null ? val : 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private int safeParseInt(String val) {
|
|
|
|
|
+ if (val == null || val.isEmpty()) {
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ return Integer.parseInt(val);
|
|
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return (int) Double.parseDouble(val);
|
|
|
|
|
+ } catch (NumberFormatException e2) {
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private BigDecimal safeBigDecimal(BigDecimal val) {
|
|
|
|
|
+ return val != null ? val : BigDecimal.ZERO;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+}
|