diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/test/java/com/bsd/pjcloud/canteen/service/impl/CanteenSpuServiceImplTest.java b/pjcloud-canteen/pjcloud-canteen-biz/src/test/java/com/bsd/pjcloud/canteen/service/impl/CanteenSpuServiceImplTest.java new file mode 100644 index 000000000..7bf377813 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/test/java/com/bsd/pjcloud/canteen/service/impl/CanteenSpuServiceImplTest.java @@ -0,0 +1,129 @@ +package com.bsd.pjcloud.canteen.service.impl; + +import com.bsd.pjcloud.canteen.dto.CanteenSpuUpdateStockReqDTO; +import com.bsd.pjcloud.canteen.entity.CanteenSpuEntity; +import com.bsd.pjcloud.canteen.mapper.CanteenSpuMapper; +import com.bsd.pjcloud.canteen.service.CanteenSpuService; +import com.bsd.pjcloud.canteen.util.CanteenMerchantScopeHelper; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import java.time.Duration; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * 每日菜谱基础 SPU 库存幂等状态机测试,重点覆盖退款任务本地回滚后的再次执行。 + */ +class CanteenSpuServiceImplTest { + + /** 被测库存服务。 */ + private CanteenSpuService service; + + /** 基础 SPU Mapper,用于验证库存增量是否真实执行。 */ + private CanteenSpuMapper spuMapper; + + /** Redis 字符串操作,用于模拟和验证库存业务状态迁移。 */ + private ValueOperations valueOperations; + + /** + * 为每个用例建立独立的事务同步上下文,模拟 Hardware 远程调用进入食堂本地事务后的状态写入。 + */ + @BeforeEach + void setUp() { + spuMapper = mock(CanteenSpuMapper.class); + StringRedisTemplate redisTemplate = mock(StringRedisTemplate.class); + valueOperations = mock(ValueOperations.class); + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + CanteenSpuServiceImpl target = new CanteenSpuServiceImpl(redisTemplate, null, null, null, null, null, + mock(CanteenMerchantScopeHelper.class)); + ReflectionTestUtils.setField(target, "baseMapper", spuMapper); + service = target; + TransactionSynchronizationManager.initSynchronization(); + } + + /** 清理线程级事务同步状态,避免测试间相互污染。 */ + @AfterEach + void tearDown() { + TransactionSynchronizationManager.clearSynchronization(); + } + + /** + * 已确认应用后完成补偿的售后库存允许再次 APPLY;否则钱包成功后的任务重试会丢失库存恢复。 + */ + @Test + void compensatedRefundInventoryShouldAllowRetry() { + CanteenSpuUpdateStockReqDTO dto = stockRequest("APPLY", 2, 10); + when(valueOperations.get("canteen:spu:order-stock:HARD_DAILY_MENU_AFTER_SALE:9")) + .thenReturn("COMPENSATED"); + when(spuMapper.selectById(1L)).thenReturn(spu(10)); + when(spuMapper.updateStockIfMatch(1L, 2, 10)).thenReturn(1); + + assertTrue(service.updateSkuStockIdempotent(dto).isOk()); + + verify(spuMapper).updateStockIfMatch(1L, 2, 10); + verify(valueOperations).set("canteen:spu:order-stock:HARD_DAILY_MENU_AFTER_SALE:9", "APPLIED", + Duration.ofDays(7)); + } + + /** + * 已执行 APPLY 的回滚必须标记为可重试补偿态,区别于阻断网络延迟 APPLY 的未确认回滚态。 + */ + @Test + void confirmedApplyRollbackShouldBecomeCompensated() { + CanteenSpuUpdateStockReqDTO dto = stockRequest("ROLLBACK", 2, 10); + when(valueOperations.get("canteen:spu:order-stock:HARD_DAILY_MENU_AFTER_SALE:9")) + .thenReturn("APPLIED"); + when(spuMapper.selectById(1L)).thenReturn(spu(12)); + when(spuMapper.updateStock(1L, -2)).thenReturn(1); + + assertTrue(service.updateSkuStockIdempotent(dto).isOk()); + + verify(spuMapper).updateStock(1L, -2); + verify(valueOperations).set("canteen:spu:order-stock:HARD_DAILY_MENU_AFTER_SALE:9", "COMPENSATED", + Duration.ofDays(7)); + } + + /** + * 构造单个基础 SPU 的幂等库存请求。 + * + * @param operation APPLY 或 ROLLBACK + * @param increment 原 APPLY 使用的库存增量 + * @param beforeStock 当前业务尝试的操作前库存快照 + * @return 可直接调用库存服务的请求 + */ + private CanteenSpuUpdateStockReqDTO stockRequest(String operation, int increment, int beforeStock) { + CanteenSpuUpdateStockReqDTO.Item item = new CanteenSpuUpdateStockReqDTO.Item(); + item.setId(1L); + item.setIncrCount(increment); + item.setBeforeStock(beforeStock); + CanteenSpuUpdateStockReqDTO dto = new CanteenSpuUpdateStockReqDTO(); + dto.setBizId("HARD_DAILY_MENU_AFTER_SALE:9"); + dto.setOperation(operation); + dto.setItems(List.of(item)); + return dto; + } + + /** + * 构造当前库存快照。 + * + * @param stock 当前基础 SPU 库存 + * @return 仅包含本用例所需字段的实体 + */ + private CanteenSpuEntity spu(int stock) { + CanteenSpuEntity entity = new CanteenSpuEntity(); + entity.setId(1L); + entity.setStock(stock); + entity.setName("测试菜品"); + return entity; + } +} diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/refund/HardAutoRefundTaskTypeEnum.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/refund/HardAutoRefundTaskTypeEnum.java new file mode 100644 index 000000000..fa11785b3 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/refund/HardAutoRefundTaskTypeEnum.java @@ -0,0 +1,33 @@ +package com.bsd.pjcloud.hard.api.constant.refund; + +import lombok.Getter; + +/** + * 自动退款任务场景;编码持久化到任务表并用于区分不同的配置和售后创建规则。 + */ +@Getter +public enum HardAutoRefundTaskTypeEnum { + + /** 用户申请免审退款后创建的技术自动退款任务。 */ + AFTER_SALE("10", "售后申请自动退款"), + + /** 每日菜谱预约餐段结束后创建的系统过期退款任务。 */ + EXPIRED_RESERVE("20", "过期预约自动退款"); + + /** 数据库存储的稳定任务场景编码。 */ + private final String type; + + /** 管理端展示名称。 */ + private final String description; + + /** + * 构造任务场景协议项。 + * + * @param type 数据库存储编码 + * @param description 展示名称 + */ + HardAutoRefundTaskTypeEnum(String type, String description) { + this.type = type; + this.description = description; + } +} diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenProductApptExportRecord.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenProductApptExportRecord.java new file mode 100644 index 000000000..cfb4aeb2a --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenProductApptExportRecord.java @@ -0,0 +1,42 @@ +package com.bsd.pjcloud.hard.api.vo.order; + +import lombok.Data; + +/** + * 食堂预约菜品导出聚合记录。 + * + *

同一模型承载商户部门聚合和商品聚合两种查询结果,商品聚合时额外填充商品字段;金额统一使用分。

+ */ +@Data +public class HardCanteenProductApptExportRecord { + + /** 预约使用日期,格式为 yyyy-MM-dd。 */ + private String apptDate; + + /** 用餐时间段协议编码。 */ + private String hourType; + + /** 食堂商户主键。 */ + private Long merchantId; + + /** 预约用户所属部门主键。 */ + private Long userDeptId; + + /** 商品主键;商户部门聚合记录为空。 */ + private Long productId; + + /** 订单快照中的商品名称;商户部门聚合记录为空。 */ + private String productName; + + /** 去重预约人数。 */ + private Long payUserCount; + + /** 去重预约次数。 */ + private Long timeCount; + + /** 预约商品总数量。 */ + private Long payCount; + + /** 预约商品总价,单位为分。 */ + private Long totalPrice; +} diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptDailyStatisticsVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptDailyStatisticsVo.java new file mode 100644 index 000000000..47c433fa9 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptDailyStatisticsVo.java @@ -0,0 +1,20 @@ +package com.bsd.pjcloud.hard.api.vo.order; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDate; + +/** + * 指定用餐日期的用户预约统计。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "食堂用户每日预约统计") +public class HardCanteenUserApptDailyStatisticsVo extends HardCanteenUserApptStatisticsSummaryVo { + + /** 统计对应的用餐日期,格式由接口序列化器输出为 yyyy-MM-dd。 */ + @Schema(description = "用餐日期") + private LocalDate date; +} diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptDishStatisticsVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptDishStatisticsVo.java new file mode 100644 index 000000000..2fbd187d0 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptDishStatisticsVo.java @@ -0,0 +1,32 @@ +package com.bsd.pjcloud.hard.api.vo.order; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 用户预约按餐段和菜品快照聚合的统计数据。 + */ +@Data +@Schema(description = "食堂用户预约菜品统计") +public class HardCanteenUserApptDishStatisticsVo { + + /** 餐段字典编码,空值表示历史订单未记录餐段。 */ + @Schema(description = "餐段编码") + private String hourType; + + /** 下单时固化的菜品名称;历史空值由服务层归一为未命名菜品。 */ + @Schema(description = "菜品名称") + private String productName; + + /** 菜品快照的总份数。 */ + @Schema(description = "菜品份数") + private Long productCount = 0L; + + /** 包含该菜品的去重预约订单数。 */ + @Schema(description = "涉及订单数") + private Long orderCount = 0L; + + /** 该菜品快照总价,单位为分。 */ + @Schema(description = "菜品总价,单位为分") + private Long totalPrice = 0L; +} diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptMealStatisticsVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptMealStatisticsVo.java new file mode 100644 index 000000000..dcd15ddd9 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptMealStatisticsVo.java @@ -0,0 +1,32 @@ +package com.bsd.pjcloud.hard.api.vo.order; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 用户预约按餐段聚合的统计数据。 + */ +@Data +@Schema(description = "食堂用户预约餐段统计") +public class HardCanteenUserApptMealStatisticsVo { + + /** 餐段字典编码,属于 PC、App 与后端共用协议。 */ + @Schema(description = "餐段编码") + private String hourType; + + /** 该餐段去重后的用户-用餐日期数;每日表示人数,范围总计表示可与每日相加的订餐人次。 */ + @Schema(description = "预约人数或人次") + private Long reservedUserCount = 0L; + + /** 该餐段的有效预约订单数。 */ + @Schema(description = "预约订单数") + private Long orderCount = 0L; + + /** 该餐段购物车快照中的菜品总份数。 */ + @Schema(description = "菜品份数") + private Long dishCount = 0L; + + /** 该餐段有效预约商品总价,单位为分。 */ + @Schema(description = "预约总价,单位为分") + private Long totalPrice = 0L; +} diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptStatisticsSummaryVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptStatisticsSummaryVo.java new file mode 100644 index 000000000..0c85a2004 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptStatisticsSummaryVo.java @@ -0,0 +1,53 @@ +package com.bsd.pjcloud.hard.api.vo.order; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 用户预约统计的通用汇总数据。 + * + *

