|
|
@@ -0,0 +1,289 @@
|
|
|
+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.common.pojo.PageResult;
|
|
|
+import com.yc.ship.framework.excel.core.util.ExcelUtils;
|
|
|
+import com.yc.ship.module.trade.controller.admin.report.vo.VoyageStockBoardItemVO;
|
|
|
+import com.yc.ship.module.trade.controller.admin.report.vo.VoyageStockBoardReqVO;
|
|
|
+import com.yc.ship.module.trade.controller.admin.report.vo.VoyageStockBoardRespVO;
|
|
|
+import com.yc.ship.module.trade.dal.mysql.report.VoyageStockBoardMapper;
|
|
|
+import com.yc.ship.module.trade.service.report.VoyageStockBoardService;
|
|
|
+import com.yc.ship.module.trade.utils.excel.VoyageStockBoardExportStyleHandler;
|
|
|
+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 实现类
|
|
|
+ */
|
|
|
+@Service
|
|
|
+@Slf4j
|
|
|
+public class VoyageStockBoardServiceImpl implements VoyageStockBoardService {
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private VoyageStockBoardMapper voyageStockBoardMapper;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public PageResult<VoyageStockBoardRespVO> getVoyageStockBoardPage(VoyageStockBoardReqVO pageReqVO) {
|
|
|
+ Long voyageId = pageReqVO.getVoyageId();
|
|
|
+
|
|
|
+ // 1. 查询航次汇总
|
|
|
+ VoyageStockBoardRespVO respVO = voyageStockBoardMapper.selectVoyageStockSummary(voyageId);
|
|
|
+ if (respVO == null) {
|
|
|
+ respVO = new VoyageStockBoardRespVO();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 查询明细
|
|
|
+ List<VoyageStockBoardItemVO> detailList = voyageStockBoardMapper.selectVoyageStockBoardDetail(voyageId);
|
|
|
+ if (CollUtil.isEmpty(detailList)) {
|
|
|
+ respVO.setDetailList(new ArrayList<>());
|
|
|
+ return new PageResult<>(Collections.singletonList(respVO), 1L);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 查询同比环比
|
|
|
+ fillYoyAndMom(detailList, voyageId);
|
|
|
+
|
|
|
+ // 4. 计算派生字段
|
|
|
+ calculateDerivedFields(detailList);
|
|
|
+
|
|
|
+ // 5. 按房型拼接名称
|
|
|
+ formatRoomModelName(detailList);
|
|
|
+
|
|
|
+ respVO.setDetailList(detailList);
|
|
|
+ return new PageResult<>(Collections.singletonList(respVO), 1L);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void exportVoyageStockBoardExcel(VoyageStockBoardReqVO pageReqVO, HttpServletResponse response) throws IOException {
|
|
|
+ Long voyageId = pageReqVO.getVoyageId();
|
|
|
+
|
|
|
+ // 1. 查询数据
|
|
|
+ VoyageStockBoardRespVO respVO = voyageStockBoardMapper.selectVoyageStockSummary(voyageId);
|
|
|
+ if (respVO == null) {
|
|
|
+ respVO = new VoyageStockBoardRespVO();
|
|
|
+ }
|
|
|
+
|
|
|
+ List<VoyageStockBoardItemVO> detailList = voyageStockBoardMapper.selectVoyageStockBoardDetail(voyageId);
|
|
|
+ if (CollUtil.isEmpty(detailList)) {
|
|
|
+ ExcelUtils.exportEmpty(response, "航次库存看板");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ fillYoyAndMom(detailList, voyageId);
|
|
|
+ calculateDerivedFields(detailList);
|
|
|
+ formatRoomModelName(detailList);
|
|
|
+ respVO.setDetailList(detailList);
|
|
|
+
|
|
|
+ // 2. 构建多级表头
|
|
|
+ List<List<String>> head = buildExportHeaders();
|
|
|
+
|
|
|
+ // 3. 转换数据
|
|
|
+ List<List<Object>> exportData = transformExportData(respVO);
|
|
|
+
|
|
|
+ response.addHeader("Content-Disposition",
|
|
|
+ "attachment;filename=" + URLEncoder.encode("航次库存看板.xlsx", StandardCharsets.UTF_8.name()));
|
|
|
+ response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");
|
|
|
+ // 4. 写出(表头从第5行开始,前4行是汇总数据)
|
|
|
+ VoyageStockBoardExportStyleHandler styleHandler = new VoyageStockBoardExportStyleHandler(respVO);
|
|
|
+ EasyExcel.write(response.getOutputStream())
|
|
|
+ .head(head)
|
|
|
+ .relativeHeadRowIndex(4)
|
|
|
+ .autoCloseStream(false)
|
|
|
+ .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
|
|
|
+ .registerWriteHandler(styleHandler)
|
|
|
+ .registerConverter(new LongStringConverter())
|
|
|
+ .sheet("航次库存看板")
|
|
|
+ .doWrite(exportData);
|
|
|
+
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 填充同比环比数据
|
|
|
+ */
|
|
|
+ private void fillYoyAndMom(List<VoyageStockBoardItemVO> detailList, Long voyageId) {
|
|
|
+ Long yoyVoyageId = voyageStockBoardMapper.selectYoyVoyageId(voyageId);
|
|
|
+ Long momVoyageId = voyageStockBoardMapper.selectMomVoyageId(voyageId);
|
|
|
+
|
|
|
+ Map<String, BigDecimal> yoyMap = new HashMap<>();
|
|
|
+ if (yoyVoyageId != null) {
|
|
|
+ List<VoyageStockBoardItemVO> yoyList = voyageStockBoardMapper.selectBookNumByVoyageId(yoyVoyageId);
|
|
|
+ yoyMap = yoyList.stream()
|
|
|
+ .filter(item -> item.getRoomModelId() != null)
|
|
|
+ .collect(Collectors.toMap(
|
|
|
+ item -> item.getRoomModelId() + "_" + item.getFloor(),
|
|
|
+ VoyageStockBoardItemVO::getBookedNum,
|
|
|
+ (v1, v2) -> v1));
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, BigDecimal> momMap = new HashMap<>();
|
|
|
+ if (momVoyageId != null) {
|
|
|
+ List<VoyageStockBoardItemVO> momList = voyageStockBoardMapper.selectBookNumByVoyageId(momVoyageId);
|
|
|
+ momMap = momList.stream()
|
|
|
+ .filter(item -> item.getRoomModelId() != null)
|
|
|
+ .collect(Collectors.toMap(
|
|
|
+ item -> item.getRoomModelId() + "_" + item.getFloor(),
|
|
|
+ VoyageStockBoardItemVO::getBookedNum,
|
|
|
+ (v1, v2) -> v1));
|
|
|
+ }
|
|
|
+
|
|
|
+ for (VoyageStockBoardItemVO item : detailList) {
|
|
|
+ String key = item.getRoomModelId() + "_" + item.getFloor();
|
|
|
+ BigDecimal currentBookNum = safeDecimal(item.getBookedNum());
|
|
|
+
|
|
|
+ BigDecimal yoyBookNum = yoyMap.get(key);
|
|
|
+ item.setYoy(calcYoyPercent(currentBookNum, yoyBookNum));
|
|
|
+
|
|
|
+ BigDecimal momBookNum = momMap.get(key);
|
|
|
+ item.setMom(calcYoyPercent(currentBookNum, momBookNum));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算派生字段
|
|
|
+ */
|
|
|
+ private void calculateDerivedFields(List<VoyageStockBoardItemVO> detailList) {
|
|
|
+ for (VoyageStockBoardItemVO item : detailList) {
|
|
|
+ BigDecimal shelfedNum = safeDecimal(item.getTotalNum());
|
|
|
+ BigDecimal depositLock = safeDecimal(item.getDepositLock());
|
|
|
+ BigDecimal nameListLock = safeDecimal(item.getNameListLock());
|
|
|
+ BigDecimal leaveRoom = safeDecimal(item.getLeaveRoom());
|
|
|
+ BigDecimal cutTotal = safeDecimal(item.getCutTotal());
|
|
|
+ BigDecimal cutLock = safeDecimal(item.getCutLock());
|
|
|
+ BigDecimal bookedNum = safeDecimal(item.getBookedNum());
|
|
|
+
|
|
|
+ // 锁位总房间 = 定金锁位 + 名单锁位
|
|
|
+ BigDecimal lockTotal = depositLock.add(nameListLock);
|
|
|
+
|
|
|
+ // 切位剩余房间数 = 切位总房间 - 其中已锁位的房间
|
|
|
+ BigDecimal cutRemain = cutTotal.subtract(cutLock);
|
|
|
+ item.setCutRemain(cutRemain.max(BigDecimal.ZERO));
|
|
|
+
|
|
|
+ // 剩余可售 = 已上架房间数 - 留位房间数 - 锁位房间数
|
|
|
+ BigDecimal remainingSell = shelfedNum.subtract(leaveRoom).subtract(lockTotal);
|
|
|
+ item.setRemainingSell(remainingSell);
|
|
|
+
|
|
|
+ // 剩余共享 = 已上架房间数 - 留位房间数 - 锁位房间数 - 切位剩余房间数
|
|
|
+ BigDecimal remainingShare = remainingSell.subtract(cutRemain);
|
|
|
+ item.setRemainingShare(remainingShare);
|
|
|
+
|
|
|
+ // 已订间数/总数
|
|
|
+ String bookedRatio = bookedNum.stripTrailingZeros().toPlainString() + "/" + shelfedNum.stripTrailingZeros().toPlainString();
|
|
|
+ item.setBookedRatio(bookedRatio);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按房型拼接汇总名称
|
|
|
+ */
|
|
|
+ private void formatRoomModelName(List<VoyageStockBoardItemVO> detailList) {
|
|
|
+ Map<Long, List<VoyageStockBoardItemVO>> groupByRoomModel = detailList.stream()
|
|
|
+ .collect(Collectors.groupingBy(VoyageStockBoardItemVO::getRoomModelId));
|
|
|
+
|
|
|
+ for (List<VoyageStockBoardItemVO> items : groupByRoomModel.values()) {
|
|
|
+ BigDecimal totalBooked = items.stream()
|
|
|
+ .map(VoyageStockBoardItemVO::getBookedNum)
|
|
|
+ .filter(Objects::nonNull)
|
|
|
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
|
+ BigDecimal totalShelfed = items.stream()
|
|
|
+ .map(VoyageStockBoardItemVO::getTotalNum)
|
|
|
+ .filter(Objects::nonNull)
|
|
|
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
|
+
|
|
|
+ String suffix = "(" + totalBooked.stripTrailingZeros().toPlainString()
|
|
|
+ + "/" + totalShelfed.stripTrailingZeros().toPlainString() + ")";
|
|
|
+
|
|
|
+ VoyageStockBoardItemVO first = items.get(0);
|
|
|
+ first.setRoomModelName(first.getRoomModelName() + " " + suffix);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算同比/环比百分比
|
|
|
+ */
|
|
|
+ private String calcYoyPercent(BigDecimal current, BigDecimal last) {
|
|
|
+ if (current == null || last == null || last.compareTo(BigDecimal.ZERO) == 0) {
|
|
|
+ return "-";
|
|
|
+ }
|
|
|
+ BigDecimal diff = current.subtract(last);
|
|
|
+ BigDecimal percent = diff.multiply(BigDecimal.valueOf(100))
|
|
|
+ .divide(last, 1, RoundingMode.HALF_UP);
|
|
|
+ if (percent.compareTo(BigDecimal.ZERO) > 0) {
|
|
|
+ return "↑ +" + percent.stripTrailingZeros().toPlainString() + "%";
|
|
|
+ } else if (percent.compareTo(BigDecimal.ZERO) < 0) {
|
|
|
+ return "↓ " + percent.stripTrailingZeros().toPlainString() + "%";
|
|
|
+ } else {
|
|
|
+ return "— 0%";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private BigDecimal safeDecimal(BigDecimal val) {
|
|
|
+ return val != null ? val : BigDecimal.ZERO;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建导出多级表头
|
|
|
+ */
|
|
|
+ private List<List<String>> buildExportHeaders() {
|
|
|
+ List<List<String>> head = new ArrayList<>();
|
|
|
+ head.add(new ArrayList<>(Collections.singletonList("房型")));
|
|
|
+ head.add(new ArrayList<>(Collections.singletonList("楼层")));
|
|
|
+ head.add(new ArrayList<>(Collections.singletonList("已订间数/总数")));
|
|
|
+ head.add(new ArrayList<>(Arrays.asList("锁位房间", "定金锁位")));
|
|
|
+ head.add(new ArrayList<>(Arrays.asList("锁位房间", "名单锁位")));
|
|
|
+ head.add(new ArrayList<>(Collections.singletonList("留位房间")));
|
|
|
+ head.add(new ArrayList<>(Arrays.asList("切位房间", "切位总房间")));
|
|
|
+ head.add(new ArrayList<>(Arrays.asList("切位房间", "切位剩余房间数")));
|
|
|
+ head.add(new ArrayList<>(Collections.singletonList("剩余可售")));
|
|
|
+ head.add(new ArrayList<>(Collections.singletonList("剩余共享")));
|
|
|
+ head.add(new ArrayList<>(Collections.singletonList("同比")));
|
|
|
+ head.add(new ArrayList<>(Collections.singletonList("环比")));
|
|
|
+ return head;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 转换导出数据
|
|
|
+ */
|
|
|
+ private List<List<Object>> transformExportData(VoyageStockBoardRespVO respVO) {
|
|
|
+ List<List<Object>> result = new ArrayList<>();
|
|
|
+ List<VoyageStockBoardItemVO> detailList = respVO.getDetailList();
|
|
|
+ if (CollUtil.isEmpty(detailList)) {
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (VoyageStockBoardItemVO item : detailList) {
|
|
|
+ List<Object> rowData = new ArrayList<>();
|
|
|
+ rowData.add(item.getRoomModelName() != null ? item.getRoomModelName() : "");
|
|
|
+ rowData.add(item.getFloor() != null ? item.getFloor() : "");
|
|
|
+ rowData.add(item.getBookedRatio() != null ? item.getBookedRatio() : "");
|
|
|
+ rowData.add(formatDecimal(item.getDepositLock()));
|
|
|
+ rowData.add(formatDecimal(item.getNameListLock()));
|
|
|
+ rowData.add(formatDecimal(item.getLeaveRoom()));
|
|
|
+ rowData.add(formatDecimal(item.getCutTotal()));
|
|
|
+ rowData.add(formatDecimal(item.getCutRemain()));
|
|
|
+ rowData.add(formatDecimal(item.getRemainingSell()));
|
|
|
+ rowData.add(formatDecimal(item.getRemainingShare()));
|
|
|
+ rowData.add(item.getYoy() != null ? item.getYoy() : "");
|
|
|
+ rowData.add(item.getMom() != null ? item.getMom() : "");
|
|
|
+ result.add(rowData);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String formatDecimal(BigDecimal val) {
|
|
|
+ if (val == null) return "0";
|
|
|
+ return val.stripTrailingZeros().toPlainString();
|
|
|
+ }
|
|
|
+}
|