a-cloud-all/ARCHITECTURE_STANDARDS.md

2029 lines
56 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# RuoYi-Cloud 微服务架构开发规范
> 本文档定义了 RuoYi-Cloud 微服务项目的标准架构、编码规范和最佳实践。
>
> **版本**: v1.0
> **更新日期**: 2026-01-17
---
## 目录
- [一、项目结构规范](#一项目结构规范)
- [二、Maven配置规范](#二maven配置规范)
- [三、配置文件规范](#三配置文件规范)
- [四、数据库层规范](#四数据库层规范)
- [五、领域层规范](#五领域层domain规范)
- [六、服务层规范](#六服务层service规范)
- [七、控制器层规范](#七控制器层controller规范)
- [八、API层规范](#八api层规范)
- [九、依赖注入规范](#九依赖注入规范)
- [十、命名规范](#十命名规范)
---
## 一、项目结构规范
### 1.1 模块目录结构
```
ruoyi-modules/{module-name}/
├── src/main/java/com/ruoyi/{module}/
│ ├── controller/ # 控制器层
│ │ ├── {Entity}Controller.java
│ │ └── convert/ # Controller层对象转换
│ ├── service/ # 服务层
│ │ ├── api/ # 服务接口
│ │ │ └── I{Entity}Service.java
│ │ ├── impl/ # 服务实现
│ │ │ └── {Entity}ServiceImpl.java
│ │ ├── dto/ # 数据传输对象
│ │ │ └── {Entity}DTO.java
│ │ └── convert/ # Service层对象转换
│ │ └── {Entity}ServiceConvert.java
│ ├── domain/ # 领域层
│ │ ├── api/ # 领域接口
│ │ │ └── I{Entity}Domain.java
│ │ ├── impl/ # 领域实现
│ │ │ └── {Entity}DomainImpl.java
│ │ ├── model/ # 领域模型
│ │ │ └── {Entity}.java
│ │ └── convert/ # Domain层对象转换
│ │ └── {Entity}DomainConvert.java
│ └── mapper/ # 数据访问层
│ ├── entity/ # 数据库实体
│ │ └── {Entity}Entity.java
│ └── {Entity}Mapper.java # MyBatis Mapper接口
├── src/main/resources/
│ ├── bootstrap.yml # 启动配置
│ └── mapper/{module}/ # MyBatis XML映射文件
│ └── {Entity}Mapper.xml
└── pom.xml # Maven配置
ruoyi-api/{module-name}-api-{submodule}/
├── src/main/java/com/ruoyi/{module}/api/
│ ├── domain/ # VO对象
│ │ └── {Entity}VO.java
│ ├── factory/ # Feign降级工厂
│ │ └── Remote{Module}FallbackFactory.java
│ └── Remote{Module}Service.java # Feign客户端接口
└── pom.xml
```
### 1.2 分层架构说明
```
Controller Layer (控制器层)
↓ VO → DTO
Service Layer (服务层)
↓ DTO → Model
Domain Layer (领域层)
↓ Model → Entity
Mapper Layer (数据访问层)
Database (数据库)
```
**各层职责:**
- **Controller层**: 处理HTTP请求参数验证调用Service层返回响应
- **Service层**: 业务逻辑编排事务管理调用Domain层
- **Domain层**: 领域业务逻辑封装Mapper操作对象转换
- **Mapper层**: 数据库访问SQL执行
### 1.3 端口分配规范
| 端口范围 | 用途 | 示例 |
|---------|------|------|
| 9200-9209 | 基础服务 | ruoyi-auth(9200), ruoyi-modules-system(9201), ruoyi-modules-gen(9202), ruoyi-modules-job(9203) |
| 9210-9299 | 业务服务 | tuoheng-device(9210), tuoheng-airline(9211), tuoheng-approval(9212), tuoheng-fms(9213), tuoheng-media(9214), tuoheng-task(9215) |
| 9100 | 监控服务 | ruoyi-visual-monitor(9100) |
| 9300+ | 特殊服务 | ruoyi-modules-file(9300) |
**端口分配原则:**
- 新增业务服务从 9216 开始递增
- 避免端口冲突
- Docker 端口映射必须与 bootstrap.yml 配置一致
---
## 二、Maven配置规范
### 2.1 根 pom.xml 配置
#### 2.1.1 添加服务名称常量
`ruoyi-common-core/src/main/java/com/ruoyi/common/core/constant/ServiceNameConstants.java` 中添加:
```java
/**
* {模块中文名}服务的serviceid
*/
public static final String {MODULE}_SERVICE = "{module-name}";
```
**示例:**
```java
/**
* 设备服务的serviceid
*/
public static final String DEVICE_SERVICE = "tuoheng-device";
/**
* 航线服务的serviceid
*/
public static final String AIRLINE_SERVICE = "tuoheng-airline";
```
#### 2.1.2 添加依赖管理
在根 `pom.xml``<dependencyManagement>` 中添加:
```xml
<!-- {模块中文名}接口 -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>{module-name}-api-{submodule}</artifactId>
<version>${ruoyi.version}</version>
</dependency>
```
**示例:**
```xml
<!-- 设备接口 -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>tuoheng-api-device</artifactId>
<version>${ruoyi.version}</version>
</dependency>
```
### 2.2 ruoyi-api/pom.xml 配置
`ruoyi-api/pom.xml``<modules>` 中添加:
```xml
<module>{module-name}-api-{submodule}</module>
```
**示例:**
```xml
<modules>
<module>ruoyi-api-system</module>
<module>tuoheng-api-device</module>
<module>tuoheng-api-airline</module>
</modules>
```
### 2.3 业务模块 pom.xml 标准配置
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-modules</artifactId>
<version>3.6.7</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>{module-name}</artifactId>
<description>
{module-name}系统模块
</description>
<dependencies>
<!-- {Module} API -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>{module-name}-api-{submodule}</artifactId>
</dependency>
<!-- SpringCloud Alibaba Nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- SpringCloud Alibaba Nacos Config -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- SpringCloud Alibaba Sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- SpringBoot Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Mysql Connector -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- RuoYi Common DataSource -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-datasource</artifactId>
</dependency>
<!-- RuoYi Common DataScope -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-datascope</artifactId>
</dependency>
<!-- RuoYi Common Log -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-log</artifactId>
</dependency>
<!-- RuoYi Common Swagger -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-swagger</artifactId>
</dependency>
<!-- Flyway Database Migration -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<!-- Flyway MySQL Support -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
```
### 2.4 API 模块 pom.xml 标准配置
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-api</artifactId>
<version>3.6.7</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>{module-name}-api-{submodule}</artifactId>
<description>
{module-name} API模块
</description>
<dependencies>
<!-- RuoYi Common Core -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<!-- Spring Cloud OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
</project>
```
---
## 三、配置文件规范
### 3.1 bootstrap.yml 标准配置
```yaml
# Tomcat
server:
port: {port} # 按照端口分配规范设置,例如: 9210
# Spring
spring:
application:
# 应用名称
name: {module-name}
profiles:
# 环境配置
active: prod
flyway:
table: flyway_{module}_schema_history # 自定义历史表名
baseline-on-migrate: true # 在nacos中也有配置
baseline-version: 0 # 在nacos中也有配置
cloud:
nacos:
discovery:
# 服务注册地址
server-addr: ruoyi-nacos:8848
config:
# 配置中心地址
server-addr: ruoyi-nacos:8848
# 配置文件格式
file-extension: yml
# 共享配置
shared-configs:
- application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
```
**配置说明:**
- `server.port`: 必须按照端口分配规范设置
- `spring.application.name`: 服务名称,用于服务注册和发现
- `flyway.table`: Flyway 历史表名,格式为 `flyway_{module}_schema_history`
### 3.2 Docker Compose 配置
`docker/docker-compose.yml` 中添加服务配置:
```yaml
{module-name}-modules-{submodule}:
container_name: {module-name}-modules-{submodule}
image: {module-name}-modules-{submodule}-runtime
build:
context: ./ruoyi/modules/{submodule}
dockerfile: dockerfile
environment:
- TZ=Asia/Shanghai
ports:
- "{port}:{port}" # 宿主机端口:容器端口必须与bootstrap.yml一致
depends_on:
- ruoyi-redis
- ruoyi-mysql
links:
- ruoyi-redis
- ruoyi-mysql
```
**重要提示:**
- Docker 端口映射格式:`宿主机端口:容器端口`
- 容器端口必须与 `bootstrap.yml` 中的 `server.port` 一致
- 宿主机端口不能重复,但容器端口可以相同(因为在不同容器中)
---
## 四、数据库层规范
### 4.1 Flyway 迁移脚本命名规范
**目录结构:**
```
src/main/resources/db/migration/
├── V1__Create_{module}_initial_tables.sql
├── V2__Add_{feature}_tables.sql
├── V3__Modify_{table}_add_{field}.sql
├── V4__Add_{table}_indexes.sql
└── V5__Update_{table}_data.sql
```
**命名规则:**
- 格式:`V{版本号}__{描述}.sql`
- 版本号从1开始递增不能跳号
- 描述:使用下划线分隔的英文描述,首字母大写
- 操作类型前缀:
- `Create`: 创建表或数据库对象
- `Add`: 添加字段、索引或数据
- `Modify`: 修改表结构或字段
- `Drop`: 删除表或字段
- `Update`: 更新数据
**示例:**
```
V1__Create_device_tables.sql
V2__Add_device_gateway_field.sql
V3__Add_foreign_key_indexes.sql
V4__Modify_device_add_status_field.sql
```
### 4.2 数据库表设计规范
```sql
CREATE TABLE IF NOT EXISTS {module}_{table_name} (
{table}_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
{field_name} VARCHAR(100) COMMENT '字段说明',
-- 继承自BaseEntity的字段
create_by VARCHAR(64) DEFAULT '' COMMENT '创建者',
create_time DATETIME COMMENT '创建时间',
update_by VARCHAR(64) DEFAULT '' COMMENT '更新者',
update_time DATETIME COMMENT '更新时间',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY ({table}_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='{表中文说明}';
```
**设计原则:**
1. **表名格式**: `{module}_{table_name}`,全小写,下划线分隔
- 示例:`device_device`, `device_dock`, `airline_route`
2. **主键命名**: `{table}_id`,类型为 `BIGINT``AUTO_INCREMENT`
- 示例:`device_id`, `dock_id`, `route_id`
3. **BaseEntity 字段**: 所有表必须包含以下5个字段
```sql
create_by VARCHAR(64) DEFAULT '' COMMENT '创建者',
create_time DATETIME COMMENT '创建时间',
update_by VARCHAR(64) DEFAULT '' COMMENT '更新者',
update_time DATETIME COMMENT '更新时间',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注'
```
4. **字符集**: 统一使用 `utf8mb4`
5. **存储引擎**: 统一使用 `InnoDB`
6. **注释**: 必须添加表注释和字段注释
7. **外键**: 不使用数据库外键约束,通过应用层维护关联关系
### 4.3 索引设计规范
```sql
-- 外键索引
CREATE INDEX idx_{table}_{foreign_key} ON {module}_{table}({foreign_key});
-- 复合索引
CREATE INDEX idx_{table}_{field1}_{field2} ON {module}_{table}({field1}, {field2});
-- 唯一索引
CREATE UNIQUE INDEX uk_{table}_{field} ON {module}_{table}({field});
```
**索引命名规则:**
- 普通索引:`idx_{table}_{field}`
- 唯一索引:`uk_{table}_{field}`
- 复合索引:`idx_{table}_{field1}_{field2}`
**索引设计原则:**
- 所有外键字段必须添加索引
- 频繁查询的字段添加索引
- 复合索引遵循最左前缀原则
- 避免过多索引影响写入性能
**示例:**
```sql
-- 外键索引
CREATE INDEX idx_dock_device_id ON device_dock(device_id);
CREATE INDEX idx_aircraft_device_id ON device_aircraft(device_id);
-- 复合索引
CREATE INDEX idx_dock_device_status ON device_dock(device_id, status);
-- 唯一索引
CREATE UNIQUE INDEX uk_device_sn ON device_device(device_sn);
```
### 4.4 Entity 实体类规范
**位置**: `src/main/java/com/ruoyi/{module}/mapper/entity/{Entity}Entity.java`
```java
package com.ruoyi.{module}.mapper.entity;
import com.ruoyi.common.core.web.domain.BaseEntity;
/**
* {表中文说明}实体对象 {module}_{table}
* Mapper 层实体,对应数据库表
*
* @author ruoyi
* @date {date}
*/
public class {Entity}Entity extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 主键ID */
private Long {entity}Id;
/** 字段说明 */
private String fieldName;
public Long get{Entity}Id()
{
return {entity}Id;
}
public void set{Entity}Id(Long {entity}Id)
{
this.{entity}Id = {entity}Id;
}
public String getFieldName()
{
return fieldName;
}
public void setFieldName(String fieldName)
{
this.fieldName = fieldName;
}
@Override
public String toString()
{
return "{Entity}Entity{" +
"{entity}Id=" + {entity}Id +
", fieldName='" + fieldName + '\'' +
'}';
}
}
```
**规范要点:**
- 必须继承 `BaseEntity`
- 类名格式:`{Entity}Entity`
- 必须实现 `serialVersionUID`
- 使用标准的 Getter/Setter 方法
- 重写 `toString()` 方法
### 4.5 Mapper 接口规范
**位置**: `src/main/java/com/ruoyi/{module}/mapper/{Entity}Mapper.java`
```java
package com.ruoyi.{module}.mapper;
import com.ruoyi.{module}.mapper.entity.{Entity}Entity;
import java.util.List;
/**
* {表中文说明}Mapper接口
*
* @author ruoyi
* @date {date}
*/
public interface {Entity}Mapper
{
/**
* 查询{表中文说明}列表
*
* @param {entity}Entity {表中文说明}
* @return {表中文说明}集合
*/
List<{Entity}Entity> select{Entity}List({Entity}Entity {entity}Entity);
/**
* 根据ID查询{表中文说明}
*
* @param {entity}Id 主键ID
* @return {表中文说明}
*/
{Entity}Entity select{Entity}ById(Long {entity}Id);
/**
* 新增{表中文说明}
*
* @param {entity}Entity {表中文说明}
* @return 结果
*/
int insert{Entity}({Entity}Entity {entity}Entity);
/**
* 修改{表中文说明}
*
* @param {entity}Entity {表中文说明}
* @return 结果
*/
int update{Entity}({Entity}Entity {entity}Entity);
/**
* 删除{表中文说明}
*
* @param {entity}Id 主键ID
* @return 结果
*/
int delete{Entity}ById(Long {entity}Id);
/**
* 批量删除{表中文说明}
*
* @param {entity}Ids 主键ID数组
* @return 结果
*/
int delete{Entity}ByIds(Long[] {entity}Ids);
}
```
**方法命名规范:**
- 查询列表:`select{Entity}List`
- 查询单个:`select{Entity}ById`
- 新增:`insert{Entity}`
- 修改:`update{Entity}`
- 删除:`delete{Entity}ById`
- 批量删除:`delete{Entity}ByIds`
### 4.6 Mapper XML 规范
**位置**: `src/main/resources/mapper/{module}/{Entity}Mapper.xml`
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.{module}.mapper.{Entity}Mapper">
<resultMap type="com.ruoyi.{module}.mapper.entity.{Entity}Entity" id="{Entity}Result">
<result property="{entity}Id" column="{entity}_id" />
<result property="fieldName" column="field_name" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
</resultMap>
<sql id="select{Entity}Vo">
select {entity}_id, field_name, create_by, create_time, update_by, update_time, remark
from {module}_{table}
</sql>
<select id="select{Entity}List" parameterType="com.ruoyi.{module}.mapper.entity.{Entity}Entity" resultMap="{Entity}Result">
<include refid="select{Entity}Vo"/>
<where>
<if test="{entity}Id != null"> and {entity}_id = #{{{entity}Id}}</if>
<if test="fieldName != null and fieldName != ''"> and field_name like concat('%', #{fieldName}, '%')</if>
</where>
</select>
<select id="select{Entity}ById" parameterType="Long" resultMap="{Entity}Result">
<include refid="select{Entity}Vo"/>
where {entity}_id = #{{{entity}Id}}
</select>
<insert id="insert{Entity}" parameterType="com.ruoyi.{module}.mapper.entity.{Entity}Entity" useGeneratedKeys="true" keyProperty="{entity}Id" keyColumn="{entity}_id">
insert into {module}_{table}
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="fieldName != null and fieldName != ''">field_name,</if>
<if test="createBy != null and createBy != ''">create_by,</if>
<if test="remark != null">remark,</if>
create_time
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="fieldName != null and fieldName != ''">#{fieldName},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if>
<if test="remark != null">#{remark},</if>
sysdate()
</trim>
</insert>
<update id="update{Entity}" parameterType="com.ruoyi.{module}.mapper.entity.{Entity}Entity">
update {module}_{table}
<trim prefix="SET" suffixOverrides=",">
<if test="fieldName != null and fieldName != ''">field_name = #{fieldName},</if>
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
<if test="remark != null">remark = #{remark},</if>
update_time = sysdate()
</trim>
where {entity}_id = #{{{entity}Id}}
</update>
<delete id="delete{Entity}ById" parameterType="Long">
delete from {module}_{table} where {entity}_id = #{{{entity}Id}}
</delete>
<delete id="delete{Entity}ByIds" parameterType="Long">
delete from {module}_{table} where {entity}_id in
<foreach item="{entity}Id" collection="array" open="(" separator="," close=")">
#{{{entity}Id}}
</foreach>
</delete>
</mapper>
```
**规范要点:**
- 使用 `<resultMap>` 定义结果映射
- 使用 `<sql>` 定义可复用的 SQL 片段
- 使用动态 SQL 标签:`<if>`, `<trim>`, `<foreach>`
- 插入操作使用 `useGeneratedKeys="true"` 返回主键
- **重要**:插入操作必须同时指定 `keyProperty``keyColumn` 属性
- 时间字段使用 `sysdate()` 函数
---
## 五、领域层Domain规范
### 5.1 Model 领域模型规范
**位置**: `src/main/java/com/ruoyi/{module}/domain/model/{Entity}.java`
```java
package com.ruoyi.{module}.domain.model;
import com.ruoyi.common.core.web.domain.BaseEntity;
/**
* {表中文说明}领域模型
* Domain 层模型,用于业务逻辑处理
*
* @author ruoyi
* @date {date}
*/
public class {Entity} extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 主键ID */
private Long {entity}Id;
/** 字段说明 */
private String fieldName;
public Long get{Entity}Id()
{
return {entity}Id;
}
public void set{Entity}Id(Long {entity}Id)
{
this.{entity}Id = {entity}Id;
}
public String getFieldName()
{
return fieldName;
}
public void setFieldName(String fieldName)
{
this.fieldName = fieldName;
}
@Override
public String toString()
{
return "{Entity}{" +
"{entity}Id=" + {entity}Id +
", fieldName='" + fieldName + '\'' +
'}';
}
}
```
**规范要点:**
- 类名不带后缀,直接使用实体名
- 必须继承 `BaseEntity`
- 字段和方法与 Entity 保持一致
### 5.2 Domain API 接口规范
**位置**: `src/main/java/com/ruoyi/{module}/domain/api/I{Entity}Domain.java`
```java
package com.ruoyi.{module}.domain.api;
import com.ruoyi.{module}.domain.model.{Entity};
import java.util.List;
/**
* {表中文说明}领域接口
*
* @author ruoyi
* @date {date}
*/
public interface I{Entity}Domain
{
/**
* 查询{表中文说明}列表
*/
List<{Entity}> select{Entity}List({Entity} {entity});
/**
* 根据ID查询{表中文说明}
*/
{Entity} select{Entity}ById(Long {entity}Id);
/**
* 新增{表中文说明}
*/
int insert{Entity}({Entity} {entity});
/**
* 修改{表中文说明}
*/
int update{Entity}({Entity} {entity});
/**
* 删除{表中文说明}
*/
int delete{Entity}ById(Long {entity}Id);
/**
* 批量删除{表中文说明}
*/
int delete{Entity}ByIds(Long[] {entity}Ids);
}
```
### 5.3 Domain 实现类规范
**位置**: `src/main/java/com/ruoyi/{module}/domain/impl/{Entity}DomainImpl.java`
```java
package com.ruoyi.{module}.domain.impl;
import com.ruoyi.{module}.domain.api.I{Entity}Domain;
import com.ruoyi.{module}.domain.convert.{Entity}DomainConvert;
import com.ruoyi.{module}.domain.model.{Entity};
import com.ruoyi.{module}.mapper.{Entity}Mapper;
import com.ruoyi.{module}.mapper.entity.{Entity}Entity;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* {表中文说明}领域实现
*
* @author ruoyi
* @date {date}
*/
@Component
public class {Entity}DomainImpl implements I{Entity}Domain
{
private final {Entity}Mapper {entity}Mapper;
public {Entity}DomainImpl({Entity}Mapper {entity}Mapper)
{
this.{entity}Mapper = {entity}Mapper;
}
@Override
public List<{Entity}> select{Entity}List({Entity} {entity})
{
{Entity}Entity entity = {Entity}DomainConvert.toEntity({entity});
List<{Entity}Entity> entityList = {entity}Mapper.select{Entity}List(entity);
return {Entity}DomainConvert.toModelList(entityList);
}
@Override
public {Entity} select{Entity}ById(Long {entity}Id)
{
{Entity}Entity entity = {entity}Mapper.select{Entity}ById({entity}Id);
return {Entity}DomainConvert.toModel(entity);
}
@Override
public int insert{Entity}({Entity} {entity})
{
{Entity}Entity entity = {Entity}DomainConvert.toEntity({entity});
int result = {entity}Mapper.insert{Entity}(entity);
// 【重要】MyBatis 会将自增主键回填到 entity 对象,需要同步回 model 对象
{entity}.set{Entity}Id(entity.get{Entity}Id());
return result;
}
@Override
public int update{Entity}({Entity} {entity})
{
{Entity}Entity entity = {Entity}DomainConvert.toEntity({entity});
return {entity}Mapper.update{Entity}(entity);
}
@Override
public int delete{Entity}ById(Long {entity}Id)
{
return {entity}Mapper.delete{Entity}ById({entity}Id);
}
@Override
public int delete{Entity}ByIds(Long[] {entity}Ids)
{
return {entity}Mapper.delete{Entity}ByIds({entity}Ids);
}
}
```
**重要规范:**
- 使用 `@Component` 注解
- 使用**构造器注入**Constructor Injection
- 字段声明为 `private final`
- 通过 Convert 类进行对象转换
- **【关键】insert 方法必须将 Entity 的主键回填到 Model 对象**
**⚠️ MyBatis 主键回填注意事项:**
MyBatis 在执行 INSERT 操作后,会将数据库自动生成的主键值回填到 `Entity` 对象中,但**不会自动同步到传入的 `Model` 对象**。
**错误示例(会导致主键丢失):**
```java
@Override
public int insert{Entity}({Entity} {entity})
{
{Entity}Entity entity = {Entity}DomainConvert.toEntity({entity});
return {entity}Mapper.insert{Entity}(entity);
// ❌ 错误entity 中的主键没有同步回 {entity} 对象
}
```
**正确示例(主键正确回填):**
```java
@Override
public int insert{Entity}({Entity} {entity})
{
{Entity}Entity entity = {Entity}DomainConvert.toEntity({entity});
int result = {entity}Mapper.insert{Entity}(entity);
// ✅ 正确:将 entity 中的主键同步回 {entity} 对象
{entity}.set{Entity}Id(entity.get{Entity}Id());
return result;
}
```
**为什么需要手动同步主键?**
1. **对象转换导致的隔离**`BeanUtils.copyProperties()` 只是属性拷贝,创建了新的 Entity 对象
2. **MyBatis 只回填 Entity**MyBatis 的 `useGeneratedKeys` 只会将主键设置到 Mapper 方法参数Entity
3. **Model 对象不会自动更新**:原始的 Model 对象与 Entity 对象是两个独立的对象,需要手动同步
**不同步主键的后果:**
如果不将主键同步回 Model 对象,会导致:
- Service 层无法获取新插入记录的主键 ID
- 后续依赖主键的业务逻辑会失败(如关联表插入)
- 可能产生重复数据或数据不一致问题
**示例场景:**
```java
// Service 层调用
Device device = new Device();
device.setDeviceName("测试设备");
deviceDomain.insertDevice(device);
// 如果没有主键回填device.getDeviceId() 将返回 null
Long deviceId = device.getDeviceId(); // 期望得到新插入的 ID
// 后续业务逻辑依赖这个 ID
Dock dock = new Dock();
dock.setDeviceId(deviceId); // 如果 deviceId 为 null会导致关联失败
dockDomain.insertDock(dock);
```
### 5.4 Domain Convert 转换类规范
**位置**: `src/main/java/com/ruoyi/{module}/domain/convert/{Entity}DomainConvert.java`
```java
package com.ruoyi.{module}.domain.convert;
import com.ruoyi.{module}.domain.model.{Entity};
import com.ruoyi.{module}.mapper.entity.{Entity}Entity;
import org.springframework.beans.BeanUtils;
import java.util.ArrayList;
import java.util.List;
/**
* {表中文说明}领域转换类
*
* @author ruoyi
* @date {date}
*/
public class {Entity}DomainConvert
{
/**
* Model 转 Entity
*/
public static {Entity}Entity toEntity({Entity} model)
{
if (model == null)
{
return null;
}
{Entity}Entity entity = new {Entity}Entity();
BeanUtils.copyProperties(model, entity);
return entity;
}
/**
* Entity 转 Model
*/
public static {Entity} toModel({Entity}Entity entity)
{
if (entity == null)
{
return null;
}
{Entity} model = new {Entity}();
BeanUtils.copyProperties(entity, model);
return model;
}
/**
* Entity List 转 Model List
*/
public static List<{Entity}> toModelList(List<{Entity}Entity> entityList)
{
if (entityList == null || entityList.isEmpty())
{
return new ArrayList<>();
}
List<{Entity}> modelList = new ArrayList<>();
for ({Entity}Entity entity : entityList)
{
modelList.add(toModel(entity));
}
return modelList;
}
/**
* Model List 转 Entity List
*/
public static List<{Entity}Entity> toEntityList(List<{Entity}> modelList)
{
if (modelList == null || modelList.isEmpty())
{
return new ArrayList<>();
}
List<{Entity}Entity> entityList = new ArrayList<>();
for ({Entity} model : modelList)
{
entityList.add(toEntity(model));
}
return entityList;
}
}
```
**规范要点:**
- 工具类,所有方法为 `public static`
- 使用 `BeanUtils.copyProperties` 进行属性拷贝
- 提供空值检查
- 提供单个对象和列表转换方法
---
## 六、服务层Service规范
### 6.1 DTO 数据传输对象规范
**位置**: `src/main/java/com/ruoyi/{module}/service/dto/{Entity}DTO.java`
```java
package com.ruoyi.{module}.service.dto;
import com.ruoyi.common.core.web.domain.BaseEntity;
/**
* {表中文说明}数据传输对象
* Service 层 DTO用于业务逻辑编排
*
* @author ruoyi
* @date {date}
*/
public class {Entity}DTO extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 主键ID */
private Long {entity}Id;
/** 字段说明 */
private String fieldName;
public Long get{Entity}Id()
{
return {entity}Id;
}
public void set{Entity}Id(Long {entity}Id)
{
this.{entity}Id = {entity}Id;
}
public String getFieldName()
{
return fieldName;
}
public void setFieldName(String fieldName)
{
this.fieldName = fieldName;
}
@Override
public String toString()
{
return "{Entity}DTO{" +
"{entity}Id=" + {entity}Id +
", fieldName='" + fieldName + '\'' +
'}';
}
}
```
**规范要点:**
- 类名格式:`{Entity}DTO`
- 必须继承 `BaseEntity`
- 字段和方法与 Model 保持一致
### 6.2 Service API 接口规范
**位置**: `src/main/java/com/ruoyi/{module}/service/api/I{Entity}Service.java`
```java
package com.ruoyi.{module}.service.api;
import com.ruoyi.{module}.service.dto.{Entity}DTO;
import java.util.List;
/**
* {表中文说明}服务接口
*
* @author ruoyi
* @date {date}
*/
public interface I{Entity}Service
{
/**
* 查询{表中文说明}列表
*/
List<{Entity}DTO> select{Entity}List({Entity}DTO {entity}DTO);
/**
* 根据ID查询{表中文说明}
*/
{Entity}DTO select{Entity}ById(Long {entity}Id);
/**
* 新增{表中文说明}
*/
int insert{Entity}({Entity}DTO {entity}DTO);
/**
* 修改{表中文说明}
*/
int update{Entity}({Entity}DTO {entity}DTO);
/**
* 删除{表中文说明}
*/
int delete{Entity}ById(Long {entity}Id);
/**
* 批量删除{表中文说明}
*/
int delete{Entity}ByIds(Long[] {entity}Ids);
}
```
### 6.3 Service 实现类规范
**位置**: `src/main/java/com/ruoyi/{module}/service/impl/{Entity}ServiceImpl.java`
```java
package com.ruoyi.{module}.service.impl;
import com.ruoyi.{module}.domain.api.I{Entity}Domain;
import com.ruoyi.{module}.domain.model.{Entity};
import com.ruoyi.{module}.service.api.I{Entity}Service;
import com.ruoyi.{module}.service.convert.{Entity}ServiceConvert;
import com.ruoyi.{module}.service.dto.{Entity}DTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* {表中文说明}服务实现
*
* @author ruoyi
* @date {date}
*/
@Service
public class {Entity}ServiceImpl implements I{Entity}Service
{
@Autowired
private I{Entity}Domain {entity}Domain;
@Override
public List<{Entity}DTO> select{Entity}List({Entity}DTO {entity}DTO)
{
{Entity} model = {Entity}ServiceConvert.toModel({entity}DTO);
List<{Entity}> modelList = {entity}Domain.select{Entity}List(model);
return {Entity}ServiceConvert.toDTOList(modelList);
}
@Override
public {Entity}DTO select{Entity}ById(Long {entity}Id)
{
{Entity} model = {entity}Domain.select{Entity}ById({entity}Id);
return {Entity}ServiceConvert.toDTO(model);
}
@Override
public int insert{Entity}({Entity}DTO {entity}DTO)
{
{Entity} model = {Entity}ServiceConvert.toModel({entity}DTO);
return {entity}Domain.insert{Entity}(model);
}
@Override
public int update{Entity}({Entity}DTO {entity}DTO)
{
{Entity} model = {Entity}ServiceConvert.toModel({entity}DTO);
return {entity}Domain.update{Entity}(model);
}
@Override
public int delete{Entity}ById(Long {entity}Id)
{
return {entity}Domain.delete{Entity}ById({entity}Id);
}
@Override
public int delete{Entity}ByIds(Long[] {entity}Ids)
{
return {entity}Domain.delete{Entity}ByIds({entity}Ids);
}
}
```
**重要规范:**
- 使用 `@Service` 注解
- 使用 `@Autowired` 字段注入Service层使用字段注入
- 通过 Convert 类进行 DTO ↔ Model 转换
### 6.4 Service Convert 转换类规范
**位置**: `src/main/java/com/ruoyi/{module}/service/convert/{Entity}ServiceConvert.java`
```java
package com.ruoyi.{module}.service.convert;
import com.ruoyi.{module}.domain.model.{Entity};
import com.ruoyi.{module}.service.dto.{Entity}DTO;
import org.springframework.beans.BeanUtils;
import java.util.ArrayList;
import java.util.List;
/**
* {表中文说明}服务转换类
*
* @author ruoyi
* @date {date}
*/
public class {Entity}ServiceConvert
{
/**
* DTO 转 Model
*/
public static {Entity} toModel({Entity}DTO dto)
{
if (dto == null)
{
return null;
}
{Entity} model = new {Entity}();
BeanUtils.copyProperties(dto, model);
return model;
}
/**
* Model 转 DTO
*/
public static {Entity}DTO toDTO({Entity} model)
{
if (model == null)
{
return null;
}
{Entity}DTO dto = new {Entity}DTO();
BeanUtils.copyProperties(model, dto);
return dto;
}
/**
* Model List 转 DTO List
*/
public static List<{Entity}DTO> toDTOList(List<{Entity}> modelList)
{
if (modelList == null || modelList.isEmpty())
{
return new ArrayList<>();
}
List<{Entity}DTO> dtoList = new ArrayList<>();
for ({Entity} model : modelList)
{
dtoList.add(toDTO(model));
}
return dtoList;
}
/**
* DTO List 转 Model List
*/
public static List<{Entity}> toModelList(List<{Entity}DTO> dtoList)
{
if (dtoList == null || dtoList.isEmpty())
{
return new ArrayList<>();
}
List<{Entity}> modelList = new ArrayList<>();
for ({Entity}DTO dto : dtoList)
{
modelList.add(toModel(dto));
}
return modelList;
}
}
```
---
## 七、控制器层Controller规范
### 7.1 Controller 类规范
**位置**: `src/main/java/com/ruoyi/{module}/controller/{Entity}Controller.java`
```java
package com.ruoyi.{module}.controller;
import com.ruoyi.common.core.web.controller.BaseController;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.core.web.page.TableDataInfo;
import com.ruoyi.common.log.annotation.Log;
import com.ruoyi.common.log.enums.BusinessType;
import com.ruoyi.common.security.annotation.RequiresPermissions;
import com.ruoyi.{module}.controller.convert.{Entity}ControllerConvert;
import com.ruoyi.{module}.service.api.I{Entity}Service;
import com.ruoyi.{module}.service.dto.{Entity}DTO;
import com.ruoyi.{module}.api.domain.{Entity}VO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* {表中文说明}控制器
*
* @author ruoyi
* @date {date}
*/
@RestController
@RequestMapping("/{entity}")
public class {Entity}Controller extends BaseController
{
@Autowired
private I{Entity}Service {entity}Service;
/**
* 查询{表中文说明}列表
*/
@RequiresPermissions("{module}:{entity}:list")
@GetMapping("/list")
public TableDataInfo list({Entity}VO {entity}VO)
{
startPage();
{Entity}DTO dto = {Entity}ControllerConvert.toDTO({entity}VO);
List<{Entity}DTO> list = {entity}Service.select{Entity}List(dto);
List<{Entity}VO> voList = {Entity}ControllerConvert.toVOList(list);
return getDataTable(voList);
}
/**
* 获取{表中文说明}详细信息
*/
@RequiresPermissions("{module}:{entity}:query")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") Long {entity}Id)
{
{Entity}DTO dto = {entity}Service.select{Entity}ById({entity}Id);
{Entity}VO vo = {Entity}ControllerConvert.toVO(dto);
return success(vo);
}
/**
* 新增{表中文说明}
*/
@RequiresPermissions("{module}:{entity}:add")
@Log(title = "{表中文说明}", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody {Entity}VO {entity}VO)
{
{Entity}DTO dto = {Entity}ControllerConvert.toDTO({entity}VO);
return toAjax({entity}Service.insert{Entity}(dto));
}
/**
* 修改{表中文说明}
*/
@RequiresPermissions("{module}:{entity}:edit")
@Log(title = "{表中文说明}", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody {Entity}VO {entity}VO)
{
{Entity}DTO dto = {Entity}ControllerConvert.toDTO({entity}VO);
return toAjax({entity}Service.update{Entity}(dto));
}
/**
* 删除{表中文说明}
*/
@RequiresPermissions("{module}:{entity}:remove")
@Log(title = "{表中文说明}", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids)
{
return toAjax({entity}Service.delete{Entity}ByIds(ids));
}
}
```
**规范要点:**
- 继承 `BaseController`
- 使用 `@RestController``@RequestMapping`
- 使用 `@Autowired` 字段注入
- 使用 `@RequiresPermissions` 进行权限控制
- 使用 `@Log` 记录操作日志
- 通过 Convert 类进行 VO ↔ DTO 转换
### 7.2 Controller Convert 转换类规范
**位置**: `src/main/java/com/ruoyi/{module}/controller/convert/{Entity}ControllerConvert.java`
```java
package com.ruoyi.{module}.controller.convert;
import com.ruoyi.{module}.api.domain.{Entity}VO;
import com.ruoyi.{module}.service.dto.{Entity}DTO;
import org.springframework.beans.BeanUtils;
import java.util.ArrayList;
import java.util.List;
/**
* {表中文说明}控制器转换类
*
* @author ruoyi
* @date {date}
*/
public class {Entity}ControllerConvert
{
/**
* VO 转 DTO
*/
public static {Entity}DTO toDTO({Entity}VO vo)
{
if (vo == null)
{
return null;
}
{Entity}DTO dto = new {Entity}DTO();
BeanUtils.copyProperties(vo, dto);
return dto;
}
/**
* DTO 转 VO
*/
public static {Entity}VO toVO({Entity}DTO dto)
{
if (dto == null)
{
return null;
}
{Entity}VO vo = new {Entity}VO();
BeanUtils.copyProperties(dto, vo);
return vo;
}
/**
* DTO List 转 VO List
*/
public static List<{Entity}VO> toVOList(List<{Entity}DTO> dtoList)
{
if (dtoList == null || dtoList.isEmpty())
{
return new ArrayList<>();
}
List<{Entity}VO> voList = new ArrayList<>();
for ({Entity}DTO dto : dtoList)
{
voList.add(toVO(dto));
}
return voList;
}
/**
* VO List 转 DTO List
*/
public static List<{Entity}DTO> toDTOList(List<{Entity}VO> voList)
{
if (voList == null || voList.isEmpty())
{
return new ArrayList<>();
}
List<{Entity}DTO> dtoList = new ArrayList<>();
for ({Entity}VO vo : voList)
{
dtoList.add(toDTO(vo));
}
return dtoList;
}
}
```
---
## 八、API层规范
### 8.1 VO 视图对象规范
**位置**: `ruoyi-api/{module-name}-api-{submodule}/src/main/java/com/ruoyi/{module}/api/domain/{Entity}VO.java`
```java
package com.ruoyi.{module}.api.domain;
import com.ruoyi.common.core.web.domain.BaseEntity;
/**
* {表中文说明}视图对象
* API 层 VO用于前后端数据交互
*
* @author ruoyi
* @date {date}
*/
public class {Entity}VO extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 主键ID */
private Long {entity}Id;
/** 字段说明 */
private String fieldName;
public Long get{Entity}Id()
{
return {entity}Id;
}
public void set{Entity}Id(Long {entity}Id)
{
this.{entity}Id = {entity}Id;
}
public String getFieldName()
{
return fieldName;
}
public void setFieldName(String fieldName)
{
this.fieldName = fieldName;
}
@Override
public String toString()
{
return "{Entity}VO{" +
"{entity}Id=" + {entity}Id +
", fieldName='" + fieldName + '\'' +
'}';
}
}
```
### 8.2 Remote Service 接口规范
**位置**: `ruoyi-api/{module-name}-api-{submodule}/src/main/java/com/ruoyi/{module}/api/Remote{Module}Service.java`
```java
package com.ruoyi.{module}.api;
import com.ruoyi.common.core.constant.SecurityConstants;
import com.ruoyi.common.core.constant.ServiceNameConstants;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.{module}.api.domain.{Entity}VO;
import com.ruoyi.{module}.api.factory.Remote{Module}FallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
/**
* {模块中文名}远程服务
*
* @author ruoyi
* @date {date}
*/
@FeignClient(contextId = "remote{Module}Service", value = ServiceNameConstants.{MODULE}_SERVICE, fallbackFactory = Remote{Module}FallbackFactory.class)
public interface Remote{Module}Service
{
/**
* 根据ID获取{表中文说明}
*/
@GetMapping("/{entity}/{id}")
R<{Entity}VO> get{Entity}ById(@PathVariable("id") Long id, @RequestHeader(SecurityConstants.FROM_SOURCE) String source);
}
```
**规范要点:**
- 使用 `@FeignClient` 注解
- `contextId` 格式:`remote{Module}Service`
- `value` 使用 `ServiceNameConstants` 中定义的常量
- 配置 `fallbackFactory` 降级工厂
- 方法参数必须包含 `@RequestHeader(SecurityConstants.FROM_SOURCE) String source`
### 8.3 Fallback Factory 降级工厂规范
**位置**: `ruoyi-api/{module-name}-api-{submodule}/src/main/java/com/ruoyi/{module}/api/factory/Remote{Module}FallbackFactory.java`
```java
package com.ruoyi.{module}.api.factory;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.{module}.api.Remote{Module}Service;
import com.ruoyi.{module}.api.domain.{Entity}VO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
/**
* {模块中文名}服务降级处理
*
* @author ruoyi
* @date {date}
*/
@Component
public class Remote{Module}FallbackFactory implements FallbackFactory<Remote{Module}Service>
{
private static final Logger log = LoggerFactory.getLogger(Remote{Module}FallbackFactory.class);
@Override
public Remote{Module}Service create(Throwable throwable)
{
log.error("{模块中文名}服务调用失败:{}", throwable.getMessage());
return new Remote{Module}Service()
{
@Override
public R<{Entity}VO> get{Entity}ById(Long id, String source)
{
return R.fail("获取{表中文说明}失败:" + throwable.getMessage());
}
};
}
}
```
**规范要点:**
- 使用 `@Component` 注解
- 实现 `FallbackFactory<Remote{Module}Service>`
- 记录错误日志
- 返回友好的错误信息
---
## 九、依赖注入规范
### 9.1 依赖注入方式选择
**Domain 层使用构造器注入Constructor Injection**
```java
@Component
public class DeviceDomainImpl implements IDeviceDomain
{
private final DeviceMapper deviceMapper;
public DeviceDomainImpl(DeviceMapper deviceMapper)
{
this.deviceMapper = deviceMapper;
}
}
```
**优点:**
- 依赖不可变final 字段)
- 更好的可测试性
- 避免循环依赖
- 符合 Spring 官方推荐
**Service 层和 Controller 层使用字段注入Field Injection**
```java
@Service
public class DeviceServiceImpl implements IDeviceService
{
@Autowired
private IDeviceDomain deviceDomain;
}
@RestController
public class DeviceController extends BaseController
{
@Autowired
private IDeviceService deviceService;
}
```
**原因:**
- 代码简洁
- 符合项目现有风格
- 减少样板代码
### 9.2 依赖注入最佳实践
1. **Domain 层必须使用构造器注入**
2. **Service 层和 Controller 层使用字段注入**
3. **避免循环依赖**
4. **依赖接口而非实现**
5. **使用 `private` 修饰注入字段**
---
## 十、命名规范
### 10.1 包命名规范
| 层级 | 包名 | 说明 |
|------|------|------|
| 控制器层 | `controller` | HTTP 请求处理 |
| 控制器转换 | `controller.convert` | VO ↔ DTO 转换 |
| 服务接口 | `service.api` | 服务接口定义 |
| 服务实现 | `service.impl` | 服务接口实现 |
| 服务DTO | `service.dto` | 数据传输对象 |
| 服务转换 | `service.convert` | DTO ↔ Model 转换 |
| 领域接口 | `domain.api` | 领域接口定义 |
| 领域实现 | `domain.impl` | 领域接口实现 |
| 领域模型 | `domain.model` | 领域模型对象 |
| 领域转换 | `domain.convert` | Model ↔ Entity 转换 |
| 数据访问 | `mapper` | MyBatis Mapper 接口 |
| 数据实体 | `mapper.entity` | 数据库实体对象 |
| API视图 | `api.domain` | VO 视图对象 |
| API服务 | `api` | Feign 远程服务 |
| API工厂 | `api.factory` | Feign 降级工厂 |
### 10.2 类命名规范
| 类型 | 命名格式 | 示例 |
|------|---------|------|
| Controller | `{Entity}Controller` | `DeviceController` |
| Service 接口 | `I{Entity}Service` | `IDeviceService` |
| Service 实现 | `{Entity}ServiceImpl` | `DeviceServiceImpl` |
| Domain 接口 | `I{Entity}Domain` | `IDeviceDomain` |
| Domain 实现 | `{Entity}DomainImpl` | `DeviceDomainImpl` |
| Mapper 接口 | `{Entity}Mapper` | `DeviceMapper` |
| Entity 实体 | `{Entity}Entity` | `DeviceEntity` |
| Domain Model | `{Entity}` | `Device` |
| Service DTO | `{Entity}DTO` | `DeviceDTO` |
| API VO | `{Entity}VO` | `DeviceVO` |
| Domain Convert | `{Entity}DomainConvert` | `DeviceDomainConvert` |
| Service Convert | `{Entity}ServiceConvert` | `DeviceServiceConvert` |
| Controller Convert | `{Entity}ControllerConvert` | `DeviceControllerConvert` |
| Remote Service | `Remote{Module}Service` | `RemoteDeviceService` |
| Fallback Factory | `Remote{Module}FallbackFactory` | `RemoteDeviceFallbackFactory` |
### 10.3 方法命名规范
| 操作类型 | 命名格式 | 示例 |
|---------|---------|------|
| 查询列表 | `select{Entity}List` | `selectDeviceList` |
| 查询单个 | `select{Entity}ById` | `selectDeviceById` |
| 新增 | `insert{Entity}` | `insertDevice` |
| 修改 | `update{Entity}` | `updateDevice` |
| 删除 | `delete{Entity}ById` | `deleteDeviceById` |
| 批量删除 | `delete{Entity}ByIds` | `deleteDeviceByIds` |
| 转换为Entity | `toEntity` | `toEntity` |
| 转换为Model | `toModel` | `toModel` |
| 转换为DTO | `toDTO` | `toDTO` |
| 转换为VO | `toVO` | `toVO` |
| 转换为List | `toModelList`, `toDTOList`, `toVOList` | `toModelList` |
### 10.4 字段命名规范
| 字段类型 | 命名格式 | 示例 |
|---------|---------|------|
| 主键ID | `{entity}_id` (数据库) <br> `{entity}Id` (Java) | `device_id` <br> `deviceId` |
| 普通字段 | `field_name` (数据库) <br> `fieldName` (Java) | `device_name` <br> `deviceName` |
| 外键字段 | `{ref_entity}_id` (数据库) <br> `{refEntity}Id` (Java) | `dock_id` <br> `dockId` |
| 布尔字段 | `is_{field}` (数据库) <br> `is{Field}` (Java) | `is_active` <br> `isActive` |
| 时间字段 | `{action}_time` (数据库) <br> `{action}Time` (Java) | `create_time` <br> `createTime` |
### 10.5 数据库命名规范
| 对象类型 | 命名格式 | 示例 |
|---------|---------|------|
| 表名 | `{module}_{table}` | `device_device`, `device_dock` |
| 主键 | `{table}_id` | `device_id`, `dock_id` |
| 普通索引 | `idx_{table}_{field}` | `idx_dock_device_id` |
| 唯一索引 | `uk_{table}_{field}` | `uk_device_sn` |
| 复合索引 | `idx_{table}_{field1}_{field2}` | `idx_dock_device_status` |
| Flyway历史表 | `flyway_{module}_schema_history` | `flyway_device_schema_history` |
---
## 十一、对象转换流程
### 11.1 数据流转图
```
前端请求
Controller 接收 VO
↓ ControllerConvert.toDTO(vo)
Service 处理 DTO
↓ ServiceConvert.toModel(dto)
Domain 处理 Model
↓ DomainConvert.toEntity(model)
Mapper 操作 Entity
↓ 数据库
Database
Mapper 返回 Entity
↑ DomainConvert.toModel(entity)
Domain 返回 Model
↑ ServiceConvert.toDTO(model)
Service 返回 DTO
↑ ControllerConvert.toVO(dto)
Controller 返回 VO
前端响应
```
### 11.2 对象职责说明
| 对象类型 | 所属层 | 职责 | 特点 |
|---------|-------|------|------|
| **VO** (View Object) | API层 | 前后端数据交互 | 可包含展示逻辑字段 |
| **DTO** (Data Transfer Object) | Service层 | 业务逻辑编排 | 可包含业务组合字段 |
| **Model** | Domain层 | 领域业务逻辑 | 纯业务模型 |
| **Entity** | Mapper层 | 数据库映射 | 与数据库表一一对应 |
### 11.3 转换类职责
| 转换类 | 转换方向 | 位置 |
|-------|---------|------|
| **ControllerConvert** | VO ↔ DTO | `controller.convert` |
| **ServiceConvert** | DTO ↔ Model | `service.convert` |
| **DomainConvert** | Model ↔ Entity | `domain.convert` |
---
## 十二、最佳实践
### 12.1 代码规范
1. **分层清晰**:严格遵守 Controller → Service → Domain → Mapper 分层
2. **单一职责**:每个类只负责一个职责
3. **依赖倒置**:依赖接口而非实现
4. **对象转换**:使用专门的 Convert 类进行对象转换
5. **异常处理**:使用统一的异常处理机制
6. **日志记录**:关键操作必须记录日志
7. **权限控制**Controller 方法必须添加权限注解
### 12.2 数据库规范
1. **表名规范**`{module}_{table}` 格式
2. **主键规范**`{table}_id`BIGINTAUTO_INCREMENT
3. **BaseEntity字段**:所有表必须包含 create_by, create_time, update_by, update_time, remark
4. **索引规范**:外键字段必须添加索引
5. **字符集**:统一使用 utf8mb4
6. **存储引擎**:统一使用 InnoDB
7. **Flyway迁移**:所有数据库变更必须通过 Flyway 脚本
### 12.3 配置规范
1. **端口分配**
- 基础服务9200-9209
- 业务服务9210-9299
- 监控服务9100
- 特殊服务9300+
2. **配置同步**
- bootstrap.yml 的 server.port 必须与 docker-compose.yml 的容器端口一致
- Flyway 历史表名格式:`flyway_{module}_schema_history`
3. **服务命名**
- 服务名称格式:`{module-name}`
- 在 ServiceNameConstants 中定义常量
### 12.4 API 规范
1. **Feign 客户端**
- 接口命名:`Remote{Module}Service`
- contextId`remote{Module}Service`
- 必须配置 fallbackFactory
2. **降级处理**
- 工厂命名:`Remote{Module}FallbackFactory`
- 必须记录错误日志
- 返回友好的错误信息
3. **安全头**
- 所有 Feign 方法必须包含 `@RequestHeader(SecurityConstants.FROM_SOURCE) String source`
### 12.5 Maven 规范
1. **新增服务步骤**
- 在 ServiceNameConstants 中添加服务名常量
- 在根 pom.xml 的 dependencyManagement 中添加 API 依赖
- 在 ruoyi-api/pom.xml 中添加模块声明
- 创建业务模块和 API 模块
2. **依赖管理**
- 版本号统一在根 pom.xml 管理
- 使用 `${ruoyi.version}` 引用版本
### 12.6 开发流程
1. **数据库设计** → 编写 Flyway 迁移脚本
2. **Mapper 层** → Entity + Mapper 接口 + Mapper XML
3. **Domain 层** → Model + Domain 接口 + Domain 实现 + Domain Convert
4. **Service 层** → DTO + Service 接口 + Service 实现 + Service Convert
5. **Controller 层** → Controller + Controller Convert
6. **API 层** → VO + Remote Service + Fallback Factory
7. **配置** → bootstrap.yml + docker-compose.yml
8. **测试** → 单元测试 + 集成测试
---
## 附录:快速参考
### A. 创建新模块检查清单
- [ ] 数据库表设计(遵循命名规范)
- [ ] Flyway 迁移脚本V{版本号}__{描述}.sql
- [ ] Entity 实体类(继承 BaseEntity
- [ ] Mapper 接口和 XML
- [ ] Domain Model领域模型
- [ ] Domain 接口和实现(构造器注入)
- [ ] Domain Convert对象转换
- [ ] Service DTO数据传输对象
- [ ] Service 接口和实现(字段注入)
- [ ] Service Convert对象转换
- [ ] Controller权限控制、日志记录
- [ ] Controller Convert对象转换
- [ ] API VO视图对象
- [ ] Remote ServiceFeign 客户端)
- [ ] Fallback Factory降级处理
- [ ] ServiceNameConstants服务名常量
- [ ] 根 pom.xml依赖管理
- [ ] ruoyi-api/pom.xml模块声明
- [ ] bootstrap.yml端口配置
- [ ] docker-compose.yml容器配置
### B. 常见问题
**Q: 为什么 Domain 层使用构造器注入Service 层使用字段注入?**
A: Domain 层使用构造器注入是为了保证依赖不可变和更好的可测试性Service 层使用字段注入是为了代码简洁和符合项目现有风格。
**Q: 为什么需要这么多对象VO、DTO、Model、Entity**
A: 不同层次的对象有不同的职责,这样可以实现关注点分离,提高代码的可维护性和可扩展性。
**Q: Convert 类为什么使用静态方法?**
A: Convert 类是无状态的工具类,使用静态方法可以避免不必要的对象创建,提高性能。
**Q: 如何处理端口冲突?**
A: 按照端口分配规范,基础服务使用 9200-9209业务服务使用 9210-9299确保 bootstrap.yml 和 docker-compose.yml 中的端口配置一致。
**Q: Flyway 迁移脚本命名有什么要求?**
A: 格式为 `V{版本号}__{描述}.sql`版本号从1开始递增不能跳号描述使用下划线分隔的英文首字母大写。
---
**文档结束**
> 本文档将持续更新,如有疑问或建议,请联系架构团队。