diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardCanteenApptTotalController.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardCanteenApptTotalController.java index f31c16ca1..097bb40a9 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardCanteenApptTotalController.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardCanteenApptTotalController.java @@ -189,9 +189,9 @@ public class HardCanteenApptTotalController { * 导出用户在指定用餐日期范围内每天的预约情况。 * * @param query 用户、日期范围、食堂、餐段和状态筛选条件;携带 userIds 时仅导出勾选用户 - * @return 用户和日期维度的 Excel 数据,包含每天未预约用户 + * @return 用户和日期维度的 Excel 数据,包含每天未预约用户,最后一行为餐段及当日金额合计 */ - @Operation(summary = "用户预约查询导出", description = "逐日导出用户预约及核销情况,包含未预约用户,最多查询两个月") + @Operation(summary = "用户预约查询导出", description = "逐日导出用户预约及核销情况,包含未预约用户和末尾金额合计,最多查询两个月") @SysLog("用户预约查询导出") @PreAuthorize("@pms.hasPermission('hard_canteenUserApptTotal_export')") @ResponseExcel(name = "用户预约查询") diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardCanteenApptTotalService.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardCanteenApptTotalService.java index 072e28b6f..a463b076e 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardCanteenApptTotalService.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardCanteenApptTotalService.java @@ -58,7 +58,7 @@ public interface HardCanteenApptTotalService { * 查询并转换用户预约导出数据,包含没有预约订单的权限范围用户。 * * @param query 用户、日期范围和预约状态筛选条件 - * @return 用户和日期维度的 Excel 导出行 + * @return 用户和日期维度的 Excel 导出行,最后一行为餐段及当日金额合计 */ List userReservationExport(HardCanteenUserApptQueryDto query); } diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCanteenApptTotalServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCanteenApptTotalServiceImpl.java index ee5ac55ca..228ebb91a 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCanteenApptTotalServiceImpl.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCanteenApptTotalServiceImpl.java @@ -96,9 +96,13 @@ public class HardCanteenApptTotalServiceImpl implements HardCanteenApptTotalServ /** 用户预约导出的二级表头行数,第一行是业务分组,第二行是具体字段。 */ private static final int USER_RESERVATION_EXPORT_HEADER_ROW_COUNT = 2; + /** 用户预约导出明细之后固定追加的合计行数量。 */ + private static final int USER_RESERVATION_EXPORT_SUMMARY_ROW_COUNT = 1; + /** 用户预约导出工作表二级表头之后允许写入的最大明细行数。 */ private static final int MAX_USER_RESERVATION_EXPORT_ROWS = - BatchExcelExportTemplate.calculateMaxDetailRowCount(USER_RESERVATION_EXPORT_HEADER_ROW_COUNT, 0); + BatchExcelExportTemplate.calculateMaxDetailRowCount(USER_RESERVATION_EXPORT_HEADER_ROW_COUNT, + USER_RESERVATION_EXPORT_SUMMARY_ROW_COUNT); /** 用户预约导出工作表名称。 */ private static final String USER_RESERVATION_EXPORT_SHEET_NAME = "用户预约查询"; @@ -281,7 +285,7 @@ public class HardCanteenApptTotalServiceImpl implements HardCanteenApptTotalServ * 查询用户预约 Excel 数据。 * * @param query 用户、日期范围和预约状态筛选条件 - * @return 用户和日期维度的导出行 + * @return 用户和日期维度的导出行,最后一行为餐段及当日金额合计 */ @Override public List userReservationExport(HardCanteenUserApptQueryDto query) { @@ -291,8 +295,19 @@ public class HardCanteenApptTotalServiceImpl implements HardCanteenApptTotalServ return List.of(); } Map mealSegmentLabelMap = fetchMealSegmentLabelMap(); - return listAllFilteredExportRows(exportQuery, getQueryDates(exportQuery)).stream() - .map(row -> toExcelRow(row, exportQuery, mealSegmentLabelMap)).toList(); + List reservationRows = + listAllFilteredExportRows(exportQuery, getQueryDates(exportQuery)); + if (reservationRows.isEmpty()) { + return List.of(); + } + UserReservationExportSummary summary = + new UserReservationExportSummary(exportQuery, mealSegmentLabelMap); + summary.addRows(reservationRows); + List excelRows = new ArrayList<>(reservationRows.size() + 1); + reservationRows.stream().map(row -> toExcelRow(row, exportQuery, mealSegmentLabelMap)) + .forEach(excelRows::add); + excelRows.add(toExcelSummaryRow(summary, mealSegmentLabelMap)); + return excelRows; } /** @@ -356,6 +371,7 @@ public class HardCanteenApptTotalServiceImpl implements HardCanteenApptTotalServ long processedUserTotal = 0L; long exportedRowTotal = 0L; long current = 1L; + UserReservationExportSummary summary = new UserReservationExportSummary(query, mealSegmentLabelMap); try (ExcelWriter excelWriter = EasyExcel.write(outputPath.toFile(), HardCanteenUserApptExcelVo.class).build()) { WriteSheet writeSheet = EasyExcel.writerSheet(USER_RESERVATION_EXPORT_SHEET_NAME).build(); @@ -367,16 +383,18 @@ public class HardCanteenApptTotalServiceImpl implements HardCanteenApptTotalServ } int remainingUserCount = Math.toIntExact(Math.min(pageRecords.size(), userTotal - processedUserTotal)); List batchUsers = pageRecords.subList(0, remainingUserCount); - List batchRows = filterRows( - buildRows(batchUsers, query, queryDates), query).stream() - .map(row -> toExcelRow(row, query, mealSegmentLabelMap)).toList(); - long nextExportedRowTotal = exportedRowTotal + batchRows.size(); + List batchReservationRows = + filterRows(buildRows(batchUsers, query, queryDates), query); + long nextExportedRowTotal = exportedRowTotal + batchReservationRows.size(); if (nextExportedRowTotal > MAX_USER_RESERVATION_EXPORT_ROWS) { throw new IllegalStateException("导出数据超过单个 Excel 工作表支持的 " + MAX_USER_RESERVATION_EXPORT_ROWS + " 条,请缩小查询范围后分批导出"); } + List batchRows = batchReservationRows.stream() + .map(row -> toExcelRow(row, query, mealSegmentLabelMap)).toList(); if (!batchRows.isEmpty()) { excelWriter.write(batchRows, writeSheet); + summary.addRows(batchReservationRows); } exportedRowTotal = nextExportedRowTotal; processedUserTotal += batchUsers.size(); @@ -387,14 +405,14 @@ public class HardCanteenApptTotalServiceImpl implements HardCanteenApptTotalServ } userPage = fetchExportUserPage(++current, USER_RESERVATION_EXPORT_BATCH_SIZE, query); } - } - - if (processedUserTotal != userTotal) { - throw new IllegalStateException("导出用户数据在执行期间发生变化,预期 " + userTotal + " 位,实际处理 " - + processedUserTotal + " 位"); - } - if (exportedRowTotal == 0L) { - throw new IllegalStateException("没有符合当前筛选条件的用户预约记录"); + if (processedUserTotal != userTotal) { + throw new IllegalStateException("导出用户数据在执行期间发生变化,预期 " + userTotal + + " 位,实际处理 " + processedUserTotal + " 位"); + } + if (exportedRowTotal == 0L) { + throw new IllegalStateException("没有符合当前筛选条件的用户预约记录"); + } + excelWriter.write(List.of(toExcelSummaryRow(summary, mealSegmentLabelMap)), writeSheet); } return exportedRowTotal; } @@ -1146,6 +1164,26 @@ public class HardCanteenApptTotalServiceImpl implements HardCanteenApptTotalServ return excel; } + /** + * 生成用户预约导出文件末尾的金额合计行。 + * + *

合计仍使用与明细相同的导出模型,姓名列标记“合计”,餐段金额和当日金额分别写入对应统计列, + * 其余人员及预约字段保持为空。

+ * + * @param summary 已累计全部导出分片的金额汇总 + * @param mealSegmentLabelMap 餐段编码到字典标签的映射 + * @return 可直接追加到 EasyExcel 工作表末尾的合计行 + */ + private HardCanteenUserApptExcelVo toExcelSummaryRow(UserReservationExportSummary summary, + Map mealSegmentLabelMap) { + HardCanteenUserApptExcelVo excel = new HardCanteenUserApptExcelVo(); + excel.setUserName("合计"); + excel.setMealSegmentTotalPrices(buildExportSummaryMealSegmentTotalPrices(summary, + mealSegmentLabelMap)); + excel.setTotalPrice(centsToYuan(summary.totalPrice)); + return excel; + } + /** * 查询用餐时间段字典并保留字典排序;远程调用失败时由实际订单编码继续完成导出。 * @@ -1225,6 +1263,24 @@ public class HardCanteenApptTotalServiceImpl implements HardCanteenApptTotalServ }).collect(Collectors.joining(";")); } + /** + * 格式化全部导出明细累计后的各餐段金额。 + * + * @param summary 跨分片累计的金额汇总 + * @param mealSegmentLabelMap 餐段编码到字典标签的映射 + * @return 例如“早餐:60.80元;午餐:119.00元”的餐段合计 + */ + private String buildExportSummaryMealSegmentTotalPrices(UserReservationExportSummary summary, + Map mealSegmentLabelMap) { + if (summary.mealSegmentTotalPrices.isEmpty()) { + return "全部餐段:0.00元"; + } + return summary.mealSegmentTotalPrices.entrySet().stream() + .map(entry -> resolveMealSegmentLabel(entry.getKey(), mealSegmentLabelMap) + + ":" + formatYuan(entry.getValue())) + .collect(Collectors.joining(";")); + } + /** * 合并当前字典餐段和订单中的历史餐段编码,供预约数量及金额摘要复用。 * @@ -1330,4 +1386,60 @@ public class HardCanteenApptTotalServiceImpl implements HardCanteenApptTotalServ return "无需核销"; } + /** + * 用户预约导出跨分片金额汇总器。 + * + *

所有金额均以分为单位累计,生成最终合计行时才统一转换为元,避免分片导出过程中出现精度损失。

+ */ + private static final class UserReservationExportSummary { + + /** 各餐段累计金额,键为订单餐段协议编码,值为商品总价,单位为分。 */ + private final Map mealSegmentTotalPrices = new LinkedHashMap<>(); + + /** 全部导出明细的当日金额累计值,单位为分。 */ + private long totalPrice; + + /** + * 创建跨分片汇总器,并按查询餐段或餐段字典顺序预置零金额。 + * + * @param query 本次导出的餐段筛选条件 + * @param mealSegmentLabelMap 餐段编码到字典标签的有序映射 + */ + private UserReservationExportSummary(HardCanteenUserApptQueryDto query, + Map mealSegmentLabelMap) { + if (StrUtil.isNotBlank(query.getHourType())) { + mealSegmentTotalPrices.put(query.getHourType(), 0L); + } else { + mealSegmentLabelMap.keySet().forEach(hourType -> + mealSegmentTotalPrices.put(hourType, 0L)); + } + } + + /** + * 累计一批已经通过全部查询条件筛选的用户日期明细。 + * + * @param rows 当前分片即将写入 Excel 的用户日期明细 + */ + private void addRows(List rows) { + rows.forEach(this::addRow); + } + + /** + * 累计单个用户日期明细的当日总金额和各餐段金额。 + * + * @param row 当前导出的一条用户日期明细 + */ + private void addRow(HardCanteenUserApptVo row) { + totalPrice = Math.addExact(totalPrice, row.getTotalPrice() == null ? 0L : row.getTotalPrice()); + if (row.getOrders() == null || row.getOrders().isEmpty()) { + return; + } + row.getOrders().stream().filter(Objects::nonNull) + .filter(order -> StrUtil.isNotBlank(order.getHourType())) + .forEach(order -> mealSegmentTotalPrices.merge(order.getHourType(), + order.getTotalPrice() == null ? 0L : order.getTotalPrice().longValue(), + Math::addExact)); + } + } + }