zhouhui.jiang

update 添加验证

...@@ -31,7 +31,8 @@ public class InvoiceAddReq { ...@@ -31,7 +31,8 @@ public class InvoiceAddReq {
31 @Schema(description = "关联订单编号", required = true) 31 @Schema(description = "关联订单编号", required = true)
32 private String orderNo; 32 private String orderNo;
33 33
34 - @Schema(description = "关联出库单编号") 34 + @NotBlank(message = "关联出库单编号不能为空")
35 + @Schema(description = "关联出库单编号", required = true)
35 private String deliveryNo; 36 private String deliveryNo;
36 37
37 @NotBlank(message = "经销商编码不能为空") 38 @NotBlank(message = "经销商编码不能为空")
......
...@@ -35,7 +35,8 @@ public class InvoiceUpdateReq { ...@@ -35,7 +35,8 @@ public class InvoiceUpdateReq {
35 @Schema(description = "关联订单编号", required = true) 35 @Schema(description = "关联订单编号", required = true)
36 private String orderNo; 36 private String orderNo;
37 37
38 - @Schema(description = "关联出库单编号") 38 + @NotBlank(message = "关联出库单编号不能为空")
39 + @Schema(description = "关联出库单编号", required = true)
39 private String deliveryNo; 40 private String deliveryNo;
40 41
41 @NotBlank(message = "经销商编码不能为空") 42 @NotBlank(message = "经销商编码不能为空")
......
...@@ -2,11 +2,13 @@ package com.apple.erp.service.impl; ...@@ -2,11 +2,13 @@ package com.apple.erp.service.impl;
2 2
3 import com.apple.erp.dto.*; 3 import com.apple.erp.dto.*;
4 import com.apple.erp.entity.DeliveryItem; 4 import com.apple.erp.entity.DeliveryItem;
5 +import com.apple.erp.entity.OrderItem;
5 import com.apple.erp.entity.OrderMain; 6 import com.apple.erp.entity.OrderMain;
6 // rebate settlement handled by RebateSettlementService 7 // rebate settlement handled by RebateSettlementService
7 import com.apple.erp.service.RebateSettlementService; 8 import com.apple.erp.service.RebateSettlementService;
8 import com.apple.erp.mapper.DeliveryItemMapper; 9 import com.apple.erp.mapper.DeliveryItemMapper;
9 import com.apple.erp.mapper.OrderMainMapper; 10 import com.apple.erp.mapper.OrderMainMapper;
11 +import com.apple.erp.mapper.OrderItemMapper;
10 // removed unused mapper imports after delegating settlement logic 12 // removed unused mapper imports after delegating settlement logic
11 import com.apple.erp.util.TimingValidationUtil; 13 import com.apple.erp.util.TimingValidationUtil;
12 import com.apple.erp.exception.DealerMismatchException; 14 import com.apple.erp.exception.DealerMismatchException;
...@@ -48,6 +50,9 @@ public class DeliveryMainServiceImpl extends ServiceImpl<DeliveryMainMapper, Del ...@@ -48,6 +50,9 @@ public class DeliveryMainServiceImpl extends ServiceImpl<DeliveryMainMapper, Del
48 private OrderMainMapper orderMainMapper; 50 private OrderMainMapper orderMainMapper;
49 51
50 @Autowired 52 @Autowired
53 + private OrderItemMapper orderItemMapper;
54 +
55 + @Autowired
51 private TimingValidationUtil timingValidationUtil; 56 private TimingValidationUtil timingValidationUtil;
52 57
53 // rebate settlement is delegated; no local mapper usage required 58 // rebate settlement is delegated; no local mapper usage required
...@@ -157,6 +162,11 @@ public class DeliveryMainServiceImpl extends ServiceImpl<DeliveryMainMapper, Del ...@@ -157,6 +162,11 @@ public class DeliveryMainServiceImpl extends ServiceImpl<DeliveryMainMapper, Del
157 } 162 }
158 } 163 }
159 164
165 + // 业务校验:出库明细需与关联订单的商品至少部分匹配
166 + validateAtLeastOneItemMatchesOrder(addReq.getOrderNo(),
167 + addReq.getDeliveryItems() == null ? new ArrayList<>() :
168 + addReq.getDeliveryItems().stream().map(DeliveryItemAddReq::getProductCode).collect(Collectors.toList()));
169 +
160 DeliveryMain deliveryMain = new DeliveryMain(); 170 DeliveryMain deliveryMain = new DeliveryMain();
161 BeanUtils.copyProperties(addReq, deliveryMain); 171 BeanUtils.copyProperties(addReq, deliveryMain);
162 deliveryMain.setCreateTime(LocalDateTime.now()); 172 deliveryMain.setCreateTime(LocalDateTime.now());
...@@ -230,6 +240,11 @@ public class DeliveryMainServiceImpl extends ServiceImpl<DeliveryMainMapper, Del ...@@ -230,6 +240,11 @@ public class DeliveryMainServiceImpl extends ServiceImpl<DeliveryMainMapper, Del
230 } 240 }
231 } 241 }
232 242
243 + // 业务校验:出库明细需与关联订单的商品至少部分匹配
244 + validateAtLeastOneItemMatchesOrder(updateReq.getOrderNo(),
245 + updateReq.getDeliveryItems() == null ? new ArrayList<>() :
246 + updateReq.getDeliveryItems().stream().map(DeliveryItemUpdateReq::getProductCode).collect(Collectors.toList()));
247 +
233 DeliveryMain deliveryMain = new DeliveryMain(); 248 DeliveryMain deliveryMain = new DeliveryMain();
234 BeanUtils.copyProperties(updateReq, deliveryMain); 249 BeanUtils.copyProperties(updateReq, deliveryMain);
235 deliveryMain.setUpdateTime(LocalDateTime.now()); 250 deliveryMain.setUpdateTime(LocalDateTime.now());
...@@ -283,6 +298,39 @@ public class DeliveryMainServiceImpl extends ServiceImpl<DeliveryMainMapper, Del ...@@ -283,6 +298,39 @@ public class DeliveryMainServiceImpl extends ServiceImpl<DeliveryMainMapper, Del
283 } 298 }
284 } 299 }
285 300
301 + /**
302 + * 校验至少有一条出库明细与关联订单的商品明细匹配(按产品编码匹配)
303 + * 若完全不匹配则抛出业务异常。
304 + *
305 + * @param orderNo 订单编号
306 + * @param deliveryProductCodes 出库明细中的产品编码列表
307 + */
308 + private void validateAtLeastOneItemMatchesOrder(String orderNo, List<String> deliveryProductCodes) {
309 + if (deliveryProductCodes == null || deliveryProductCodes.isEmpty()) {
310 + throw new IllegalArgumentException("出库明细不可为空");
311 + }
312 +
313 + LambdaQueryWrapper<OrderItem> orderItemWrapper = Wrappers.lambdaQuery(OrderItem.class)
314 + .eq(OrderItem::getOrderNo, orderNo)
315 + .eq(OrderItem::getDelFlag, "0");
316 + List<OrderItem> orderItems = orderItemMapper.selectList(orderItemWrapper);
317 +
318 + if (orderItems == null || orderItems.isEmpty()) {
319 + // 订单无商品时,视为不匹配
320 + throw new IllegalArgumentException("关联的订单编号,商品明细项为空");
321 + }
322 +
323 + // 判定是否存在至少一条产品编码匹配
324 + boolean hasAnyMatch = orderItems.stream()
325 + .map(OrderItem::getProductCode)
326 + .filter(code -> code != null && !code.isEmpty())
327 + .anyMatch(deliveryProductCodes::contains);
328 +
329 + if (!hasAnyMatch) {
330 + throw new IllegalArgumentException("出库明细必须至少包含一项与关联订单对应的产品明细,请检查后重新提交");
331 + }
332 + }
333 +
286 // 结转逻辑已抽取到 RebateSettlementService 334 // 结转逻辑已抽取到 RebateSettlementService
287 335
288 @Override 336 @Override
......
...@@ -4,11 +4,15 @@ import com.apple.erp.dto.*; ...@@ -4,11 +4,15 @@ import com.apple.erp.dto.*;
4 import com.apple.erp.entity.InvoiceItem; 4 import com.apple.erp.entity.InvoiceItem;
5 import com.apple.erp.entity.OrderMain; 5 import com.apple.erp.entity.OrderMain;
6 import com.apple.erp.entity.DeliveryMain; 6 import com.apple.erp.entity.DeliveryMain;
7 +import com.apple.erp.entity.OrderItem;
8 +import com.apple.erp.entity.DeliveryItem;
7 // rebate settlement handled by RebateSettlementService 9 // rebate settlement handled by RebateSettlementService
8 import com.apple.erp.service.RebateSettlementService; 10 import com.apple.erp.service.RebateSettlementService;
9 import com.apple.erp.mapper.InvoiceItemMapper; 11 import com.apple.erp.mapper.InvoiceItemMapper;
10 import com.apple.erp.mapper.OrderMainMapper; 12 import com.apple.erp.mapper.OrderMainMapper;
11 import com.apple.erp.mapper.DeliveryMainMapper; 13 import com.apple.erp.mapper.DeliveryMainMapper;
14 +import com.apple.erp.mapper.OrderItemMapper;
15 +import com.apple.erp.mapper.DeliveryItemMapper;
12 // removed unused mapper imports after delegating settlement logic 16 // removed unused mapper imports after delegating settlement logic
13 import com.apple.erp.util.TimingValidationUtil; 17 import com.apple.erp.util.TimingValidationUtil;
14 import com.apple.erp.exception.InvoiceValidationException; 18 import com.apple.erp.exception.InvoiceValidationException;
...@@ -56,6 +60,12 @@ public class InvoiceMainServiceImpl extends ServiceImpl<InvoiceMainMapper, Invoi ...@@ -56,6 +60,12 @@ public class InvoiceMainServiceImpl extends ServiceImpl<InvoiceMainMapper, Invoi
56 @Autowired 60 @Autowired
57 private DeliveryMainMapper deliveryMainMapper; 61 private DeliveryMainMapper deliveryMainMapper;
58 62
63 + @Autowired
64 + private OrderItemMapper orderItemMapper;
65 +
66 + @Autowired
67 + private DeliveryItemMapper deliveryItemMapper;
68 +
59 // rebate settlement is delegated; no local mapper usage required 69 // rebate settlement is delegated; no local mapper usage required
60 70
61 @Autowired 71 @Autowired
...@@ -134,13 +144,23 @@ public class InvoiceMainServiceImpl extends ServiceImpl<InvoiceMainMapper, Invoi ...@@ -134,13 +144,23 @@ public class InvoiceMainServiceImpl extends ServiceImpl<InvoiceMainMapper, Invoi
134 throw new IllegalArgumentException("发票编号已存在"); 144 throw new IllegalArgumentException("发票编号已存在");
135 } 145 }
136 146
137 - // 验证关联的订单是否存在且经销商一致 147 + // 关联必填
148 + if (!StringUtils.hasText(addReq.getOrderNo())) {
149 + throw new IllegalArgumentException("关联订单编号不能为空");
150 + }
151 + if (!StringUtils.hasText(addReq.getDeliveryNo())) {
152 + throw new IllegalArgumentException("关联出库单编号不能为空");
153 + }
154 +
155 + // 基础一致性
138 validateOrderConsistency(addReq.getOrderNo(), addReq.getDealerCode()); 156 validateOrderConsistency(addReq.getOrderNo(), addReq.getDealerCode());
157 + validateDeliveryConsistency(addReq.getDeliveryNo(), addReq.getDealerCode());
139 158
140 - // 验证关联的出库单是否存在且经销商一致(如果提供了出库单号) 159 + // 收集发票明细产品编码并进行双重“至少一项匹配”校验
141 - if (StringUtils.hasText(addReq.getDeliveryNo())) { 160 + List<String> invoiceProductCodes = (addReq.getInvoiceItems() == null ? java.util.Collections.emptyList()
142 - validateDeliveryConsistency(addReq.getDeliveryNo(), addReq.getDealerCode()); 161 + : addReq.getInvoiceItems().stream().map(InvoiceItemAddReq::getProductCode).collect(Collectors.toList()));
143 - } 162 + validateAtLeastOneItemMatchesOrder(addReq.getOrderNo(), invoiceProductCodes);
163 + validateAtLeastOneItemMatchesDelivery(addReq.getDeliveryNo(), invoiceProductCodes);
144 164
145 boolean isValid = true; 165 boolean isValid = true;
146 // 时序逻辑验证:开票时间不得早于出库时间 166 // 时序逻辑验证:开票时间不得早于出库时间
...@@ -206,13 +226,20 @@ public class InvoiceMainServiceImpl extends ServiceImpl<InvoiceMainMapper, Invoi ...@@ -206,13 +226,20 @@ public class InvoiceMainServiceImpl extends ServiceImpl<InvoiceMainMapper, Invoi
206 throw new IllegalArgumentException("发票编号已存在"); 226 throw new IllegalArgumentException("发票编号已存在");
207 } 227 }
208 228
209 - // 验证关联的订单是否存在且经销商一致 229 + if (!StringUtils.hasText(updateReq.getOrderNo())) {
230 + throw new IllegalArgumentException("关联订单编号不能为空");
231 + }
232 + if (!StringUtils.hasText(updateReq.getDeliveryNo())) {
233 + throw new IllegalArgumentException("关联出库单编号不能为空");
234 + }
235 +
210 validateOrderConsistency(updateReq.getOrderNo(), updateReq.getDealerCode()); 236 validateOrderConsistency(updateReq.getOrderNo(), updateReq.getDealerCode());
237 + validateDeliveryConsistency(updateReq.getDeliveryNo(), updateReq.getDealerCode());
211 238
212 - // 验证关联的出库单是否存在且经销商一致(如果提供了出库单号) 239 + List<String> invoiceProductCodes = (updateReq.getInvoiceItems() == null ? java.util.Collections.emptyList()
213 - if (StringUtils.hasText(updateReq.getDeliveryNo())) { 240 + : updateReq.getInvoiceItems().stream().map(InvoiceItemUpdateReq::getProductCode).collect(Collectors.toList()));
214 - validateDeliveryConsistency(updateReq.getDeliveryNo(), updateReq.getDealerCode()); 241 + validateAtLeastOneItemMatchesOrder(updateReq.getOrderNo(), invoiceProductCodes);
215 - } 242 + validateAtLeastOneItemMatchesDelivery(updateReq.getDeliveryNo(), invoiceProductCodes);
216 243
217 boolean isValid = true; 244 boolean isValid = true;
218 // 时序逻辑验证:开票时间不得早于出库时间 245 // 时序逻辑验证:开票时间不得早于出库时间
...@@ -292,6 +319,52 @@ public class InvoiceMainServiceImpl extends ServiceImpl<InvoiceMainMapper, Invoi ...@@ -292,6 +319,52 @@ public class InvoiceMainServiceImpl extends ServiceImpl<InvoiceMainMapper, Invoi
292 } 319 }
293 } 320 }
294 321
322 + /**
323 + * 校验发票明细至少包含一项与关联订单对应的产品(按产品编码)。
324 + */
325 + private void validateAtLeastOneItemMatchesOrder(String orderNo, List<String> invoiceProductCodes) {
326 + if (invoiceProductCodes == null || invoiceProductCodes.isEmpty()) {
327 + throw new IllegalArgumentException("发票明细必须至少包含一项与关联订单对应的产品明细,请检查后重新提交");
328 + }
329 + LambdaQueryWrapper<OrderItem> orderItemWrapper = Wrappers.lambdaQuery(OrderItem.class)
330 + .eq(OrderItem::getOrderNo, orderNo)
331 + .eq(OrderItem::getDelFlag, "0");
332 + List<OrderItem> orderItems = orderItemMapper.selectList(orderItemWrapper);
333 + if (orderItems == null || orderItems.isEmpty()) {
334 + throw new IllegalArgumentException("发票明细必须至少包含一项与关联订单对应的产品明细,请检查后重新提交");
335 + }
336 + boolean hasAnyMatch = orderItems.stream()
337 + .map(OrderItem::getProductCode)
338 + .filter(code -> code != null && !code.isEmpty())
339 + .anyMatch(invoiceProductCodes::contains);
340 + if (!hasAnyMatch) {
341 + throw new IllegalArgumentException("发票明细必须至少包含一项与关联订单对应的产品明细,请检查后重新提交");
342 + }
343 + }
344 +
345 + /**
346 + * 校验发票明细至少包含一项与关联出库单对应的产品(按产品编码)。
347 + */
348 + private void validateAtLeastOneItemMatchesDelivery(String deliveryNo, List<String> invoiceProductCodes) {
349 + if (invoiceProductCodes == null || invoiceProductCodes.isEmpty()) {
350 + throw new IllegalArgumentException("发票明细必须至少包含一项与关联出库单对应的产品明细,请检查后重新提交");
351 + }
352 + LambdaQueryWrapper<DeliveryItem> deliveryItemWrapper = Wrappers.lambdaQuery(DeliveryItem.class)
353 + .eq(DeliveryItem::getDeliveryNo, deliveryNo)
354 + .eq(DeliveryItem::getDelFlag, "0");
355 + List<DeliveryItem> deliveryItems = deliveryItemMapper.selectList(deliveryItemWrapper);
356 + if (deliveryItems == null || deliveryItems.isEmpty()) {
357 + throw new IllegalArgumentException("发票明细必须至少包含一项与关联出库单对应的产品明细,请检查后重新提交");
358 + }
359 + boolean hasAnyMatch = deliveryItems.stream()
360 + .map(DeliveryItem::getProductCode)
361 + .filter(code -> code != null && !code.isEmpty())
362 + .anyMatch(invoiceProductCodes::contains);
363 + if (!hasAnyMatch) {
364 + throw new IllegalArgumentException("发票明细必须至少包含一项与关联出库单对应的产品明细,请检查后重新提交");
365 + }
366 + }
367 +
295 @Override 368 @Override
296 @Transactional 369 @Transactional
297 public boolean deleteInvoice(Long invoiceId) { 370 public boolean deleteInvoice(Long invoiceId) {
......
...@@ -1079,7 +1079,23 @@ const handleSubmitAdd = async () => { ...@@ -1079,7 +1079,23 @@ const handleSubmitAdd = async () => {
1079 fetchDeliveries() // 刷新列表 1079 fetchDeliveries() // 刷新列表
1080 } catch (error) { 1080 } catch (error) {
1081 console.error('新增出库失败:', error) 1081 console.error('新增出库失败:', error)
1082 - showMessage('新增出库失败, 请重试', 'error') 1082 + // 优先展示后端返回的具体错误信息
1083 + const anyError: any = error as any
1084 + let errorMessage = '新增出库失败, 请重试'
1085 + if (anyError && anyError.response && anyError.response.data) {
1086 + const errorData = anyError.response.data
1087 + if (typeof errorData.message === 'string' && errorData.message.trim().length > 0) {
1088 + errorMessage = errorData.message
1089 + }
1090 + // 如果有字段级校验错误,拼接展示
1091 + if (errorData.data && typeof errorData.data === 'object') {
1092 + const fieldErrors = Object.values(errorData.data).filter((msg: any) => typeof msg === 'string') as string[]
1093 + if (fieldErrors.length > 0) {
1094 + errorMessage = fieldErrors.join('; ')
1095 + }
1096 + }
1097 + }
1098 + showMessage(errorMessage, 'error')
1083 } 1099 }
1084 } 1100 }
1085 1101
...@@ -1117,7 +1133,23 @@ const handleSubmitEdit = async () => { ...@@ -1117,7 +1133,23 @@ const handleSubmitEdit = async () => {
1117 fetchDeliveries() // 刷新列表 1133 fetchDeliveries() // 刷新列表
1118 } catch (error) { 1134 } catch (error) {
1119 console.error('编辑出库失败:', error) 1135 console.error('编辑出库失败:', error)
1120 - showMessage('编辑出库失败, 请重试', 'error') 1136 + // 优先展示后端返回的具体错误信息
1137 + const anyError: any = error as any
1138 + let errorMessage = '编辑出库失败, 请重试'
1139 + if (anyError && anyError.response && anyError.response.data) {
1140 + const errorData = anyError.response.data
1141 + if (typeof errorData.message === 'string' && errorData.message.trim().length > 0) {
1142 + errorMessage = errorData.message
1143 + }
1144 + // 如果有字段级校验错误,拼接展示
1145 + if (errorData.data && typeof errorData.data === 'object') {
1146 + const fieldErrors = Object.values(errorData.data).filter((msg: any) => typeof msg === 'string') as string[]
1147 + if (fieldErrors.length > 0) {
1148 + errorMessage = fieldErrors.join('; ')
1149 + }
1150 + }
1151 + }
1152 + showMessage(errorMessage, 'error')
1121 } 1153 }
1122 } 1154 }
1123 1155
......
...@@ -991,7 +991,23 @@ const handleSubmit = async () => { ...@@ -991,7 +991,23 @@ const handleSubmit = async () => {
991 fetchOrders() 991 fetchOrders()
992 } catch (error) { 992 } catch (error) {
993 console.error('保存订单失败:', error) 993 console.error('保存订单失败:', error)
994 - showMessage('保存订单失败, 请重试', 'error') 994 + // 优先展示后端返回的具体错误信息
995 + const anyError: any = error as any
996 + let errorMessage = '保存订单失败, 请重试'
997 + if (anyError && anyError.response && anyError.response.data) {
998 + const errorData = anyError.response.data
999 + if (typeof errorData.message === 'string' && errorData.message.trim().length > 0) {
1000 + errorMessage = errorData.message
1001 + }
1002 + // 如果有字段级校验错误,拼接展示
1003 + if (errorData.data && typeof errorData.data === 'object') {
1004 + const fieldErrors = Object.values(errorData.data).filter((msg: any) => typeof msg === 'string') as string[]
1005 + if (fieldErrors.length > 0) {
1006 + errorMessage = fieldErrors.join('; ')
1007 + }
1008 + }
1009 + }
1010 + showMessage(errorMessage, 'error')
995 } 1011 }
996 } 1012 }
997 1013
......
1 { 1 {
2 - "token": "eyJhbGciOiJIUzUxMiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwic3ViIjoiYWRtaW4iLCJpYXQiOjE3NjAzMjY2OTIsImV4cCI6MTc2MDQxMzA5Mn0.g_bbqdvMIIoPws2bOC8AJpFHYo-zSFgybXAEJUF3rvheltchQG1q_jZZSSw2tWLQvZOjzFeJxOROJwKbu5JnIQ", 2 + "token": "eyJhbGciOiJIUzUxMiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwic3ViIjoiYWRtaW4iLCJpYXQiOjE3NjA0Mjc2NTEsImV4cCI6MTc2MDUxNDA1MX0.vtyVtnZ-g6QJB3CnB9yrB23oLxNkL5doezafBfgyT3byTSUOUD162ZEFw6K3aFT8wYkn25TVyTvLVHY9CufIgg",
3 - "saved_at": 1760326692 3 + "saved_at": 1760427651
4 } 4 }
...\ No newline at end of file ...\ No newline at end of file
......