worklog/doc/规范/架构设计规范.md
zhangjf dbcc06edbc feat: 完成后端核心业务模块开发
阶段二:认证授权模块
- User实体类、Mapper、DataService
- Token服务(Redis存储)、密码加密(BCrypt)
- 认证拦截器、UserContext上下文
- 登录/登出接口

阶段三:核心业务模块
- 用户管理:CRUD、状态管理、密码重置
- 模板管理:CRUD、状态管理
- 工作日志:CRUD、权限控制

配置分离架构
- env.properties(环境敏感配置)
- service.properties(服务配置)
- logback-spring.xml更新

部署脚本
- deploy/目录(Nginx配置、启停脚本、备份脚本)

单元测试:29个测试全部通过
2026-02-24 16:10:26 +08:00

40 KiB
Raw Permalink Blame History

架构设计规范

本文档基于参考文档 参考Agents.md 中的内容,抽取并整理出与具体业务项目无关的通用架构设计约定,可复用到其它类似的微服务项目中。


1. 后端模块结构规范

1.1 服务模块目录结构

建议单体或微服务项目的后端服务模块遵循统一的目录结构,便于团队协作和代码治理:

service-xxx/
├── src/main/java/com/company/project/xxx/
│   ├── XxxApplication.java      # 启动类
│   ├── config/                  # 配置类
│   ├── controller/              # 控制器层
│   ├── service/                 # 业务服务层
│   │   └── impl/                # 业务服务实现
│   ├── data/                    # 数据访问层MyBatis-Plus相关
│   │   ├── entity/              # 实体类
│   │   ├── mapper/              # Mapper接口
│   │   ├── service/             # XxxDataService数据服务
│   │   │   └── impl/            # XxxDataServiceImpl数据服务实现
│   │   └── xml/                 # Mapper XML文件可选
│   ├── dto/                     # 数据传输对象
│   ├── vo/                      # 视图对象
│   └── enums/                   # 枚举类
├── src/main/resources/
│   ├── application.yml          # 主配置
│   ├── application-docker.yml   # 容器环境配置(可选)
│   └── logback-spring.xml       # 日志配置
└── pom.xml

1.2 公共模块结构

对于多服务项目,建议抽取公共能力到独立的 common 模块,供各业务服务复用:

common/
├── src/main/java/com/company/project/common/
│   ├── auth/                     # 认证相关
│   ├── cache/                    # 缓存服务
│   ├── config/                   # 通用配置
│   ├── context/                  # 上下文管理
│   ├── core/                     # 核心类Result、PageResult 等)
│   ├── exception/                # 异常处理
│   ├── feign/                    # 远程调用拦截器
│   ├── loadbalancer/             # 负载均衡
│   ├── mybatis/                  # ORM / 数据访问配置
│   ├── nacos/                    # 注册配置(如使用 Nacos
│   └── web/                      # Web 拦截器

基础模块依赖方式

强制要求:基础模块(如 common 模块)必须采用 Maven/Gradle 项目依赖方式引入,不允许通过拷贝代码或 JAR 包方式使用。

<!-- 在业务服务的 pom.xml 中引入基础模块 -->
<dependency>
    <groupId>com.wjbl.project</groupId>
    <artifactId>common</artifactId>
    <version>${project.version}</version>
</dependency>

优势说明

  • 统一版本管理,避免基础代码不一致。
  • 便于基础能力的统一升级和维护。
  • 支持 Maven 的传递依赖管理。
  • 确保各业务服务使用的基础模块版本一致。

1.3 启动类与组件扫描

所有业务服务建议显式配置组件扫描路径,以确保能扫描到业务模块和公共模块:

@SpringBootApplication(scanBasePackages = {
    "com.wjbl.project.xxx",
    "com.wjbl.project.common"
})
@EnableDiscoveryClient
public class XxxApplication {
    public static void main(String[] args) {
        SpringApplication.run(XxxApplication.class, args);
    }
}

注意:基于 WebFlux 的网关类服务不应扫描包含 Servlet 相关依赖的公共模块包,以避免 WebFlux 与 Servlet 依赖冲突。


2. 数据库设计规范

2.1 审计字段

所有业务表建议统一包含以下审计字段,用于记录数据的创建与修改信息:

created_by      BIGINT       NOT NULL COMMENT '创建人ID',
created_time    DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_by      BIGINT       NOT NULL COMMENT '更新人ID',
updated_time    DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted         TINYINT(1)   NOT NULL DEFAULT 0 COMMENT '逻辑删除0-未删除1-已删除)'

2.2 主键策略

强制要求

  1. 数据库主键字段类型

    • 使用 VARCHAR(20)CHAR(19) 字符串类型存储主键。
    • 存储内容为雪花算法Snowflake生成的 19 位数字 ID。
  2. 实体类主键配置

    @TableId(type = IdType.ASSIGN_ID)
    private String id;  // 注意:使用 String 类型
    
  3. 雪花算法配置

    • MyBatis-Plus 默认支持雪花算法生成 Long 类型 ID。
    • 通过自定义 IdentifierGenerator 或在插入前转换为字符串存储。

优势说明

  • 字符串存储避免前端 JavaScript 处理长整型精度丢失问题JS Number 最大安全整数为 2^53-1
  • 保持雪花算法的分布式唯一性和时间有序性优势。
  • 便于跨语言系统集成(如 Python、Go 等)。

2.3 逻辑删除

  • 统一使用 deleted 字段表示逻辑删除状态。
  • 0 = 未删除1 = 已删除。
  • 在 ORM 框架中统一配置逻辑删除注解(例如:@TableLogic)。

3. MyBatis-Plus 架构规范

3.1 数据访问层目录结构

MyBatis-Plus 相关的所有文件统一存放在 data 目录下,实现数据访问与业务逻辑的明确分离:

src/main/java/com/company/project/xxx/data/
├── entity/                      # 实体类(对应数据库表)
│   ├── Customer.java
│   └── Project.java
├── mapper/                      # Mapper 接口
│   ├── CustomerMapper.java
│   └── ProjectMapper.java
├── service/                     # 数据服务接口XxxDataService
│   ├── CustomerDataService.java
│   └── ProjectDataService.java
└── impl/                        # 数据服务实现XxxDataServiceImpl
    ├── CustomerDataServiceImpl.java
    └── ProjectDataServiceImpl.java

3.2 数据服务命名规范

强制要求

  1. 数据服务接口:命名为 XxxDataService,继承 IService<Entity>

    public interface CustomerDataService extends IService<Customer> {
        // 数据访问相关方法
    }
    
  2. 数据服务实现:命名为 XxxDataServiceImpl,继承 ServiceImpl<Mapper, Entity>

    @Service
    public class CustomerDataServiceImpl 
        extends ServiceImpl<CustomerMapper, Customer> 
        implements CustomerDataService {
        // 数据访问实现
    }
    
  3. 命名原因

    • 避免与业务 ServiceCustomerService)产生命名冲突。
    • 清晰区分数据访问层DataService和业务逻辑层Service
    • 符合单一职责原则DataService 只负责数据的 CRUD 操作。

3.3 业务服务与数据服务的协作规范

