From 6e07d9a98b83263e92137be736e70e9884f49366 Mon Sep 17 00:00:00 2001 From: caojia <1301869380@qq.com> Date: Mon, 21 Sep 2026 17:59:09 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B4=A6=E5=8D=95bug=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=EF=BC=8C=E4=BB=A5=E5=8F=8A=E5=8A=A0=E5=85=A5=E8=BD=AC=E5=8D=95?= =?UTF-8?q?=E5=8F=B7=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/controller/ar/ArBillController.java | 35 ++++++- .../serializer/StrictDateDeserializer.java | 49 +++++++++ .../web/exception/GlobalExceptionHandler.java | 30 +++--- .../com/ruoyi/ar/domain/ArBillDetail.java | 2 + .../ar/domain/vo/ArBillGenerateQuery.java | 4 +- .../ar/domain/vo/ArBillGenerateRequest.java | 9 +- .../impl/ArBalanceWriteoffServiceImpl.java | 3 + .../ar/service/impl/ArBillServiceImpl.java | 13 ++- .../mapper/ar/ArBillDetailMapper.xml | 17 ++-- .../mapper/price/PrcOrderBillMapper.xml | 20 ++-- bms-back/sql/ar/ar_bill.sql | 2 + .../sql/ar/ar_bill_transfer_no_migration.sql | 49 +++++++++ .../sql/ar/ar_export_template_restore.sql | 99 +++++++++++++++++++ bms-front/src/components/BasicTable/index.vue | 4 + bms-front/src/router/index.js | 15 +++ .../views/ar/bill/components/BillDetail.vue | 4 +- .../ar/bill/components/GenerateBillDialog.vue | 6 +- .../customerinfo/customermoney/index.vue | 58 ++++++++--- 18 files changed, 366 insertions(+), 53 deletions(-) create mode 100644 bms-back/ruoyi-common/src/main/java/com/ruoyi/common/config/serializer/StrictDateDeserializer.java create mode 100644 bms-back/sql/ar/ar_bill_transfer_no_migration.sql create mode 100644 bms-back/sql/ar/ar_export_template_restore.sql diff --git a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ar/ArBillController.java b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ar/ArBillController.java index 4d2a753c..b1186477 100644 --- a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ar/ArBillController.java +++ b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ar/ArBillController.java @@ -1,12 +1,17 @@ package com.ruoyi.web.controller.ar; import java.util.List; +import java.util.Date; +import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.WebDataBinder; import org.springframework.validation.annotation.Validated; +import org.springframework.beans.propertyeditors.CustomDateEditor; +import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -31,6 +36,7 @@ import com.ruoyi.common.annotation.Log; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.uuid.IdUtils; @@ -49,12 +55,23 @@ public class ArBillController extends BaseController { @Autowired private IArBillService billService; + /** + * 查询参数中的日期必须严格按 yyyy-MM-dd 解析,禁止非法日期自动滚动到其他日期。 + */ + @InitBinder + public void initDateBinder(WebDataBinder binder) { + java.text.SimpleDateFormat formatter = new java.text.SimpleDateFormat("yyyy-MM-dd"); + formatter.setLenient(false); + binder.registerCustomEditor(Date.class, new CustomDateEditor(formatter, true)); + } + /** * 查询账单列表;分页参数由 BaseController.startPage 交给 MyBatis PageHelper 处理。 */ @PreAuthorize("@ss.hasPermi('ar:bill:list')") @GetMapping("/list") - public TableDataInfo list(ArBillQuery query) { + public TableDataInfo list(ArBillQuery query, HttpServletRequest request) { + rejectMultiValueParameter(request, "customerId"); startPage(); List list = billService.selectArBillList(query); return getDataTable(list); @@ -104,7 +121,8 @@ public class ArBillController extends BaseController { */ @PreAuthorize("@ss.hasPermi('ar:bill:generate')") @GetMapping("/generate/preview") - public AjaxResult preview(ArBillGenerateQuery query) { + public AjaxResult preview(ArBillGenerateQuery query, HttpServletRequest request) { + rejectMultiValueParameter(request, "customerId"); startPage(); List list = billService.previewBill(query); TableDataInfo pageData = getDataTable(list); @@ -252,4 +270,17 @@ public class ArBillController extends BaseController { if (StringUtils.isNotEmpty(headerRequestId)) return headerRequestId; return IdUtils.fastSimpleUUID(); } + + /** + * 禁止把 customerId 以数组参数或重复参数提交,避免参数绑定失败后悄悄退化为无客户条件查询。 + */ + private void rejectMultiValueParameter(HttpServletRequest request, String parameterName) { + if (request.getParameterMap().containsKey(parameterName + "[]")) { + throw new ServiceException("查询参数[" + parameterName + "]格式不正确,请传入单个客户ID"); + } + String[] values = request.getParameterValues(parameterName); + if (values != null && values.length > 1) { + throw new ServiceException("查询参数[" + parameterName + "]只能传入一个值"); + } + } } diff --git a/bms-back/ruoyi-common/src/main/java/com/ruoyi/common/config/serializer/StrictDateDeserializer.java b/bms-back/ruoyi-common/src/main/java/com/ruoyi/common/config/serializer/StrictDateDeserializer.java new file mode 100644 index 00000000..7694d977 --- /dev/null +++ b/bms-back/ruoyi-common/src/main/java/com/ruoyi/common/config/serializer/StrictDateDeserializer.java @@ -0,0 +1,49 @@ +package com.ruoyi.common.config.serializer; + +import java.io.IOException; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.format.ResolverStyle; +import java.util.Date; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; + +/** + * 严格解析 yyyy-MM-dd 日期,避免 SimpleDateFormat 的宽松解析把非法日期自动滚动成其他日期。 + */ +public class StrictDateDeserializer extends JsonDeserializer +{ + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd") + .withResolverStyle(ResolverStyle.STRICT); + + @Override + public Date deserialize(JsonParser parser, DeserializationContext context) throws IOException + { + if (!parser.hasToken(JsonToken.VALUE_STRING)) + { + throw InvalidFormatException.from(parser, "日期必须使用 yyyy-MM-dd 格式", parser.getValueAsString(), Date.class); + } + + String value = parser.getText(); + if (value == null || value.trim().isEmpty()) + { + return null; + } + + try + { + LocalDate date = LocalDate.parse(value.trim(), FORMATTER); + return Date.from(date.atStartOfDay(ZoneId.systemDefault()).toInstant()); + } + catch (DateTimeParseException ex) + { + throw InvalidFormatException.from(parser, "日期必须是有效的 yyyy-MM-dd 日期", value, Date.class); + } + } +} diff --git a/bms-back/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java b/bms-back/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java index 1ca02831..1a0ecb80 100644 --- a/bms-back/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java +++ b/bms-back/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java @@ -6,6 +6,7 @@ import org.slf4j.LoggerFactory; import org.springframework.security.access.AccessDeniedException; import org.springframework.validation.BindException; import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingPathVariableException; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -13,11 +14,9 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import com.ruoyi.common.constant.HttpStatus; import com.ruoyi.common.core.domain.AjaxResult; -import com.ruoyi.common.core.text.Convert; import com.ruoyi.common.exception.DemoModeException; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.StringUtils; -import com.ruoyi.common.utils.html.EscapeUtil; /** * 全局异常处理器 @@ -81,13 +80,19 @@ public class GlobalExceptionHandler public AjaxResult handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) { String requestURI = request.getRequestURI(); - String value = Convert.toStr(e.getValue()); - if (StringUtils.isNotEmpty(value)) - { - value = EscapeUtil.clean(value); - } log.error("请求参数类型不匹配'{}',发生系统异常.", requestURI, e); - return AjaxResult.error(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getName(), e.getRequiredType().getName(), value)); + return AjaxResult.error(String.format("请求参数[%s]格式不正确,请检查输入内容", e.getName())); + } + + /** + * 请求体无法解析时只返回用户可处理的提示,避免把 Jackson 或数据库内部信息直接返回前端。 + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + public AjaxResult handleHttpMessageNotReadableException(HttpMessageNotReadableException e, + HttpServletRequest request) + { + log.error("请求地址'{}',请求体格式不正确.", request.getRequestURI(), e); + return AjaxResult.error("请求参数格式不正确,请检查日期、金额等字段"); } /** @@ -98,7 +103,7 @@ public class GlobalExceptionHandler { String requestURI = request.getRequestURI(); log.error("请求地址'{}',发生未知异常.", requestURI, e); - return AjaxResult.error(e.getMessage()); + return AjaxResult.error("系统处理异常,请稍后重试"); } /** @@ -109,7 +114,7 @@ public class GlobalExceptionHandler { String requestURI = request.getRequestURI(); log.error("请求地址'{}',发生系统异常.", requestURI, e); - return AjaxResult.error(e.getMessage()); + return AjaxResult.error("系统处理异常,请稍后重试"); } /** @@ -119,7 +124,7 @@ public class GlobalExceptionHandler public AjaxResult handleBindException(BindException e) { log.error(e.getMessage(), e); - String message = e.getAllErrors().get(0).getDefaultMessage(); + String message = e.getAllErrors().isEmpty() ? "请求参数校验失败" : e.getAllErrors().get(0).getDefaultMessage(); return AjaxResult.error(message); } @@ -130,7 +135,8 @@ public class GlobalExceptionHandler public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { log.error(e.getMessage(), e); - String message = e.getBindingResult().getFieldError().getDefaultMessage(); + String message = e.getBindingResult().getFieldError() == null + ? "请求参数校验失败" : e.getBindingResult().getFieldError().getDefaultMessage(); return AjaxResult.error(message); } diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBillDetail.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBillDetail.java index 94cb66d9..2c8b2d03 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBillDetail.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBillDetail.java @@ -21,6 +21,8 @@ public class ArBillDetail implements Serializable { private Integer detailNo; private String orderNo; private String trackingNo; + /** 转单号快照;账单生成时从来源计费订单复制,保证历史账单可追溯。 */ + private String transferNo; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date receiveTime; private String productCode; diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArBillGenerateQuery.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArBillGenerateQuery.java index 911b768b..e2901755 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArBillGenerateQuery.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArBillGenerateQuery.java @@ -17,9 +17,9 @@ public class ArBillGenerateQuery private String currency; - /** 订单号或运单号关键词,按 OR 条件模糊查询。 */ + /** 订单号、运单号或转单号关键词,按 OR 条件模糊查询。 */ private String orderOrTrackingNo; - /** 全选模式下,排除弹窗使用的附加订单号或运单号关键词。 */ + /** 全选模式下,排除弹窗使用的附加订单号、运单号或转单号关键词。 */ private String excludeOrderOrTrackingNo; } diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArBillGenerateRequest.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArBillGenerateRequest.java index 26939407..65a86ffb 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArBillGenerateRequest.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArBillGenerateRequest.java @@ -3,8 +3,11 @@ package com.ruoyi.ar.domain.vo; import java.util.Date; import java.util.List; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import com.ruoyi.common.config.serializer.StrictDateDeserializer; import com.ruoyi.ar.enums.ArBillSelectionMode; import lombok.Data; @@ -15,11 +18,13 @@ public class ArBillGenerateRequest @NotNull(message = "客户不能为空") private Long customerId; @NotBlank(message = "账单名称不能为空") + @Size(max = 100, message = "账单名称不能超过100个字符") private String billName; private String customerCode; private String customerName; @NotNull(message = "账单截止日不能为空") @JsonFormat(pattern = "yyyy-MM-dd") + @JsonDeserialize(using = StrictDateDeserializer.class) private Date billPeriodEnd; /** 手动模式选中的来源订单 ID;全选模式为空。 */ private List sourceBillIds; @@ -28,13 +33,15 @@ public class ArBillGenerateRequest /** 全选模式中用户明确排除的来源订单 ID。 */ private List excludedSourceBillIds; @JsonFormat(pattern = "yyyy-MM-dd") + @JsonDeserialize(using = StrictDateDeserializer.class) private Date billDate; private String currency = "CNY"; - /** 订单号或运单号关键词,生成时用于再次校验筛选条件。 */ + /** 订单号、运单号或转单号关键词,生成时用于再次校验筛选条件。 */ private String orderOrTrackingNo; private String bankAccountName; private String bankAccountNo; private String bankName; + @Size(max = 500, message = "备注不能超过500个字符") private String remark; /** 前端幂等号;重复提交同一请求时复用原任务。 */ private String requestId; diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBalanceWriteoffServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBalanceWriteoffServiceImpl.java index d408bcde..40df1189 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBalanceWriteoffServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBalanceWriteoffServiceImpl.java @@ -90,6 +90,9 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService { throw new ServiceException("核销金额必须大于0"); } amount = amount.setScale(MONEY_SCALE, RoundingMode.HALF_UP); + if (amount.compareTo(BigDecimal.ZERO) <= 0) { + throw new ServiceException("核销金额保留两位小数后必须大于0"); + } // 1. 行锁锁定账单(加锁顺序:账单 -> 账户,与反核销保持一致) ArBill bill = billMapper.selectArBillByIdForUpdate(cmd.getArBillId()); diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBillServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBillServiceImpl.java index a13e2bbd..3636a669 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBillServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBillServiceImpl.java @@ -984,6 +984,7 @@ public class ArBillServiceImpl implements IArBillService { detail.setDetailNo(detailNo); detail.setOrderNo(source.getOrderNo()); detail.setTrackingNo(source.getTrackingNo()); + detail.setTransferNo(source.getTransferNo()); detail.setReceiveTime(source.getOrderTime()); detail.setProductCode(source.getProductCode()); detail.setProductName(source.getProductName()); @@ -1631,15 +1632,23 @@ public class ArBillServiceImpl implements IArBillService { } private void validatePeriod(ArBillGenerateRequest request) { - if (request == null || request.getCustomerId() == null || StringUtils.isEmpty(request.getBillName()) + if (request == null || request.getCustomerId() == null || request.getCustomerId() <= 0 + || StringUtils.isEmpty(request.getBillName()) || request.getBillName().trim().isEmpty() || request.getBillPeriodEnd() == null) { throw new ServiceException("客户、账单名称和账单截止日不能为空"); } + if (request.getBillName().trim().length() > 100) { + throw new ServiceException("账单名称不能超过100个字符"); + } + if (StringUtils.isNotEmpty(request.getRemark()) && request.getRemark().trim().length() > 500) { + throw new ServiceException("备注不能超过500个字符"); + } } private void validatePreviewQuery(ArBillGenerateQuery query) { - if (query == null || query.getCustomerId() == null || query.getBillPeriodEnd() == null) { + if (query == null || query.getCustomerId() == null || query.getCustomerId() <= 0 + || query.getBillPeriodEnd() == null) { throw new ServiceException("客户和账单截止日不能为空"); } } diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBillDetailMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBillDetailMapper.xml index 73acf498..e0743705 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBillDetailMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBillDetailMapper.xml @@ -11,6 +11,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -32,7 +33,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - select d.detail_id, d.ar_bill_id, d.prc_order_bill_id, d.detail_no, d.order_no, d.tracking_no, d.receive_time, + select d.detail_id, d.ar_bill_id, d.prc_order_bill_id, d.detail_no, d.order_no, d.tracking_no, d.transfer_no, d.receive_time, d.product_code, d.product_name, d.goods_description, d.country_code, d.country_name, d.dest_postcode, d.actual_weight, d.volume_weight, d.charge_weight, d.pkg_length, d.pkg_width, d.pkg_height, d.currency, d.total_amount, d.remark, d.create_by, d.create_time, @@ -94,7 +95,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - insert into ar_bill_detail (ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, receive_time, + insert into ar_bill_detail (ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, transfer_no, receive_time, product_code, product_name, goods_description, country_code, country_name, dest_postcode, actual_weight, volume_weight, charge_weight, pkg_length, pkg_width, pkg_height, currency, total_amount, remark, create_by, create_time) - values (#{arBillId}, #{prcOrderBillId}, #{detailNo}, #{orderNo}, #{trackingNo}, #{receiveTime}, + values (#{arBillId}, #{prcOrderBillId}, #{detailNo}, #{orderNo}, #{trackingNo}, #{transferNo}, #{receiveTime}, #{productCode}, #{productName}, #{goodsDescription}, #{countryCode}, #{countryName}, #{destPostcode}, #{actualWeight}, #{volumeWeight}, #{chargeWeight}, #{pkgLength}, #{pkgWidth}, #{pkgHeight}, #{currency}, #{totalAmount}, #{remark}, #{createBy}, sysdate()) @@ -120,14 +121,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" insert into ar_bill_detail ( - ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, receive_time, product_code, + ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, transfer_no, receive_time, product_code, product_name, goods_description, country_code, country_name, dest_postcode, actual_weight, volume_weight, charge_weight, pkg_length, pkg_width, pkg_height, currency, total_amount, remark, create_by, create_time ) values (#{item.arBillId}, #{item.prcOrderBillId}, #{item.detailNo}, #{item.orderNo}, #{item.trackingNo}, - #{item.receiveTime}, #{item.productCode}, #{item.productName}, #{item.goodsDescription}, + #{item.transferNo}, #{item.receiveTime}, #{item.productCode}, #{item.productName}, #{item.goodsDescription}, #{item.countryCode}, #{item.countryName}, #{item.destPostcode}, #{item.actualWeight}, #{item.volumeWeight}, #{item.chargeWeight}, #{item.pkgLength}, #{item.pkgWidth}, #{item.pkgHeight}, #{item.currency}, #{item.totalAmount}, #{item.remark}, #{item.createBy}, sysdate()) diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcOrderBillMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcOrderBillMapper.xml index 6e1b3797..de1eb16a 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcOrderBillMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcOrderBillMapper.xml @@ -225,7 +225,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - +