添加库存数量大于零的查询条件

在StockController的list和warning方法以及DashboardServiceImpl的
getExpiryWarning方法中添加了.gt(Stock::getQuantity, 0)条件,
避免返回无库存记录的药品信息

feat(stock-in): 实现出库单详情中的药品名称显示功能

在StockInController中注入medicineService,并在getById方法中
通过流式处理将药品ID映射为药品名称,填充到出库详情中

feat(stock-out): 实现出库单详情中的药品名称显示功能

在StockOutController中注入medicineService,并在getById方法中
通过流式处理将药品ID映射为药品名称,填充到出库详情中

refactor(stock-out): 优化出库库存检查逻辑并实现FIFO扣减

将原有的简单库存总量检查改为按批次和仓库精确检查,
实现先进先出(FIFO)的库存扣减策略,确保先过期的药品优先出库

feat(frontend): 提交表单时计算并包含明细总价字段

在StockInForm和StockOutForm中修改提交逻辑,为每个详情项
计算totalPrice字段,确保后端接收到完整的金额信息
This commit is contained in:
2026-07-07 09:47:09 +08:00
parent cc3c1aba96
commit 23411ea1b5
7 changed files with 47 additions and 23 deletions

View File

@@ -29,6 +29,7 @@ public class StockController {
@RequestParam(required = false) String medicineName, @RequestParam(required = false) String medicineName,
@RequestParam(required = false) String batchNo) { @RequestParam(required = false) String batchNo) {
var wrapper = new LambdaQueryWrapper<Stock>() var wrapper = new LambdaQueryWrapper<Stock>()
.gt(Stock::getQuantity, 0)
.like(StringUtils.hasText(batchNo), Stock::getBatchNo, batchNo) .like(StringUtils.hasText(batchNo), Stock::getBatchNo, batchNo)
.orderByAsc(Stock::getExpiryDate); .orderByAsc(Stock::getExpiryDate);
// 按药品名搜索需先查 medicine 表 // 按药品名搜索需先查 medicine 表
@@ -48,6 +49,7 @@ public class StockController {
public Result<Page<Stock>> warning(@RequestParam(defaultValue = "1") Integer page, public Result<Page<Stock>> warning(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size) { @RequestParam(defaultValue = "10") Integer size) {
var wrapper = new LambdaQueryWrapper<Stock>() var wrapper = new LambdaQueryWrapper<Stock>()
.gt(Stock::getQuantity, 0)
.gt(Stock::getExpiryDate, java.time.LocalDate.now()) .gt(Stock::getExpiryDate, java.time.LocalDate.now())
.orderByAsc(Stock::getExpiryDate); .orderByAsc(Stock::getExpiryDate);
Page<Stock> result = stockService.page(new Page<>(page, size), wrapper); Page<Stock> result = stockService.page(new Page<>(page, size), wrapper);

View File

@@ -7,8 +7,10 @@ import com.kronecker.backend.common.Result;
import com.kronecker.backend.dto.StockInCreateDTO; import com.kronecker.backend.dto.StockInCreateDTO;
import com.kronecker.backend.entity.*; import com.kronecker.backend.entity.*;
import com.kronecker.backend.service.StockInService; import com.kronecker.backend.service.StockInService;
import com.baomidou.mybatisplus.extension.service.IService;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import java.util.stream.Collectors;
import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -24,6 +26,7 @@ public class StockInController {
private final IService<StockInRecord> recordService; private final IService<StockInRecord> recordService;
private final IService<StockInDetail> detailService; private final IService<StockInDetail> detailService;
private final IService<Supplier> supplierService; private final IService<Supplier> supplierService;
private final IService<Medicine> medicineService;
private final StockInService stockInService; private final StockInService stockInService;
@GetMapping @GetMapping
@@ -39,8 +42,15 @@ public class StockInController {
public Result<StockInRecord> getById(@PathVariable Long id) { public Result<StockInRecord> getById(@PathVariable Long id) {
StockInRecord record = recordService.getById(id); StockInRecord record = recordService.getById(id);
if (record != null) { if (record != null) {
record.setDetails(detailService.list( var details = detailService.list(
new LambdaQueryWrapper<StockInDetail>().eq(StockInDetail::getRecordId, id))); new LambdaQueryWrapper<StockInDetail>().eq(StockInDetail::getRecordId, id));
if (!details.isEmpty()) {
var medIds = details.stream().map(StockInDetail::getMedicineId).collect(Collectors.toSet());
var medMap = medicineService.listByIds(medIds).stream()
.collect(Collectors.toMap(Medicine::getId, Medicine::getName));
details.forEach(d -> d.setMedicineName(medMap.getOrDefault(d.getMedicineId(), "")));
}
record.setDetails(details);
var supp = supplierService.getById(record.getSupplierId()); var supp = supplierService.getById(record.getSupplierId());
if (supp != null) record.setSupplierName(supp.getName()); if (supp != null) record.setSupplierName(supp.getName());
} }

View File

@@ -7,8 +7,10 @@ import com.kronecker.backend.common.Result;
import com.kronecker.backend.dto.StockOutCreateDTO; import com.kronecker.backend.dto.StockOutCreateDTO;
import com.kronecker.backend.entity.*; import com.kronecker.backend.entity.*;
import com.kronecker.backend.service.StockOutService; import com.kronecker.backend.service.StockOutService;
import com.baomidou.mybatisplus.extension.service.IService;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import java.util.stream.Collectors;
import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -24,6 +26,7 @@ public class StockOutController {
private final IService<StockOutRecord> recordService; private final IService<StockOutRecord> recordService;
private final IService<StockOutDetail> detailService; private final IService<StockOutDetail> detailService;
private final IService<Customer> customerService; private final IService<Customer> customerService;
private final IService<Medicine> medicineService;
private final StockOutService stockOutService; private final StockOutService stockOutService;
@GetMapping @GetMapping
@@ -39,8 +42,16 @@ public class StockOutController {
public Result<StockOutRecord> getById(@PathVariable Long id) { public Result<StockOutRecord> getById(@PathVariable Long id) {
StockOutRecord record = recordService.getById(id); StockOutRecord record = recordService.getById(id);
if (record != null) { if (record != null) {
record.setDetails(detailService.list( var details = detailService.list(
new LambdaQueryWrapper<StockOutDetail>().eq(StockOutDetail::getRecordId, id))); new LambdaQueryWrapper<StockOutDetail>().eq(StockOutDetail::getRecordId, id));
// 填充药品名
if (!details.isEmpty()) {
var medIds = details.stream().map(StockOutDetail::getMedicineId).collect(Collectors.toSet());
var medMap = medicineService.listByIds(medIds).stream()
.collect(Collectors.toMap(Medicine::getId, Medicine::getName));
details.forEach(d -> d.setMedicineName(medMap.getOrDefault(d.getMedicineId(), "")));
}
record.setDetails(details);
if (record.getCustomerId() != null) { if (record.getCustomerId() != null) {
var cust = customerService.getById(record.getCustomerId()); var cust = customerService.getById(record.getCustomerId());
if (cust != null) record.setCustomerName(cust.getName()); if (cust != null) record.setCustomerName(cust.getName());

View File

@@ -38,6 +38,7 @@ public class DashboardServiceImpl implements DashboardService {
public Page<Stock> getExpiryWarning(Page<Stock> page) { public Page<Stock> getExpiryWarning(Page<Stock> page) {
Page<Stock> result = stockMapper.selectPage(page, Page<Stock> result = stockMapper.selectPage(page,
new LambdaQueryWrapper<Stock>() new LambdaQueryWrapper<Stock>()
.gt(Stock::getQuantity, 0)
.le(Stock::getExpiryDate, LocalDate.now().plusDays(90)) .le(Stock::getExpiryDate, LocalDate.now().plusDays(90))
.orderByAsc(Stock::getExpiryDate)); .orderByAsc(Stock::getExpiryDate));
populateStockNames(result.getRecords()); populateStockNames(result.getRecords());

View File

@@ -64,26 +64,23 @@ public class StockOutServiceImpl implements StockOutService {
var details = detailMapper.selectList( var details = detailMapper.selectList(
new LambdaQueryWrapper<StockOutDetail>().eq(StockOutDetail::getRecordId, id)); new LambdaQueryWrapper<StockOutDetail>().eq(StockOutDetail::getRecordId, id));
// 检查库存是否充足 // 检查库存是否充足 + 扣减库存FIFO: 先过期先出)
for (var d : details) { for (var d : details) {
int totalStock = stockMapper.selectList(new LambdaQueryWrapper<Stock>() var qw = new LambdaQueryWrapper<Stock>()
.eq(Stock::getMedicineId, d.getMedicineId())
.eq(Stock::getWarehouseId, record.getWarehouseId())
.eq(Stock::getBatchNo, d.getBatchNo()))
.stream().mapToInt(Stock::getQuantity).sum();
if (totalStock < d.getQuantity()) {
throw new BusinessException("药品批次 " + d.getBatchNo() + " 库存不足");
}
}
// 扣减库存FIFO: 先过期先出)
for (var d : details) {
int remaining = d.getQuantity();
var stocks = stockMapper.selectList(new LambdaQueryWrapper<Stock>()
.eq(Stock::getMedicineId, d.getMedicineId()) .eq(Stock::getMedicineId, d.getMedicineId())
.eq(Stock::getWarehouseId, record.getWarehouseId()) .eq(Stock::getWarehouseId, record.getWarehouseId())
.eq(Stock::getBatchNo, d.getBatchNo()) .gt(Stock::getQuantity, 0);
.orderByAsc(Stock::getExpiryDate)); if (d.getBatchNo() != null && !d.getBatchNo().isBlank()) {
qw.eq(Stock::getBatchNo, d.getBatchNo());
}
qw.orderByAsc(Stock::getExpiryDate);
var stocks = stockMapper.selectList(qw);
int remaining = d.getQuantity();
int available = stocks.stream().mapToInt(Stock::getQuantity).sum();
if (available < remaining) {
throw new BusinessException("药品库存不足,需要" + remaining + ",可用" + available);
}
for (Stock s : stocks) { for (Stock s : stocks) {
if (remaining <= 0) break; if (remaining <= 0) break;
int deduct = Math.min(s.getQuantity(), remaining); int deduct = Math.min(s.getQuantity(), remaining);

View File

@@ -32,7 +32,8 @@ async function handleSubmit() {
if (!form.supplierId) { ElMessage.warning('请选择供应商'); return } if (!form.supplierId) { ElMessage.warning('请选择供应商'); return }
if (!details.value.length) { ElMessage.warning('请添加药品明细'); return } if (!details.value.length) { ElMessage.warning('请添加药品明细'); return }
updateTotal() updateTotal()
await createStockIn({ ...form, details: details.value, totalAmount: totalAmount.value }) const submitDetails = details.value.map(d => ({ ...d, totalPrice: d.costPrice * d.quantity }))
await createStockIn({ ...form, details: submitDetails, totalAmount: totalAmount.value })
ElMessage.success('入库单已创建,等待审核') ElMessage.success('入库单已创建,等待审核')
router.push('/stock-in/list') router.push('/stock-in/list')
} }

View File

@@ -42,7 +42,9 @@ async function handleSubmit() {
if (!form.customerId) { ElMessage.warning('请选择客户'); return } if (!form.customerId) { ElMessage.warning('请选择客户'); return }
if (!details.value.length) { ElMessage.warning('请添加药品明细'); return } if (!details.value.length) { ElMessage.warning('请添加药品明细'); return }
updateTotal() updateTotal()
await createStockOut({ ...form, details: details.value, totalAmount: totalAmount.value }) // 补齐每条明细的 totalPrice
const submitDetails = details.value.map(d => ({ ...d, totalPrice: detailTotal(d) }))
await createStockOut({ ...form, details: submitDetails, totalAmount: totalAmount.value })
ElMessage.success('出库单已创建,等待审核') ElMessage.success('出库单已创建,等待审核')
router.push('/stock-out/list') router.push('/stock-out/list')
} }