輔助類與配置文件是系統工程化程度的體現。本文梳理圖書銷售系統中的工具類體系、常量定義、配置管理、異常處理、日誌規範與第三方集成配置,構建可維護、可擴展的基礎設施層。
src/main/resources/
├── application.yml # 核心配置 (數據源、Redis、端口、上下文路徑)
├── application-dev.yml # 開發環境覆蓋
├── application-prod.yml # 生產環境覆蓋 (密碼加密、連接池調優)
├── application-test.yml # 測試環境 (內存數據庫、Mock 服務)
├── logback-spring.xml # 日誌配置 (按環境區分輸出)
├── mybatis-config.xml # MyBatis 全局配置 (類型別名、映射器、插件)
├── mapper/ # SQL 映射文件目錄
├── templates/ # 郵件/短信/發票/面單模板
├── validation-messages.properties # Bean Validation 國際化訊息
└── messages.properties # 業務錯誤碼/提示訊息 (i18n 基礎)
spring:
datasource:
url: jdbc:mysql://${DB_HOST:localhost}:3306/bookstore?useSSL=false&serverTimezone=Asia/Shanghai
username: ${DB_USER:root}
password: ${DB_PASS:}
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
redis:
host: ${REDIS_HOST:localhost}
port: 6379
lettuce:
pool:
max-active: 8
servlet:
multipart:
max-file-size: 10MB
max-request-size: 50MB
mybatis:
mapper-locations: classpath:mapper/**/*.xml
type-aliases-package: com.bookstore.domain
configuration:
map-underscore-to-camel-case: true
cache-enabled: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
app:
jwt:
secret: ${JWT_SECRET:change-me-in-prod}
expiration: 86400000 # 24h
upload:
path: /data/uploads
cdn-prefix: https://cdn.bookstore.com
email:
host: smtp.exmail.qq.com
port: 465
ssl: true
pagination:
default-page-size: 20
max-page-size: 100
// Result.java - 所有 Controller 返回統一格式
@Data @Builder @AllArgsConstructor @NoArgsConstructor
public class Result<T> {
private int code; // 200 成功、4xx 客戶端錯誤、5xx 服務端錯誤
private String message; // 用戶可讀提示
private T data; // 業務數據
private long timestamp; // 響應時間戳
private String traceId; // 鏈路追蹤 ID
public static <T> Result<T> ok(T data) { return Result.<T>builder().code(200).message("成功").data(data).build(); }
public static <T> Result<T> fail(String msg) { return Result.<T>builder().code(400).message(msg).build(); }
public static <T> Result<T> fail(int code, String msg) { return Result.<T>builder().code(code).message(msg).build(); }
}
// BizException.java - 業務異常基類
public class BizException extends RuntimeException {
private final int code;
private final String userMessage; // 給用戶看
private final String devMessage; // 給開發看 (含堆棧)
// 構造器、靜態工廠方法...
}
// GlobalExceptionHandler.java - 全局異常攔截
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(BizException.class)
public Result<Void> handleBiz(BizException e, HttpServletRequest req) {
log.warn("BizException: code={}, userMsg={}, traceId={}", e.getCode(), e.getUserMessage(), MDC.get("traceId"));
return Result.fail(e.getCode(), e.getUserMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValid(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
.collect(Collectors.joining("; "));
return Result.fail(400, "參數校驗失敗: " + msg);
}
@ExceptionHandler(Exception.class)
public Result<Void> handleOther(Exception e, HttpServletRequest req) {
log.error("Unexpected error: {}", req.getRequestURI(), e);
return Result.fail(500, "系統繁忙,請稍後重試");
}
}
// OrderStatus.java - 訂單狀態機枚舉
public enum OrderStatus {
UNPAID(0, "待付款", "UNPAID"),
PAID(1, "已付款", "PAID"),
SHIPPED(2, "已發貨", "SHIPPED"),
COMPLETED(3, "已完成", "COMPLETED"),
CANCELLED(4, "已取消", "CANCELLED"),
REFUNDED(5, "已退款", "REFUNDED");
private final int value;
private final String label;
private final String code;
// 狀態流轉規則
public static final Map<OrderStatus, Set<OrderStatus>> TRANSITIONS = Map.of(
UNPAID, Set.of(PAID, CANCELLED),
PAID, Set.of(SHIPPED, REFUNDED, CANCELLED),
SHIPPED, Set.of(COMPLETED, REFUNDED),
COMPLETED, Set.of(REFUNDED)
);
public boolean canTransitionTo(OrderStatus target) {
return TRANSITIONS.getOrDefault(this, Collections.emptySet()).contains(target);
}
}
| 類名 | 核心方法 | 用途 |
|---|---|---|
| SnowflakeIdUtil | nextId() | 分布式唯一 ID (訂單號、流水號) |
| PriceUtil | yuanToFen(), fenToYuan(), round() | 金額分/元轉換、銀行家圓整 |
| BeanCopyUtil | copyProperties(), copyList() | DTO/VO/Entity 互轉 (基於 MapStruct/BeanUtils) |
| PageUtil | toPageResult(Page<T>) | MyBatis PageInfo → 統一分頁 VO |
| IpUtil | getClientIp(request) | 穿透代理獲取真實 IP |
| IdCardUtil | mask(), validate() | 身份證脫敏/校驗 (實名認證) |
| ExcelUtil | export(), importTemplate() | POI 封裝、大數據流式導出 |
// @OperationLog - 方法級操作審計
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME)
public @interface OperationLog {
String module(); // 模塊: ORDER/BOOK/USER
String action(); // 動作: CREATE/UPDATE/DELETE/EXPORT
String description() default "";
}
// OperationLogAspect.java
@Aspect @Component @Slf4j
public class OperationLogAspect {
@Around("@annotation(opLog)")
public Object log(ProceedingJoinPoint pjp, OperationLog opLog) throws Throwable {
long start = System.currentTimeMillis();
String traceId = MDC.get("traceId");
Object result = pjp.proceed();
long cost = System.currentTimeMillis() - start;
// 異步持久化操作日誌 (不阻塞主流程)
logService.asyncSave(OperationLogDTO.builder()
.traceId(traceId).module(opLog.module()).action(opLog.action())
.operator(getCurrentUser()).method(pjp.getSignature().toShortString())
.params(JSON.toJSONString(pjp.getArgs())).result(JSON.toJSONString(result))
.costMs(cost).success(true).build());
return result;
}
}
# Dockerfile
FROM eclipse-temurin:17-jre-alpine
ARG JAR_FILE=target/bookstore-*.jar
COPY ${JAR_FILE} app.jar
ENV JAVA_OPTS="-Xms512m -Xmx1g -XX:+UseG1GC"
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app.jar"]
# docker-compose.yml (開發環境)
version: '3.8'
services:
app:
build: .
ports: ["8080:8080"]
environment:
- DB_HOST=mysql
- REDIS_HOST=redis
depends_on: [mysql, redis]
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: bookstore
volumes: ["mysql_data:/var/lib/mysql"]
redis:
image: redis:7-alpine
volumes: {mysql_data: {}}
完善的輔助類與配置體系,讓業務代碼聚焦核心邏輯,基礎設施層支撐系統穩健演進。這是從「能跑通」走向「好維護」的關鍵分水嶺。