|
|
@@ -2668,39 +2668,90 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
|
|
|
return CommonResult.success("修改成功");
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+
|
|
|
+ private String getPersonTypeAll(String type) {
|
|
|
+ String des;
|
|
|
+ switch (type) {
|
|
|
+ case "leader":
|
|
|
+ des = "领队";
|
|
|
+ break;
|
|
|
+ case "with":
|
|
|
+ des = "陪同";
|
|
|
+ break;
|
|
|
+ case "childTake":
|
|
|
+ case "childPlus":
|
|
|
+ case "childNonTake":
|
|
|
+ des = "儿童";
|
|
|
+ break;
|
|
|
+ case "babyTake":
|
|
|
+ case "babyPlus":
|
|
|
+ case "babyNonTake":
|
|
|
+ des = "婴儿";
|
|
|
+ break;
|
|
|
+ case "adultTake":
|
|
|
+ case "adultPlus":
|
|
|
+ default:
|
|
|
+ des = "成人";
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ return des;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 导出游客名单列表(按订单和房间分组)
|
|
|
+ *
|
|
|
+ * 功能说明:
|
|
|
+ * 1. 查询订单基础信息(船名、航期、航向)
|
|
|
+ * 2. 查询游客列表(包含订单、房间、游客详细信息)
|
|
|
+ * 3. 使用Excel模板进行数据填充
|
|
|
+ * 4. 按订单和房间自动合并单元格
|
|
|
+ *
|
|
|
+ * 导出结构:
|
|
|
+ * - 订单详情(8列):代理商、订单号、团号、航向、航期、应收款、实收款、定金
|
|
|
+ * - 房间详情(3列):序号、房型、入住类型
|
|
|
+ * - 游客详情(8列):姓名、性别、证件类型、证件号、游客类型、国籍、增值服务、备注
|
|
|
+ *
|
|
|
+ * @param reqVO 查询条件
|
|
|
+ * @return 导出的Excel文件
|
|
|
+ */
|
|
|
@Override
|
|
|
public File exportTouristList(TradeOrderPageReqVO reqVO) {
|
|
|
// 1. 查询订单基础信息(船名、航期、航向)
|
|
|
Map<String, Object> baseInfo = tradeOrderMapper.selectTouristExportBase(reqVO);
|
|
|
|
|
|
- // 2. 查询游客列表
|
|
|
+ // 2. 查询游客列表(包含订单、房间、游客详细信息)
|
|
|
List<TouristExportVisitorVO> visitorList = tradeVisitorMapper.selectTouristExportVisitor(reqVO);
|
|
|
|
|
|
- // 3. 加载模板
|
|
|
+ // 3. 加载Excel模板
|
|
|
InputStream template = getClass().getClassLoader().getResourceAsStream("templates/tourist_template.xlsx");
|
|
|
- String fileName = String.valueOf(System.currentTimeMillis());
|
|
|
+ String fileName = String.valueOf(System.currentTimeMillis());
|
|
|
String tmpFile = "/tmp/" + fileName + "_tourist.xlsx";
|
|
|
//String tmpFile = "D:/tmp/" + fileName + "_tourist.xlsx";
|
|
|
|
|
|
- // 先不合并,直接导出
|
|
|
+ // 4. 准备ExcelWriter,注册样式处理器和合并策略
|
|
|
+ // - ExcelStyleHandler: 设置单元格样式
|
|
|
+ // - TouristListMergeStrategy: 按订单和房间自动合并单元格
|
|
|
ExcelWriter excelWriter = EasyExcel.write(tmpFile).withTemplate(template).build();
|
|
|
- WriteSheet writeSheet = EasyExcelFactory.writerSheet().registerWriteHandler(new ExcelStyleHandler(9)).build();
|
|
|
+ WriteSheet writeSheet = EasyExcelFactory.writerSheet()
|
|
|
+ .registerWriteHandler(new ExcelStyleHandler(visitorList))
|
|
|
+ /* .registerWriteHandler(new TouristListMergeStrategy(visitorList))*/
|
|
|
+ .build();
|
|
|
FillConfig fillConfig = FillConfig.builder().forceNewRow(true).autoStyle(true).build();
|
|
|
|
|
|
- // 4. 准备基础信息数据
|
|
|
+ // 5. 准备基础信息数据(填充到Excel表头区域)
|
|
|
Map<String, Object> baseData = new HashMap<>();
|
|
|
|
|
|
// 船名
|
|
|
baseData.put("shipName", baseInfo != null && baseInfo.get("shipName") != null ? baseInfo.get("shipName") : "");
|
|
|
|
|
|
- // 航期
|
|
|
- // 航期
|
|
|
+ // 航期(格式化:yyyy.M.d)
|
|
|
baseData.put("travelDate", baseInfo != null && baseInfo.get("travelDate") != null
|
|
|
? ((LocalDateTime) baseInfo.get("travelDate")).format(DateTimeFormatter.ofPattern("yyyy.M.d"))
|
|
|
: "");
|
|
|
|
|
|
-
|
|
|
- // 航向
|
|
|
+ // 航向(1=宜昌-重庆,其他=重庆-宜昌)
|
|
|
Integer direction = baseInfo != null && baseInfo.get("direction") != null ? (Integer) baseInfo.get("direction") : 0;
|
|
|
baseData.put("direction", direction != null && direction == 1 ? "宜昌-重庆" : "重庆-宜昌");
|
|
|
|
|
|
@@ -2709,6 +2760,7 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
|
|
|
StringBuilder countryStat = new StringBuilder();
|
|
|
|
|
|
if (visitorList != null && !visitorList.isEmpty()) {
|
|
|
+ // 按国籍分组统计人数
|
|
|
Map<String, Long> countryMap = visitorList.stream()
|
|
|
.filter(item -> ObjectUtil.isNotEmpty(item) && StringUtils.isNotEmpty(item.getNationalityName()))
|
|
|
.collect(Collectors.groupingBy(
|
|
|
@@ -2716,6 +2768,7 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
|
|
|
Collectors.counting()
|
|
|
));
|
|
|
|
|
|
+ // 拼接国籍统计字符串(格式:数量/国籍)
|
|
|
for (Map.Entry<String, Long> entry : countryMap.entrySet()) {
|
|
|
if (countryStat.length() > 0) {
|
|
|
countryStat.append(" ");
|
|
|
@@ -2726,41 +2779,174 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
|
|
|
baseData.put("totalCount", totalCount);
|
|
|
baseData.put("countryStat", countryStat.toString());
|
|
|
|
|
|
- // 5. 准备游客明细列表
|
|
|
+ // 6. 准备游客明细列表数据(按订单→房间→游客三级分组填充)
|
|
|
List<Map<String, Object>> touristData = new ArrayList<>();
|
|
|
|
|
|
- if (visitorList != null) {
|
|
|
- for (int i = 0; i < visitorList.size(); i++) {
|
|
|
- TouristExportVisitorVO visitor = visitorList.get(i);
|
|
|
- Map<String, Object> item = new HashMap<>();
|
|
|
- if (ObjectUtil.isEmpty(visitor)) {
|
|
|
- continue;
|
|
|
+ if (visitorList != null && !visitorList.isEmpty()) {
|
|
|
+ // 按订单号分组(使用 LinkedHashMap 保持 visitorList 的原始顺序)
|
|
|
+ Map<String, List<TouristExportVisitorVO>> orderGroupMap = visitorList.stream()
|
|
|
+ .collect(Collectors.groupingBy(
|
|
|
+ TouristExportVisitorVO::getOrderNo,
|
|
|
+ LinkedHashMap::new,
|
|
|
+ Collectors.toList()
|
|
|
+ ));
|
|
|
+
|
|
|
+ // 按订单号计算 payAmount 总和
|
|
|
+ Map<String, BigDecimal> orderPayAmountMap = visitorList.stream()
|
|
|
+ .collect(Collectors.groupingBy(
|
|
|
+ TouristExportVisitorVO::getOrderNo,
|
|
|
+ LinkedHashMap::new,
|
|
|
+ Collectors.reducing(
|
|
|
+ BigDecimal.ZERO,
|
|
|
+ v -> v.getPayAmount() != null ? v.getPayAmount() : BigDecimal.ZERO,
|
|
|
+ BigDecimal::add
|
|
|
+ )
|
|
|
+ ));
|
|
|
+
|
|
|
+ // 房间序号从1开始累加
|
|
|
+ int roomIndex = 1;
|
|
|
+ for (List<TouristExportVisitorVO> orderVisitors : orderGroupMap.values()) {
|
|
|
+ // 按房间索引分组(同一房间的游客合并显示房间详情)
|
|
|
+ // 使用 LinkedHashMap 保持房间在 visitorList 中的出现顺序
|
|
|
+ Map<String, List<TouristExportVisitorVO>> roomGroupMap = orderVisitors.stream()
|
|
|
+ .collect(Collectors.groupingBy(
|
|
|
+ v -> v.getRoomIndexId() != null ? v.getRoomIndexId() : "",
|
|
|
+ LinkedHashMap::new,
|
|
|
+ Collectors.toList()
|
|
|
+ ));
|
|
|
+
|
|
|
+
|
|
|
+ for (List<TouristExportVisitorVO> roomVisitors : roomGroupMap.values()) {
|
|
|
+ // 准备房间入住类型描述(如:2个成人/1个儿童)
|
|
|
+ String roomDescription = prepareRoomDescription(roomVisitors);
|
|
|
+
|
|
|
+ // 为该房间的每个游客创建一行数据
|
|
|
+ for (TouristExportVisitorVO visitor : roomVisitors) {
|
|
|
+ Map<String, Object> item = new HashMap<>();
|
|
|
+
|
|
|
+ // 订单详情(8列):同一订单的游客合并显示这些列
|
|
|
+ item.put("sourceName", StringUtils.isEmpty(visitor.getSourceName()) ? "" : visitor.getSourceName()); // 代理商名称
|
|
|
+ item.put("orderNo", StringUtils.isEmpty(visitor.getOrderNo()) ? "" : visitor.getOrderNo()); // 订单号
|
|
|
+ item.put("groupNo", StringUtils.isEmpty(visitor.getGroupNo()) ? "" : visitor.getGroupNo()); // 团号
|
|
|
+ item.put("direction", StringUtils.isEmpty(visitor.getDirection()) ? "" : visitor.getDirection()); // 航向(宜昌-重庆/重庆-宜昌)
|
|
|
+ item.put("travelDate", StringUtils.isEmpty(visitor.getTravelDate()) ? "" : visitor.getTravelDate()); // 航期(格式:yyyy.M.d)
|
|
|
+ item.put("amount", visitor.getAmount() != null ? visitor.getAmount() : BigDecimal.ZERO); // 应收款
|
|
|
+
|
|
|
+ // 实收款计算逻辑:根据支付状态判断
|
|
|
+ //Integer payStatus = visitor.getPayStatus(); // 支付状态
|
|
|
+ //BigDecimal payAmount = visitor.getPayAmount() != null ? visitor.getPayAmount() : BigDecimal.ZERO; // 实际金额
|
|
|
+ BigDecimal deposi = visitor.getDeposi() != null ? visitor.getDeposi() : BigDecimal.ZERO; // 定金
|
|
|
+ BigDecimal applyPayAmount = BigDecimal.ZERO; // 实际收款
|
|
|
+
|
|
|
+ item.put("payAmount", orderPayAmountMap.getOrDefault(visitor.getOrderNo(), BigDecimal.ZERO)); // 实收款
|
|
|
+
|
|
|
+ item.put("deposi", deposi); // 定金
|
|
|
+
|
|
|
+ // 房间详情(3列):同一房间的游客合并显示这些列
|
|
|
+ item.put("roomIndex", roomIndex); // 序号(按订单内的房间顺序)
|
|
|
+ item.put("roomType", StringUtils.isEmpty(visitor.getRoomType()) ? "" : visitor.getRoomType()); // 房型(如:豪华标准间)
|
|
|
+ item.put("roomDescription", roomDescription); // 入住类型(如:2个成人/1个儿童)
|
|
|
+
|
|
|
+ // 游客详情(8列):每个游客独立显示
|
|
|
+ item.put("name", StringUtils.isEmpty(visitor.getName()) ? "" : visitor.getName()); // 游客姓名
|
|
|
+ item.put("gender", visitor.getGender() != null ? (visitor.getGender() == 1 ? "男" : "女") : ""); // 性别(1=男,其他=女)
|
|
|
+ item.put("credentialType", DictFrameworkUtils.getDictDataLabel(DictTypeConstants.VISITOR_CREDENTIAL_TYPE, visitor.getCredentialType())); // 证件类型(身份证/护照等)
|
|
|
+ item.put("credentialNo", StringUtils.isEmpty(visitor.getCredentialNo()) ? "" : visitor.getCredentialNo()); // 证件号
|
|
|
+ item.put("visitorType", StringUtils.isEmpty(visitor.getVisitorType()) ? "" : getPersonTypeAll(visitor.getVisitorType())); // 游客类型(成人/儿童/婴儿)
|
|
|
+ item.put("nationalityName", StringUtils.isEmpty(visitor.getNationalityName()) ? "" : visitor.getNationalityName()); // 国籍(如:中国、美国)
|
|
|
+ item.put("valueAddedService", StringUtils.isEmpty(visitor.getValueAddedService()) ? "" : formatPolicyName(visitor.getValueAddedService())); // 增值服务(如:接送站、保险等)
|
|
|
+ item.put("policyName", ""); // 优惠政策(如:早鸟优惠、团立减等)
|
|
|
+ item.put("remark", StringUtils.isEmpty(visitor.getRemark()) ? "" : visitor.getRemark()); // 备注信息
|
|
|
+
|
|
|
+ touristData.add(item);
|
|
|
+ }
|
|
|
+ roomIndex++;
|
|
|
}
|
|
|
- // 序号
|
|
|
- item.put("xh", i + 1);
|
|
|
- // 姓名
|
|
|
- item.put("name", StringUtils.isEmpty(visitor.getName()) ? "" : visitor.getName());
|
|
|
- // 国籍
|
|
|
- item.put("nationalityName", StringUtils.isEmpty(visitor.getNationalityName()) ? "" : visitor.getNationalityName());
|
|
|
- // 证件类型
|
|
|
- item.put("credentialType", DictFrameworkUtils.getDictDataLabel(DictTypeConstants.VISITOR_CREDENTIAL_TYPE, visitor.getCredentialType()));
|
|
|
- // 证件号
|
|
|
- item.put("credentialNo", StringUtils.isEmpty(visitor.getCredentialNo()) ? "" : visitor.getCredentialNo());
|
|
|
- // 出生年月
|
|
|
- item.put("birthday", StringUtils.isEmpty(visitor.getBirthday()) ? "" : visitor.getBirthday());
|
|
|
- // 备注
|
|
|
- item.put("remark", StringUtils.isEmpty(visitor.getRemark()) ? "" : visitor.getRemark());
|
|
|
-
|
|
|
- touristData.add(item);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 6. 填充数据
|
|
|
- excelWriter.fill(new FillWrapper("visitor", touristData),fillConfig, writeSheet);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 7. 填充数据到Excel模板
|
|
|
+ // 先填充游客明细列表数据,再填充基础信息
|
|
|
+ excelWriter.fill(new FillWrapper("visitor", touristData), fillConfig, writeSheet);
|
|
|
excelWriter.fill(baseData, writeSheet);
|
|
|
excelWriter.finish();
|
|
|
|
|
|
return new File(tmpFile);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 准备房间入住类型描述
|
|
|
+ *
|
|
|
+ * 功能说明:
|
|
|
+ * 统计房间内各类型游客的数量,生成入住类型描述字符串
|
|
|
+ *
|
|
|
+ * 示例:
|
|
|
+ * - 2个成人 → "2个成人"
|
|
|
+ * - 1个成人/1个儿童 → "1个成人/1个儿童"
|
|
|
+ * - 2个成人/1个儿童/1个婴儿 → "2个成人/1个儿童/1个婴儿"
|
|
|
+ *
|
|
|
+ * @param roomVisitors 同一房间的游客列表
|
|
|
+ * @return 入住类型描述字符串
|
|
|
+ */
|
|
|
+ private String prepareRoomDescription(List<TouristExportVisitorVO> roomVisitors) {
|
|
|
+ if (roomVisitors == null || roomVisitors.isEmpty()) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按游客类型统计数量
|
|
|
+ Map<String, Long> typeCountMap = roomVisitors.stream()
|
|
|
+ .filter(v -> StringUtils.isNotEmpty(v.getVisitorType()))
|
|
|
+ .collect(Collectors.groupingBy(TouristExportVisitorVO::getVisitorType, Collectors.counting()));
|
|
|
+
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
+ for (Map.Entry<String, Long> entry : typeCountMap.entrySet()) {
|
|
|
+ if (sb.length() > 0) {
|
|
|
+ sb.append("/");
|
|
|
+ }
|
|
|
+ sb.append(entry.getValue()).append("个").append(getPersonTypeDes1(entry.getKey()));
|
|
|
+ }
|
|
|
+
|
|
|
+ return sb.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 格式化优惠政策名称
|
|
|
+ *
|
|
|
+ * 功能说明:
|
|
|
+ * 将优惠政策字符串按 "、" 分割后,添加序号并用逗号连接
|
|
|
+ *
|
|
|
+ * 示例:
|
|
|
+ * - 输入:null 或 "" → 输出:""
|
|
|
+ * - 输入:"升舱礼遇2升3" → 输出:"①升舱礼遇2升3"
|
|
|
+ * - 输入:"升舱礼遇2升3、3升4、5升6" → 输出:"①升舱礼遇2升3"
|
|
|
+ *
|
|
|
+ * @param policyName 原始优惠政策字符串
|
|
|
+ * @return 格式化后的优惠政策字符串
|
|
|
+ */
|
|
|
+ private String formatPolicyName(String policyName) {
|
|
|
+ if (StringUtils.isEmpty(policyName)) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按 "," 分割
|
|
|
+ String[] policies = policyName.split(",");
|
|
|
+ if (policies.length == 0) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ // 序号字符:①②③④⑤⑥⑦⑧⑨⑩...
|
|
|
+ String[] numberSymbols = {"①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨", "⑩", "⑪", "⑫", "⑬", "⑭", "⑮"};
|
|
|
+
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
+ for (int i = 0; i < policies.length; i++) {
|
|
|
+ if (sb.length() > 0) {
|
|
|
+ sb.append("\n");
|
|
|
+ }
|
|
|
+ // 如果序号超过预定义范围,使用普通数字编号
|
|
|
+ String prefix = i < numberSymbols.length ? numberSymbols[i] : (i + 1) + ".";
|
|
|
+ sb.append(prefix).append(policies[i].trim());
|
|
|
+ }
|
|
|
+
|
|
|
+ return sb.toString();
|
|
|
+ }
|
|
|
}
|