Commit 4d862d6f by Lizh

本地部署前端代码与后端代码进行自测,同时修复自测过程中的问题

parent c701c5ab
......@@ -9,7 +9,6 @@
.project
.settings
*.log
/logs/
.trae/
.vscode/
.claude/
......
package com.jomalls.custom.app.dto;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.jomalls.custom.page.PageRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import jakarta.validation.constraints.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.io.Serializable;
......@@ -56,7 +47,6 @@ public class CustomProductInfoSaveSnakeDTO implements Serializable {
* 材质
*/
@Schema(description = "材质", example = "")
@NotNull(message= "材质不能为空")
private String material;
/**
......@@ -78,35 +68,30 @@ public class CustomProductInfoSaveSnakeDTO implements Serializable {
* 产地编码
*/
@Schema(description = "产地编码")
@NotBlank(message = "产地不能为空")
private String origin_code;
/**
* 产地中文名
*/
@Schema(description = "产地中文名")
@NotBlank(message = "产地不能为空")
private String origin_name_cn;
/**
* 产地英文名
*/
@Schema(description = "产地英文名")
@NotBlank(message = "产地不能为空")
private String origin_name_en;
/**
* 币种编码
*/
@Schema(description = "币种编码")
@NotBlank(message = "币种不能为空")
private String currency_code;
/**
* 币种名称
*/
@Schema(description = "币种名称")
@NotBlank(message = "币种不能为空")
private String currency_name;
/**
......@@ -165,7 +150,6 @@ public class CustomProductInfoSaveSnakeDTO implements Serializable {
* 图片列表
*/
@Schema(description = "普通图片列表")
@NotEmpty(message = "商品图片不能为空")
private List<CustomProductImageSnakeDTO> imageList;
/**
......@@ -203,7 +187,6 @@ public class CustomProductInfoSaveSnakeDTO implements Serializable {
* 商品明细
*/
@Schema(description = "商品明细", implementation = CustomProductItemSnakeDTO.class)
@NotEmpty(message = "商品明细不能为空")
private List<CustomProductItemSnakeDTO> productList;
/**
......@@ -237,14 +220,12 @@ public class CustomProductInfoSaveSnakeDTO implements Serializable {
*
*/
@Schema(description = "商品属性ID 1", example = "")
@NotNull(message= "商品属性不能为空")
private Integer property1_cate_id;
/**
*
*/
@Schema(description = "商品属性英文名称 1", example = "")
@NotNull(message= "商品属性名不能为空")
private String property1_enname;
/**
......
......@@ -28,6 +28,9 @@ public class FactoryPriceRelSnakeDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "主键ID)")
private Integer id;
@Schema(description = "子项 ID(custom_product_item 的 id)")
private Integer item_id;
......
package com.jomalls.custom.app.exception;
import com.jomalls.custom.enums.CodeEnum;
import lombok.Getter;
import java.io.Serial;
......@@ -16,19 +17,53 @@ public class InvalidTokenException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
private Integer code = 401;
/**
* 错误码
*/
@Getter
private Integer code;
/**
* 错误提示
*/
private String message;
/**
* 错误明细,内部调试错误
* <p>
* 和 {@link CommonResult#getDetailMessage()} 一致的设计
*/
@Getter
private String detailMessage;
/**
* 空构造方法,避免反序列化问题
*/
public InvalidTokenException() {
super("未登录或登录已过期");
}
public InvalidTokenException(String message) {
super(message);
this(message, CodeEnum.UNAUTHORIZED.getCode());
}
public InvalidTokenException(Integer code, String message) {
super(message);
public InvalidTokenException(String message, Integer code) {
this.message = message;
this.code = code;
}
@Override
public String getMessage() {
return message;
}
public InvalidTokenException setMessage(String message) {
this.message = message;
return this;
}
public InvalidTokenException setDetailMessage(String detailMessage) {
this.detailMessage = detailMessage;
return this;
}
}
\ No newline at end of file
......@@ -269,10 +269,26 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
}
}
}
/**
* 因为CustomProductInfoSaveSnakeDTO对象中一些字段是新增与修改通用的
* 但修改时不需要验证,新增时需要验证,所以无法使用类似‌@NotBlank这种方式校验
*/
private void validRequiredField(CustomProductInfoSaveSnakeDTO dto) {
if (CollectionUtils.isEmpty(dto.getImageList())) {
throw new ServiceException("商品图片不能为空");
}
if (CollectionUtils.isEmpty(dto.getProductList())) {
throw new ServiceException("商品明细不能为空");
}
if (dto.getProperty1_cate_id() == null || StringUtils.isBlank(dto.getProperty1_enname())) {
throw new ServiceException("商品主图不能为空");
}
}
@Override
public void saveFull(CustomProductInfoSaveSnakeDTO dto) {
CustomAsserts.nonNull(dto, "创建参数不能为空");
// 必填字段校验
validRequiredField(dto);
// 1. 生成 SKU 调用SysBillRuleService
String sku = sysBillRuleService.getProductSku(SkuGenerateEnums.TEMPLATE_PRODUCT.getCode());
......@@ -1631,6 +1647,9 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
private void rebuildProperties(List<CustomProductInfoPropertyDTO> skuProperties,
List<CustomProductInfoPropertyDTO> normalProperties,
Integer productId) {
if (CollectionUtils.isEmpty(skuProperties) && CollectionUtils.isEmpty(normalProperties)) {
return;
}
// 删除旧属性
customProductInfoPropertyDomainService.remove(
new LambdaQueryWrapper<CustomProductInfoPropertyEntity>()
......@@ -1719,34 +1738,46 @@ public class CustomProductInfoServiceImpl implements CustomProductInfoService {
List<Integer> craftIds, List<Integer> diyUserIds,
List<FactoryPriceIntervalRelSnakeDTO> intervals, Integer productId) {
// 产品-工厂关联
productFactoryRelDomainService.remove(
new LambdaQueryWrapper<ProductFactoryRelEntity>()
.eq(ProductFactoryRelEntity::getProductId, productId));
saveFactoryRels(factoryIds, productId);
if (CollectionUtils.isNotEmpty(factoryIds)) {
productFactoryRelDomainService.remove(
new LambdaQueryWrapper<ProductFactoryRelEntity>()
.eq(ProductFactoryRelEntity::getProductId, productId));
saveFactoryRels(factoryIds, productId);
}
// 仓库关联
customProductWarehouseRelDomainService.remove(
new LambdaQueryWrapper<CustomProductWarehouseRelEntity>()
.eq(CustomProductWarehouseRelEntity::getProductId, productId));
saveWarehouseRels(warehouseIds, productId);
if (CollectionUtils.isNotEmpty(warehouseIds)) {
customProductWarehouseRelDomainService.remove(
new LambdaQueryWrapper<CustomProductWarehouseRelEntity>()
.eq(CustomProductWarehouseRelEntity::getProductId, productId));
saveWarehouseRels(warehouseIds, productId);
}
// 工艺关联
customProductCraftRelDomainService.remove(
new LambdaQueryWrapper<CustomProductCraftRelEntity>()
.eq(CustomProductCraftRelEntity::getProductId, productId));
saveCraftRels(craftIds, productId);
if (CollectionUtils.isNotEmpty(craftIds)) {
customProductCraftRelDomainService.remove(
new LambdaQueryWrapper<CustomProductCraftRelEntity>()
.eq(CustomProductCraftRelEntity::getProductId, productId));
saveCraftRels(craftIds, productId);
}
// DIY 用户关联
customProductDiyUserRelDomainService.remove(
new LambdaQueryWrapper<CustomProductDiyUserRelEntity>()
.eq(CustomProductDiyUserRelEntity::getProductId, productId));
saveDiyUserRels(diyUserIds, productId);
if (CollectionUtils.isNotEmpty(diyUserIds)) {
customProductDiyUserRelDomainService.remove(
new LambdaQueryWrapper<CustomProductDiyUserRelEntity>()
.eq(CustomProductDiyUserRelEntity::getProductId, productId));
saveDiyUserRels(diyUserIds, productId);
}
// 价格区间关联
customProductFactoryPriceIntervalRelDomainService.remove(
new LambdaQueryWrapper<CustomProductFactoryPriceIntervalRelEntity>()
.eq(CustomProductFactoryPriceIntervalRelEntity::getProductId, productId));
saveFactoryPriceIntervalRels(intervals, productId);
if (CollectionUtils.isNotEmpty(intervals)) {
customProductFactoryPriceIntervalRelDomainService.remove(
new LambdaQueryWrapper<CustomProductFactoryPriceIntervalRelEntity>()
.eq(CustomProductFactoryPriceIntervalRelEntity::getProductId, productId));
saveFactoryPriceIntervalRels(intervals, productId);
}
}
/**
......
......@@ -19,7 +19,12 @@ public enum CodeEnum {
/**
* 请求成功
*/
SUCCESS(200, "OK", HttpStatus.OK),
SUCCESS(200, "Success", HttpStatus.OK),
/*
* 适配原来的请求成功代码,custom前端判断成功是10000
*/
OK(10000, "OK", HttpStatus.OK),
/**
* 服务器内部错误
......
......@@ -23,24 +23,24 @@ public class PageRequest implements Pageable, Serializable {
/**
* 默认当前页码
*/
private static final long DEFAULT_CURRENT = 1L;
private static final long DEFAULT_PAGE = 1L;
/**
* 默认分页大小
*/
private static final long DEFAULT_SIZE = 10L;
private static final long DEFAULT_ROWS = 10L;
/**
* 当前页码
* 当前页码(前端字段名: page)
*/
@Schema(description = "当前页码", example = "1", defaultValue = "1")
private long current;
private long page;
/**
* 分页大小
* 分页大小(前端字段名: rows)
*/
@Schema(description = "分页大小", example = "10", defaultValue = "10")
private long size;
private long rows;
/**
* 是否需要统计总条数
......@@ -48,18 +48,23 @@ public class PageRequest implements Pageable, Serializable {
private boolean searchCount;
public PageRequest() {
this(DEFAULT_CURRENT, DEFAULT_SIZE);
this(DEFAULT_PAGE, DEFAULT_ROWS);
}
public PageRequest(long current, long size) {
this.current = current;
this.size = size;
searchCount = true;
this.page = current > 0 ? current : DEFAULT_PAGE;
this.rows = size > 0 ? size : DEFAULT_ROWS;
this.searchCount = true;
}
@Override
public long getCurrent() {
return current;
return page > 0 ? page : DEFAULT_PAGE;
}
@Override
public long getSize() {
return rows > 0 ? rows : DEFAULT_ROWS;
}
@Override
......
......@@ -33,15 +33,15 @@ public class R<T> implements Serializable {
}
public static <T> R<T> ok() {
return new R<>(CodeEnum.SUCCESS.getCode(), CodeEnum.SUCCESS.getMsg(), null);
return new R<>(CodeEnum.OK.getCode(), CodeEnum.OK.getMsg(), null);
}
public static <T> R<T> ok(T data) {
return new R<>(CodeEnum.SUCCESS.getCode(), CodeEnum.SUCCESS.getMsg(), data);
return new R<>(CodeEnum.OK.getCode(), CodeEnum.OK.getMsg(), data);
}
public static <T> R<T> ok(String msg, T data) {
return new R<>(CodeEnum.SUCCESS.getCode(), msg, data);
return new R<>(CodeEnum.OK.getCode(), msg, data);
}
public static <T> R<T> fail(int code, String msg) {
......
#!/bin/bash
APP_NAME="custom-server"
APP_JAR="custom-server-starter-1.0-SNAPSHOT.jar"
APP_DIR="/opt/custom-server"
LOG_FILE="$APP_DIR/logs/custom-server.log"
# 检查进程是否存在
PID=$(ps -ef | grep $APP_JAR | grep -v grep | awk '{print $2}')
if [ -n "$PID" ]; then
echo "Stopping $APP_NAME (PID: $PID)..."
kill -TERM $PID
sleep 5
fi
echo "Starting $APP_NAME..."
nohup java -Xms2g -Xmx4g -XX:+UseG1GC \
-jar $APP_DIR/$APP_JAR \
--spring.profiles.active=prod \
--spring.config.location=file:$APP_DIR/config/ \
> $LOG_FILE 2>&1 &
echo "$APP_NAME started successfully."
\ No newline at end of file
#!/bin/bash
APP_NAME="custom-server"
APP_JAR="custom-server-starter-1.0-SNAPSHOT.jar"
# 检查进程是否存在
PID=$(ps -ef | grep $APP_JAR | grep -v grep | awk '{print $2}')
if [ -z "$PID" ]; then
echo "$APP_NAME is not running."
exit 0
fi
echo "Stopping $APP_NAME (PID: $PID)..."
# 优雅停止(发送 SIGTERM 信号)
kill -TERM $PID
# 等待进程退出(最多等待 30 秒)
MAX_WAIT=30
WAIT_COUNT=0
while kill -0 $PID 2>/dev/null; do
if [ $WAIT_COUNT -ge $MAX_WAIT ]; then
echo "Force killing $APP_NAME (PID: $PID)..."
kill -KILL $PID
break
fi
sleep 1
WAIT_COUNT=$((WAIT_COUNT + 1))
done
echo "$APP_NAME stopped successfully."
\ No newline at end of file
......@@ -31,7 +31,7 @@ public class CommonExceptionHandlerAdvice {
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<R<Object>> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
log.debug("[ JSON反序列化失败 ] {}", e.getMessage(), e);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
return ResponseEntity.status(HttpStatus.OK)
.body(R.fail(CodeEnum.FAIL.getCode(), "请求参数格式错误: " + e.getLocalizedMessage()));
}
......@@ -44,7 +44,7 @@ public class CommonExceptionHandlerAdvice {
.map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
.collect(Collectors.joining("; "));
log.debug("[ 参数校验失败 ] {}", detail);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
return ResponseEntity.status(HttpStatus.OK)
.body(R.fail(CodeEnum.BAD_REQUEST.getCode(), "参数校验失败: " + detail));
}
......@@ -57,7 +57,7 @@ public class CommonExceptionHandlerAdvice {
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining("; "));
log.debug("[ 约束校验失败 ] {}", detail);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
return ResponseEntity.status(HttpStatus.OK)
.body(R.fail(CodeEnum.BAD_REQUEST.getCode(), "参数校验失败: " + detail));
}
......@@ -67,7 +67,7 @@ public class CommonExceptionHandlerAdvice {
@ExceptionHandler(InvalidTokenException.class)
public ResponseEntity<R<Object>> handleInvalidTokenException(InvalidTokenException e) {
log.debug("[ Token验证失败 ] {}", e.getMessage());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
return ResponseEntity.status(HttpStatus.OK)
.body(R.fail(CodeEnum.UNAUTHORIZED.getCode(), e.getMessage()));
}
......@@ -77,7 +77,7 @@ public class CommonExceptionHandlerAdvice {
@ExceptionHandler(PermissionDeniedException.class)
public ResponseEntity<R<Object>> handlePermissionDeniedException(PermissionDeniedException e) {
log.debug("[ 权限拒绝 ] {}", e.getMessage());
return ResponseEntity.status(HttpStatus.FORBIDDEN)
return ResponseEntity.status(HttpStatus.OK)
.body(R.fail(CodeEnum.FORBIDDEN.getCode(), e.getMessage()));
}
......
package com.jomalls.custom.config;
import com.jomalls.custom.enums.CodeEnum;
import com.jomalls.custom.app.exception.InvalidTokenException;
import com.jomalls.custom.app.utils.RequestHolder;
import com.jomalls.custom.security.LoginUser;
import com.jomalls.custom.security.SecurityUtils;
import com.jomalls.custom.security.TokenHandle;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.jspecify.annotations.NonNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* 安全拦截器
* 负责用户登录权限检查,保存登录信息到线程本地变量
......@@ -48,7 +43,7 @@ public class SecurityInterceptor implements HandlerInterceptor {
// 如果用户没有登录,返回提示
if (loginUser == null) {
throw new InvalidTokenException(CodeEnum.UNAUTHORIZED.getCode(), "未登录或登录已过期");
throw new InvalidTokenException("未登录或登录已过期");
}
// 验证token是否有效
......@@ -58,7 +53,7 @@ public class SecurityInterceptor implements HandlerInterceptor {
return true;
} else {
log.warn("用户[{}]token已过期,请求URI: {}", loginUser.getUsername(), request.getRequestURI());
throw new InvalidTokenException(CodeEnum.UNAUTHORIZED.getCode(), "未登录或登录已过期");
throw new InvalidTokenException("未登录或登录已过期");
}
}
......
......@@ -2,10 +2,14 @@ package com.jomalls.custom.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
......@@ -29,8 +33,7 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
"/webjars/**",
"/sys/Serf/Health/*",
"/error",
"/actuator/health",
"/health/check",
"/api/v3/actuator/*",
"/.well-known/**",
"/favicon.ico",
"/static/**");
......@@ -38,16 +41,23 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
/**
* 配置服务器跨域请求被允许
*
* @param registry
* <p>
* 使用 {@link CorsFilter}(Servlet Filter 层)而非 {@code addCorsMappings}(MVC 层),
* 因为 MVC 层 CORS 依赖 Controller 匹配,无法处理浏览器 OPTIONS 预检请求。
* CorsFilter 在 DispatcherServlet 之前运行,覆盖所有请求。
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "OPTIONS", "DELETE", "PATCH")
.allowCredentials(true)
.maxAge(3600)
.allowedHeaders("*");
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of("*"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "OPTIONS", "DELETE", "PATCH"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
config.addExposedHeader("Content-Disposition");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
\ No newline at end of file
## 数据库连接配置
spring.datasource.url=jdbc:mysql://172.16.19.99:3306/foxpsd-lizh?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false
spring.datasource.username=root
spring.datasource.password=joshine
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
## Hikari连接池配置
#最小空闲连接数
spring.datasource.hikari.minimum-idle=5
#最大连接池大小
spring.datasource.hikari.maximum-pool-size=50
#连接超时时间(60秒)
spring.datasource.hikari.connection-timeout=60000
#空闲连接超时时间(600秒)
spring.datasource.hikari.idle-timeout=600000
#最大连接生命周期(900秒)
spring.datasource.hikari.max-lifetime=900000
#验证超时时间(3000秒)
spring.datasource.hikari.validation-timeout=3000
## MyBatis-Plus SQL日志(可选开启)
#mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
\ No newline at end of file
# ============================================================
# Custom Server — 数据源配置(datasource profile)
# ============================================================
spring:
datasource:
url: jdbc:mysql://172.16.19.99:3306/foxpsd-lizh?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: joshine
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
# 最小空闲连接数
minimum-idle: 5
# 最大连接池大小
maximum-pool-size: 50
# 连接超时时间(60 秒)
connection-timeout: 60000
# 空闲连接超时时间(600 秒)
idle-timeout: 600000
# 最大连接生命周期(900 秒)
max-lifetime: 900000
# 验证超时时间(3 秒)
validation-timeout: 3000
## Redis连接配置
spring.data.redis.host=172.16.19.100
spring.data.redis.port=6379
spring.data.redis.password=joshine.dev
spring.data.redis.database=7
## 超时配置(毫秒)
# 读取/写入超时
spring.data.redis.timeout=5000
# 连接超时(缩短,避免长时间等待)
spring.data.redis.connect-timeout=3000
## 连接池配置(适配RedissonClient)
spring.data.redis.pool.max-active=8
spring.data.redis.pool.max-idle=4
spring.data.redis.pool.min-idle=2
## Lettuce连接池配置(Spring Data Redis使用)
spring.data.redis.lettuce.pool.enabled=true
spring.data.redis.lettuce.pool.max-active=8
spring.data.redis.lettuce.pool.max-idle=4
spring.data.redis.lettuce.pool.min-idle=2
spring.data.redis.lettuce.pool.max-wait=5000
## Redisson特有配置
# 重试次数
spring.data.redis.redisson.retry-attempts=3
# 重试间隔(毫秒)
spring.data.redis.redisson.retry-interval=1000
# 心跳间隔(毫秒)
spring.data.redis.redisson.ping-interval=60000
# 空闲连接超时(毫秒)
spring.data.redis.redisson.idle-timeout=10000
\ No newline at end of file
# ============================================================
# Custom Server — Redis 配置(redis profile)
# ============================================================
spring:
data:
redis:
host: 172.16.19.100
port: 6379
password: joshine.dev
database: 7
# 读取/写入超时(毫秒)
timeout: 5000
# 连接超时(毫秒)
connect-timeout: 3000
# Jedis 连接池配置
pool:
max-active: 8
max-idle: 4
min-idle: 2
# Lettuce 连接池配置(Spring Data Redis 使用)
lettuce:
pool:
enabled: true
max-active: 8
max-idle: 4
min-idle: 2
max-wait: 5000
# Redisson 特有配置
redisson:
# 重试次数
retry-attempts: 3
# 重试间隔(毫秒)
retry-interval: 1000
# 心跳间隔(毫秒)
ping-interval: 60000
# 空闲连接超时(毫秒)
idle-timeout: 10000
## 服务器配置
server.port=40071
server.servlet.context-path=/
## Tomcat配置
server.tomcat.uri-encoding=UTF-8
server.tomcat.accept-count=1000
server.tomcat.threads.max=200
server.tomcat.threads.min-spare=100
## Spring配置
spring.application.name=custom-server
spring.profiles.active=datasource,redis
spring.main.allow-circular-references=true
## Jackson配置
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
## SpringDoc OpenAPI配置
springdoc.api-docs.path=/api-docs
springdoc.swagger-ui.path=/swagger-ui.html
## MyBatis-Plus配置
mybatis-plus.mapper-locations=classpath*:mapper/**/*.xml
mybatis-plus.type-aliases-package=com.jomalls.custom.domain.entity
mybatis-plus.configuration.call-setters-on-nulls=true
## 时区配置
TZ=Asia/Shanghai
# 令牌配置,是否需要认证(测试时可以不认证)
server.needAuthentication=true
# 令牌自定义标识
token.header=Authorization
# 令牌密钥(兼容旧版本)
token.secret=custom
# 令牌有效期(单位:分钟)
token.expireTime=720
# HTTP Client Configuration
# 连接超时(5000毫秒 = 5秒)
http.client.connect-timeout=5000
# 读取超时(30000毫秒 = 30秒)
http.client.read-timeout=30000
# 写入超时(30000毫秒 = 30秒)
http.client.write-timeout=30000
# 连接池最大连接数
http.client.pool-max-connections=50
# 连接池获取连接超时(毫秒)
http.client.pool-acquire-timeout=2000
# Resilience4j Retry Configuration
# 最大重试次数:最多重试3次(首次 + 2次重试)
resilience4j.retry.configs.default.max-attempts=3
# 初始等待时间:第一次重试前等待500毫秒
resilience4j.retry.configs.default.wait-duration=500ms
# 启用指数退避:每次重试间隔翻倍(500ms → 1000ms → 2000ms)
resilience4j.retry.configs.default.enable-exponential-backoff=true
# 指数退避倍数:每次等待时间乘以2
resilience4j.retry.configs.default.exponential-backoff-multiplier=2
# 以下异常触发重试(网络异常 + 服务端5xx错误)
resilience4j.retry.configs.default.retry-exceptions=java.util.concurrent.TimeoutException,java.io.IOException,java.net.ConnectException,org.springframework.web.reactive.function.client.WebClientResponseException
# 以下异常不触发重试(客户端4xx错误已包装为RemoteServiceException,重试无意义)
resilience4j.retry.configs.default.ignore-exceptions=com.jomalls.custom.integrate.exception.RemoteServiceException
# 远程 API 重试实例使用默认配置
resilience4j.retry.instances.remoteApi.base-config=default
server.admin.base-url=https://admin.jomalls.com
\ No newline at end of file
## Spring MVC配置
# ============================================================
# Custom Server — 主配置文件
# ============================================================
# 服务器配置
server:
port: 40071
servlet:
context-path: /
tomcat:
uri-encoding: UTF-8
accept-count: 1000
threads:
max: 200
min-spare: 100
# 自定义:是否需要认证(测试时可以不认证)
needAuthentication: true
# 自定义:saasAdmin管理后台地址,获取商品类别信息
admin:
base-url: https://admin.jomalls.com
# Spring 配置
spring:
application:
name: custom-server
profiles:
active: datasource
mvc:
pathmatch:
matching-strategy: ant_path_matcher
active: datasource,redis
main:
allow-circular-references: true
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
## MyBatis-Plus配置
# SpringDoc OpenAPI 配置
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
# MyBatis-Plus 配置
mybatis-plus:
mapper-locations: classpath*:mapper/**/*.xml
type-aliases-package: com.jomalls.custom.domain.dal.entity
configuration:
call-setters-on-nulls: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
mapper-locations: classpath*:mapper/**/*.xml
\ No newline at end of file
# ============================================================
# 令牌配置
# ============================================================
token:
header: Authorization
# 令牌密钥(兼容旧版本)
secret: custom
# 令牌有效期(单位:分钟)
expireTime: 720
# ============================================================
# HTTP Client 配置
# ============================================================
http:
client:
# 连接超时(毫秒)
connect-timeout: 5000
# 读取超时(毫秒)
read-timeout: 30000
# 连接池最大连接数
pool-max-connections: 50
# 连接池获取连接超时(毫秒)
pool-acquire-timeout: 2000
# ============================================================
# Resilience4j 重试配置
# ============================================================
resilience4j:
retry:
configs:
default:
# 最大重试次数
max-attempts: 3
# 初始等待时间
wait-duration: 500ms
# 启用指数退避
enable-exponential-backoff: true
# 指数退避倍数
exponential-backoff-multiplier: 2
# 触发重试的异常
retry-exceptions:
- java.util.concurrent.TimeoutException
- java.io.IOException
- java.net.ConnectException
- org.springframework.web.reactive.function.client.WebClientResponseException
# 不触发重试的异常
ignore-exceptions:
- com.jomalls.custom.integrate.exception.RemoteServiceException
instances:
remoteApi:
base-config: default
# ============================================================
# Actuator 管理端点
# ============================================================
management:
endpoints:
web:
base-path: /api/v3/actuator
exposure:
include: metrics
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 日志存放路径(开发环境用相对路径,生产环境通过 logging.file.path 覆盖) -->
<!--<property name="log.path" value="${LOG_PATH:-logs}"/>-->
<property name="log.path" value="./custom-v2/logs"/>
<!-- 日志输出格式 -->
<property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%method,%line] - %msg%n"/>
<configuration scan="true" scanPeriod="300 seconds" debug="false">
<property name="max.history.days" value="30"/>
<property name="max.file.size" value="50MB"/>
<property name="max.total.size" value="2GB"/>
<!-- ==================== 日志路径 ==================== -->
<!--
生效优先级(由 Spring Boot LoggingApplicationListener 处理):
1. application.yml: logging.file.path: /var/log/app
2. JVM 启动参数: -DLOG_PATH=/var/log/app
3. 环境变量: export LOG_PATH=/var/log/app
4. 以上均未设置 → defaultValue: ./custom-v3/logs
LOG_PATH → logging.file.path 的映射由 Spring Boot 内置完成,
springProperty 从 Environment 中读取最终的 logging.file.path 值。
-->
<springProperty scope="context" name="log.path" source="logging.file.path" defaultValue="./custom-v3/logs"/>
<!-- ==================== 日志格式 ==================== -->
<property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%X{traceId}] - [%method,%line] - %msg%n"/>
<!-- ==================== 控制台输出 ==================== -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
......@@ -18,13 +31,22 @@
</appender>
<!-- ==================== 文件输出 ==================== -->
<!--
滚动策略:SizeAndTimeBasedRollingPolicy
- 按天 + 按大小双重滚动,单文件超过 maxFileSize 自动切分
- totalSizeCap 限制归档总量,超过自动清理最旧文件
- cleanHistoryOnStart 启动时清理超出限制的归档
-->
<!-- 系统日志:INFO 及以上(含 WARN) -->
<!-- 系统日志:仅 INFO -->
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>60</maxHistory>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>${max.file.size}</maxFileSize>
<maxHistory>${max.history.days}</maxHistory>
<totalSizeCap>${max.total.size}</totalSizeCap>
<cleanHistoryOnStart>true</cleanHistoryOnStart>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
......@@ -37,11 +59,15 @@
</filter>
</appender>
<!-- 警告日志:仅 WARN -->
<appender name="file_warn" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-warn.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/sys-warn.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>60</maxHistory>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${log.path}/sys-warn.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>${max.file.size}</maxFileSize>
<maxHistory>${max.history.days}</maxHistory>
<totalSizeCap>${max.total.size}</totalSizeCap>
<cleanHistoryOnStart>true</cleanHistoryOnStart>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
......@@ -57,9 +83,12 @@
<!-- 错误日志:仅 ERROR -->
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>60</maxHistory>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>${max.file.size}</maxFileSize>
<maxHistory>${max.history.days}</maxHistory>
<totalSizeCap>${max.total.size}</totalSizeCap>
<cleanHistoryOnStart>true</cleanHistoryOnStart>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
......@@ -72,16 +101,44 @@
</filter>
</appender>
<!-- ==================== 异步包装 ==================== -->
<!-- 文件 I/O 不阻塞业务线程 -->
<appender name="async_file_info" class="ch.qos.logback.classic.AsyncAppender">
<neverBlock>true</neverBlock>
<queueSize>512</queueSize>
<discardingThreshold>0</discardingThreshold>
<appender-ref ref="file_info"/>
</appender>
<appender name="async_file_warn" class="ch.qos.logback.classic.AsyncAppender">
<neverBlock>true</neverBlock>
<queueSize>256</queueSize>
<discardingThreshold>0</discardingThreshold>
<appender-ref ref="file_warn"/>
</appender>
<appender name="async_file_error" class="ch.qos.logback.classic.AsyncAppender">
<neverBlock>true</neverBlock>
<queueSize>256</queueSize>
<discardingThreshold>0</discardingThreshold>
<appender-ref ref="file_error"/>
</appender>
<!-- ==================== 框架日志级别控制 ==================== -->
<!-- MyBatis / MyBatis-Plus -->
<!--<logger name="org.mybatis" level="DEBUG"/>-->
<!--<logger name="com.baomidou.mybatisplus" level="DEBUG"/>-->
<!-- MyBatis / MyBatis-Plus:默认关闭 SQL 日志,排查问题时临时打开
<logger name="org.mybatis" level="DEBUG"/>
<logger name="com.baomidou.mybatisplus" level="DEBUG"/>
-->
<!-- ==================== 环境差异配置 ==================== -->
<!-- 本地开发:项目包 DEBUG 级别,方便调试 -->
<springProfile name="default">
<logger name="com.jomalls.custom" level="DEBUG"/>
</springProfile>
<!-- ==================== 根配置 ==================== -->
<root level="DEBUG">
<root level="INFO">
<appender-ref ref="console"/>
<appender-ref ref="file_info"/>
<appender-ref ref="file_warn"/>
<appender-ref ref="file_error"/>
<appender-ref ref="async_file_info"/>
<appender-ref ref="async_file_warn"/>
<appender-ref ref="async_file_error"/>
</root>
</configuration>
</configuration>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment