feat:商品预约导出
feat:个人菜谱预约统计 feat:预定扣库存 feat:过期订单自动退款
This commit is contained in:
1 parent
e1cf5d5b90
commit
e4d8b4f354
11 files changed
+637
No files matched your search
+129
@@ -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<String, String> 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;
|
||||
}
|
||||
}
|
||||
+33
@@ -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;
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.bsd.pjcloud.hard.api.vo.order;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 食堂预约菜品导出聚合记录。
|
||||
*
|
||||
* <p>同一模型承载商户部门聚合和商品聚合两种查询结果,商品聚合时额外填充商品字段;金额统一使用分。</p>
|
||||
*/
|
||||
@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;
|
||||
}
|
||||
+20
@@ -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;
|
||||
}
|
||||
+32
@@ -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;
|
||||
}
|
||||
+32
@@ -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;
|
||||
}
|
||||
+53
@@ -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;
|
||||
|
||||
/**
|
||||
* 用户预约统计的通用汇总数据。
|
||||
*
|
||||
* <p>金额字段统一使用分;用户数量按“用户-用餐日期”记录累计,避免多日查询把同一用户重复隐藏。</p>
|
||||
*/
|
||||
@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<HardCanteenUserApptMealStatisticsVo> meals = new ArrayList<>();
|
||||
|
||||
/** 按餐段和菜品名称聚合的菜品份数。 */
|
||||
@Schema(description = "菜品统计")
|
||||
private List<HardCanteenUserApptDishStatisticsVo> dishes = new ArrayList<>();
|
||||
}
|
||||
+23
@@ -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<HardCanteenUserApptDailyStatisticsVo> daily = new ArrayList<>();
|
||||
}
|
||||
+93
@@ -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;
|
||||
|
||||
/**
|
||||
* 食堂预约菜品统计自定义导出行。
|
||||
*
|
||||
* <p>一组日期和餐段按汇总行、商户部门行、商品明细行组织;不同层级只填写所属列,配合自定义合并处理器形成树形报表。</p>
|
||||
*/
|
||||
@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;
|
||||
}
|
||||
+130
@@ -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;
|
||||
|
||||
/**
|
||||
* 食堂预约菜品统计导出合并处理器。
|
||||
*
|
||||
* <p>数据行通过各层级专属列识别:日期餐段行填写第 0 列,商户部门行填写第 6 列,商品行填写第 14 列;
|
||||
* 处理器在工作簿关闭前合并父层级字段,避免分批写入时依赖 Easy Excel 的行缓存。</p>
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* 工作簿关闭前统一处理数据行样式、层级合并和列宽。
|
||||
*
|
||||
* <p>Easy Excel 已为金额列设置数字格式,居中时必须从原样式复制再覆盖对齐属性,
|
||||
* 不能直接替换成全新的样式,否则金额会失去两位小数格式。</p>
|
||||
*
|
||||
* @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<Short, CellStyle> 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();
|
||||
}
|
||||
}
|
||||
+50
@@ -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;
|
||||
|
||||
/**
|
||||
* 食堂预约统计导出服务。
|
||||
*
|
||||
* <p>导出属于独立的长耗时任务边界,负责异步任务、分批读取、Easy Excel 写入和文件中心上传;
|
||||
* 普通统计服务只负责页面查询和统计,不持有导出任务状态。</p>
|
||||
*/
|
||||
public interface HardCanteenApptTotalExportService {
|
||||
|
||||
/**
|
||||
* 提交用户预约异步导出任务。
|
||||
*
|
||||
* @param query 用户预约日期、部门、食堂、餐段和状态筛选条件
|
||||
* @return 可供前端轮询的初始任务进度
|
||||
*/
|
||||
AsyncTaskProgressVO submitUserReservationExport(HardCanteenUserApptQueryDto query);
|
||||
|
||||
/**
|
||||
* 兼容旧同步用户预约导出接口。
|
||||
*
|
||||
* @param query 用户预约筛选条件
|
||||
* @return 用户预约导出行
|
||||
*/
|
||||
List<HardCanteenUserApptExcelVo> userReservationExport(HardCanteenUserApptQueryDto query);
|
||||
|
||||
/**
|
||||
* 提交菜品预约异步导出任务。
|
||||
*
|
||||
* @param query 菜品预约统计筛选条件
|
||||
* @return 可供前端轮询的初始任务进度
|
||||
*/
|
||||
AsyncTaskProgressVO submitProductReservationExport(HardOrderDto query);
|
||||
|
||||
/**
|
||||
* 查询菜品预约自定义 Excel 行,供同步下载和异步写入复用。
|
||||
*
|
||||
* @param query 菜品预约统计筛选条件
|
||||
* @return 按日期餐段、商户部门、商品三级结构排列的导出行
|
||||
*/
|
||||
List<HardCanteenProductApptExcelVo> productReservationExport(HardOrderDto query);
|
||||
}
|
||||
Reference in new issue
Block a user