zhouhui.jiang

update 添加验证

......@@ -31,7 +31,8 @@ public class InvoiceAddReq {
@Schema(description = "关联订单编号", required = true)
private String orderNo;
@Schema(description = "关联出库单编号")
@NotBlank(message = "关联出库单编号不能为空")
@Schema(description = "关联出库单编号", required = true)
private String deliveryNo;
@NotBlank(message = "经销商编码不能为空")
......
......@@ -35,7 +35,8 @@ public class InvoiceUpdateReq {
@Schema(description = "关联订单编号", required = true)
private String orderNo;
@Schema(description = "关联出库单编号")
@NotBlank(message = "关联出库单编号不能为空")
@Schema(description = "关联出库单编号", required = true)
private String deliveryNo;
@NotBlank(message = "经销商编码不能为空")
......
......@@ -2,11 +2,13 @@ package com.apple.erp.service.impl;
import com.apple.erp.dto.*;
import com.apple.erp.entity.DeliveryItem;
import com.apple.erp.entity.OrderItem;
import com.apple.erp.entity.OrderMain;
// rebate settlement handled by RebateSettlementService
import com.apple.erp.service.RebateSettlementService;
import com.apple.erp.mapper.DeliveryItemMapper;
import com.apple.erp.mapper.OrderMainMapper;
import com.apple.erp.mapper.OrderItemMapper;
// removed unused mapper imports after delegating settlement logic
import com.apple.erp.util.TimingValidationUtil;
import com.apple.erp.exception.DealerMismatchException;
......@@ -48,6 +50,9 @@ public class DeliveryMainServiceImpl extends ServiceImpl<DeliveryMainMapper, Del
private OrderMainMapper orderMainMapper;
@Autowired
private OrderItemMapper orderItemMapper;
@Autowired
private TimingValidationUtil timingValidationUtil;
// rebate settlement is delegated; no local mapper usage required
......@@ -157,6 +162,11 @@ public class DeliveryMainServiceImpl extends ServiceImpl<DeliveryMainMapper, Del
}
}
// 业务校验:出库明细需与关联订单的商品至少部分匹配
validateAtLeastOneItemMatchesOrder(addReq.getOrderNo(),
addReq.getDeliveryItems() == null ? new ArrayList<>() :
addReq.getDeliveryItems().stream().map(DeliveryItemAddReq::getProductCode).collect(Collectors.toList()));
DeliveryMain deliveryMain = new DeliveryMain();
BeanUtils.copyProperties(addReq, deliveryMain);
deliveryMain.setCreateTime(LocalDateTime.now());
......@@ -230,6 +240,11 @@ public class DeliveryMainServiceImpl extends ServiceImpl<DeliveryMainMapper, Del
}
}
// 业务校验:出库明细需与关联订单的商品至少部分匹配
validateAtLeastOneItemMatchesOrder(updateReq.getOrderNo(),
updateReq.getDeliveryItems() == null ? new ArrayList<>() :
updateReq.getDeliveryItems().stream().map(DeliveryItemUpdateReq::getProductCode).collect(Collectors.toList()));
DeliveryMain deliveryMain = new DeliveryMain();
BeanUtils.copyProperties(updateReq, deliveryMain);
deliveryMain.setUpdateTime(LocalDateTime.now());
......@@ -283,6 +298,39 @@ public class DeliveryMainServiceImpl extends ServiceImpl<DeliveryMainMapper, Del
}
}
/**
* 校验至少有一条出库明细与关联订单的商品明细匹配(按产品编码匹配)
* 若完全不匹配则抛出业务异常。
*
* @param orderNo 订单编号
* @param deliveryProductCodes 出库明细中的产品编码列表
*/
private void validateAtLeastOneItemMatchesOrder(String orderNo, List<String> deliveryProductCodes) {
if (deliveryProductCodes == null || deliveryProductCodes.isEmpty()) {
throw new IllegalArgumentException("出库明细不可为空");
}
LambdaQueryWrapper<OrderItem> orderItemWrapper = Wrappers.lambdaQuery(OrderItem.class)
.eq(OrderItem::getOrderNo, orderNo)
.eq(OrderItem::getDelFlag, "0");
List<OrderItem> orderItems = orderItemMapper.selectList(orderItemWrapper);
if (orderItems == null || orderItems.isEmpty()) {
// 订单无商品时,视为不匹配
throw new IllegalArgumentException("关联的订单编号,商品明细项为空");
}
// 判定是否存在至少一条产品编码匹配
boolean hasAnyMatch = orderItems.stream()
.map(OrderItem::getProductCode)
.filter(code -> code != null && !code.isEmpty())
.anyMatch(deliveryProductCodes::contains);
if (!hasAnyMatch) {
throw new IllegalArgumentException("出库明细必须至少包含一项与关联订单对应的产品明细,请检查后重新提交");
}
}
// 结转逻辑已抽取到 RebateSettlementService
@Override
......
......@@ -4,11 +4,15 @@ import com.apple.erp.dto.*;
import com.apple.erp.entity.InvoiceItem;
import com.apple.erp.entity.OrderMain;
import com.apple.erp.entity.DeliveryMain;
import com.apple.erp.entity.OrderItem;
import com.apple.erp.entity.DeliveryItem;
// rebate settlement handled by RebateSettlementService
import com.apple.erp.service.RebateSettlementService;
import com.apple.erp.mapper.InvoiceItemMapper;
import com.apple.erp.mapper.OrderMainMapper;
import com.apple.erp.mapper.DeliveryMainMapper;
import com.apple.erp.mapper.OrderItemMapper;
import com.apple.erp.mapper.DeliveryItemMapper;
// removed unused mapper imports after delegating settlement logic
import com.apple.erp.util.TimingValidationUtil;
import com.apple.erp.exception.InvoiceValidationException;
......@@ -56,6 +60,12 @@ public class InvoiceMainServiceImpl extends ServiceImpl<InvoiceMainMapper, Invoi
@Autowired
private DeliveryMainMapper deliveryMainMapper;
@Autowired
private OrderItemMapper orderItemMapper;
@Autowired
private DeliveryItemMapper deliveryItemMapper;
// rebate settlement is delegated; no local mapper usage required
@Autowired
......@@ -134,13 +144,23 @@ public class InvoiceMainServiceImpl extends ServiceImpl<InvoiceMainMapper, Invoi
throw new IllegalArgumentException("发票编号已存在");
}
// 验证关联的订单是否存在且经销商一致
// 关联必填
if (!StringUtils.hasText(addReq.getOrderNo())) {
throw new IllegalArgumentException("关联订单编号不能为空");
}
if (!StringUtils.hasText(addReq.getDeliveryNo())) {
throw new IllegalArgumentException("关联出库单编号不能为空");
}
// 基础一致性
validateOrderConsistency(addReq.getOrderNo(), addReq.getDealerCode());
validateDeliveryConsistency(addReq.getDeliveryNo(), addReq.getDealerCode());
// 验证关联的出库单是否存在且经销商一致(如果提供了出库单号)
if (StringUtils.hasText(addReq.getDeliveryNo())) {
validateDeliveryConsistency(addReq.getDeliveryNo(), addReq.getDealerCode());
}
// 收集发票明细产品编码并进行双重“至少一项匹配”校验
List<String> invoiceProductCodes = (addReq.getInvoiceItems() == null ? java.util.Collections.emptyList()
: addReq.getInvoiceItems().stream().map(InvoiceItemAddReq::getProductCode).collect(Collectors.toList()));
validateAtLeastOneItemMatchesOrder(addReq.getOrderNo(), invoiceProductCodes);
validateAtLeastOneItemMatchesDelivery(addReq.getDeliveryNo(), invoiceProductCodes);
boolean isValid = true;
// 时序逻辑验证:开票时间不得早于出库时间
......@@ -206,13 +226,20 @@ public class InvoiceMainServiceImpl extends ServiceImpl<InvoiceMainMapper, Invoi
throw new IllegalArgumentException("发票编号已存在");
}
// 验证关联的订单是否存在且经销商一致
if (!StringUtils.hasText(updateReq.getOrderNo())) {
throw new IllegalArgumentException("关联订单编号不能为空");
}
if (!StringUtils.hasText(updateReq.getDeliveryNo())) {
throw new IllegalArgumentException("关联出库单编号不能为空");
}
validateOrderConsistency(updateReq.getOrderNo(), updateReq.getDealerCode());
validateDeliveryConsistency(updateReq.getDeliveryNo(), updateReq.getDealerCode());
// 验证关联的出库单是否存在且经销商一致(如果提供了出库单号)
if (StringUtils.hasText(updateReq.getDeliveryNo())) {
validateDeliveryConsistency(updateReq.getDeliveryNo(), updateReq.getDealerCode());
}
List<String> invoiceProductCodes = (updateReq.getInvoiceItems() == null ? java.util.Collections.emptyList()
: updateReq.getInvoiceItems().stream().map(InvoiceItemUpdateReq::getProductCode).collect(Collectors.toList()));
validateAtLeastOneItemMatchesOrder(updateReq.getOrderNo(), invoiceProductCodes);
validateAtLeastOneItemMatchesDelivery(updateReq.getDeliveryNo(), invoiceProductCodes);
boolean isValid = true;
// 时序逻辑验证:开票时间不得早于出库时间
......@@ -292,6 +319,52 @@ public class InvoiceMainServiceImpl extends ServiceImpl<InvoiceMainMapper, Invoi
}
}
/**
* 校验发票明细至少包含一项与关联订单对应的产品(按产品编码)。
*/
private void validateAtLeastOneItemMatchesOrder(String orderNo, List<String> invoiceProductCodes) {
if (invoiceProductCodes == null || invoiceProductCodes.isEmpty()) {
throw new IllegalArgumentException("发票明细必须至少包含一项与关联订单对应的产品明细,请检查后重新提交");
}
LambdaQueryWrapper<OrderItem> orderItemWrapper = Wrappers.lambdaQuery(OrderItem.class)
.eq(OrderItem::getOrderNo, orderNo)
.eq(OrderItem::getDelFlag, "0");
List<OrderItem> orderItems = orderItemMapper.selectList(orderItemWrapper);
if (orderItems == null || orderItems.isEmpty()) {
throw new IllegalArgumentException("发票明细必须至少包含一项与关联订单对应的产品明细,请检查后重新提交");
}
boolean hasAnyMatch = orderItems.stream()
.map(OrderItem::getProductCode)
.filter(code -> code != null && !code.isEmpty())
.anyMatch(invoiceProductCodes::contains);
if (!hasAnyMatch) {
throw new IllegalArgumentException("发票明细必须至少包含一项与关联订单对应的产品明细,请检查后重新提交");
}
}
/**
* 校验发票明细至少包含一项与关联出库单对应的产品(按产品编码)。
*/
private void validateAtLeastOneItemMatchesDelivery(String deliveryNo, List<String> invoiceProductCodes) {
if (invoiceProductCodes == null || invoiceProductCodes.isEmpty()) {
throw new IllegalArgumentException("发票明细必须至少包含一项与关联出库单对应的产品明细,请检查后重新提交");
}
LambdaQueryWrapper<DeliveryItem> deliveryItemWrapper = Wrappers.lambdaQuery(DeliveryItem.class)
.eq(DeliveryItem::getDeliveryNo, deliveryNo)
.eq(DeliveryItem::getDelFlag, "0");
List<DeliveryItem> deliveryItems = deliveryItemMapper.selectList(deliveryItemWrapper);
if (deliveryItems == null || deliveryItems.isEmpty()) {
throw new IllegalArgumentException("发票明细必须至少包含一项与关联出库单对应的产品明细,请检查后重新提交");
}
boolean hasAnyMatch = deliveryItems.stream()
.map(DeliveryItem::getProductCode)
.filter(code -> code != null && !code.isEmpty())
.anyMatch(invoiceProductCodes::contains);
if (!hasAnyMatch) {
throw new IllegalArgumentException("发票明细必须至少包含一项与关联出库单对应的产品明细,请检查后重新提交");
}
}
@Override
@Transactional
public boolean deleteInvoice(Long invoiceId) {
......
......@@ -1079,7 +1079,23 @@ const handleSubmitAdd = async () => {
fetchDeliveries() // 刷新列表
} catch (error) {
console.error('新增出库失败:', error)
showMessage('新增出库失败, 请重试', 'error')
// 优先展示后端返回的具体错误信息
const anyError: any = error as any
let errorMessage = '新增出库失败, 请重试'
if (anyError && anyError.response && anyError.response.data) {
const errorData = anyError.response.data
if (typeof errorData.message === 'string' && errorData.message.trim().length > 0) {
errorMessage = errorData.message
}
// 如果有字段级校验错误,拼接展示
if (errorData.data && typeof errorData.data === 'object') {
const fieldErrors = Object.values(errorData.data).filter((msg: any) => typeof msg === 'string') as string[]
if (fieldErrors.length > 0) {
errorMessage = fieldErrors.join('; ')
}
}
}
showMessage(errorMessage, 'error')
}
}
......@@ -1117,7 +1133,23 @@ const handleSubmitEdit = async () => {
fetchDeliveries() // 刷新列表
} catch (error) {
console.error('编辑出库失败:', error)
showMessage('编辑出库失败, 请重试', 'error')
// 优先展示后端返回的具体错误信息
const anyError: any = error as any
let errorMessage = '编辑出库失败, 请重试'
if (anyError && anyError.response && anyError.response.data) {
const errorData = anyError.response.data
if (typeof errorData.message === 'string' && errorData.message.trim().length > 0) {
errorMessage = errorData.message
}
// 如果有字段级校验错误,拼接展示
if (errorData.data && typeof errorData.data === 'object') {
const fieldErrors = Object.values(errorData.data).filter((msg: any) => typeof msg === 'string') as string[]
if (fieldErrors.length > 0) {
errorMessage = fieldErrors.join('; ')
}
}
}
showMessage(errorMessage, 'error')
}
}
......
......@@ -991,7 +991,23 @@ const handleSubmit = async () => {
fetchOrders()
} catch (error) {
console.error('保存订单失败:', error)
showMessage('保存订单失败, 请重试', 'error')
// 优先展示后端返回的具体错误信息
const anyError: any = error as any
let errorMessage = '保存订单失败, 请重试'
if (anyError && anyError.response && anyError.response.data) {
const errorData = anyError.response.data
if (typeof errorData.message === 'string' && errorData.message.trim().length > 0) {
errorMessage = errorData.message
}
// 如果有字段级校验错误,拼接展示
if (errorData.data && typeof errorData.data === 'object') {
const fieldErrors = Object.values(errorData.data).filter((msg: any) => typeof msg === 'string') as string[]
if (fieldErrors.length > 0) {
errorMessage = fieldErrors.join('; ')
}
}
}
showMessage(errorMessage, 'error')
}
}
......
{
"token": "eyJhbGciOiJIUzUxMiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwic3ViIjoiYWRtaW4iLCJpYXQiOjE3NjAzMjY2OTIsImV4cCI6MTc2MDQxMzA5Mn0.g_bbqdvMIIoPws2bOC8AJpFHYo-zSFgybXAEJUF3rvheltchQG1q_jZZSSw2tWLQvZOjzFeJxOROJwKbu5JnIQ",
"saved_at": 1760326692
"token": "eyJhbGciOiJIUzUxMiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwic3ViIjoiYWRtaW4iLCJpYXQiOjE3NjA0Mjc2NTEsImV4cCI6MTc2MDUxNDA1MX0.vtyVtnZ-g6QJB3CnB9yrB23oLxNkL5doezafBfgyT3byTSUOUD162ZEFw6K3aFT8wYkn25TVyTvLVHY9CufIgg",
"saved_at": 1760427651
}
\ No newline at end of file
......