This commit is contained in:
dzc committed 2026-08-18 20:19:46 +08:00
1 parent c6a6251c42
commit a859b0ab19
3 files changed
+131 -19

No files matched your search

@@ -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 = "用户预约查询")
@@ -58,7 +58,7 @@ public interface HardCanteenApptTotalService {
* 查询并转换用户预约导出数据,包含没有预约订单的权限范围用户。
*
* @param query 用户、日期范围和预约状态筛选条件
* @return 用户和日期维度的 Excel 导出行
* @return 用户和日期维度的 Excel 导出行,最后一行为餐段及当日金额合计
*/
List<HardCanteenUserApptExcelVo> userReservationExport(HardCanteenUserApptQueryDto query);
}
@@ -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<HardCanteenUserApptExcelVo> userReservationExport(HardCanteenUserApptQueryDto query) {
@@ -291,8 +295,19 @@ public class HardCanteenApptTotalServiceImpl implements HardCanteenApptTotalServ
return List.of();
}
Map<String, String> mealSegmentLabelMap = fetchMealSegmentLabelMap();
return listAllFilteredExportRows(exportQuery, getQueryDates(exportQuery)).stream()
.map(row -> toExcelRow(row, exportQuery, mealSegmentLabelMap)).toList();
List<HardCanteenUserApptVo> reservationRows =
listAllFilteredExportRows(exportQuery, getQueryDates(exportQuery));
if (reservationRows.isEmpty()) {
return List.of();
}
UserReservationExportSummary summary =
new UserReservationExportSummary(exportQuery, mealSegmentLabelMap);
summary.addRows(reservationRows);
List<HardCanteenUserApptExcelVo> 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<UserVO> batchUsers = pageRecords.subList(0, remainingUserCount);
List<HardCanteenUserApptExcelVo> batchRows = filterRows(
buildRows(batchUsers, query, queryDates), query).stream()
.map(row -> toExcelRow(row, query, mealSegmentLabelMap)).toList();
long nextExportedRowTotal = exportedRowTotal + batchRows.size();
List<HardCanteenUserApptVo> 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<HardCanteenUserApptExcelVo> 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;
}
/**
* 生成用户预约导出文件末尾的金额合计行。
*
* <p>合计仍使用与明细相同的导出模型,姓名列标记“合计”,餐段金额和当日金额分别写入对应统计列,
* 其余人员及预约字段保持为空。</p>
*
* @param summary 已累计全部导出分片的金额汇总
* @param mealSegmentLabelMap 餐段编码到字典标签的映射
* @return 可直接追加到 EasyExcel 工作表末尾的合计行
*/
private HardCanteenUserApptExcelVo toExcelSummaryRow(UserReservationExportSummary summary,
Map<String, String> 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<String, String> 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 "无需核销";
}
/**
* 用户预约导出跨分片金额汇总器。
*
* <p>所有金额均以分为单位累计,生成最终合计行时才统一转换为元,避免分片导出过程中出现精度损失。</p>
*/
private static final class UserReservationExportSummary {
/** 各餐段累计金额,键为订单餐段协议编码,值为商品总价,单位为分。 */
private final Map<String, Long> mealSegmentTotalPrices = new LinkedHashMap<>();
/** 全部导出明细的当日金额累计值,单位为分。 */
private long totalPrice;
/**
* 创建跨分片汇总器,并按查询餐段或餐段字典顺序预置零金额。
*
* @param query 本次导出的餐段筛选条件
* @param mealSegmentLabelMap 餐段编码到字典标签的有序映射
*/
private UserReservationExportSummary(HardCanteenUserApptQueryDto query,
Map<String, String> 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<HardCanteenUserApptVo> 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));
}
}
}