✨ Spring Boot 精粹:为寄售详情打造专属库存聚合 API 📈
在许多业务场景中,除了查看详细数据列表,我们还需要快速了解关键的聚合指标。例如,在“寄售库存详情”管理中,实时掌握总库存数量和总库存价值至关重要。本文将详细阐述如何使用 Spring Boot 为 ConsignmentDetail
(寄售库存详情)模块创建一个专属的 API (Application Programming Interface - 应用程序编程接口) 端点,用于高效计算和提供这些关键的库存聚合数据。
📜 内容焦点
方面 | 描述 | 关键组件/实现 |
---|---|---|
核心需求 | 获取指定寄售库存下的总库存数量 (totalStockCount ) 和总库存价值 (totalStockValue ) | 独立的 GET API 端点 |
数据载体 (DTO - Data Transfer Object) | ConsignmentDetailStockAggregationDTO | 封装 totalStockCount 和 totalStockValue |
Service 层核心 | getConsignmentDetailStockAggregates 方法 | 负责业务逻辑计算 |
辅助方法 fillStockAndValueForDetail | 计算单个详情的 stock 和 stockValue (代码复用) | |
Controller 层 | 新增 GET /api/consignmentSettlement/details/stockAggregates 端点 | 暴露聚合查询功能 |
数据准确性 | 依赖 ConsignmentSummary 计算实时 stock | ConsignmentSummaryRepository.sumStockInCountBy... |
🤔 为何需要专属聚合 API?
当我们需要快速了解一个业务集合的整体情况时(比如一个寄售批次的总库存和总价值),如果这些聚合数据与明细列表数据混在一起,或者需要客户端自行计算,会带来诸多不便:
- 🐢 客户端计算缓慢/不准确:将大量明细数据传输到客户端再进行聚合,效率低下且容易出错。
- ⚙️ 后端列表接口臃肿:如果列表接口被迫承担聚合计算,会使其职责不清,响应变慢。
- 🔄 数据获取不灵活:有时我们只需要聚合数据,而不需要完整的明细列表。
因此,设计一个专门的、服务端的聚合 API 是更清晰、高效的解决方案。
💡 解决方案:精准打击,一步到位
我们将通过以下步骤构建这个专属的聚合 API:
- 定义 DTO:
ConsignmentDetailStockAggregationDTO
用于清晰地返回聚合结果。 - Service 层实现:
getConsignmentDetailStockAggregates
方法作为核心,负责从数据库获取所有相关详情,然后逐条计算其库存和价值,并进行累加。fillStockAndValueForDetail
辅助方法,用于统一计算单个ConsignmentDetail
的stock
(依赖ConsignmentSummary
)和stockValue
。
- Controller 层暴露端点:创建一个新的
GET
请求端点来调用 Service 层的聚合方法。
1. DTO 定义:ConsignmentDetailStockAggregationDTO
📊
这是我们聚合数据的标准输出格式:
// com.productQualification.api.dto.ConsignmentDetailStockAggregationDTO
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "ConsignmentDetailStockAggregationDTO", description = "寄售库存详情的库存相关聚合数据DTO")
public class ConsignmentDetailStockAggregationDTO {
@ApiModelProperty(value = "总库存数量", example = "1500")
private Integer totalStockCount;
@ApiModelProperty(value = "总库存价值", example = "75000.00")
private BigDecimal totalStockValue;
}
2. Service 层核心逻辑 🧠
// com.productQualification.api.service.consignmentSettlement.ConsignmentDetailService
@Service
public class ConsignmentDetailService {
@Autowired
private ConsignmentDetailRepository consignmentDetailRepository;
@Autowired
private ConsignmentSummaryRepository consignmentSummaryRepository; // 用于计算库存
private static final Logger log = LoggerFactory.getLogger(ConsignmentDetailService.class);
private static final List<Integer> STOCK_CALCULATION_STATUSES = Arrays.asList(1, 2, 3, 4, 5, 6); // 计算库存的状态
/**
* 辅助方法:为单个 ConsignmentDetail 实体填充 stock 和 stockValue。
*/
private void fillStockAndValueForDetail(ConsignmentDetail detail) {
if (detail == null) return;
if (detail.getProductId() == null || detail.getAdminId() == null || detail.getConsignmentSettlementId() == null) {
log.warn("跳过库存计算 (Detail ID: {}), 原因: productId, adminId, 或 consignmentSettlementId 缺失.", detail.getId());
detail.setStock(0); detail.setStockValue(BigDecimal.ZERO); return;
}
Integer calculatedStock = consignmentSummaryRepository.sumStockInCountByConsignmentSettlementAndProductAndAdminAndStatusIn(
detail.getConsignmentSettlementId(), detail.getProductId(), detail.getAdminId(), STOCK_CALCULATION_STATUSES
);
detail.setStock(calculatedStock != null ? calculatedStock : 0);
if (detail.getStock() > 0 && detail.getSettlementPrice() != null) {
detail.setStockValue(BigDecimal.valueOf(detail.getStock()).multiply(detail.getSettlementPrice()).setScale(2, BigDecimal.ROUND_HALF_UP));
} else {
detail.setStockValue(BigDecimal.ZERO);
}
log.trace("已填充库存信息 (Detail ID {}): stock={}, stockValue={}", detail.getId(), detail.getStock(), detail.getStockValue());
}
/**
* 获取指定寄售库存ID和管理员ID下的库存聚合数据 (总库存数和总库存价值)。
*/
@Transactional(readOnly = true)
public ConsignmentDetailStockAggregationDTO getConsignmentDetailStockAggregates(
Integer adminId, Integer consignmentSettlementId) {
log.debug("开始计算库存聚合数据: adminId={}, consignmentSettlementId={}", adminId, consignmentSettlementId);
// 1. 获取指定 consignmentSettlementId 和 adminId 下的所有 ConsignmentDetail 记录
List<ConsignmentDetail> allDetails = consignmentDetailRepository.findAllByConsignmentSettlementIdAndAdminId(
consignmentSettlementId, adminId
);
int totalStockCount = 0;
BigDecimal totalStockValue = BigDecimal.ZERO;
if (!CollectionUtils.isEmpty(allDetails)) {
log.info("找到 {} 条寄售详情记录进行聚合计算 (ConsignmentSettlement ID: {})", allDetails.size(), consignmentSettlementId);
// 2. 遍历所有详情记录
for (ConsignmentDetail detail : allDetails) {
// 3. 为每条详情记录填充其当前的 stock 和 stockValue
fillStockAndValueForDetail(detail);
// 4. 累加库存数量和库存价值
totalStockCount += detail.getStock();
if (detail.getStockValue() != null) { // 防御性编程
totalStockValue = totalStockValue.add(detail.getStockValue());
}
}
} else {
log.info("未找到寄售详情记录进行聚合计算 (ConsignmentSettlement ID: {})", consignmentSettlementId);
}
log.info("库存聚合计算完成 (ConsignmentSettlement ID {}): 总库存数量={}, 总库存价值={}",
consignmentSettlementId, totalStockCount, totalStockValue);
// 5. 返回包含聚合结果的 DTO
return new ConsignmentDetailStockAggregationDTO(totalStockCount, totalStockValue);
}
// ... (其他与分页列表相关的 Service 方法,如 findPaginatedDetails,这里不再赘述其内部实现细节)
}
核心思路:getConsignmentDetailStockAggregates
方法不关心分页,它获取指定条件下的 所有 ConsignmentDetail
记录,然后为每一条计算最新的 stock
和 stockValue
(通过调用 fillStockAndValueForDetail
,该方法内部会查询 ConsignmentSummary
),最后将这些值累加起来。
3. Controller 层暴露 API 端点 📡
// com.productQualification.api.controller.consignmentSettlement.ConsignmentSettlementController
@RestController
@RequestMapping("/api/consignmentSettlement")
public class ConsignmentSettlementController {
@Autowired
private ConsignmentDetailService consignmentDetailService;
private static final Logger log = LoggerFactory.getLogger(ConsignmentSettlementController.class);
private static final int CONSIGNMENT_TOOL_TYPE = 10; // 假设的常量
/**
* 获取指定寄售库存的详情库存聚合数据 (总库存数和总库存价值)。
*/
@GetMapping("/details/stockAggregates") // 清晰的 GET 请求路径
@ApiOperation("获取指定寄售库存的详情库存聚合数据")
public BaseResult getConsignmentDetailStockAggregates(
@EffectiveAdminId(toolType = CONSIGNMENT_TOOL_TYPE) Integer adminId, // 从会话或Token获取管理员ID
@RequestParam @ApiParam(value = "寄售库存ID", required = true, example = "1") Integer consignmentSettlementId) {
try {
log.info("请求获取库存聚合数据: adminId={}, consignmentSettlementId={}", adminId, consignmentSettlementId);
ConsignmentDetailStockAggregationDTO aggregates = consignmentDetailService.getConsignmentDetailStockAggregates(adminId, consignmentSettlementId);
return BaseResult.success("查询聚合数据成功", aggregates);
} catch (Exception e) {
log.error("查询寄售库存详情聚合数据失败: adminId={}, csId={}, error={}", adminId, consignmentSettlementId, e.getMessage(), e);
// 返回统一的错误结构
return BaseResult.failure(BaseResult.FAILURE, "查询聚合数据失败:" + e.getMessage());
}
}
// ... (其他 Controller 端点,例如用于获取分页列表的 POST /details/listByPage)
}
🌊 Service 层聚合计算流程图 (Mermaid Flowchart)
展示 getConsignmentDetailStockAggregates
方法的逻辑:
🔄 时序图:一次聚合数据请求的旅程 (Mermaid Sequence Diagram)
🌟 专属聚合 API 的优势
- 🎯 目标明确:接口只做一件事——提供聚合数据。
- 🚀 按需获取:客户端只在需要时才请求聚合信息,避免不必要的计算和传输。
- 🧹 后端清爽:保持列表查询接口的轻量级,聚合逻辑独立封装。
- ⚡ 潜在性能更优:如果聚合逻辑复杂或数据量大,未来可以在 Repository 层使用 JPQL (Java Persistence Query Language - Java持久化查询语言) / SQL (Structured Query Language - 结构化查询语言) 直接进行数据库层面的聚合,性能会更好。当前 Service 层计算对于中小型数据量是可行的。
- 🪜 可扩展性:未来如果需要其他类型的聚合,可以类似地添加新的专属 API。
📚 英文缩写全称及中文解释
- API: Application Programming Interface - 应用程序编程接口
- DTO: Data Transfer Object - 数据传输对象
- JPA: Java Persistence API - Java持久化应用程序接口
- SQL: Structured Query Language - 结构化查询语言
- HTTP: Hypertext Transfer Protocol - 超文本传输协议
- GET: Hypertext Transfer Protocol GET method - 超文本传输协议GET方法 (一种HTTP请求方法)
🧠 思维导图总结
通过实现这个专属的库存聚合 API,我们能够为前端提供一个快速、准确获取关键业务指标的方式,同时也优化了后端服务的结构。希望这篇聚焦于聚合功能的博客能帮助你!💪