package com.ruoyi.sales.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.enums.ReviewStatusEnum;
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.sales.dto.SalesLedgerArrivalDto;
import com.ruoyi.sales.mapper.SalesLedgerArrivalMapper;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.mapper.StockOutRecordSalesLedgerMapper;
import com.ruoyi.sales.pojo.SalesLedger;
import com.ruoyi.sales.pojo.SalesLedgerArrival;
import com.ruoyi.sales.pojo.StockOutRecordSalesLedger;
import com.ruoyi.sales.service.ISalesLedgerArrivalService;
import com.ruoyi.stock.mapper.StockOutRecordMapper;
import com.ruoyi.stock.pojo.StockOutRecord;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.util.List;
/**
* 销售台账到货记录Service业务层处理
*
* 到货记录挂在「一次发货」(油品出库单)下:出库单与销售台账的对应关系由绑定表
* stock_out_record_sales_ledger 给出,明细 id、车辆、车牌一律以出库单为准,不信前端传入。
* 代储台账不产绑定行,出库单的单值列 sales_ledger_id 即为权威对应关系(见 requireBinding)。
*
* @author ruoyi
* @date 2026-09-21
*/
@Service
@RequiredArgsConstructor
public class SalesLedgerArrivalServiceImpl extends ServiceImpl
implements ISalesLedgerArrivalService {
private static final String LEDGER_TYPE_DEPOSIT = "代储";
private final SalesLedgerArrivalMapper salesLedgerArrivalMapper;
private final StockOutRecordSalesLedgerMapper stockOutRecordSalesLedgerMapper;
private final StockOutRecordMapper stockOutRecordMapper;
private final SalesLedgerMapper salesLedgerMapper;
@Override
public IPage listPage(Page page, SalesLedgerArrivalDto req) {
// 该查询用 /*data_scope*/ 跳过数据权限,而 MP 默认会把 count 语句优化成「SELECT COUNT(*) FROM 主表 WHERE ...」,
// 优化后标记丢失、count 反被追加权限条件,total 会比实际少。关掉 count 优化后退化成「SELECT COUNT(*) FROM (原句) TOTAL」
page.setOptimizeCountSql(false);
return salesLedgerArrivalMapper.listPage(page, req);
}
@Override
public List bindableVehicles(Long salesLedgerId) {
if (salesLedgerId == null) {
throw new BaseException("请传入销售台账id");
}
return salesLedgerArrivalMapper.bindableVehicles(salesLedgerId);
}
@Override
public boolean addArrival(SalesLedgerArrivalDto req) {
if (req == null) {
throw new BaseException("到货记录不能为空");
}
StockOutRecord outRecord = requireApprovedOilOut(req.getStockOutRecordId());
StockOutRecordSalesLedger binding = requireBinding(outRecord, req.getSalesLedgerId());
applyOutRecordVehicle(req, outRecord);
req.setSalesLedgerProductId(binding.getSalesLedgerProductId());
checkArrivalQuantity(req, binding, null);
req.setId(null);
return save(req);
}
@Override
public boolean updateArrival(SalesLedgerArrivalDto req) {
if (req == null || req.getId() == null) {
throw new BaseException("请传来到货记录id");
}
SalesLedgerArrivalDto existing = salesLedgerArrivalMapper.selectArrivalById(req.getId());
if (existing == null) {
throw new BaseException("该到货记录不存在,无法修改,id:" + req.getId());
}
// 出库单/台账是记录的身份:前端不回传就沿用原值,改了则按新身份重新校验
Long stockOutRecordId = req.getStockOutRecordId() != null
? req.getStockOutRecordId() : existing.getStockOutRecordId();
Long salesLedgerId = req.getSalesLedgerId() != null
? req.getSalesLedgerId() : existing.getSalesLedgerId();
StockOutRecord outRecord = requireApprovedOilOut(stockOutRecordId);
StockOutRecordSalesLedger binding = requireBinding(outRecord, salesLedgerId);
req.setStockOutRecordId(stockOutRecordId);
req.setSalesLedgerId(salesLedgerId);
req.setSalesLedgerProductId(binding.getSalesLedgerProductId());
applyOutRecordVehicle(req, outRecord);
if (req.getArrivalQuantity() == null) {
req.setArrivalQuantity(existing.getArrivalQuantity());
}
checkArrivalQuantity(req, binding, req.getId());
return updateById(req);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteArrivals(List ids) {
if (CollectionUtils.isEmpty(ids)) {
throw new BaseException("请传入要删除的到货记录id");
}
return removeByIds(ids);
}
/**
* 出库单必须是「审批通过的油品出库」(type 为空),否则它不是发货台账的父行,挂到货记录会变成孤儿
*/
private StockOutRecord requireApprovedOilOut(Long stockOutRecordId) {
if (stockOutRecordId == null) {
throw new BaseException("请选择出库单");
}
StockOutRecord outRecord = stockOutRecordMapper.selectById(stockOutRecordId);
if (outRecord == null) {
throw new BaseException("所选出库单不存在,出库单id:" + stockOutRecordId);
}
if (!ReviewStatusEnum.APPROVED.getCode().equals(outRecord.getApprovalStatus())) {
throw new BaseException("出库单未审批通过,不能登记到货,出库单号:" + outRecord.getOutboundBatches());
}
if (StringUtils.isNotEmpty(outRecord.getType())) {
throw new BaseException("该出库单不是油品出库,不能登记到货,出库单号:" + outRecord.getOutboundBatches());
}
return outRecord;
}
/**
* 台账必须与该出库单有绑定关系 —— 绑定行同时给出明细 id 和本台账分摊到的出库数量(到货上限)。
* 不能看 stock_out_record.sales_ledger_id,那一列只存了首项台账。
* 代储例外:代储不产绑定行(绑定行同时是开票/收款候选的数据源,代储不能进),但它「一次出库只能绑一个台账」,
* 单值列反而权威,故按整单出库量造一行临时的绑定(不落库)用于后续校验
*/
private StockOutRecordSalesLedger requireBinding(StockOutRecord outRecord, Long salesLedgerId) {
if (salesLedgerId == null) {
throw new BaseException("请选择销售台账");
}
SalesLedger salesLedger = salesLedgerMapper.selectById(salesLedgerId);
if (salesLedger == null) {
throw new BaseException("所选销售台账不存在,销售台账id:" + salesLedgerId);
}
StockOutRecordSalesLedger binding = stockOutRecordSalesLedgerMapper.selectOne(
Wrappers.lambdaQuery()
.eq(StockOutRecordSalesLedger::getStockOutRecordId, outRecord.getId())
.eq(StockOutRecordSalesLedger::getSalesLedgerId, salesLedgerId));
if (binding != null) {
return binding;
}
if (LEDGER_TYPE_DEPOSIT.equals(salesLedger.getLedgerType())
&& salesLedgerId.equals(outRecord.getSalesLedgerId())) {
StockOutRecordSalesLedger deposit = new StockOutRecordSalesLedger();
deposit.setStockOutRecordId(outRecord.getId());
deposit.setSalesLedgerId(salesLedgerId);
deposit.setQuantity(outRecord.getStockOutNum());
return deposit;
}
throw new BaseException("该出库单未绑定此销售台账,不能登记到货");
}
/**
* 一次出库只对应一辆车,车辆与车牌一律取出库单上登记的,前端传的只用于比对。
* 出库单没登记车辆时(代储/客存出库不强制选车)允许留空,不阻塞登记
*/
private void applyOutRecordVehicle(SalesLedgerArrivalDto req, StockOutRecord outRecord) {
if (outRecord.getVehicleId() == null) {
req.setVehicleId(null);
req.setTruckPlateNo(null);
return;
}
if (req.getVehicleId() != null && !req.getVehicleId().equals(outRecord.getVehicleId())) {
throw new BaseException("所选车辆与该出库单登记的车辆不一致,出库单号:" + outRecord.getOutboundBatches());
}
req.setVehicleId(outRecord.getVehicleId());
req.setTruckPlateNo(outRecord.getTruckPlateNo());
}
/**
* 到货数量上限 = 该台账在本出库单上分摊到的数量(不是台账明细的合同数量,否则挡不住「到货超过实发」)
*/
private void checkArrivalQuantity(SalesLedgerArrivalDto req, StockOutRecordSalesLedger binding, Long excludeId) {
BigDecimal quantity = req.getArrivalQuantity();
if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0) {
throw new BaseException("到货数量必须大于0");
}
BigDecimal limit = binding.getQuantity() == null ? BigDecimal.ZERO : binding.getQuantity();
BigDecimal arrived = salesLedgerArrivalMapper.sumArrivedQuantity(
binding.getStockOutRecordId(), binding.getSalesLedgerId(), excludeId);
if (arrived.add(quantity).compareTo(limit) > 0) {
throw new BaseException("到货数量超出该台账本次出库数量,出库数量:" + limit.toPlainString()
+ ",已到货:" + arrived.toPlainString());
}
}
}