feat(core): 增强文件上传配置和异常处理
- 配置文件上传大小限制,单个文件最大10MB,请求最大50MB - 添加文件写入磁盘阈值配置,超过2MB写入临时文件 - 实现文件上传超限异常处理,返回友好提示信息 - 优化应用启动日志,显示访问地址和运行环境信息 - 增加支付订单查询和更新的日志记录 - 创建阿里云OSS配置属性类,统一管理OSS参数 - 添加业务异常类,支持自定义错误码和消息 - 完善系统架构文档,描述前后端包结构和核心组件 - 新增商品创建请求DTO,支持SKU列表和校验规则 - 添加风控相关的客户信息和商品信息DTO - 配置Logback日志框架,支持不同环境的日志输出策略
This commit is contained in:
@@ -1,13 +1,42 @@
|
||||
package com.mtkj.mtpay;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
@Slf4j
|
||||
@SpringBootApplication
|
||||
public class MtPayApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MtPayApplication.class, args);
|
||||
try {
|
||||
SpringApplication app = new SpringApplication(MtPayApplication.class);
|
||||
Environment env = app.run(args).getEnvironment();
|
||||
|
||||
String applicationName = env.getProperty("spring.application.name", "mt-pay");
|
||||
String serverPort = env.getProperty("server.port", "8080");
|
||||
String contextPath = env.getProperty("server.servlet.context-path", "");
|
||||
String activeProfiles = String.join(",", env.getActiveProfiles());
|
||||
|
||||
log.info("""
|
||||
|
||||
========================================
|
||||
应用启动成功!
|
||||
========================================
|
||||
应用名称: {}
|
||||
运行环境: {}
|
||||
访问地址: http://localhost:{}{}
|
||||
========================================
|
||||
""",
|
||||
applicationName,
|
||||
activeProfiles.isEmpty() ? "default" : activeProfiles,
|
||||
serverPort,
|
||||
contextPath);
|
||||
} catch (Exception e) {
|
||||
log.error("应用启动失败", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.mtkj.mtpay.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 阿里云OSS配置属性
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "aliyun.oss")
|
||||
public class AliyunOSSProperties {
|
||||
|
||||
/**
|
||||
* AccessKeyId
|
||||
*/
|
||||
private String accessId;
|
||||
|
||||
/**
|
||||
* AccessKeySecret
|
||||
*/
|
||||
private String accessKey;
|
||||
|
||||
/**
|
||||
* Endpoint(地域节点)
|
||||
*/
|
||||
private String endpoint;
|
||||
|
||||
/**
|
||||
* Bucket名称
|
||||
*/
|
||||
private String bucketName;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.mtkj.mtpay.dto.request;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 创建商品请求DTO
|
||||
*/
|
||||
@Data
|
||||
public class CreateProductRequestDTO implements Serializable {
|
||||
|
||||
/**
|
||||
* 商品名称
|
||||
*/
|
||||
@NotBlank(message = "商品名称不能为空")
|
||||
@Size(max = 255, message = "商品名称长度不能超过255")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 商品价格(基础价格)
|
||||
*/
|
||||
@NotNull(message = "商品价格不能为空")
|
||||
private BigDecimal price;
|
||||
|
||||
/**
|
||||
* 主图URL
|
||||
*/
|
||||
@Size(max = 4000, message = "主图URL长度不能超过4000")
|
||||
private String mainImage;
|
||||
|
||||
/**
|
||||
* 商品状态:ACTIVE-上架,INACTIVE-下架
|
||||
*/
|
||||
private String status = "ACTIVE";
|
||||
|
||||
/**
|
||||
* 店铺ID
|
||||
*/
|
||||
@NotNull(message = "店铺ID不能为空")
|
||||
private Long shopId;
|
||||
|
||||
/**
|
||||
* SKU列表
|
||||
*/
|
||||
@Valid
|
||||
@NotNull(message = "SKU列表不能为空")
|
||||
@Size(min = 1, message = "至少需要一个SKU")
|
||||
private List<CreateProductSkuDTO> skus;
|
||||
|
||||
/**
|
||||
* SKU DTO
|
||||
*/
|
||||
@Data
|
||||
public static class CreateProductSkuDTO implements Serializable {
|
||||
|
||||
/**
|
||||
* SKU编码
|
||||
*/
|
||||
@NotBlank(message = "SKU编码不能为空")
|
||||
@Size(max = 2000, message = "SKU编码长度不能超过2000")
|
||||
private String sku;
|
||||
|
||||
/**
|
||||
* 价格
|
||||
*/
|
||||
@NotNull(message = "SKU价格不能为空")
|
||||
private BigDecimal price;
|
||||
|
||||
/**
|
||||
* 货币(ISO 4217三位币种代码)
|
||||
*/
|
||||
@NotBlank(message = "货币不能为空")
|
||||
@Size(min = 3, max = 3, message = "货币必须为3位ISO 4217代码")
|
||||
private String currency = "USD";
|
||||
|
||||
/**
|
||||
* 库存数量
|
||||
*/
|
||||
@NotNull(message = "库存数量不能为空")
|
||||
private Integer stock = 0;
|
||||
|
||||
/**
|
||||
* 销售属性(JSON格式)
|
||||
*/
|
||||
private String salesAttrs;
|
||||
|
||||
/**
|
||||
* SKU图片URL
|
||||
*/
|
||||
@Size(max = 4000, message = "SKU图片URL长度不能超过4000")
|
||||
private String skuImage;
|
||||
|
||||
/**
|
||||
* 重量(单位:克)
|
||||
*/
|
||||
private BigDecimal weight;
|
||||
|
||||
/**
|
||||
* 大小/尺寸(JSON格式)
|
||||
*/
|
||||
@Size(max = 200, message = "尺寸JSON长度不能超过200")
|
||||
private String size;
|
||||
|
||||
/**
|
||||
* 规格(文本描述)
|
||||
*/
|
||||
@Size(max = 2000, message = "规格长度不能超过2000")
|
||||
private String specification;
|
||||
|
||||
/**
|
||||
* SKU状态:ACTIVE-启用,INACTIVE-禁用
|
||||
*/
|
||||
private String status = "ACTIVE";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.mtkj.mtpay.dto.risk;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 客户信息DTO
|
||||
*/
|
||||
@Data
|
||||
public class CustomerDTO implements Serializable {
|
||||
|
||||
private String customerId;
|
||||
|
||||
private String firstName;
|
||||
|
||||
private String lastName;
|
||||
|
||||
private String email;
|
||||
|
||||
private String domain;
|
||||
|
||||
private String phone;
|
||||
|
||||
private String mobile;
|
||||
|
||||
private String workPhone;
|
||||
|
||||
private String identificationType;
|
||||
|
||||
private String identificationId;
|
||||
|
||||
private String registerTime;
|
||||
|
||||
private String registerIp;
|
||||
|
||||
private String registerTerminal;
|
||||
|
||||
private String registerCountry;
|
||||
|
||||
private String registerRange;
|
||||
|
||||
private String orderTime;
|
||||
|
||||
private String orderIp;
|
||||
|
||||
private String orderCountry;
|
||||
|
||||
private String payIp;
|
||||
|
||||
private String payCountry;
|
||||
|
||||
private String loginTime;
|
||||
|
||||
private String loginIp;
|
||||
|
||||
private String lastPayTime;
|
||||
|
||||
private String acquisitionChannel;
|
||||
|
||||
private String firstOrder;
|
||||
|
||||
private String nonMemberOrder;
|
||||
|
||||
private String preferentialOrder;
|
||||
|
||||
private String birthDate;
|
||||
|
||||
private String customerStatus;
|
||||
}
|
||||
|
||||
27
mt-pay/src/main/java/com/mtkj/mtpay/dto/risk/GoodsDTO.java
Normal file
27
mt-pay/src/main/java/com/mtkj/mtpay/dto/risk/GoodsDTO.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.mtkj.mtpay.dto.risk;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 商品信息DTO
|
||||
*/
|
||||
@Data
|
||||
public class GoodsDTO implements Serializable {
|
||||
|
||||
private String name;
|
||||
|
||||
private String description;
|
||||
|
||||
private String sku;
|
||||
|
||||
private String averageUnitPrice;
|
||||
|
||||
private String number;
|
||||
|
||||
private String virtualProduct;
|
||||
|
||||
private String imgUrl;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.mtkj.mtpay.exception;
|
||||
|
||||
import com.mtkj.mtpay.common.ResultCode;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 业务异常
|
||||
*/
|
||||
@Getter
|
||||
public class BusinessException extends RuntimeException {
|
||||
|
||||
private final String code;
|
||||
|
||||
public BusinessException(String message) {
|
||||
super(message);
|
||||
this.code = ResultCode.FAIL.getCode();
|
||||
}
|
||||
|
||||
public BusinessException(String code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public BusinessException(ResultCode resultCode) {
|
||||
super(resultCode.getMessage());
|
||||
this.code = resultCode.getCode();
|
||||
}
|
||||
|
||||
public BusinessException(ResultCode resultCode, String message) {
|
||||
super(message);
|
||||
this.code = resultCode.getCode();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -45,6 +46,17 @@ public class GlobalExceptionHandler {
|
||||
.body(Result.fail(ResultCode.VALIDATION_ERROR.getCode(), ResultCode.VALIDATION_ERROR.getMessage(), errors));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理文件上传大小超限异常
|
||||
*/
|
||||
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||||
public ResponseEntity<Result<Object>> handleMaxUploadSizeExceededException(MaxUploadSizeExceededException e) {
|
||||
log.warn("文件上传大小超限: {}", e.getMessage());
|
||||
String message = "文件大小超过限制,单个文件最大10MB,请压缩后重试";
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Result.fail(ResultCode.PARAM_ERROR.getCode(), message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理运行时异常
|
||||
*/
|
||||
|
||||
@@ -41,6 +41,7 @@ public class PaymentOrderServiceImpl implements PaymentOrderService {
|
||||
request.getMerchantTransactionId()
|
||||
);
|
||||
if (existingOrder.isPresent()) {
|
||||
log.warn("订单号已存在,商户订单号: {}", request.getMerchantTransactionId());
|
||||
throw new BusinessException(ResultCode.ORDER_EXISTS);
|
||||
}
|
||||
|
||||
@@ -93,29 +94,49 @@ public class PaymentOrderServiceImpl implements PaymentOrderService {
|
||||
|
||||
@Override
|
||||
public Optional<PaymentOrder> findByMerchantTransactionId(String merchantTransactionId) {
|
||||
return paymentOrderMapper.findByMerchantTransactionId(merchantTransactionId);
|
||||
log.debug("查询支付订单,商户订单号: {}", merchantTransactionId);
|
||||
Optional<PaymentOrder> order = paymentOrderMapper.findByMerchantTransactionId(merchantTransactionId);
|
||||
if (order.isPresent()) {
|
||||
log.debug("找到支付订单,商户订单号: {}, 订单状态: {}", merchantTransactionId, order.get().getStatus());
|
||||
} else {
|
||||
log.debug("未找到支付订单,商户订单号: {}", merchantTransactionId);
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<PaymentOrder> findByTransactionId(String transactionId) {
|
||||
return paymentOrderMapper.findByTransactionId(transactionId);
|
||||
log.debug("查询支付订单,PingPong交易流水号: {}", transactionId);
|
||||
Optional<PaymentOrder> order = paymentOrderMapper.findByTransactionId(transactionId);
|
||||
if (order.isPresent()) {
|
||||
log.debug("找到支付订单,PingPong交易流水号: {}, 订单状态: {}", transactionId, order.get().getStatus());
|
||||
} else {
|
||||
log.debug("未找到支付订单,PingPong交易流水号: {}", transactionId);
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public PaymentOrder updateOrderStatus(String merchantTransactionId, String status, String transactionId) {
|
||||
log.info("更新订单状态,商户订单号: {}, 新状态: {}, PingPong交易流水号: {}",
|
||||
merchantTransactionId, status, transactionId);
|
||||
Optional<PaymentOrder> orderOpt = paymentOrderMapper.findByMerchantTransactionId(merchantTransactionId);
|
||||
if (orderOpt.isEmpty()) {
|
||||
log.warn("订单不存在,无法更新状态,商户订单号: {}", merchantTransactionId);
|
||||
throw new BusinessException(ResultCode.ORDER_NOT_FOUND);
|
||||
}
|
||||
|
||||
PaymentOrder order = orderOpt.get();
|
||||
String oldStatus = order.getStatus();
|
||||
order.setStatus(status);
|
||||
if (transactionId != null && !transactionId.isEmpty()) {
|
||||
order.setTransactionId(transactionId);
|
||||
}
|
||||
|
||||
paymentOrderMapper.updateById(order);
|
||||
log.info("订单状态更新成功,商户订单号: {}, 原状态: {}, 新状态: {}",
|
||||
merchantTransactionId, oldStatus, status);
|
||||
return order;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,16 @@ pingpong:
|
||||
# 服务器配置
|
||||
server:
|
||||
port: 8080
|
||||
servlet:
|
||||
context-path: /
|
||||
# 文件上传配置
|
||||
multipart:
|
||||
# 单个文件最大大小(10MB)
|
||||
max-file-size: 10MB
|
||||
# 请求最大大小(50MB,支持多文件上传)
|
||||
max-request-size: 50MB
|
||||
# 文件写入磁盘的阈值(超过此大小会写入临时文件)
|
||||
file-size-threshold: 2MB
|
||||
|
||||
# 阿里云OSS相关配置
|
||||
aliyun:
|
||||
@@ -80,6 +90,4 @@ aliyun:
|
||||
accessKey: sAQR2swByBgmMOofH97hSJT638aVcJ
|
||||
endpoint: https://oss-cn-hangzhou.aliyuncs.com
|
||||
bucketName: mtkj2025
|
||||
servlet:
|
||||
context-path: /
|
||||
|
||||
|
||||
119
mt-pay/src/main/resources/logback-spring.xml
Normal file
119
mt-pay/src/main/resources/logback-spring.xml
Normal file
@@ -0,0 +1,119 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<!-- 定义日志文件的存储地址 -->
|
||||
<property name="LOG_HOME" value="./logs"/>
|
||||
|
||||
<!-- 控制台输出 -->
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- 按照每天生成日志文件 -->
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<!-- 日志文件输出的文件名 -->
|
||||
<FileNamePattern>${LOG_HOME}/mt-pay.%d{yyyy-MM-dd}.log</FileNamePattern>
|
||||
<!-- 日志文件保留天数 -->
|
||||
<MaxHistory>30</MaxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
<!-- 日志文件最大的大小 -->
|
||||
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
|
||||
<MaxFileSize>10MB</MaxFileSize>
|
||||
</triggeringPolicy>
|
||||
</appender>
|
||||
|
||||
<!-- 错误日志文件 -->
|
||||
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<FileNamePattern>${LOG_HOME}/mt-pay-error.%d{yyyy-MM-dd}.log</FileNamePattern>
|
||||
<MaxHistory>30</MaxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
|
||||
<MaxFileSize>10MB</MaxFileSize>
|
||||
</triggeringPolicy>
|
||||
<!-- 此日志文件只记录ERROR级别的 -->
|
||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||
<level>ERROR</level>
|
||||
<onMatch>ACCEPT</onMatch>
|
||||
<onMismatch>DENY</onMismatch>
|
||||
</filter>
|
||||
</appender>
|
||||
|
||||
<!-- 异步输出 -->
|
||||
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
|
||||
<!-- 不丢失日志,默认如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
|
||||
<discardingThreshold>0</discardingThreshold>
|
||||
<!-- 更改默认的队列的深度,该值会影响性能,默认值为256 -->
|
||||
<queueSize>512</queueSize>
|
||||
<appender-ref ref="FILE"/>
|
||||
</appender>
|
||||
|
||||
<!-- 异步输出错误日志 -->
|
||||
<appender name="ASYNC_ERROR_FILE" class="ch.qos.logback.classic.AsyncAppender">
|
||||
<discardingThreshold>0</discardingThreshold>
|
||||
<queueSize>512</queueSize>
|
||||
<appender-ref ref="ERROR_FILE"/>
|
||||
</appender>
|
||||
|
||||
<!-- 开发环境:控制台和文件都输出 -->
|
||||
<springProfile name="dev">
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
<appender-ref ref="ASYNC_FILE"/>
|
||||
<appender-ref ref="ASYNC_ERROR_FILE"/>
|
||||
</root>
|
||||
<!-- 项目包路径,控制日志级别 -->
|
||||
<logger name="com.mtkj.mtpay" level="DEBUG"/>
|
||||
<!-- MyBatis SQL日志 -->
|
||||
<logger name="com.mtkj.mtpay.mapper" level="DEBUG"/>
|
||||
</springProfile>
|
||||
|
||||
<!-- 测试环境:只输出文件 -->
|
||||
<springProfile name="test">
|
||||
<root level="INFO">
|
||||
<appender-ref ref="ASYNC_FILE"/>
|
||||
<appender-ref ref="ASYNC_ERROR_FILE"/>
|
||||
</root>
|
||||
<logger name="com.mtkj.mtpay" level="INFO"/>
|
||||
</springProfile>
|
||||
|
||||
<!-- 生产环境:只输出文件,日志级别为INFO -->
|
||||
<springProfile name="prod">
|
||||
<root level="INFO">
|
||||
<appender-ref ref="ASYNC_FILE"/>
|
||||
<appender-ref ref="ASYNC_ERROR_FILE"/>
|
||||
</root>
|
||||
<logger name="com.mtkj.mtpay" level="INFO"/>
|
||||
<!-- 生产环境关闭MyBatis SQL日志 -->
|
||||
<logger name="com.mtkj.mtpay.mapper" level="WARN"/>
|
||||
</springProfile>
|
||||
|
||||
<!-- 默认配置 -->
|
||||
<springProfile name="!dev & !test & !prod">
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
<appender-ref ref="ASYNC_FILE"/>
|
||||
<appender-ref ref="ASYNC_ERROR_FILE"/>
|
||||
</root>
|
||||
<logger name="com.mtkj.mtpay" level="INFO"/>
|
||||
</springProfile>
|
||||
|
||||
<!-- 第三方框架日志级别 -->
|
||||
<logger name="org.springframework" level="WARN"/>
|
||||
<logger name="org.mybatis" level="WARN"/>
|
||||
<logger name="com.alibaba.druid" level="WARN"/>
|
||||
<logger name="com.aliyun.oss" level="WARN"/>
|
||||
|
||||
</configuration>
|
||||
|
||||
Reference in New Issue
Block a user