金额字段统一使用分;用户数量按“用户-用餐日期”记录累计,避免多日查询把同一用户重复隐藏。

+ */ +@Data +@Schema(description = "食堂用户预约统计汇总") +public class HardCanteenUserApptStatisticsSummaryVo { + + /** 当前统计范围内的用户-用餐日期记录数。 */ + @Schema(description = "用户日期记录数") + private Long userCount = 0L; + + /** 当前统计范围内去重后的用户数。 */ + @Schema(description = "去重用户数") + private Long uniqueUserCount = 0L; + + /** 有效预约记录的用户-用餐日期数。 */ + @Schema(description = "已订餐记录数") + private Long reservedUserCount = 0L; + + /** 没有有效预约记录的用户-用餐日期数。 */ + @Schema(description = "未订餐记录数") + private Long unreservedUserCount = 0L; + + /** 有效预约订单数。 */ + @Schema(description = "预约订单数") + private Long orderCount = 0L; + + /** 购物车快照中的菜品总份数。 */ + @Schema(description = "菜品总份数") + private Long dishCount = 0L; + + /** 有效预约订单商品总价,单位为分。 */ + @Schema(description = "预约总价,单位为分") + private Long totalPrice = 0L; + + /** 按餐段聚合的预约人数、订单数和菜品份数。 */ + @Schema(description = "餐段统计") + private List meals = new ArrayList<>(); + + /** 按餐段和菜品名称聚合的菜品份数。 */ + @Schema(description = "菜品统计") + private List dishes = new ArrayList<>(); +} diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptStatisticsVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptStatisticsVo.java new file mode 100644 index 000000000..5cc19bec8 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptStatisticsVo.java @@ -0,0 +1,23 @@ +package com.bsd.pjcloud.hard.api.vo.order; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 用户预约统计响应,包含查询范围总计和逐日统计。 + */ +@Data +@Schema(description = "食堂用户预约统计") +public class HardCanteenUserApptStatisticsVo { + + /** 当前查询条件下所有用餐日期的汇总结果。 */ + @Schema(description = "查询范围总计") + private HardCanteenUserApptStatisticsSummaryVo total = new HardCanteenUserApptStatisticsSummaryVo(); + + /** 当前查询条件下按用餐日期升序排列的每日结果。 */ + @Schema(description = "每日统计") + private List daily = new ArrayList<>(); +} diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/excel/HardCanteenProductApptExcelVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/excel/HardCanteenProductApptExcelVo.java new file mode 100644 index 000000000..a7e345f09 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/excel/HardCanteenProductApptExcelVo.java @@ -0,0 +1,93 @@ +package com.bsd.pjcloud.hard.api.vo.order.excel; + +import com.alibaba.excel.annotation.ExcelIgnore; +import com.alibaba.excel.annotation.ExcelProperty; +import com.alibaba.excel.annotation.format.NumberFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * 食堂预约菜品统计自定义导出行。 + * + *

一组日期和餐段按汇总行、商户部门行、商品明细行组织;不同层级只填写所属列,配合自定义合并处理器形成树形报表。

+ */ +@Data +public class HardCanteenProductApptExcelVo { + + /** 日期餐段汇总中的预约日期。 */ + @ExcelProperty({"日期餐段汇总", "预约时间"}) + private LocalDate summaryDate; + + /** 日期餐段汇总中的餐段编码或字典名称。 */ + @ExcelProperty({"日期餐段汇总", "预约时间段"}) + private String summaryHourType; + + /** 日期餐段汇总中的去重预约人数。 */ + @ExcelProperty({"日期餐段汇总", "预约人数"}) + private Long summaryPayUserCount; + + /** 日期餐段汇总中的预约次数。 */ + @ExcelProperty({"日期餐段汇总", "预约次数"}) + private Long summaryTimeCount; + + /** 日期餐段汇总中的商品总数量。 */ + @ExcelProperty({"日期餐段汇总", "预约商品数量"}) + private Long summaryPayCount; + + /** 日期餐段汇总中的总价,单位为元。 */ + @ExcelProperty({"日期餐段汇总", "总价"}) + @NumberFormat("#,##0.00") + private BigDecimal summaryTotalPrice; + + /** 商户部门统计中的食堂名称。 */ + @ExcelProperty({"商户部门统计", "商户"}) + private String merchantName; + + /** 商户部门统计中的用户部门名称。 */ + @ExcelProperty({"商户部门统计", "部门"}) + private String departmentName; + + /** 商户部门统计中的预约日期。 */ + @ExcelProperty({"商户部门统计", "预约时间"}) + private LocalDate merchantDate; + + /** 商户部门统计中的餐段名称。 */ + @ExcelProperty({"商户部门统计", "预约时间段"}) + private String merchantHourType; + + /** 商户部门统计中的去重预约人数。 */ + @ExcelProperty({"商户部门统计", "预约人数"}) + private Long merchantPayUserCount; + + /** 商户部门统计中的预约次数。 */ + @ExcelProperty({"商户部门统计", "预约次数"}) + private Long merchantTimeCount; + + /** 商户部门统计中的商品总数量。 */ + @ExcelProperty({"商户部门统计", "预约商品数量"}) + private Long merchantPayCount; + + /** 商户部门统计中的总价,单位为元。 */ + @ExcelProperty({"商户部门统计", "总价"}) + @NumberFormat("#,##0.00") + private BigDecimal merchantTotalPrice; + + /** 商品明细中的商品名称。 */ + @ExcelProperty({"商品明细", "商品"}) + private String productName; + + /** 商品明细中的商品数量。 */ + @ExcelProperty({"商品明细", "数量"}) + private Long productCount; + + /** 商品明细中的商品总价,单位为元。 */ + @ExcelProperty({"商品明细", "总价"}) + @NumberFormat("#,##0.00") + private BigDecimal productTotalPrice; + + /** 导出行层级,仅供合并处理器识别,不写入 Excel。 */ + @ExcelIgnore + private String rowLevel; +} diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/excel/HardCanteenProductApptExportMergeHandler.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/excel/HardCanteenProductApptExportMergeHandler.java new file mode 100644 index 000000000..ad62bf197 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/excel/HardCanteenProductApptExportMergeHandler.java @@ -0,0 +1,130 @@ +package com.bsd.pjcloud.hard.excel; + +import com.alibaba.excel.write.handler.WorkbookWriteHandler; +import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.util.CellRangeAddress; + +import java.util.HashMap; +import java.util.Map; + +/** + * 食堂预约菜品统计导出合并处理器。 + * + *

数据行通过各层级专属列识别:日期餐段行填写第 0 列,商户部门行填写第 6 列,商品行填写第 14 列; + * 处理器在工作簿关闭前合并父层级字段,避免分批写入时依赖 Easy Excel 的行缓存。

+ */ +public class HardCanteenProductApptExportMergeHandler implements WorkbookWriteHandler { + + /** 日期餐段汇总列。 */ + private static final int[] SUMMARY_COLUMNS = {0, 1, 2, 3, 4, 5}; + + /** 商户部门统计列。 */ + private static final int[] MERCHANT_COLUMNS = {6, 7, 8, 9, 10, 11, 12, 13}; + + /** Easy Excel 双层表头占用两行,数据从第三行开始。 */ + private static final int DATA_START_ROW = 2; + + /** + * 工作簿关闭前统一处理数据行样式、层级合并和列宽。 + * + *

Easy Excel 已为金额列设置数字格式,居中时必须从原样式复制再覆盖对齐属性, + * 不能直接替换成全新的样式,否则金额会失去两位小数格式。

+ * + * @param holder Easy Excel 当前工作簿持有者 + */ + @Override + public void afterWorkbookDispose(WriteWorkbookHolder holder) { + Sheet sheet = holder.getWorkbook().getNumberOfSheets() == 0 ? null : holder.getWorkbook().getSheetAt(0); + if (sheet == null || sheet.getLastRowNum() < DATA_START_ROW) { + return; + } + Map centeredStyles = new HashMap<>(); + int lastRow = sheet.getLastRowNum(); + for (int rowIndex = DATA_START_ROW; rowIndex <= lastRow; rowIndex++) { + Row row = sheet.getRow(rowIndex); + if (row == null) { + continue; + } + for (int column = 0; column < 17; column++) { + Cell cell = row.getCell(column); + if (cell != null) { + CellStyle originalStyle = cell.getCellStyle(); + short styleIndex = originalStyle.getIndex(); + CellStyle centeredStyle = centeredStyles.computeIfAbsent(styleIndex, key -> { + CellStyle copiedStyle = holder.getWorkbook().createCellStyle(); + copiedStyle.cloneStyleFrom(originalStyle); + copiedStyle.setAlignment(HorizontalAlignment.CENTER); + copiedStyle.setVerticalAlignment(VerticalAlignment.CENTER); + return copiedStyle; + }); + cell.setCellStyle(centeredStyle); + } + } + } + mergeSummaryGroups(sheet, lastRow); + mergeMerchantGroups(sheet, lastRow); + int[] widths = {13, 12, 11, 11, 13, 14, 18, 18, 13, 12, 11, 11, 13, 14, 24, 11, 14}; + for (int column = 0; column < widths.length; column++) { + sheet.setColumnWidth(column, widths[column] * 256); + } + } + + /** 合并每个日期餐段组的汇总列。 */ + private void mergeSummaryGroups(Sheet sheet, int lastRow) { + int groupStart = -1; + for (int rowIndex = DATA_START_ROW; rowIndex <= lastRow + 1; rowIndex++) { + boolean boundary = rowIndex > lastRow || hasValue(sheet, rowIndex, 0); + if (!boundary) { + continue; + } + if (groupStart >= 0) { + mergeColumns(sheet, groupStart, rowIndex - 1, SUMMARY_COLUMNS); + } + groupStart = rowIndex > lastRow ? -1 : rowIndex; + } + } + + /** 合并每个商户部门组的统计列,商品明细行共享商户统计。 */ + private void mergeMerchantGroups(Sheet sheet, int lastRow) { + int groupStart = -1; + for (int rowIndex = DATA_START_ROW; rowIndex <= lastRow + 1; rowIndex++) { + boolean summaryRow = rowIndex <= lastRow && hasValue(sheet, rowIndex, 0); + boolean merchantRow = rowIndex <= lastRow && hasValue(sheet, rowIndex, 6); + // 日期汇总行也是商户分组的边界,不能让上一日期最后一个商户跨过汇总行继续合并。 + boolean boundary = rowIndex > lastRow || summaryRow || (merchantRow && groupStart >= 0); + if (boundary && groupStart >= 0) { + mergeColumns(sheet, groupStart, rowIndex - 1, MERCHANT_COLUMNS); + groupStart = -1; + } + if (merchantRow) { + groupStart = rowIndex; + } + } + } + + /** 按列范围添加合并单元格。 */ + private void mergeColumns(Sheet sheet, int firstRow, int lastRow, int[] columns) { + if (firstRow >= lastRow) { + return; + } + for (int column : columns) { + sheet.addMergedRegion(new CellRangeAddress(firstRow, lastRow, column, column)); + } + } + + /** 判断指定单元格是否包含非空文本。 */ + private boolean hasValue(Sheet sheet, int rowIndex, int columnIndex) { + Row row = sheet.getRow(rowIndex); + if (row == null) { + return false; + } + Cell cell = row.getCell(columnIndex); + return cell != null && !cell.toString().trim().isEmpty(); + } +} diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardCanteenApptTotalExportService.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardCanteenApptTotalExportService.java new file mode 100644 index 000000000..53172794d --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardCanteenApptTotalExportService.java @@ -0,0 +1,50 @@ +package com.bsd.pjcloud.hard.service; + +import com.bsd.pjcloud.common.asynctask.vo.AsyncTaskProgressVO; +import com.bsd.pjcloud.hard.api.dto.order.HardCanteenUserApptQueryDto; +import com.bsd.pjcloud.hard.api.dto.order.HardOrderDto; +import com.bsd.pjcloud.hard.api.vo.order.excel.HardCanteenProductApptExcelVo; +import com.bsd.pjcloud.hard.api.vo.order.excel.HardCanteenUserApptExcelVo; + +import java.util.List; + +/** + * 食堂预约统计导出服务。 + * + *

导出属于独立的长耗时任务边界,负责异步任务、分批读取、Easy Excel 写入和文件中心上传; + * 普通统计服务只负责页面查询和统计,不持有导出任务状态。

+ */ +public interface HardCanteenApptTotalExportService { + + /** + * 提交用户预约异步导出任务。 + * + * @param query 用户预约日期、部门、食堂、餐段和状态筛选条件 + * @return 可供前端轮询的初始任务进度 + */ + AsyncTaskProgressVO submitUserReservationExport(HardCanteenUserApptQueryDto query); + + /** + * 兼容旧同步用户预约导出接口。 + * + * @param query 用户预约筛选条件 + * @return 用户预约导出行 + */ + List userReservationExport(HardCanteenUserApptQueryDto query); + + /** + * 提交菜品预约异步导出任务。 + * + * @param query 菜品预约统计筛选条件 + * @return 可供前端轮询的初始任务进度 + */ + AsyncTaskProgressVO submitProductReservationExport(HardOrderDto query); + + /** + * 查询菜品预约自定义 Excel 行,供同步下载和异步写入复用。 + * + * @param query 菜品预约统计筛选条件 + * @return 按日期餐段、商户部门、商品三级结构排列的导出行 + */ + List productReservationExport(HardOrderDto query); +}