强制要求

  1. 业务 Service 必须通过 DataService 访问数据

    @Service
    @RequiredArgsConstructor
    public class CustomerServiceImpl implements CustomerService {
    
        private final CustomerDataService customerDataService;  // 注入 DataService
    
        @Override
        public CustomerVO getCustomerById(String id) {
            // 通过 DataService 查询数据
            Customer customer = customerDataService.getById(id);
            // 业务逻辑处理和转换
            return convertToVO(customer);
        }
    }
    
  2. 禁止业务 Service 直接注入 Mapper

    // ❌ 错误:业务层直接使用 Mapper
    @Service
    public class CustomerServiceImpl implements CustomerService {
        @Autowired
        private CustomerMapper customerMapper;  // 不允许
    }
    
  3. DataService 职责

    • 封装所有数据库查询和操作。
    • 提供基础的 CRUD 方法(继承自 MyBatis-Plus 的 IService
    • 提供复杂查询的封装方法(如多表关联、统计等)。
  4. 业务 Service 职责

    • 实现业务逻辑和流程控制。
    • 调用一个或多个 DataService 完成数据操作。
    • 处理 DTO 到实体、实体到 VO 的转换。
    • 处理事务管理(@Transactional

3.4 分层示例

完整的分层调用示例:

// 1. Controller 层
@RestController
@RequestMapping("/api/v1/customer")
@RequiredArgsConstructor
public class CustomerController {
    private final CustomerService customerService;
    
    @GetMapping("/{id}")
    public Result<CustomerVO> getCustomer(@PathVariable String id) {
        return Result.success(customerService.getCustomerById(id));
    }
}

// 2. 业务 Service 层
@Service
@RequiredArgsConstructor
public class CustomerServiceImpl implements CustomerService {
    private final CustomerDataService customerDataService;
    
    @Override
    public CustomerVO getCustomerById(String id) {
        Customer customer = customerDataService.getById(id);
        if (customer == null) {
            throw new BusinessException("客户不存在");
        }
        return BeanUtil.copyProperties(customer, CustomerVO.class);
    }
}

// 3. 数据 Service 层
public interface CustomerDataService extends IService<Customer> {
    // 可添加自定义数据访问方法
    List<Customer> listByStatus(Integer status);
}

@Service
public class CustomerDataServiceImpl 
    extends ServiceImpl<CustomerMapper, Customer> 
    implements CustomerDataService {
    
    @Override
    public List<Customer> listByStatus(Integer status) {
        return lambdaQuery()
            .eq(Customer::getStatus, status)
            .list();
    }
}

// 4. Mapper 层
public interface CustomerMapper extends BaseMapper<Customer> {
    // 复杂SQL可在此定义
}

4. API 设计规范

4.1 服务间调用规范

强制要求:服务模块(对外提供 API 服务)之间的调用必须采用 OpenFeign 进行,禁止以下方式:

  • 禁止通过 RestTemplate 直接调用其他服务
  • 禁止通过 HttpClient 等工具类直接调用其他服务
  • 禁止通过共享数据库方式进行服务间通信

OpenFeign 调用规范

  1. 定义 Feign 客户端接口
@FeignClient(name = "service-customer", path = "/cust/api/v1")
public interface CustomerFeignClient {
    
    @GetMapping("/customer/{id}")
    Result<CustomerVO> getCustomer(@PathVariable("id") String id);
    
    @PostMapping("/customer")
    Result<String> createCustomer(@RequestBody CustomerCreateDTO dto);
}
  1. 服务提供方职责

    • 确保接口遵循 RESTful 规范
    • 返回统一的 Result<T> 格式
    • 接口文档及时更新(推荐使用 Swagger/OpenAPI
  2. 服务调用方职责

    • 通过 @Autowired@RequiredArgsConstructor 注入 Feign 客户端
    • 做好异常处理和降级策略(可选配置 Fallback
    • 避免循环调用和过深的调用链
  3. Feign 配置建议

feign:
  client:
    config:
      default:
        connectTimeout: 5000      # 连接超时 5 秒
        readTimeout: 10000        # 读取超时 10 秒
  compression:
    request:
      enabled: true
      mime-types: text/xml,application/xml,application/json
    response:
      enabled: true
  1. 示例:服务间调用
@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
    
    private final CustomerFeignClient customerFeignClient;
    
    @Override
    public OrderVO createOrder(OrderCreateDTO dto) {
        // 通过 Feign 调用客户服务获取客户信息
        Result<CustomerVO> customerResult = customerFeignClient.getCustomer(dto.getCustomerId());
        
        if (!customerResult.isSuccess()) {
            throw new BusinessException("客户信息获取失败:" + customerResult.getMessage());
        }
        
        CustomerVO customer = customerResult.getData();
        // 业务逻辑处理...
        return null;
    }
}

优势说明

  • 统一服务调用方式,便于管理和监控
  • 自动集成负载均衡(结合注册中心)
  • 支持熔断降级(结合 Sentinel 或 Hystrix
  • 便于链路追踪(自动传递 TraceId、SpanId 等)
  • 接口调用类型安全,编译期检查

4.2 URL 设计

RESTful 风格接口推荐使用统一的 URL 模式:

# 基础格式
/{模块}/api/v1/{资源}/{动作}

# 示例
POST   /sys/api/v1/auth/login          # 登录
GET    /cust/api/v1/customer/{id}      # 查询客户
POST   /cust/api/v1/customer           # 创建客户
PUT    /cust/api/v1/customer/{id}      # 更新客户
DELETE /cust/api/v1/customer/{id}      # 删除客户
GET    /cust/api/v1/customer/page      # 分页查询

4.3 HTTP 方法语义

方法 用途 示例
GET 查询 查询列表、详情
POST 创建 新增资源
PUT 更新 修改资源信息
DELETE 删除 删除资源

4.4 统一响应格式

后端服务建议统一使用泛型包装类 Result<T>

{
  "code": 200,
  "message": "success",
  "data": { "...": "..." },
  "success": true
}

错误响应示例:

{
  "code": 400,
  "message": "参数错误xxx 不能为空",
  "data": null,
  "success": false
}

4.5 分页数据格式

{
  "code": 200,
  "message": "success",
  "data": {
    "pageNum": 1,
    "pageSize": 10,
    "total": 100,
    "list": []
  },
  "success": true
}

4.6 状态码规范

状态码 说明
200 成功
400 参数错误
401 未授权 / Token 无效
403 权限不足
404 资源不存在
500 服务器内部错误

4.7 前后端 API 管理

前端项目应采用独立目录或文件集中管理与后端交互的 API避免在页面组件中硬编码 URL

src/api/
├── index.ts          # API 定义入口,统一导出
├── request.ts        # Axios 实例配置、拦截器
└── modules/          # 按业务模块拆分(可选)
    ├── auth.ts       # 认证相关 API
    ├── customer.ts   # 客户管理 API
    └── expense.ts    # 其它业务 API

规范要求:

  • 禁止硬编码:组件中禁止直接调用 request.get('/xxx/xxx')
  • 统一入口:所有 API 函数从 @/api 统一导出,按模块分组。
  • 路径简化API 内部使用简化路径,由 baseURL 或拦截器统一添加前缀。
  • 便于维护:后端接口变更时,只需修改 API 定义位置即可。

5. 链路追踪规范

5.1 基本概念

  • TraceId:全链路唯一标识,一次请求从入口到各个下游服务共享同一个 TraceId。
  • SpanId:链路中单个服务或调用节点的唯一标识,用于区分各个调用段。

5.2 日志格式

强制要求:所有日志必须包含 TraceId 和 SpanId便于进行日志跟踪和问题排查。

推荐在日志格式中统一输出 TraceId 与 SpanId

%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId:-}][%X{spanId:-}] %-5level %logger{50} - %msg%n

日志示例:

2026-02-20 18:10:25.240 [http-nio-8100-exec-1] [a1b2c3d4e5f6...][1234567890abcdef] INFO  c.c.p.controller.CustomerController - 查询客户列表

配置要点

  • 使用 %X{traceId:-}%X{spanId:-} 从 MDCMapped Diagnostic Context中提取追踪信息。
  • - 作为默认值,当上下文中不存在追踪信息时显示。
  • 需配合拦截器或过滤器在请求入口将 TraceId 和 SpanId 放入 MDC。

5.3 HTTP Header 透传

Header 说明
X-Trace-Id 全链路追踪 ID
X-Span-Id 当前服务 Span
X-Source-Service 来源服务名
X-Request-Time 请求发起时间戳
X-User-Id 用户 ID
X-Username 用户名

5.4 响应头返回

服务在响应中也应返回追踪信息,便于排查问题:

X-Trace-Id: a1b2c3d4e5f67890a1b2c3d4e5f67890
X-Span-Id: 1234567890abcdef

6. Token 认证规范

6.1 Token 机制

可采用 随机 Token + 缓存(如 Redis 的会话机制(非 JWT

// 生成 Token
String token = tokenService.generateToken(userId, username, tenantId);

// Token 存储到 Redis
Key: auth:token:{token}
Value: TokenInfo(userId, username, tenantId, expireTime)
TTL: 24 小时或配置项

6.2 请求认证

Authorization: Bearer {token}

6.3 白名单路径

可将登录、健康检查等接口配置为无需 Token 验证,例如:

/sys/api/v1/auth/login
/sys/api/v1/sys/health

7. 配置与部署规范

7.1 配置文件分离架构

推荐采用「统一配置 + 服务个性化配置」分离的方式:

project-root/
├── scripts/
│   └── env.properties           # 统一配置(所有服务共用)
├── service-a/src/main/resources/
│   └── service.properties       # 个性化配置(单服务独立)
├── service-b/src/main/resources/
│   └── service.properties
└── ... 其他服务
  • env.properties:环境级通用配置(注册中心、缓存、限流、文件上传、日志等)。
  • 各服务 service.properties:服务名、实例名、多租户路由、端口等个性化配置。

统一配置示例:

# ==================== 数据库配置 ====================
DB_HOST=localhost
DB_PORT=3306
DB_NAME=worklog
DB_USERNAME=worklog
DB_PASSWORD=Wlog@123
DB_URL=jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true

# 连接池配置
DB_POOL_MIN_IDLE=5
DB_POOL_MAX_SIZE=20
DB_POOL_CONNECTION_TIMEOUT=30000
DB_POOL_IDLE_TIMEOUT=600000
DB_POOL_MAX_LIFETIME=1800000

# ==================== Redis 配置 ====================
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=zjf@123456
REDIS_DATABASE=0
REDIS_TIMEOUT=5000

# Redis 连接池配置
REDIS_POOL_MAX_ACTIVE=8
REDIS_POOL_MAX_WAIT=-1
REDIS_POOL_MAX_IDLE=8
REDIS_POOL_MIN_IDLE=0

# ==================== Nacos 配置 ====================
NACOS_SERVER_ADDR=localhost:8848
NACOS_NAMESPACE=worklog-dev
NACOS_GROUP=DEFAULT_GROUP
NACOS_USERNAME=nacos
NACOS_PASSWORD=nacos

# ==================== 文件上传配置 ====================
FILE_UPLOAD_MAX_SIZE=50MB
FILE_UPLOAD_MAX_REQUEST_SIZE=100MB
FILE_STORAGE_PATH=./uploads

# ==================== 日志配置 ====================
# 日志路径(扁平目录结构,无子目录)
LOG_PATH=./logs

# 日志级别
LOG_LEVEL_ROOT=INFO
LOG_LEVEL_APP=DEBUG

# 日志文件配置
LOG_FILE_MAX_SIZE=100MB
LOG_FILE_MAX_HISTORY=30

# ==================== JVM 配置 ====================
JVM_XMS=512m
JVM_XMX=1024m
JVM_METASPACE_SIZE=128m
JVM_MAX_METASPACE_SIZE=256m

# GC 配置
JVM_GC_TYPE=G1GC
JVM_MAX_GC_PAUSE_MILLIS=200

# ==================== 业务配置 ====================
# Token 配置
TOKEN_EXPIRE_TIME=86400
TOKEN_PREFIX=auth:token:

# 日志内容限制
WORKLOG_MAX_CONTENT_LENGTH=2000

# 允许的上传文件扩展名
UPLOAD_ALLOWED_EXTENSIONS=jpg,jpeg,png,gif,pdf,doc,docx,xls,xlsx

个性化配置示例:

# 服务名称
APP_NAME=service-sys

# 实例名称(多租户场景使用,默认与 APP_NAME 相同)
INSTANCE_NAME=${APP_NAME}

# 租户标识(多租户场景使用,用于路由)
TENANT_ID=

# ==================== 服务端口配置 ====================
# 服务端口(可覆盖 application.yml 中配置)
SERVER_PORT=8080

# ==================== 环境标识 ====================
# 运行环境dev-开发, test-测试, prod-生产
SPRING_PROFILES_ACTIVE=prod

# ==================== 个性化覆盖配置(可选) ====================
# 如果当前服务需要使用不同的日志路径,可在此覆盖
# LOG_PATH=/var/logs/worklog-api

# 如果当前服务需要使用不同的日志级别,可在此覆盖
# LOG_LEVEL_APP=INFO

7.2 配置加载顺序

启动脚本中建议按以下顺序加载配置(后加载覆盖先加载):

# 1. 加载统一配置
load_properties "${APP_HOME}/conf/env.properties"

# 2. 加载个性化配置(覆盖同名参数)
load_properties "${APP_HOME}/conf/service.properties"

7.3 打包目录结构

通过打包插件(如 Maven Assembly推荐输出统一的目录结构

service-sys/                      # 服务根目录
├── bin/                          # 脚本目录
│   ├── start.sh                  # 启动脚本
│   ├── stop.sh                   # 停止脚本
│   ├── restart.sh                # 重启脚本
│   └── status.sh                 # 状态查看脚本
├── lib/                          # 依赖 JAR 目录
│   └── service-sys.jar           # 服务 JAR 包
└── conf/                         # 配置文件目录
    ├── env.properties            # 统一配置
    ├── service.properties        # 个性化配置
    ├── application.yml           # 主配置
    ├── bootstrap.yml             # 启动配置
    └── logback-spring.xml        # 日志配置

7.4 日志配置集中化

强制要求

  1. 日志文件命名

    • 主日志文件统一命名为 app.log
    • 所有日志文件(包括不同级别)统一输出到 logs/ 目录下,不再创建子目录。
  2. 日志目录结构

    logs/
    ├── app.log               # 应用主日志(所有级别)
    ├── error.log             # ERROR 级别日志(可选)
    ├── sql.log               # MyBatis-Plus SQL 日志(强制)
    └── aop.log               # API 请求日志(可选)
    

日志管理建议统一由 logback-spring.xml 等日志框架配置文件控制,从环境变量读取关键参数:

<!-- 从环境变量读取配置 -->
<springProperty scope="context" name="APP_NAME" source="spring.application.name"/>
<springProperty scope="context" name="LOG_PATH" source="logging.file.path" defaultValue="/var/logs/apps"/>
<springProperty scope="context" name="LOG_LEVEL_ROOT" source="logging.level.root" defaultValue="INFO"/>
<springProperty scope="context" name="LOG_LEVEL_APP" source="logging.level.app" defaultValue="DEBUG"/>

<!-- 日志格式:强制包含 traceId 和 spanId -->
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId:-}][%X{spanId:-}] %-5level %logger{50} - %msg%n"/>

<!-- 主日志文件 appender - 直接输出到 logs/ 目录 -->
<appender name="FILE_APP" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${LOG_PATH}/app.log</file>
    <encoder>
        <pattern>${LOG_PATTERN}</pattern>
    </encoder>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>${LOG_PATH}/app-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
        <maxFileSize>100MB</maxFileSize>
        <maxHistory>30</maxHistory>
    </rollingPolicy>
</appender>

<!-- ERROR 日志文件 appender可选- 直接输出到 logs/ 目录 -->
<appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${LOG_PATH}/error.log</file>
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
        <level>ERROR</level>
        <onMatch>ACCEPT</onMatch>
        <onMismatch>DENY</onMismatch>
    </filter>
    <encoder>
        <pattern>${LOG_PATTERN}</pattern>
    </encoder>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>${LOG_PATH}/error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
        <maxFileSize>100MB</maxFileSize>
        <maxHistory>30</maxHistory>
    </rollingPolicy>
</appender>

<!-- SQL 日志文件 appender强制- MyBatis-Plus SQL 日志独立输出 -->
<appender name="FILE_SQL" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${LOG_PATH}/sql.log</file>
    <encoder>
        <pattern>${LOG_PATTERN}</pattern>
    </encoder>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>${LOG_PATH}/sql-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
        <maxFileSize>100MB</maxFileSize>
        <maxHistory>30</maxHistory>
    </rollingPolicy>
</appender>

<!-- MyBatis-Plus SQL 日志配置 -->
<logger name="com.baomidou.mybatisplus" level="DEBUG" additivity="false">
    <appender-ref ref="FILE_SQL"/>
    <appender-ref ref="CONSOLE"/>
</logger>

<!-- 项目 Mapper 日志配置 - 根据实际包名调整 -->
<logger name="com.wjbl.project.*.data.mapper" level="DEBUG" additivity="false">
    <appender-ref ref="FILE_SQL"/>
    <appender-ref ref="CONSOLE"/>
</logger>

SQL 日志配置说明

  1. 独立文件MyBatis-Plus 的 SQL 日志单独输出到 sql.log,便于 SQL 分析和问题排查。
  2. 日志级别SQL 日志级别设置为 DEBUG,确保能记录完整的 SQL 语句和参数。
  3. 包名配置
    • com.baomidou.mybatisplusMyBatis-Plus 框架的 SQL 日志
    • com.wjbl.project.*.data.mapper:项目 Mapper 的 SQL 日志(需根据实际包名调整)
  4. additivity="false":防止 SQL 日志重复输出到主日志文件。
  5. CONSOLE 输出:开发环境可以同时输出到控制台,生产环境可移除。

与应用配置文件的职责划分建议为:业务配置写在 application.yml日志输出规则集中在 logback-spring.xml 管理

7.5 Redis 与注册中心配置示例

Redis 配置(示例):

spring:
  data:
    redis:
      host: localhost
      port: 6379
      password: your-password
      database: 0

注册中心配置(以 Nacos 为例):

spring:
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        namespace: project-namespace
        group: DEFAULT_GROUP
        username: nacos
        password: nacos

8. 多租户架构规范(可选)

说明:本章节内容适用于需要支持多租户的项目。如果项目不涉及多租户场景,可跳过本章节。

8.1 数据库多租户字段

支持多租户的业务表必须增加 tenant_id 字段:

tenant_id       VARCHAR(12)  NOT NULL COMMENT '租户ID'

字段说明

  • 数据类型:VARCHAR(12) 字符串类型
  • 长度限制12 个字符
  • 非空约束:必填字段

8.2 租户 ID 传递

多租户系统中,推荐采用以下方式透传租户信息:

  1. 前端请求:通过 HTTP Header 传递 X-Tenant-Id
  2. 网关层:网关过滤器提取并透传租户 ID。
  3. 后端服务:使用 TenantContextHolder 或等价机制将租户 ID 存入上下文(如 ThreadLocal
  4. 数据访问层ORM / MyBatis 拦截器自动注入 tenant_id 条件,实现数据隔离。

8.3 租户字段自动填充

// 实体类字段 - 使用 String 类型
@TableField(fill = FieldFill.INSERT)
private String tenantId;  // 注意:使用 String 类型,长度最大 12 字符

// 元数据填充处理
public class TenantMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "tenantId", String.class,
            TenantContextHolder.getTenantId());
    }
}

8.4 租户上下文操作

// 获取当前租户ID返回 String 类型)
String tenantId = TenantContextHolder.getTenantId();

// 设置租户ID通常由拦截器自动设置
TenantContextHolder.setTenantId(tenantId);

// 清除上下文(请求结束时)
TenantContextHolder.clear();

注意事项

  • 租户 ID 统一使用 String 类型,最大长度 12 字符。
  • TenantContextHolder 需要对应调整为存储 String 类型。

8.5 HTTP Header 透传

多租户场景下,需要在链路追踪 Header 基础上增加租户信息:

Header 说明
X-Tenant-Id 租户 ID

8.6 多租户部署结构

多实例多租户部署时,可通过目录和配置分离不同实例:

/opt/project/deploy/
├── service-sys/                  # 共享实例
│   ├── bin/
│   ├── lib/
│   └── conf/
│       └── service.properties    # INSTANCE_NAME=service-sys, TENANT_ID=
├── service-sys-vip001/           # VIP 实例 1
│   ├── bin/
│   ├── lib/
│   └── conf/
│       └── service.properties    # INSTANCE_NAME=service-sys-vip001, TENANT_ID=vip001
└── service-sys-vip002/           # VIP 实例 2
    └── ...

个性化配置示例(多租户场景)

# 服务名称
APP_NAME=service-sys

# 实例名称(多租户场景使用,默认与 APP_NAME 相同)
INSTANCE_NAME=service-sys-vip001

# 租户标识(多租户场景使用,用于路由)
TENANT_ID=vip001

# 服务端口(可覆盖 application.yml 中配置)
# SERVER_PORT=8100

9. 常见架构问题与处理建议

9.1 Bean 名称冲突

多模块项目中,配置类命名需避免冲突:

// 不推荐:多个模块都存在同名配置类
@Configuration
public class RedisConfig { }

// 推荐:添加模块前缀或语义前缀
@Configuration
public class GatewayRedisConfig { }  // 网关专用配置

9.2 WebFlux 与 Servlet 冲突

基于 WebFlux 的网关或服务不能使用 Servlet API

// 错误:在 WebFlux 项目中使用 Servlet API
public class TokenAuthFilter implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response) { }
}

