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

在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 batchNo) {
var wrapper = new LambdaQueryWrapper<Stock>()
.gt(Stock::getQuantity, 0)
.like(StringUtils.hasText(batchNo), Stock::getBatchNo, batchNo)
.orderByAsc(Stock::getExpiryDate);
// 按药品名搜索需先查 medicine 表
@@ -48,6 +49,7 @@ public class StockController {
public Result<Page<Stock>> warning(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
var wrapper = new LambdaQueryWrapper<Stock>()
.gt(Stock::getQuantity, 0)
.gt(Stock::getExpiryDate, java.time.LocalDate.now())
.orderByAsc(Stock::getExpiryDate);
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.entity.*;
import com.kronecker.backend.service.StockInService;
import com.baomidou.mybatisplus.extension.service.IService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import java.util.stream.Collectors;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
@@ -24,6 +26,7 @@ public class StockInController {
private final IService<StockInRecord> recordService;
private final IService<StockInDetail> detailService;
private final IService<Supplier> supplierService;
private final IService<Medicine> medicineService;
private final StockInService stockInService;
@GetMapping
@@ -39,8 +42,15 @@ public class StockInController {
public Result<StockInRecord> getById(@PathVariable Long id) {
StockInRecord record = recordService.getById(id);
if (record != null) {
record.setDetails(detailService.list(
new LambdaQueryWrapper<StockInDetail>().eq(StockInDetail::getRecordId, id)));
var details = detailService.list(
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());
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.entity.*;
import com.kronecker.backend.service.StockOutService;
import com.baomidou.mybatisplus.extension.service.IService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import java.util.stream.Collectors;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
@@ -24,6 +26,7 @@ public class StockOutController {
private final IService<StockOutRecord> recordService;
private final IService<StockOutDetail> detailService;
private final IService<Customer> customerService;
private final IService<Medicine> medicineService;
private final StockOutService stockOutService;
@GetMapping
@@ -39,8 +42,16 @@ public class StockOutController {
public Result<StockOutRecord> getById(@PathVariable Long id) {
StockOutRecord record = recordService.getById(id);
if (record != null) {
record.setDetails(detailService.list(
new LambdaQueryWrapper<StockOutDetail>().eq(StockOutDetail::getRecordId, id)));
var details = detailService.list(
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) {
var cust = customerService.getById(record.getCustomerId());
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) {
Page<Stock> result = stockMapper.selectPage(page,
new LambdaQueryWrapper<Stock>()
.gt(Stock::getQuantity, 0)
.le(Stock::getExpiryDate, LocalDate.now().plusDays(90))
.orderByAsc(Stock::getExpiryDate));
populateStockNames(result.getRecords());

View File

@@ -64,26 +64,23 @@ public class StockOutServiceImpl implements StockOutService {
var details = detailMapper.selectList(
new LambdaQueryWrapper<StockOutDetail>().eq(StockOutDetail::getRecordId, id));
// 检查库存是否充足
// 检查库存是否充足 + 扣减库存FIFO: 先过期先出)
for (var d : details) {
int totalStock = stockMapper.selectList(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>()
var qw = new LambdaQueryWrapper<Stock>()
.eq(Stock::getMedicineId, d.getMedicineId())
.eq(Stock::getWarehouseId, record.getWarehouseId())
.eq(Stock::getBatchNo, d.getBatchNo())
.orderByAsc(Stock::getExpiryDate));
.gt(Stock::getQuantity, 0);
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) {
if (remaining <= 0) break;
int deduct = Math.min(s.getQuantity(), remaining);

View File

@@ -32,7 +32,8 @@ async function handleSubmit() {
if (!form.supplierId) { ElMessage.warning('请选择供应商'); return }
if (!details.value.length) { ElMessage.warning('请添加药品明细'); return }
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('入库单已创建,等待审核')
router.push('/stock-in/list')
}

View File

@@ -42,7 +42,9 @@ async function handleSubmit() {
if (!form.customerId) { ElMessage.warning('请选择客户'); return }
if (!details.value.length) { ElMessage.warning('请添加药品明细'); return }
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('出库单已创建,等待审核')
router.push('/stock-out/list')
}