该模板统一管理分页循环、预统计数量校验、SXSSF 临时文件和最终 XLSX 临时文件,业务方只需提供 + * 模板初始化、分页查询、逐行写入、尾部写入、进度通知和文件消费回调。该类不依赖异步任务或文件中心, + * 可由不同业务按需组合。
+ */ +public final class BatchExcelExportTemplate { + + /** Excel 2007 单个工作表支持的最大行数。 */ + private static final int XLSX_MAX_ROW_COUNT = SpreadsheetVersion.EXCEL2007.getMaxRows(); + + /** 默认分页大小和 SXSSF 内存行窗口大小。 */ + private static final int DEFAULT_BATCH_SIZE = 1000; + + /** 默认临时文件名前缀,必须满足 JDK 至少三个字符的约束。 */ + private static final String DEFAULT_TEMPORARY_FILE_PREFIX = "batch-excel-export-"; + + /** 公共导出模板日志记录器,仅记录临时文件清理失败等基础设施异常。 */ + private static final Logger LOGGER = LoggerFactory.getLogger(BatchExcelExportTemplate.class); + + /** + * 工具类不允许实例化。 + */ + private BatchExcelExportTemplate() { + } + + /** + * 创建一次分批导出的链式配置器。 + * + * @param expectedTotal 导出前按相同筛选条件统计的明细总数,必须大于零 + * @param每个查询批次占一步,文件上传、异步任务成功落库等非批次动作由业务方通过额外步数声明。
+ * + * @param expectedTotal 导出前预统计的明细总数,必须大于零 + * @param batchSize 单次分页查询大小,必须大于零 + * @param extraSteps 文件上传等额外步骤数量,不得小于零 + * @return 批次数与额外步骤之和 + */ + public static int calculateProgressTotal(long expectedTotal, int batchSize, int extraSteps) { + if (extraSteps < 0) { + throw new IllegalArgumentException("导出额外进度步数不得小于零"); + } + long progressTotal = (long) calculateBatchCount(expectedTotal, batchSize) + extraSteps; + if (progressTotal > Integer.MAX_VALUE) { + throw new IllegalArgumentException("导出进度总步数超过整数上限"); + } + return (int) progressTotal; + } + + /** + * 校验正整数参数。 + * + * @param value 待校验数值 + * @param message 参数非法时使用的异常信息 + */ + private static void validatePositive(long value, String message) { + if (value <= 0) { + throw new IllegalArgumentException(message); + } + } + + /** + * 打开模板输入流的回调,返回的流由公共导出模板负责关闭。 + */ + @FunctionalInterface + public interface TemplateSupplier { + + /** + * 打开本次导出使用的 XLSX 模板。 + * + * @return 非空的模板输入流 + * @throws Exception 模板不存在或无法读取时抛出 + */ + InputStream open() throws Exception; + } + + /** + * 模板初始化回调,用于读取样式、替换标题和移除占位行。 + * + * @param配置器是有状态且非线程安全的,应在每次导出时新建并仅由当前线程使用。
+ * + * @param查询以用户全集为主数据源,再关联指定用餐日期范围内的有效预约订单,并按用户和日期逐日生成结果, + * 因此每天没有预约记录的用户也会返回。
+ */ +@Data +@Schema(description = "食堂用户预约查询条件") +public class HardCanteenUserApptQueryDto implements BaseIsIncludeTreeInterface { + + /** 用餐日期闭区间,固定为开始、结束两个服务端本地日期,业务层限制跨度最多为两个自然月。 */ + @NotNull(message = "用餐日期范围不能为空") + @Size(min = 2, max = 2, message = "用餐日期范围必须包含开始和结束日期") + @Schema(description = "用餐日期范围,固定为开始日期和结束日期", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDate[] apptUseDateList; + + /** 用户姓名、登录账号或手机号的模糊查询关键字;为空时不限制。 */ + @Schema(description = "用户姓名、登录账号或手机号") + private String nameOrPhone; + + /** 部门树选中的部门主键集合;为空时使用当前操作者完整的数据权限范围。 */ + @Schema(description = "部门主键集合") + private List类中同时存在 {@code userName} 和 {@code username},两组访问器均需显式声明,避免 Lombok + * 因 JavaBean 方法名相近而跳过其中一组。
+ * + * @return 页面展示姓名;用户资料均未维护时由服务层返回兜底文案 + */ + public String getUserName() { + return userName; + } + + /** + * 设置页面展示姓名。 + * + * @param userName 服务层按姓名、昵称和登录账号顺序解析出的展示姓名 + */ + public void setUserName(String userName) { + this.userName = userName; + } + + /** + * 获取用户登录账号。 + * + *显式声明该访问器以保证接口序列化和业务赋值均使用 {@code username} 字段。
+ * + * @return 用户登录账号;未维护时为空 + */ + public String getUsername() { + return username; + } + + /** + * 设置用户登录账号。 + * + * @param username UPMS 返回的用户登录账号,未维护时可为空 + */ + public void setUsername(String username) { + this.username = username; + } + + /** + * 获取公共用户信息组件使用的标准姓名字段。 + * + * @return 服务层解析后的用户展示姓名 + */ + @Override + public String getName() { + return userName; + } + + /** + * 设置公共用户信息组件使用的标准姓名字段。 + * + * @param name 服务层解析后的用户展示姓名 + */ + @Override + public void setName(String name) { + this.userName = name; + } + + /** + * 获取历史用户部门契约中的手机号字段。 + * + * @return 用户手机号;未绑定时为空 + */ + @Override + public String getUserMobile() { + return phone; + } + + /** + * 设置历史用户部门契约中的手机号字段。 + * + * @param userMobile 用户手机号,未绑定时可为空 + */ + @Override + public void setUserMobile(String userMobile) { + this.phone = userMobile; + } + + /** + * 获取历史用户部门契约中的头像字段。 + * + * @return 用户头像地址;未维护时为空 + */ + @Override + public String getUserAvatar() { + return avatar; + } + + /** + * 设置历史用户部门契约中的头像字段。 + * + * @param userAvatar 用户头像地址,未维护时可为空 + */ + @Override + public void setUserAvatar(String userAvatar) { + this.avatar = userAvatar; + } + + /** + * 获取历史用户部门契约中的部门主键字段。 + * + * @return 用户当前所属部门主键;未分配部门时为空 + */ + @Override + public Long getUserDeptId() { + return deptId; + } + + /** + * 设置历史用户部门契约中的部门主键字段。 + * + * @param userDeptId 用户当前所属部门主键,未分配部门时可为空 + */ + @Override + public void setUserDeptId(Long userDeptId) { + this.deptId = userDeptId; + } + + /** + * 获取历史用户部门契约中的部门名称字段。 + * + * @return 用户当前所属部门名称;未分配部门时为空 + */ + @Override + public String getUserDeptName() { + return deptName; + } + + /** + * 设置历史用户部门契约中的部门名称字段。 + * + * @param userDeptName 用户当前所属部门名称,未分配部门时可为空 + */ + @Override + public void setUserDeptName(String userDeptName) { + this.deptName = userDeptName; + } +} diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/excel/HardCanteenUserApptExcelVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/excel/HardCanteenUserApptExcelVo.java new file mode 100644 index 000000000..c4cb50a6b --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/excel/HardCanteenUserApptExcelVo.java @@ -0,0 +1,136 @@ +package com.bsd.pjcloud.hard.api.vo.order.excel; + +import com.alibaba.excel.annotation.ExcelProperty; +import com.alibaba.excel.annotation.format.NumberFormat; +import com.alibaba.excel.annotation.write.style.ColumnWidth; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * 用户和用餐日期维度的食堂预约导出行。 + */ +@Data +@ColumnWidth(22) +public class HardCanteenUserApptExcelVo { + + /** 系统用户主键,作为同名用户的稳定识别信息。 */ + @ExcelProperty({"基础信息", "用户编号"}) + private Long userId; + + /** 用户姓名,便于按人员核对预约结果。 */ + @ExcelProperty({"基础信息", "姓名"}) + private String userName; + + /** 用户邮箱;未维护时为空。 */ + @ExcelProperty({"基础信息", "邮箱"}) + private String email; + + /** 用户当前所属部门名称。 */ + @ExcelProperty({"组织与账号", "部门名称"}) + private String deptName; + + /** 用户岗位名称,多个岗位使用中文逗号分隔。 */ + @ExcelProperty({"组织与账号", "岗位名称"}) + private String postNames; + + /** 用户默认身份使用的手机号;未维护时为空。 */ + @ExcelProperty({"默认身份", "手机号"}) + private String phone; + + /** 用户拥有的身份中文名称,多个身份使用中文逗号分隔。 */ + @ExcelProperty({"身份信息", "身份类型"}) + private String identityTypeNames; + + /** 学生身份学号;没有学生身份时为空。 */ + @ExcelProperty({"学生身份", "学号"}) + private String studentNo; + + /** 学生身份学校名称;没有学生身份时为空。 */ + @ExcelProperty({"学生身份", "学校名称"}) + private String schoolName; + + /** 学生身份班级名称;没有学生身份时为空。 */ + @ExcelProperty({"学生身份", "班级名称"}) + private String className; + + /** 学生身份监护人手机号,多个号码沿用业务存储的英文逗号分隔格式。 */ + @ExcelProperty({"学生身份", "监护人手机号"}) + private String guardianPhones; + + /** 老师身份学号;没有老师身份时为空。 */ + @ExcelProperty({"老师身份", "老师学号"}) + private String teacherNo; + + /** 查询指定的用餐日期。 */ + @ExcelProperty({"预约信息", "用餐日期"}) + private LocalDate apptUseDate; + + /** 是否存在符合当前食堂、餐段筛选的有效预约。 */ + @ExcelProperty({"预约信息", "是否订餐"}) + private String reservationStatus; + + /** 有效预约涉及的餐段名称,多个值使用中文分号分隔。 */ + @ExcelProperty({"预约信息", "用餐时间段"}) + private String hourTypes; + + /** 有效预约的商品描述,多个值使用中文分号分隔。 */ + @ExcelProperty({"预约信息", "订餐内容"}) + private String productDescriptions; + + /** 有效预约所属食堂,多个值使用中文分号分隔。 */ + @ExcelProperty({"预约信息", "食堂"}) + private String merchantNames; + + /** 早餐预约状态;查询排除早餐时显示“未查询”。 */ + @ExcelProperty({"餐段预约", "早餐"}) + private String breakfastReservation; + + /** 午餐预约状态;查询排除午餐时显示“未查询”。 */ + @ExcelProperty({"餐段预约", "午餐"}) + private String lunchReservation; + + /** 晚餐预约状态;查询排除晚餐时显示“未查询”。 */ + @ExcelProperty({"餐段预约", "晚餐"}) + private String dinnerReservation; + + /** 夜间餐预约状态;查询排除夜间餐时显示“未查询”。 */ + @ExcelProperty({"餐段预约", "夜间餐"}) + private String lateNightReservation; + + /** 各餐段有效预约商品总价,按餐段输出元金额并使用中文分号分隔。 */ + @ExcelProperty({"金额统计", "餐段总金额(元)"}) + private String mealSegmentTotalPrices; + + /** 用户本行日期内有效预约商品总价,单位为元并保留两位小数。 */ + @ExcelProperty({"金额统计", "当日总金额(元)"}) + @NumberFormat("#,##0.00") + private BigDecimal totalPrice; + + /** 用户当天有效预约的整体核销状态。 */ + @ExcelProperty({"核销信息", "核销状态"}) + private String verificationStatus; + + /** 已核销订单的核销时间,多个值使用中文分号分隔。 */ + @ExcelProperty({"核销信息", "核销时间"}) + private String verificationTimes; + + /** + * 获取导出行中的用户姓名。 + * + * @return 导出展示的用户姓名 + */ + public String getUserName() { + return userName; + } + + /** + * 设置导出行中的用户姓名。 + * + * @param userName 服务层解析出的用户展示姓名 + */ + public void setUserName(String userName) { + this.userName = userName; + } +} diff --git a/pjcloud-hardware/pjcloud-hardware-biz/pom.xml b/pjcloud-hardware/pjcloud-hardware-biz/pom.xml index a69771160..12a4eb9cd 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/pom.xml +++ b/pjcloud-hardware/pjcloud-hardware-biz/pom.xml @@ -118,6 +118,14 @@接口以当前操作者数据权限内的全部用户为主数据源,每个用户在范围内每天生成一行, + * 因此没有预约日志的人员和日期也会返回;日期范围最多为两个自然月。
+ * + * @param page 用户和日期维度的分页参数 + * @param query 用户、日期范围、食堂、餐段和状态筛选条件 + * @return 包含每天未预约用户的预约分页 + */ + @Operation(summary = "用户预约查询", description = "按用户和日期逐日查询预约及核销情况,包含未预约用户,最多查询两个月") + @ApiCryptoAes + @PostMapping("/user-reserv-page") + @PreAuthorize("@pms.hasPermission('hard_canteenUserApptTotal_page')") + public R用于已订餐及核销状态查询先定位候选用户,再交由 UPMS 应用用户关键字、部门和数据权限, + * 避免导出完整资料时扫描与预约条件无关的全部用户。
+ * + * @param dto 预约日期、食堂、餐段、订单类型和有效状态等筛选条件 + * @return 当前租户内命中预约订单的去重用户主键,按主键升序排列 + */ + List用户预约查询始终以 UPMS 返回的数据权限用户为主表,再关联 Hardware 预约订单,避免遗漏没有预约日志的人员。
*/ @Validated @Slf4j @@ -24,8 +72,111 @@ import java.time.LocalDate; @Service public class HardCanteenApptTotalServiceImpl implements HardCanteenApptTotalService { + /** UPMS 用户全量扫描的单页数量,避免状态筛选和导出产生过大的单次响应。 */ + private static final long USER_BATCH_SIZE = 500L; + + /** 单次订单查询的用户主键数量,避免 SQL IN 条件过长。 */ + private static final int ORDER_USER_BATCH_SIZE = 500; + + /** 用户预约导出任务的 Redis Key 命名空间。 */ + private static final String USER_RESERVATION_EXPORT_REDIS_KEY = "hard:canteen-user-appt:export"; + + /** 用户预约导出任务在统一进度弹窗中的展示名称。 */ + private static final String USER_RESERVATION_EXPORT_TASK_NAME = "用户预约查询导出"; + + /** 查询实际权限用户数量前创建异步任务使用的占位总数。 */ + private static final int USER_RESERVATION_EXPORT_INITIAL_TOTAL = 1; + + /** 用户预约导出每批读取的权限用户数,单批最多展开为该人数乘以查询自然日数。 */ + private static final int USER_RESERVATION_EXPORT_BATCH_SIZE = 200; + + /** 文件上传和异步任务成功收尾占用的额外进度步数。 */ + private static final int USER_RESERVATION_EXPORT_EXTRA_PROGRESS_STEPS = 2; + + /** 用户预约导出的二级表头行数,第一行是业务分组,第二行是具体字段。 */ + private static final int USER_RESERVATION_EXPORT_HEADER_ROW_COUNT = 2; + + /** 用户预约导出工作表二级表头之后允许写入的最大明细行数。 */ + private static final int MAX_USER_RESERVATION_EXPORT_ROWS = + BatchExcelExportTemplate.calculateMaxDetailRowCount(USER_RESERVATION_EXPORT_HEADER_ROW_COUNT, 0); + + /** 用户预约导出工作表名称。 */ + private static final String USER_RESERVATION_EXPORT_SHEET_NAME = "用户预约查询"; + + /** 文件中心保存用户预约导出文件的业务目录。 */ + private static final String USER_RESERVATION_EXPORT_FILE_DIR = "hard-export"; + + /** 文件中心区分用户预约导出文件的类型编码。 */ + private static final String USER_RESERVATION_EXPORT_FILE_TYPE = "HARD_CANTEEN_USER_APPT_EXPORT"; + + /** 用户预约 Excel 文件上传时使用的标准 MIME 类型。 */ + private static final String EXCEL_CONTENT_TYPE = + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + + /** 异步导出文件名使用的时间格式,避免连续任务产生同名文件。 */ + private static final DateTimeFormatter EXPORT_FILE_TIME_FORMATTER = + DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + + /** 用户预约查询允许的最大自然月跨度,开始和结束日期均计入查询范围。 */ + private static final long MAX_QUERY_MONTHS = 2L; + + /** 页面协议中的已订餐状态。 */ + private static final String RESERVATION_STATUS_RESERVED = "RESERVED"; + + /** 页面协议中的未订餐状态。 */ + private static final String RESERVATION_STATUS_NOT_RESERVED = "NOT_RESERVED"; + + /** 页面协议中的无需核销状态,表示当前筛选口径没有有效预约。 */ + private static final String VERIFICATION_STATUS_NONE = "NONE"; + + /** 页面协议中的全部待核销状态。 */ + private static final String VERIFICATION_STATUS_UNVERIFIED = "UNVERIFIED"; + + /** 页面协议中的部分预约已核销状态。 */ + private static final String VERIFICATION_STATUS_PARTIAL = "PARTIAL"; + + /** 页面协议中的全部已核销状态。 */ + private static final String VERIFICATION_STATUS_VERIFIED = "VERIFIED"; + + /** 允许的订餐状态筛选值,用于拒绝未知协议值。 */ + private static final Set没有订餐或核销状态筛选时,只加载当前“用户 × 日期”分页涉及的用户;已订餐及核销状态先按订单定位 + * 候选用户,未订餐和无需核销仍遍历权限用户,随后生成逐日记录并筛选,保证结果总数和翻页准确。
+ * + * @param page 用户和日期维度的分页参数 + * @param query 用户、日期范围和预约状态筛选条件 + * @return 包含每天未预约用户的预约分页结果 + */ + @Override + public Page详情查询只使用用户和用餐日期,不继承列表上的食堂、餐段及状态筛选,保证弹窗能看到当天完整记录; + * 查询订单前先通过 UPMS 权限用户分页确认该用户仍在当前操作者的数据权限内。
+ * + * @param userId 列表行中的系统用户主键 + * @param apptUseDate 需要查看的用餐日期 + * @return 当天全部有效食堂预约;没有预约时返回空列表 + */ + @Override + public List请求线程只校验并复制筛选条件;后台任务恢复当前租户和登录用户上下文后,按权限用户分页读取并 + * 分批写入 Excel,避免长时间占用 HTTP 连接和一次性持有全部导出行。
+ * + * @param query 用户、日期范围、食堂、餐段和状态筛选条件 + * @return 已创建任务的初始进度 + */ + @Override + public AsyncTaskProgressVO submitUserReservationExport(HardCanteenUserApptQueryDto query) { + validateQuery(query); + HardCanteenUserApptQueryDto querySnapshot = copyUserReservationQuery(query); + return asyncTaskExecutorService.submitWithResult(USER_RESERVATION_EXPORT_REDIS_KEY, + USER_RESERVATION_EXPORT_TASK_NAME, USER_RESERVATION_EXPORT_INITIAL_TOTAL, + reporter -> exportUserReservations(querySnapshot, reporter)); + } + + /** + * 查询用户预约 Excel 数据。 + * + * @param query 用户、日期范围和预约状态筛选条件 + * @return 用户和日期维度的导出行 + */ + @Override + public List订餐和核销状态依赖订单关联结果,因此在每批用户展开日期后执行;批次写入完成即可释放, + * 不会把整个日期范围的全部用户行同时留在 JVM 堆中。
+ * + * @param outputPath 最终上传前使用的临时 XLSX 文件 + * @param query 已冻结的筛选条件 + * @param firstUserPage 已用于取得用户总数的第一页,避免重复远程调用 + * @param userTotal 任务开始时取得的权限用户总数 + * @param reporter 当前异步任务进度报告器 + * @return 实际写入 Excel 的用户日期行数 + */ + private long writeUserReservationWorkbook(Path outputPath, HardCanteenUserApptQueryDto query, + Page存在关联状态筛选时最终行数只能在分批关联订单后确定,写入过程仍会执行单工作表上限保护。
+ * + * @param userTotal 当前用户筛选和数据权限范围内的用户总数 + * @param query 已冻结的日期和预约状态筛选条件 + */ + private void validateUserReservationExportRowCapacity(long userTotal, HardCanteenUserApptQueryDto query) { + if (hasJoinedStatusFilter(query)) { + return; + } + long rowTotal; + try { + rowTotal = Math.multiplyExact(userTotal, (long) getQueryDates(query).size()); + } catch (ArithmeticException exception) { + throw new IllegalStateException("用户预约导出数据量过大,请缩小查询范围后分批导出", exception); + } + if (rowTotal > MAX_USER_RESERVATION_EXPORT_ROWS) { + throw new IllegalStateException("导出数据超过单个 Excel 工作表支持的 " + + MAX_USER_RESERVATION_EXPORT_ROWS + " 条,请缩小查询范围后分批导出"); + } + } + + /** + * 将已关闭的完整 XLSX 临时文件上传到统一文件中心。 + * + * @param outputPath 已完成写入的临时文件 + * @param fileName 用户下载时使用的业务文件名 + * @return 文件中心提供的下载地址 + * @throws Exception 临时文件读取或远程上传失败时抛出 + */ + private String uploadUserReservationWorkbook(Path outputPath, String fileName) throws Exception { + MultipartFile exportFile = new MockMultipartFile("file", encodeUploadOriginalFilename(fileName), + EXCEL_CONTENT_TYPE, Files.readAllBytes(outputPath)); + R> uploadResult = remoteFileService.innerUpload(exportFile, USER_RESERVATION_EXPORT_FILE_DIR, null, + USER_RESERVATION_EXPORT_FILE_TYPE); + if (uploadResult == null || !uploadResult.isOk() + || !(uploadResult.getData() instanceof Map, ?> resultData)) { + throw new IllegalStateException("文件中心未返回有效的用户预约导出结果"); + } + Object resultUrl = resultData.get("url"); + if (resultUrl == null || StrUtil.isBlank(resultUrl.toString())) { + throw new IllegalStateException("文件中心未返回用户预约导出下载地址"); + } + return resultUrl.toString(); + } + + /** + * 复制用户预约查询条件,避免异步任务与控制器请求对象共享可变数组或列表。 + * + * @param source 已通过基础校验的请求筛选条件 + * @return 后台线程独占的完整筛选条件副本 + */ + private HardCanteenUserApptQueryDto copyUserReservationQuery(HardCanteenUserApptQueryDto source) { + HardCanteenUserApptQueryDto target = new HardCanteenUserApptQueryDto(); + target.setApptUseDateList(source.getApptUseDateList() == null + ? null : source.getApptUseDateList().clone()); + target.setNameOrPhone(source.getNameOrPhone()); + target.setDeptIds(copyList(source.getDeptIds())); + target.setIsInclude(source.getIsInclude()); + target.setMerchantId(source.getMerchantId()); + target.setHourType(source.getHourType()); + target.setReservationStatus(source.getReservationStatus()); + target.setVerificationStatus(source.getVerificationStatus()); + target.setUserIds(copyList(source.getUserIds())); + return target; + } + + /** + * 复制可能为空的筛选列表,保留空值与空集合的原始查询语义。 + * + * @param source 请求对象中的可变列表 + * @param已订餐以及待核销、部分核销、已核销都可以先从订单表反查候选用户;未订餐和无需核销属于 + * “订单不存在”条件,仍需以全部权限用户为主表执行左关联。
+ * + * @param query 预约状态和核销状态筛选条件 + * @return 可以先按预约订单收窄用户范围时返回 true + */ + private boolean requiresExistingReservation(HardCanteenUserApptQueryDto query) { + return Objects.equals(query.getReservationStatus(), RESERVATION_STATUS_RESERVED) + || (StrUtil.isNotBlank(query.getVerificationStatus()) + && !Objects.equals(query.getVerificationStatus(), VERIFICATION_STATUS_NONE)); + } + + /** + * 对必然存在预约的列表或导出条件先查询候选用户,并写回独立查询快照。 + * + *候选 ID 只负责缩小 UPMS 查询范围,姓名、部门和当前操作者的数据权限仍由 UPMS 最终过滤; + * 原查询已指定用户时,订单查询也会先应用该范围,因此不会扩大导出对象。
+ * + * @param query 当前列表或导出流程独占、允许写入候选用户主键的查询快照 + * @return 无需预筛或存在候选用户时返回 true,确定没有匹配预约时返回 false + */ + private boolean narrowQueryToReservationCandidates(HardCanteenUserApptQueryDto query) { + if (!requiresExistingReservation(query)) { + return true; + } + List与页面列表的轻量用户扫描分离,只有导出需要角色、岗位和账号扩展字段时才调用 UPMS 完整导出分页。
+ * + * @param query 查询条件 + * @param queryDates 查询范围内按升序排列的全部日期 + * @return 携带完整用户主资料和身份资料的预约导出行 + */ + private List先根据用户总数计算“用户 × 日期”总行数,再只加载当前页覆盖的用户,避免普通翻页展开全部权限用户。
+ * + * @param current 当前页码 + * @param size 每页用户日期行数 + * @param query 用户和预约查询条件 + * @param queryDates 查询范围内按升序排列的全部日期 + * @return 未应用订餐或核销状态筛选的用户日期分页 + */ + private Page管理端默认走异步分批导出;该方法仅保持原同步接口与新二级表头字段一致。
+ * + * @param query 用户筛选条件 + * @return 权限范围内携带角色岗位和身份资料的全部用户 + */ + private List统一复用可保证预查询命中的用户与最终写入 Excel 的有效预约口径一致,避免日期、食堂、餐段、 + * 订单状态或预约关联条件在两次查询间发生偏差。
+ * + * @param query 用户预约页面提交的日期、食堂和餐段筛选条件 + * @param userIds 需要限制的用户主键;为空时查询当前租户内全部候选预约用户 + * @return 可直接交给订单 Mapper 的完整预约条件 + */ + private HardOrderDto buildReservationOrderQuery(HardCanteenUserApptQueryDto query, List硬件业务异步调用继续使用原有线程池,带安全上下文的任务使用独立固定线程池。
+ */ @Configuration public class ExecutorConfig { + /** + * 创建硬件业务默认异步线程池。 + *公共异步任务模块会额外注册专用线程池,因此将原有硬件线程池标记为主候选,确保公共核心 + * {@code AsyncConfiguration} 仍将全局 {@code @Async} 调用交给该线程池;公共异步任务自身通过 Bean 名称 + * 显式选择专用线程池,不受主候选设置影响。
+ * + * @return 用于硬件业务异步调用的线程池 + */ @Bean + @Primary public ThreadPoolTaskExecutor hardTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); @@ -22,6 +36,11 @@ public class ExecutorConfig { return executor; } + /** + * 创建能够把当前 Spring Security 上下文传递到工作线程的执行器。 + * + * @return 包装固定线程池的安全上下文执行器 + */ @Bean public DelegatingSecurityContextExecutor delegatingSecurityContextExecutor() { ExecutorService executorService = Executors.newFixedThreadPool(10); diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardOrderMapper.xml b/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardOrderMapper.xml index 8e59846ee..989dd6dcd 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardOrderMapper.xml +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardOrderMapper.xml @@ -282,6 +282,17 @@