// 正确WebFlux 环境下使用 Reactive API
public class TokenAuthFilter implements GlobalFilter {
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { }
}

9.3 注册中心服务发现失败

使用注册中心(如 Nacos应确保依赖与配置完整例如

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

并检查:

  • 注册中心地址、命名空间、分组等配置是否正确;
  • 服务名是否唯一;
  • 客户端网络是否能访问注册中心。

十、开发规范

10.1 命名规范

10.1.1 包命名

  • 全部小写
  • 建议格式:com.{公司或组织}.{项目或域}.{模块}

示例:

com.wjbl.project.customer.controller
com.wjbl.project.customer.service
com.wjbl.project.customer.service.impl
com.wjbl.project.customer.data.mapper
com.wjbl.project.customer.data.entity
com.wjbl.project.customer.dto
com.wjbl.project.customer.vo

10.1.2 类命名

类型 命名规则 示例
实体类 名词PascalCase Customer, Project
Controller 名词 + Controller CustomerController
Service 接口 名词 + Service CustomerService
Service 实现 名词 + ServiceImpl CustomerServiceImpl
DataService 接口 名词 + DataService CustomerDataService
DataService 实现 名词 + DataServiceImpl CustomerDataServiceImpl
Mapper 名词 + Mapper CustomerMapper
DTO 动作 + 名词 + DTO CustomerCreateDTO, CustomerUpdateDTO
VO 名词 + VO CustomerVO, ProjectVO
配置类 功能 + Config RedisConfig, MybatisConfig

10.1.3 方法命名

操作 Service 方法 Controller 方法
创建 createXxx() createXxx()
更新 updateXxx() updateXxx()
删除 deleteXxx() deleteXxx()
查询单个 getXxxById() getXxx()
分页查询 pageXxxs() pageXxxs()
列表查询 listXxxs() listXxxs()

10.1.4 数据库命名

  • 表名:小写,下划线分隔,如 customer, project_member
  • 字段名:小写,下划线分隔,如 customer_name, created_time
  • 索引名idx_表名_字段名,如 idx_customer_status
  • 主键名表名_id,如 customer_id(数据库中使用字符串类型存储)。

10.2 Git 分支与提交规范

10.2.1 分支命名

推荐的分支模型:

master          # 主分支(稳定版本)
develop         # 开发分支(集成测试)
feature/xxx     # 功能分支
bugfix/xxx      # Bug 修复分支
hotfix/xxx      # 紧急线上修复分支
  • 新功能从 developmaster 拉取对应的 feature/* 分支。
  • Bug 修复从对应问题所在分支拉取 bugfix/*hotfix/* 分支。

10.2.2 提交信息格式

推荐使用简洁且结构化的提交信息格式:

<type>: <subject>

<body>

type 类型约定

类型 说明
feat 新功能
fix Bug 修复
docs 文档更新
style 代码格式调整
refactor 重构
test 测试相关
chore 构建/工具相关

示例 1功能提交

feat: 新增客户管理分页查询接口

- 支持按客户名称模糊查询
- 支持按状态筛选
- 完成分页参数校验

示例 2缺陷修复

fix: 修复路由配置错误

xxx 服务路由路径配置错误,修正为 /cust/**

要求

  • subject 尽量 50 字以内,描述清晰、可读。
  • body 用于说明设计思路、变更范围、注意事项等,可选但推荐完整编写。

10.3 代码分层与编写规范

10.3.1 Controller 层

Controller 仅负责:

  • 请求参数接收与基础校验(可结合 @Valid)。
  • 调用 Service 层完成业务处理。
  • 将结果封装为统一返回对象输出。

示例:

@RestController
@RequestMapping("/api/v1/customer")
@RequiredArgsConstructor
public class CustomerController {

    private final CustomerService customerService;

    @PostMapping
    public Result<String> createCustomer(@Valid @RequestBody CustomerCreateDTO dto) {
        String id = customerService.createCustomer(dto);
        return Result.success(id);
    }

    @GetMapping("/page")
    public Result<PageResult<CustomerVO>> pageCustomers(
            @RequestParam(defaultValue = "1") int pageNum,
            @RequestParam(defaultValue = "10") int pageSize,
            @RequestParam(required = false) String keyword) {
        Page<CustomerVO> page = customerService.pageCustomers(pageNum, pageSize, keyword);
        return Result.success(PageResult.of(page));
    }
}

10.3.2 Service 层

Service 层聚焦业务逻辑,典型步骤:

  1. 参数及前置条件校验。
  2. 业务规则处理。
  3. 调用 DataService 进行数据操作。
  4. 组装并返回结果。

示例:

@Service
@RequiredArgsConstructor
public class CustomerServiceImpl implements CustomerService {

    private final CustomerDataService customerDataService;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public String createCustomer(CustomerCreateDTO dto) {
        // 1. 参数校验
        // 2. 业务逻辑
        // 3. 通过 DataService 进行数据持久化
        // 4. 返回结果
        return null;
    }
}

重要:业务 Service 必须通过 DataService 访问数据,禁止直接注入 Mapper。

10.3.3 DTO / VO 规范

  • DTOData Transfer Object:用于接收请求参数或封装服务间调用参数,只包含与接口输入相关的字段。
  • VOView Object:用于接口响应展示,只包含与前端展示相关的字段。
  • 使用校验注解约束 DTO 字段,确保基本数据正确性。

示例:

@Data
public class CustomerCreateDTO {

    @NotBlank(message = "客户名称不能为空")
    @Size(max = 200, message = "客户名称最长 200 字符")
    private String customerName;

    @NotBlank(message = "联系人不能为空")
    private String contact;

    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;
}

10.4 日志使用规范

10.4.1 日志级别

级别 使用场景
ERROR 异常、错误、需要立即处理的问题
WARN 警告、潜在问题
INFO 关键业务流程、重要操作
DEBUG 调试信息、详细流程(仅在调试环境启用)

要求

  • 线上默认不开启 DEBUG避免大量无效日志。
  • 禁止在日志中输出敏感信息(密码、密钥、完整身份证号等)。
  • 所有日志必须包含 traceId 和 spanId,便于链路追踪。

10.4.2 API 请求日志

推荐通过 AOP 统一记录 API 请求日志,内容包括:

  • 请求路径、HTTP 方法。
  • 请求参数(适当脱敏)。
  • 响应时间。
  • 结果状态(成功/失败)。

示例切面:

@Around("execution(* com.wjbl.project..controller..*(..))")
public Object logApiRequest(ProceedingJoinPoint joinPoint) {
    // 记录:请求方法、参数、响应时间、结果状态
    // 日志中必须包含 traceId 和 spanId
    return null;
}

10.5 异常处理规范

10.5.1 统一异常处理

建议在网关或应用入口层统一捕获并处理异常,输出统一格式的错误响应:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public Result<Void> handleBusinessException(BusinessException e) {
        return Result.error(e.getCode(), e.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e) {
        log.error("系统异常", e);
        return Result.error(500, "系统异常,请稍后重试");
    }
}

10.5.2 业务异常定义

业务异常类用于表达可预期的业务错误,并携带错误码和错误信息:

public class BusinessException extends RuntimeException {
    private final int code;

    public BusinessException(String message) {
        super(message);
        this.code = 400;
    }

    public BusinessException(int code, String message) {
        super(message);
        this.code = code;
    }
}

10.5.3 业务代码中使用规范

  • 对于参数错误、状态不合法等可预期问题,直接抛出 BusinessException,携带明确的提示信息。
  • 对于系统级异常如网络、IO、第三方服务异常在适当位置记录详细日志并抛出统一异常让全局处理器输出通用提示。

示例:

// 参数校验
if (customerName == null || customerName.isEmpty()) {
    throw new BusinessException("客户名称不能为空");
}

// 业务校验
Customer customer = customerDataService.getById(id);
if (customer == null) {
    throw new BusinessException(404, "客户不存在");
}

10.6 规范落地建议

为确保上述规范真正落地,建议在团队内按以下步骤推进:

  1. 纳入项目模板
    • 将本架构设计规范纳入项目脚手架或模板仓库。
  2. 代码评审检查
    • 在 Code Review 中重点检查命名、分层、日志、异常处理是否符合规范。
  3. 自动化校验
    • 结合静态检查工具、提交钩子等,对提交信息格式、基础代码规范进行自动化校验。
  4. 文档持续维护
    • 根据实际项目经验定期补充常见错误案例和最佳实践,将实践沉淀回本规范文档中。