aigroup-workflow 2.2.2 → 2.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex/config.toml +22 -39
- package/docs/rules/java_zn/coding-style.md +169 -0
- package/docs/rules/java_zn/mybatis.md +102 -0
- package/package.json +1 -1
- package/skills/java/spring-boot-engineer/SKILL.md +3 -2
- package/skills/java/spring-boot-engineer/references/mybatis-plus.md +592 -0
- package/skills/java/spring-boot-engineer_zn/SKILL.md +129 -0
- package/skills/java/spring-boot-engineer_zn/references/architecture.md +23 -0
- package/skills/java/spring-boot-engineer_zn/references/concurrency.md +9 -0
- package/skills/java/spring-boot-engineer_zn/references/exception-logging.md +31 -0
- package/skills/java/spring-boot-engineer_zn/references/persistence.md +13 -0
- package/skills/java/spring-boot-engineer_zn/references/pojo-lombok.md +48 -0
- package/skills/java/spring-boot-engineer_zn/references/security.md +9 -0
- package/skills/java/spring-boot-engineer_zn/references/testing.md +7 -0
- package/skills/java/spring-boot-engineer_zn/references/validation.md +80 -0
package/.codex/config.toml
CHANGED
|
@@ -1,23 +1,6 @@
|
|
|
1
|
-
#:schema https://developers.openai.com/codex/config-schema.json
|
|
2
|
-
|
|
3
|
-
# aiGroup — Codex 本地配置
|
|
4
|
-
#
|
|
5
|
-
# 用法:
|
|
6
|
-
# - 拷贝到 ~/.codex/config.toml 作为全局默认
|
|
7
|
-
# - 或保留在项目根 .codex/config.toml 作为项目级配置
|
|
8
|
-
#
|
|
9
|
-
# 文档参考:
|
|
10
|
-
# - https://developers.openai.com/codex/config-reference
|
|
11
|
-
# - https://developers.openai.com/codex/multi-agent
|
|
12
|
-
|
|
13
|
-
# ---------- Runtime ----------
|
|
14
|
-
# model / model_provider 留空,使用 Codex CLI 当前默认,除非明确要固定版本
|
|
15
1
|
approval_policy = "on-request"
|
|
16
2
|
sandbox_mode = "workspace-write"
|
|
17
3
|
web_search = "live"
|
|
18
|
-
|
|
19
|
-
# ---------- Persistent Instructions ----------
|
|
20
|
-
# 持久指令在每次 prompt 后追加(与 model_instructions_file 替换不同)
|
|
21
4
|
persistent_instructions = """
|
|
22
5
|
Follow project AGENTS.md as the source of truth.
|
|
23
6
|
Load skills by task intent, not by role ownership.
|
|
@@ -25,44 +8,41 @@ aiGroup has retired the legacy three-role model (ella/jarvis/kyle) — do not de
|
|
|
25
8
|
Use the multi-agent roles declared below for exploration, planning, implementation, review, build fixing, and docs research.
|
|
26
9
|
"""
|
|
27
10
|
|
|
28
|
-
# ---------- MCP Servers ----------
|
|
29
|
-
# aiGroup 项目级精简基线。遵守 docs/rules/performance.md:
|
|
30
|
-
# 单项目启用 MCP < 10,活跃工具 < 80;重型服务器放到用户级配置按需叠加。
|
|
31
11
|
[mcp_servers.github]
|
|
32
12
|
command = "npx"
|
|
33
|
-
args = [
|
|
13
|
+
args = [
|
|
14
|
+
"-y",
|
|
15
|
+
"@modelcontextprotocol/server-github",
|
|
16
|
+
]
|
|
34
17
|
startup_timeout_sec = 30
|
|
35
18
|
|
|
36
19
|
[mcp_servers.context7]
|
|
37
20
|
command = "npx"
|
|
38
|
-
args = [
|
|
21
|
+
args = [
|
|
22
|
+
"-y",
|
|
23
|
+
"@upstash/context7-mcp@latest",
|
|
24
|
+
]
|
|
39
25
|
startup_timeout_sec = 30
|
|
40
26
|
|
|
41
27
|
[mcp_servers.memory]
|
|
42
28
|
command = "npx"
|
|
43
|
-
args = [
|
|
29
|
+
args = [
|
|
30
|
+
"-y",
|
|
31
|
+
"@modelcontextprotocol/server-memory",
|
|
32
|
+
]
|
|
44
33
|
startup_timeout_sec = 30
|
|
45
34
|
|
|
46
35
|
[mcp_servers.sequential-thinking]
|
|
47
36
|
command = "npx"
|
|
48
|
-
args = [
|
|
37
|
+
args = [
|
|
38
|
+
"-y",
|
|
39
|
+
"@modelcontextprotocol/server-sequential-thinking",
|
|
40
|
+
]
|
|
49
41
|
startup_timeout_sec = 30
|
|
50
42
|
|
|
51
|
-
# 按需取消注释:
|
|
52
|
-
# [mcp_servers.playwright]
|
|
53
|
-
# command = "npx"
|
|
54
|
-
# args = ["-y", "@playwright/mcp@latest", "--extension"]
|
|
55
|
-
# startup_timeout_sec = 30
|
|
56
|
-
#
|
|
57
|
-
# [mcp_servers.exa]
|
|
58
|
-
# url = "https://mcp.exa.ai/mcp"
|
|
59
|
-
|
|
60
|
-
# ---------- Features ----------
|
|
61
43
|
[features]
|
|
62
44
|
multi_agent = true
|
|
63
45
|
|
|
64
|
-
# ---------- Profiles ----------
|
|
65
|
-
# 切换:codex -p <name>
|
|
66
46
|
[profiles.strict]
|
|
67
47
|
approval_policy = "on-request"
|
|
68
48
|
sandbox_mode = "read-only"
|
|
@@ -73,9 +53,6 @@ approval_policy = "never"
|
|
|
73
53
|
sandbox_mode = "workspace-write"
|
|
74
54
|
web_search = "live"
|
|
75
55
|
|
|
76
|
-
# ---------- Codex 原生 Persona ----------
|
|
77
|
-
# 仅保留 Codex 自身需要"切换 persona"的 3 个场景。
|
|
78
|
-
# 其他工作(planning / implementation / build-fixing)由 Codex 主对话 + 加载对应 skill 完成。
|
|
79
56
|
[agents]
|
|
80
57
|
max_threads = 3
|
|
81
58
|
max_depth = 1
|
|
@@ -91,3 +68,9 @@ config_file = "agents/reviewer.toml"
|
|
|
91
68
|
[agents.docs_researcher]
|
|
92
69
|
description = "API, framework, and release-note verification via documentation sources."
|
|
93
70
|
config_file = "agents/docs-researcher.toml"
|
|
71
|
+
|
|
72
|
+
[shell_environment_policy]
|
|
73
|
+
inherit = "core"
|
|
74
|
+
|
|
75
|
+
[shell_environment_policy.set]
|
|
76
|
+
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1"
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.java"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Java 代码风格 Rule
|
|
7
|
+
|
|
8
|
+
本规则以《Java 开发手册》为基准,覆盖 Java / Spring / Spring Boot 项目的**纯编码风格**。生成代码时优先遵守项目现有约定;项目无明确约定时按本规则执行。
|
|
9
|
+
|
|
10
|
+
完整工程规约(DTO 校验、Lombok、异常与日志、安全、MySQL/ORM、并发、单元测试、工程分层、Review 清单)见结尾「参考」节。
|
|
11
|
+
|
|
12
|
+
## 1. 总体原则
|
|
13
|
+
|
|
14
|
+
- 代码应清晰、稳定、可维护,命名表达完整业务含义,不为追求简短牺牲可读性。
|
|
15
|
+
- 强制遵守命名、格式、不可变性、错误处理等编码规约。
|
|
16
|
+
- 不引入无必要的新依赖;项目已使用 Spring Utils、Apache Commons Lang3、Jakarta Validation 时优先复用。
|
|
17
|
+
- 修改代码时同步补充或调整必要的单元测试。
|
|
18
|
+
|
|
19
|
+
## 2. 命名规则
|
|
20
|
+
|
|
21
|
+
- 类名使用 `UpperCamelCase`。
|
|
22
|
+
- 方法名、参数名、成员变量、局部变量使用 `lowerCamelCase`。
|
|
23
|
+
- 常量使用全大写,下划线分隔,如 `MAX_STOCK_COUNT`。
|
|
24
|
+
- 包名全小写,使用单数语义,避免拼音、中文、随意缩写。
|
|
25
|
+
- 抽象类以 `Abstract` 或 `Base` 开头;异常类以 `Exception` 结尾;测试类以被测类名开头并以 `Test` 结尾。
|
|
26
|
+
- 领域模型命名遵守:
|
|
27
|
+
- 数据对象:`xxxDO`
|
|
28
|
+
- 数据传输对象:`xxxDTO`
|
|
29
|
+
- 展示对象:`xxxVO`
|
|
30
|
+
- 业务对象:`xxxBO`
|
|
31
|
+
- 禁止命名为 `xxxPOJO`
|
|
32
|
+
- Service / DAO 暴露接口,实现类使用 `Impl` 后缀。
|
|
33
|
+
- 获取单个对象用 `get` 前缀,获取集合用 `list` 前缀,统计用 `count` 前缀,新增用 `save` / `insert`,删除用 `remove` / `delete`,修改用 `update`。
|
|
34
|
+
|
|
35
|
+
## 3. 格式规则
|
|
36
|
+
|
|
37
|
+
- 使用 4 个空格缩进,禁止 tab。
|
|
38
|
+
- 单行代码建议不超过 120 字符,超出需按语义换行。
|
|
39
|
+
- `if` / `for` / `while` / `switch` / `do` 与左括号之间必须有空格。
|
|
40
|
+
- 二目、三目运算符两侧必须有空格。
|
|
41
|
+
- 非空代码块左大括号不换行,右大括号独占一行;`else` 等连续结构紧跟右大括号。
|
|
42
|
+
- 所有 `if` / `else` / `for` / `while` / `do` 语句必须使用大括号。
|
|
43
|
+
- 单个方法建议不超过 80 行。超过时优先拆分为语义明确的私有方法或独立组件。
|
|
44
|
+
- 不同逻辑、不同语义、不同业务代码之间用空行分隔。
|
|
45
|
+
- 每个文件只放一个 public 顶层类型;成员顺序建议:常量、字段、构造器、public 方法、protected 方法、private 方法。
|
|
46
|
+
|
|
47
|
+
## 4. 不可变性
|
|
48
|
+
|
|
49
|
+
- 值类型优先使用 `record`(Java 16+)。
|
|
50
|
+
- 字段默认声明为 `final`,仅在确有需要时才使用可变状态。
|
|
51
|
+
- 公共 API 返回防御性拷贝:`List.copyOf()`、`Map.copyOf()`、`Set.copyOf()`。
|
|
52
|
+
- Copy-on-write:返回新实例而不是就地修改已有对象。
|
|
53
|
+
|
|
54
|
+
```java
|
|
55
|
+
// 推荐 —— 不可变值类型
|
|
56
|
+
public record OrderSummary(Long id, String customerName, BigDecimal total) {}
|
|
57
|
+
|
|
58
|
+
// 推荐 —— final 字段,无 setter
|
|
59
|
+
public class Order {
|
|
60
|
+
private final Long id;
|
|
61
|
+
private final List<LineItem> items;
|
|
62
|
+
|
|
63
|
+
public List<LineItem> getItems() {
|
|
64
|
+
return List.copyOf(items);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 5. 现代 Java 特性
|
|
70
|
+
|
|
71
|
+
在能提升清晰度的场景使用现代语言特性:
|
|
72
|
+
|
|
73
|
+
- **Record** 用于 DTO 和值类型(Java 16+)。
|
|
74
|
+
- **Sealed 类**用于封闭类型层次(Java 17+)。
|
|
75
|
+
- **`instanceof` 模式匹配** —— 无需显式强转(Java 16+)。
|
|
76
|
+
- **文本块**用于多行字符串 —— SQL、JSON 模板(Java 15+)。
|
|
77
|
+
- **Switch 表达式**使用箭头语法(Java 14+)。
|
|
78
|
+
- **Switch 模式匹配** —— 对 sealed 类型做穷尽处理(Java 21+)。
|
|
79
|
+
|
|
80
|
+
```java
|
|
81
|
+
// instanceof 模式匹配
|
|
82
|
+
if (shape instanceof Circle c) {
|
|
83
|
+
return Math.PI * c.radius() * c.radius();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// sealed 类型层次
|
|
87
|
+
public sealed interface PaymentMethod permits CreditCard, BankTransfer, Wallet {}
|
|
88
|
+
|
|
89
|
+
// switch 表达式
|
|
90
|
+
String label = switch (status) {
|
|
91
|
+
case ACTIVE -> "Active";
|
|
92
|
+
case SUSPENDED -> "Suspended";
|
|
93
|
+
case CLOSED -> "Closed";
|
|
94
|
+
};
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## 6. Optional 使用
|
|
98
|
+
|
|
99
|
+
- finder 方法可能无结果时返回 `Optional<T>`。
|
|
100
|
+
- 使用 `map()`、`flatMap()`、`orElseThrow()`;不要在未 `isPresent()` 判断时裸调 `get()`。
|
|
101
|
+
- 不要把 `Optional` 作为字段类型或方法参数类型。
|
|
102
|
+
|
|
103
|
+
```java
|
|
104
|
+
// 推荐
|
|
105
|
+
return repository.findById(id)
|
|
106
|
+
.map(ResponseDto::from)
|
|
107
|
+
.orElseThrow(() -> new OrderNotFoundException(id));
|
|
108
|
+
|
|
109
|
+
// 禁止 —— Optional 作参数
|
|
110
|
+
public void process(Optional<String> name) {}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## 7. 错误处理(核心)
|
|
114
|
+
|
|
115
|
+
- 领域错误优先使用非受检异常。
|
|
116
|
+
- 自定义领域异常继承 `RuntimeException`。
|
|
117
|
+
- 避免宽泛的 `catch (Exception e)`,仅顶层处理器例外。
|
|
118
|
+
- 异常消息必须带上下文信息。
|
|
119
|
+
- 流和资源使用 `try-with-resources`。
|
|
120
|
+
- 禁止空 catch;`finally` 中禁止 `return`。
|
|
121
|
+
|
|
122
|
+
```java
|
|
123
|
+
public class OrderNotFoundException extends RuntimeException {
|
|
124
|
+
public OrderNotFoundException(Long id) {
|
|
125
|
+
super("Order not found: id=" + id);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
> 完整异常处理与日志规则见 skill:`spring-boot-engineer_zn` 的 `references/exception-logging.md`。
|
|
131
|
+
|
|
132
|
+
## 8. 集合与 Streams
|
|
133
|
+
|
|
134
|
+
集合处理:
|
|
135
|
+
|
|
136
|
+
- `Set` 中对象必须正确实现 `equals` 和 `hashCode`。
|
|
137
|
+
- 不要在 `foreach` 中对集合执行 `add` / `remove`,删除时使用 `Iterator`。
|
|
138
|
+
- 集合转数组必须使用 `toArray(T[] array)`。
|
|
139
|
+
- `Arrays.asList` 返回的集合不可直接增删。
|
|
140
|
+
- `subList` 结果不可强转为 `ArrayList`,且要注意原集合修改带来的影响。
|
|
141
|
+
- `Map` 遍历优先使用 `entrySet`。
|
|
142
|
+
- 集合初始化时尽量指定容量。
|
|
143
|
+
- 调用 `addAll` 前应对入参集合做空判断。
|
|
144
|
+
|
|
145
|
+
Streams:
|
|
146
|
+
|
|
147
|
+
- 使用 stream 做转换,管道保持简短(最多 3–4 个操作)。
|
|
148
|
+
- 可读时优先使用方法引用:`.map(Order::getTotal)`。
|
|
149
|
+
- 避免在 stream 操作中产生副作用。
|
|
150
|
+
- 逻辑复杂时优先使用循环,而不是写出晦涩的 stream 管道。
|
|
151
|
+
|
|
152
|
+
## 9. 控制语句
|
|
153
|
+
|
|
154
|
+
- 表达异常分支时优先使用卫语句,减少嵌套。
|
|
155
|
+
- `if ... else if ... else` 嵌套不要超过 3 层;超过时应拆分方法、使用策略模式、状态模式或表驱动。
|
|
156
|
+
- 条件表达式中不要执行复杂逻辑,先赋值给语义明确的变量。
|
|
157
|
+
- 不要在条件表达式中插入赋值语句。
|
|
158
|
+
- 循环体内尽量避免重复创建对象、重复查询数据库、重复执行正则编译等高成本操作。
|
|
159
|
+
|
|
160
|
+
## 10. 注释规则
|
|
161
|
+
|
|
162
|
+
- 类、类属性、类方法按项目要求使用 Javadoc,接口和抽象方法必须说明参数、返回值、异常和语义。
|
|
163
|
+
- 注释应解释业务语义、边界条件、设计意图,不要复述代码。
|
|
164
|
+
- 修改代码时同步更新注释。
|
|
165
|
+
- 不要长期保留无用的注释代码;确需保留时说明原因、负责人和时间。
|
|
166
|
+
|
|
167
|
+
## 参考
|
|
168
|
+
|
|
169
|
+
完整 Java / Spring Boot 工程规约(DTO 校验、Lombok、异常与日志、安全、MySQL/ORM、并发、单元测试、工程分层、Review 清单)见 skill:`spring-boot-engineer_zn`。
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.java"
|
|
4
|
+
- "**/*Mapper.xml"
|
|
5
|
+
- "**/mapper/**/*.xml"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# MyBatis 数据访问与分层 Rule
|
|
9
|
+
|
|
10
|
+
本规则约定 MyBatis(-Plus) 项目的数据访问方式与 Service / Mapper 分层依赖方向。生成或修改数据访问相关代码时遵守本规则;完整 MySQL/ORM 与工程分层规约见结尾「参考」节。
|
|
11
|
+
|
|
12
|
+
## 1. 分层与依赖方向
|
|
13
|
+
|
|
14
|
+
- **Mapper 在底层**:只负责持久化与数据查询,不写业务逻辑。
|
|
15
|
+
- **Service 在上层**:承载业务编排、结果组装与事务边界。
|
|
16
|
+
- **依赖只能自上而下**:`Service → Mapper`;Mapper 不得反向依赖 Service。
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
上层 Service ──依赖──▶ 底层 Mapper(接口 + XML)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 2. 简单查询用 IService,复杂联查走 Mapper XML 原生 SQL
|
|
23
|
+
|
|
24
|
+
- **简单**(单表 CRUD、简单条件查询、分页):Service 接口继承 MyBatis-Plus `IService<T>`、实现类继承 `ServiceImpl<Mapper, T>`,**直接使用继承得到的现成接口**(`getById`、`getOne`、`list`、`listByIds`、`save`、`saveBatch`、`updateById`、`removeById`、`page`、`lambdaQuery()` / `lambdaUpdate()` 等),不写 XML、不重复封装。
|
|
25
|
+
- **复杂多表联查**(多表 JOIN、聚合、分组、子查询等)用 `IService` / Wrapper 表达不便或可读性差时:在 **Mapper 接口声明方法**,在 **XML 中写原生 SQL** 完成查询,由 Service 调用该 Mapper 方法(`ServiceImpl` 中通过 `baseMapper` 访问)。
|
|
26
|
+
- XML 原生 SQL 要求:参数一律用 `#{}` 绑定(禁止 `${}` 拼接用户输入)、显式列出字段(禁止 `select *`)、结果用 `resultMap` 或 DTO 映射、命中必要索引;分页 `count` 为 0 时直接返回。
|
|
27
|
+
|
|
28
|
+
示例:
|
|
29
|
+
|
|
30
|
+
```java
|
|
31
|
+
// Mapper 接口:仅声明复杂联查方法;简单 CRUD 由 BaseMapper 提供,无需声明
|
|
32
|
+
public interface OrderMapper extends BaseMapper<OrderDO> {
|
|
33
|
+
|
|
34
|
+
List<OrderDetailDTO> listOrderDetail(@Param("query") OrderQuery query);
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```xml
|
|
39
|
+
<!-- OrderMapper.xml:原生 SQL 写复杂联查 -->
|
|
40
|
+
<select id="listOrderDetail" resultType="com.example.order.dto.OrderDetailDTO">
|
|
41
|
+
SELECT o.id, o.order_no, u.user_name, p.product_name, oi.quantity
|
|
42
|
+
FROM t_order o
|
|
43
|
+
JOIN t_user u ON u.id = o.user_id
|
|
44
|
+
JOIN t_order_item oi ON oi.order_id = o.id
|
|
45
|
+
JOIN t_product p ON p.id = oi.product_id
|
|
46
|
+
WHERE o.org_id = #{query.orgId}
|
|
47
|
+
<if test="query.status != null">
|
|
48
|
+
AND o.status = #{query.status}
|
|
49
|
+
</if>
|
|
50
|
+
</select>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```java
|
|
54
|
+
// Service 接口:继承 IService,获得现成 CRUD / 简单查询能力
|
|
55
|
+
public interface OrderService extends IService<OrderDO> {
|
|
56
|
+
|
|
57
|
+
List<OrderDetailDTO> listDetail(OrderQuery query); // 复杂联查单独声明
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 实现类:继承 ServiceImpl 并绑定对应 Mapper
|
|
61
|
+
@Service
|
|
62
|
+
public class OrderServiceImpl extends ServiceImpl<OrderMapper, OrderDO> implements OrderService {
|
|
63
|
+
|
|
64
|
+
// 简单查询直接用继承自 IService 的方法,无需另写:
|
|
65
|
+
// getById(id) / list(wrapper) / save(entity) / page(page, wrapper) / lambdaQuery()...
|
|
66
|
+
|
|
67
|
+
// 复杂联查:调用 Mapper 的原生 SQL 方法(baseMapper 即 OrderMapper)
|
|
68
|
+
@Override
|
|
69
|
+
@Transactional(readOnly = true)
|
|
70
|
+
public List<OrderDetailDTO> listDetail(OrderQuery query) {
|
|
71
|
+
return baseMapper.listOrderDetail(query);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 3. 跨 Service 复用:依赖 Service,不要越层调 Mapper
|
|
77
|
+
|
|
78
|
+
- 其他 Service 需要这块数据或能力时,**依赖这个 Service**(`Service → Service`),由它对外暴露方法。
|
|
79
|
+
- **禁止**跨模块直接调用别人的 Mapper 越过其 Service 层——绕过 Service 会丢失业务规则、事务与权限控制。
|
|
80
|
+
- Service 之间避免循环依赖;出现循环依赖时,把公共逻辑下沉到更底层的 Service / Manager 或独立组件。
|
|
81
|
+
|
|
82
|
+
```java
|
|
83
|
+
// 其他 Service 依赖 OrderService,而不是直接用 OrderMapper
|
|
84
|
+
@Service
|
|
85
|
+
public class FulfillmentService {
|
|
86
|
+
|
|
87
|
+
private final OrderService orderService; // 依赖上层暴露的 Service
|
|
88
|
+
|
|
89
|
+
public FulfillmentService(OrderService orderService) {
|
|
90
|
+
this.orderService = orderService;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public void fulfill(OrderQuery query) {
|
|
94
|
+
List<OrderDetailDTO> details = orderService.listDetail(query);
|
|
95
|
+
// ... 履约业务编排,不直接接触 OrderMapper
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## 参考
|
|
101
|
+
|
|
102
|
+
完整数据访问规约(MySQL/ORM、`#{}` 参数绑定、索引、分页、事务)与工程分层、依赖方向见 skill:`spring-boot-engineer_zn`(`references/persistence.md`、`references/architecture.md`)。
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: spring-boot-engineer
|
|
3
|
-
description: Generates Spring Boot 3.x configurations, creates REST controllers, implements Spring Security 6 authentication flows, sets up Spring Data JPA
|
|
3
|
+
description: Generates Spring Boot 3.x configurations, creates REST controllers, implements Spring Security 6 authentication flows, sets up Spring Data JPA or MyBatis-Plus data access, and configures reactive WebFlux endpoints. Use when building Spring Boot 3.x applications, microservices, or reactive Java applications; invoke for Spring Data JPA, MyBatis-Plus, Spring Security 6, WebFlux, Spring Cloud integration, Java REST API design, or Microservices Java architecture.
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: https://github.com/Jeffallan
|
|
7
7
|
version: "1.1.0"
|
|
8
8
|
domain: backend
|
|
9
|
-
triggers: Spring Boot, Spring Framework, Spring Cloud, Spring Security, Spring Data JPA, Spring WebFlux, Microservices Java, Java REST API, Reactive Java
|
|
9
|
+
triggers: Spring Boot, Spring Framework, Spring Cloud, Spring Security, Spring Data JPA, MyBatis-Plus, Spring WebFlux, Microservices Java, Java REST API, Reactive Java
|
|
10
10
|
role: specialist
|
|
11
11
|
scope: implementation
|
|
12
12
|
output-format: code
|
|
@@ -32,6 +32,7 @@ Load detailed guidance based on context:
|
|
|
32
32
|
|-------|-----------|-----------|
|
|
33
33
|
| Web Layer | `references/web.md` | Controllers, REST APIs, validation, exception handling |
|
|
34
34
|
| Data Access | `references/data.md` | Spring Data JPA, repositories, transactions, projections |
|
|
35
|
+
| MyBatis-Plus | `references/mybatis-plus.md` | MyBatis-Plus, BaseMapper, XML mapper queries, wrappers |
|
|
35
36
|
| Security | `references/security.md` | Spring Security 6, OAuth2, JWT, method security |
|
|
36
37
|
| Cloud Native | `references/cloud.md` | Spring Cloud, Config, Discovery, Gateway, resilience |
|
|
37
38
|
| Testing | `references/testing.md` | @SpringBootTest, MockMvc, Testcontainers, test slices |
|
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
# Data Access - MyBatis-Plus
|
|
2
|
+
|
|
3
|
+
## Core Rule
|
|
4
|
+
|
|
5
|
+
Use MyBatis-Plus to remove repetitive single-table SQL, not to hide business rules.
|
|
6
|
+
|
|
7
|
+
| Scenario | Preferred Pattern |
|
|
8
|
+
|----------|-------------------|
|
|
9
|
+
| CRUD by id | `BaseMapper#selectById`, `insert`, `updateById`, `deleteById` |
|
|
10
|
+
| Simple single-table filters | `LambdaQueryWrapper` / `LambdaUpdateWrapper` |
|
|
11
|
+
| Simple paging | `BaseMapper#selectPage` with `Page<T>` |
|
|
12
|
+
| Count / exists | `selectCount` with a lambda wrapper |
|
|
13
|
+
| Complex joins | Mapper interface method + XML SQL |
|
|
14
|
+
| Aggregation / reports | XML SQL with dedicated result DTO |
|
|
15
|
+
| Dynamic SQL with many branches | XML SQL, not a huge Java wrapper chain |
|
|
16
|
+
| Vendor-specific SQL | XML SQL, isolated in the mapper layer |
|
|
17
|
+
|
|
18
|
+
Do not write XML for trivial `WHERE id = ?`, simple `LIKE`, simple status filters, or normal pagination. Do not force complex joins into wrapper chains. When a query needs `JOIN`, `GROUP BY`, window functions, `UNION`, CTE, or a hand-tuned execution plan, put it in XML.
|
|
19
|
+
|
|
20
|
+
## Package Layout
|
|
21
|
+
|
|
22
|
+
Keep MyBatis-Plus details inside the data/service boundary. Controllers should talk to services and DTOs, not mappers or persistence objects.
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
src/main/java/com/example/user/
|
|
26
|
+
controller/
|
|
27
|
+
UserController.java
|
|
28
|
+
dto/
|
|
29
|
+
UserCreateRequest.java
|
|
30
|
+
UserResponse.java
|
|
31
|
+
UserSearchCriteria.java
|
|
32
|
+
UserOrderSummary.java
|
|
33
|
+
entity/
|
|
34
|
+
UserDO.java
|
|
35
|
+
mapper/
|
|
36
|
+
UserMapper.java
|
|
37
|
+
service/
|
|
38
|
+
UserService.java
|
|
39
|
+
|
|
40
|
+
src/main/resources/
|
|
41
|
+
mapper/
|
|
42
|
+
UserMapper.xml
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
If the project already uses `Entity`, `PO`, or another suffix, follow the project. In new MyBatis-Plus code, prefer `DO` for database objects so they do not look like API DTOs or domain responses.
|
|
46
|
+
|
|
47
|
+
## Dependency and Configuration
|
|
48
|
+
|
|
49
|
+
Use the project-managed MyBatis-Plus version. For MyBatis-Plus `3.5.9+`, add `mybatis-plus-jsqlparser` when using `PaginationInnerInterceptor`.
|
|
50
|
+
|
|
51
|
+
```xml
|
|
52
|
+
<dependency>
|
|
53
|
+
<groupId>com.baomidou</groupId>
|
|
54
|
+
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
|
55
|
+
</dependency>
|
|
56
|
+
|
|
57
|
+
<dependency>
|
|
58
|
+
<groupId>com.baomidou</groupId>
|
|
59
|
+
<artifactId>mybatis-plus-jsqlparser</artifactId>
|
|
60
|
+
</dependency>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
mybatis-plus:
|
|
65
|
+
mapper-locations: classpath*:mapper/**/*.xml
|
|
66
|
+
type-aliases-package: com.example.user.entity
|
|
67
|
+
configuration:
|
|
68
|
+
map-underscore-to-camel-case: true
|
|
69
|
+
global-config:
|
|
70
|
+
db-config:
|
|
71
|
+
id-type: auto
|
|
72
|
+
logic-delete-field: deleted
|
|
73
|
+
logic-delete-value: 1
|
|
74
|
+
logic-not-delete-value: 0
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Keep SQL logging in local/test profiles only. Do not enable verbose SQL logs with bind values in production when the query may contain PII.
|
|
78
|
+
|
|
79
|
+
## MyBatis-Plus Configuration
|
|
80
|
+
|
|
81
|
+
```java
|
|
82
|
+
@Configuration
|
|
83
|
+
@MapperScan("com.example.user.mapper")
|
|
84
|
+
public class MybatisPlusConfig {
|
|
85
|
+
|
|
86
|
+
@Bean
|
|
87
|
+
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
|
88
|
+
var interceptor = new MybatisPlusInterceptor();
|
|
89
|
+
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
|
|
90
|
+
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
|
|
91
|
+
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
|
|
92
|
+
return interceptor;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Adjust `DbType` to the actual database. Add tenant or dynamic-table interceptors only when the project really needs them.
|
|
98
|
+
|
|
99
|
+
## Entity Pattern
|
|
100
|
+
|
|
101
|
+
Entities are mutable persistence objects. Keep API validation and response shaping in DTOs, not in `DO` classes.
|
|
102
|
+
|
|
103
|
+
```java
|
|
104
|
+
@TableName("sys_user")
|
|
105
|
+
@Getter
|
|
106
|
+
@Setter
|
|
107
|
+
@NoArgsConstructor
|
|
108
|
+
public class UserDO {
|
|
109
|
+
|
|
110
|
+
@TableId(type = IdType.AUTO)
|
|
111
|
+
private Long id;
|
|
112
|
+
|
|
113
|
+
private Long tenantId;
|
|
114
|
+
|
|
115
|
+
private String email;
|
|
116
|
+
|
|
117
|
+
private String username;
|
|
118
|
+
|
|
119
|
+
private UserStatus status;
|
|
120
|
+
|
|
121
|
+
@TableField(fill = FieldFill.INSERT)
|
|
122
|
+
private LocalDateTime createdAt;
|
|
123
|
+
|
|
124
|
+
@TableField(fill = FieldFill.INSERT_UPDATE)
|
|
125
|
+
private LocalDateTime updatedAt;
|
|
126
|
+
|
|
127
|
+
@TableLogic
|
|
128
|
+
private Integer deleted;
|
|
129
|
+
|
|
130
|
+
@Version
|
|
131
|
+
private Integer version;
|
|
132
|
+
|
|
133
|
+
@TableField(exist = false)
|
|
134
|
+
private List<String> roleNames;
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Prefer `@Getter` / `@Setter` over `@Data` for persistence objects. `@Data` generates `equals`, `hashCode`, and `toString`, which can accidentally include large or sensitive fields.
|
|
139
|
+
|
|
140
|
+
## Auto Fill
|
|
141
|
+
|
|
142
|
+
Use MyBatis-Plus auto-fill for audit fields, but keep user-specific audit data explicit when it depends on security context.
|
|
143
|
+
|
|
144
|
+
```java
|
|
145
|
+
@Component
|
|
146
|
+
public class AuditMetaObjectHandler implements MetaObjectHandler {
|
|
147
|
+
|
|
148
|
+
@Override
|
|
149
|
+
public void insertFill(MetaObject metaObject) {
|
|
150
|
+
var now = LocalDateTime.now();
|
|
151
|
+
strictInsertFill(metaObject, "createdAt", LocalDateTime.class, now);
|
|
152
|
+
strictInsertFill(metaObject, "updatedAt", LocalDateTime.class, now);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@Override
|
|
156
|
+
public void updateFill(MetaObject metaObject) {
|
|
157
|
+
strictUpdateFill(metaObject, "updatedAt", LocalDateTime.class, LocalDateTime.now());
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Auto-fill runs when MyBatis-Plus receives an entity. If you call `update(Wrapper<T>)` without an entity, update fill will not run. Prefer `updateById(entity)` or `update(entity, wrapper)` when `updatedAt` must be maintained automatically.
|
|
163
|
+
|
|
164
|
+
## Mapper Pattern
|
|
165
|
+
|
|
166
|
+
Simple mappers extend `BaseMapper` and stay empty until a real custom query is needed.
|
|
167
|
+
|
|
168
|
+
```java
|
|
169
|
+
public interface UserMapper extends BaseMapper<UserDO> {
|
|
170
|
+
|
|
171
|
+
IPage<UserOrderSummary> selectOrderSummaries(
|
|
172
|
+
Page<UserOrderSummary> page,
|
|
173
|
+
@Param("criteria") UserOrderSearchCriteria criteria
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Use `@MapperScan` at configuration level. Add `@Mapper` only if the project does not use scanner configuration.
|
|
179
|
+
|
|
180
|
+
## BaseMapper and IService
|
|
181
|
+
|
|
182
|
+
Both `BaseMapper` and `IService` are MyBatis-Plus built-in capabilities. This guide defaults to explicit constructor injection of mapper interfaces because it matches the service-layer style used by this skill.
|
|
183
|
+
|
|
184
|
+
If an existing project already extends `ServiceImpl<UserMapper, UserDO>`, keep that convention. Use built-in methods such as `getById`, `list`, `page`, `save`, and `updateById` for simple operations, but do not let service classes become CRUD pass-through wrappers with no business meaning.
|
|
185
|
+
|
|
186
|
+
Avoid the static `Db` kit in normal service code. It makes dependencies implicit and is harder to test than constructor-injected mappers or services.
|
|
187
|
+
|
|
188
|
+
## Simple Queries
|
|
189
|
+
|
|
190
|
+
For simple reads, build lambda wrappers in the service layer. Use method references so refactors fail at compile time instead of becoming broken SQL column strings.
|
|
191
|
+
|
|
192
|
+
```java
|
|
193
|
+
@Service
|
|
194
|
+
@RequiredArgsConstructor
|
|
195
|
+
@Transactional(readOnly = true)
|
|
196
|
+
public class UserService {
|
|
197
|
+
|
|
198
|
+
private static final long MAX_PAGE_SIZE = 100;
|
|
199
|
+
|
|
200
|
+
private final UserMapper userMapper;
|
|
201
|
+
|
|
202
|
+
public Optional<UserResponse> findById(Long id) {
|
|
203
|
+
return Optional.ofNullable(userMapper.selectById(id))
|
|
204
|
+
.map(UserResponse::from);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
public IPage<UserResponse> search(UserSearchCriteria criteria) {
|
|
208
|
+
var page = Page.<UserDO>of(
|
|
209
|
+
criteria.pageNo(),
|
|
210
|
+
Math.min(criteria.pageSize(), MAX_PAGE_SIZE)
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
var query = Wrappers.lambdaQuery(UserDO.class)
|
|
214
|
+
.eq(UserDO::getTenantId, criteria.tenantId())
|
|
215
|
+
.eq(criteria.status() != null, UserDO::getStatus, criteria.status())
|
|
216
|
+
.like(StringUtils.hasText(criteria.keyword()), UserDO::getUsername, criteria.keyword())
|
|
217
|
+
.orderByDesc(UserDO::getCreatedAt);
|
|
218
|
+
|
|
219
|
+
return userMapper.selectPage(page, query)
|
|
220
|
+
.convert(UserResponse::from);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
public boolean existsByEmail(Long tenantId, String email) {
|
|
224
|
+
var query = Wrappers.lambdaQuery(UserDO.class)
|
|
225
|
+
.eq(UserDO::getTenantId, tenantId)
|
|
226
|
+
.eq(UserDO::getEmail, email);
|
|
227
|
+
|
|
228
|
+
return userMapper.selectCount(query) > 0;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Do not return unbounded `List<T>` from public APIs. Use pagination unless the result set is naturally tiny and bounded by business rules.
|
|
234
|
+
|
|
235
|
+
## Simple Writes
|
|
236
|
+
|
|
237
|
+
Keep transaction boundaries on services. Check affected rows for updates and deletes that must touch exactly one row.
|
|
238
|
+
|
|
239
|
+
```java
|
|
240
|
+
@Service
|
|
241
|
+
@RequiredArgsConstructor
|
|
242
|
+
public class UserCommandService {
|
|
243
|
+
|
|
244
|
+
private final UserMapper userMapper;
|
|
245
|
+
|
|
246
|
+
@Transactional
|
|
247
|
+
public UserResponse create(UserCreateRequest request) {
|
|
248
|
+
var user = new UserDO();
|
|
249
|
+
user.setTenantId(request.tenantId());
|
|
250
|
+
user.setEmail(request.email());
|
|
251
|
+
user.setUsername(request.username());
|
|
252
|
+
user.setStatus(UserStatus.ACTIVE);
|
|
253
|
+
|
|
254
|
+
userMapper.insert(user);
|
|
255
|
+
return UserResponse.from(user);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@Transactional
|
|
259
|
+
public void disable(Long tenantId, Long userId) {
|
|
260
|
+
var update = new UserDO();
|
|
261
|
+
update.setStatus(UserStatus.DISABLED);
|
|
262
|
+
|
|
263
|
+
var condition = Wrappers.lambdaUpdate(UserDO.class)
|
|
264
|
+
.eq(UserDO::getTenantId, tenantId)
|
|
265
|
+
.eq(UserDO::getId, userId)
|
|
266
|
+
.eq(UserDO::getStatus, UserStatus.ACTIVE);
|
|
267
|
+
|
|
268
|
+
int rows = userMapper.update(update, condition);
|
|
269
|
+
if (rows != 1) {
|
|
270
|
+
throw new UserNotFoundException(userId);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Avoid `update(null, wrapper)` when audit auto-fill or optimistic locking matters. Avoid `remove` / `delete` operations without a business condition.
|
|
277
|
+
|
|
278
|
+
## Complex XML Queries
|
|
279
|
+
|
|
280
|
+
Use XML when the query is a real SQL artifact. Keep the Java interface small and make parameters explicit with `@Param`.
|
|
281
|
+
|
|
282
|
+
```java
|
|
283
|
+
public record UserOrderSearchCriteria(
|
|
284
|
+
Long tenantId,
|
|
285
|
+
String keyword,
|
|
286
|
+
LocalDateTime startAt,
|
|
287
|
+
LocalDateTime endAt,
|
|
288
|
+
OrderStatus orderStatus,
|
|
289
|
+
long pageNo,
|
|
290
|
+
long pageSize
|
|
291
|
+
) {}
|
|
292
|
+
|
|
293
|
+
public record UserOrderSummary(
|
|
294
|
+
Long userId,
|
|
295
|
+
String username,
|
|
296
|
+
String email,
|
|
297
|
+
Long orderCount,
|
|
298
|
+
BigDecimal totalAmount,
|
|
299
|
+
LocalDateTime lastOrderAt
|
|
300
|
+
) {}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
```xml
|
|
304
|
+
<?xml version="1.0" encoding="UTF-8" ?>
|
|
305
|
+
<!DOCTYPE mapper
|
|
306
|
+
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|
307
|
+
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
|
308
|
+
|
|
309
|
+
<mapper namespace="com.example.user.mapper.UserMapper">
|
|
310
|
+
|
|
311
|
+
<resultMap id="UserOrderSummaryMap" type="com.example.user.dto.UserOrderSummary">
|
|
312
|
+
<constructor>
|
|
313
|
+
<arg column="user_id" javaType="java.lang.Long" />
|
|
314
|
+
<arg column="username" javaType="java.lang.String" />
|
|
315
|
+
<arg column="email" javaType="java.lang.String" />
|
|
316
|
+
<arg column="order_count" javaType="java.lang.Long" />
|
|
317
|
+
<arg column="total_amount" javaType="java.math.BigDecimal" />
|
|
318
|
+
<arg column="last_order_at" javaType="java.time.LocalDateTime" />
|
|
319
|
+
</constructor>
|
|
320
|
+
</resultMap>
|
|
321
|
+
|
|
322
|
+
<select id="selectOrderSummaries" resultMap="UserOrderSummaryMap">
|
|
323
|
+
SELECT
|
|
324
|
+
u.id AS user_id,
|
|
325
|
+
u.username,
|
|
326
|
+
u.email,
|
|
327
|
+
COUNT(o.id) AS order_count,
|
|
328
|
+
COALESCE(SUM(o.total_amount), 0) AS total_amount,
|
|
329
|
+
MAX(o.created_at) AS last_order_at
|
|
330
|
+
FROM sys_user u
|
|
331
|
+
INNER JOIN orders o
|
|
332
|
+
ON o.user_id = u.id
|
|
333
|
+
AND o.deleted = 0
|
|
334
|
+
WHERE u.deleted = 0
|
|
335
|
+
AND u.tenant_id = #{criteria.tenantId}
|
|
336
|
+
<if test="criteria.keyword != null and criteria.keyword != ''">
|
|
337
|
+
AND (
|
|
338
|
+
u.username LIKE CONCAT('%', #{criteria.keyword}, '%')
|
|
339
|
+
OR u.email LIKE CONCAT('%', #{criteria.keyword}, '%')
|
|
340
|
+
)
|
|
341
|
+
</if>
|
|
342
|
+
<if test="criteria.startAt != null">
|
|
343
|
+
AND o.created_at >= #{criteria.startAt}
|
|
344
|
+
</if>
|
|
345
|
+
<if test="criteria.endAt != null">
|
|
346
|
+
AND o.created_at < #{criteria.endAt}
|
|
347
|
+
</if>
|
|
348
|
+
<if test="criteria.orderStatus != null">
|
|
349
|
+
AND o.status = #{criteria.orderStatus}
|
|
350
|
+
</if>
|
|
351
|
+
GROUP BY u.id, u.username, u.email
|
|
352
|
+
ORDER BY last_order_at DESC
|
|
353
|
+
</select>
|
|
354
|
+
|
|
355
|
+
</mapper>
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Let the pagination interceptor add paging SQL. Do not add manual `LIMIT` / `OFFSET` to a paged XML method unless the interceptor is intentionally not used.
|
|
359
|
+
|
|
360
|
+
## XML Pagination Usage
|
|
361
|
+
|
|
362
|
+
```java
|
|
363
|
+
@Transactional(readOnly = true)
|
|
364
|
+
public IPage<UserOrderSummary> searchOrderSummaries(UserOrderSearchCriteria criteria) {
|
|
365
|
+
var page = Page.<UserOrderSummary>of(
|
|
366
|
+
criteria.pageNo(),
|
|
367
|
+
Math.min(criteria.pageSize(), MAX_PAGE_SIZE)
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
return userMapper.selectOrderSummaries(page, criteria);
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
If a grouped XML page query has an expensive generated count SQL, define a dedicated count statement or split the count query from the data query.
|
|
375
|
+
|
|
376
|
+
## DTO Mapping
|
|
377
|
+
|
|
378
|
+
Use records for request/response DTOs when the project is on Java 16+.
|
|
379
|
+
|
|
380
|
+
```java
|
|
381
|
+
public record UserResponse(
|
|
382
|
+
Long id,
|
|
383
|
+
String email,
|
|
384
|
+
String username,
|
|
385
|
+
UserStatus status,
|
|
386
|
+
LocalDateTime createdAt
|
|
387
|
+
) {
|
|
388
|
+
public static UserResponse from(UserDO user) {
|
|
389
|
+
return new UserResponse(
|
|
390
|
+
user.getId(),
|
|
391
|
+
user.getEmail(),
|
|
392
|
+
user.getUsername(),
|
|
393
|
+
user.getStatus(),
|
|
394
|
+
user.getCreatedAt()
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Do not expose `UserDO` from controllers. Persistence objects often contain internal fields such as `tenantId`, `deleted`, `version`, or audit metadata.
|
|
401
|
+
|
|
402
|
+
## SQL Safety
|
|
403
|
+
|
|
404
|
+
Use `#{}` for all runtime values.
|
|
405
|
+
|
|
406
|
+
```xml
|
|
407
|
+
WHERE u.email = #{email}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
Avoid `${}`. It performs string substitution and can become SQL injection. The only acceptable cases are whitelisted identifiers such as sort columns or dynamic table names after strict validation.
|
|
411
|
+
|
|
412
|
+
```java
|
|
413
|
+
private static final Map<String, SFunction<UserDO, ?>> SORT_FIELDS = Map.of(
|
|
414
|
+
"createdAt", UserDO::getCreatedAt,
|
|
415
|
+
"username", UserDO::getUsername
|
|
416
|
+
);
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Avoid `wrapper.last(...)`, `apply(...)`, or raw SQL fragments with user input. If a fragment is unavoidable, keep user input bound with parameters and whitelist all identifiers.
|
|
420
|
+
|
|
421
|
+
## Transactions
|
|
422
|
+
|
|
423
|
+
Put `@Transactional` on service methods, not controllers or mappers.
|
|
424
|
+
|
|
425
|
+
| Operation | Transaction Pattern |
|
|
426
|
+
|-----------|---------------------|
|
|
427
|
+
| Read-only query | `@Transactional(readOnly = true)` |
|
|
428
|
+
| Single insert/update with side effects | `@Transactional` |
|
|
429
|
+
| Multi-step write | One service method with one transaction boundary |
|
|
430
|
+
| XML report query | `readOnly = true`, no state mutation |
|
|
431
|
+
|
|
432
|
+
Avoid self-invocation traps. A `@Transactional` method called from another method in the same class will not pass through Spring's proxy.
|
|
433
|
+
|
|
434
|
+
## Logical Delete and Optimistic Locking
|
|
435
|
+
|
|
436
|
+
Use `@TableLogic` for soft delete when records must be retained. Remember that XML SQL does not automatically add every business rule you forget to write. In XML, explicitly filter `deleted = 0`.
|
|
437
|
+
|
|
438
|
+
Use `@Version` for rows that can be concurrently edited. Check the update row count and return a conflict/domain exception when the row was not updated.
|
|
439
|
+
|
|
440
|
+
```java
|
|
441
|
+
@Transactional
|
|
442
|
+
public void rename(Long userId, String username, Integer version) {
|
|
443
|
+
var user = new UserDO();
|
|
444
|
+
user.setId(userId);
|
|
445
|
+
user.setUsername(username);
|
|
446
|
+
user.setVersion(version);
|
|
447
|
+
|
|
448
|
+
int rows = userMapper.updateById(user);
|
|
449
|
+
if (rows != 1) {
|
|
450
|
+
throw new ConcurrentUpdateException("User was modified: id=" + userId);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
## Performance Practices
|
|
456
|
+
|
|
457
|
+
- Select only needed columns for list pages. Avoid `SELECT *` in XML.
|
|
458
|
+
- Add indexes for equality filters, join keys, and common sort fields.
|
|
459
|
+
- Keep large report queries in XML so they can be reviewed with `EXPLAIN`.
|
|
460
|
+
- Prefer keyset pagination for very deep pages when the UI allows it.
|
|
461
|
+
- Do not use `like '%keyword%'` on large tables without understanding index impact.
|
|
462
|
+
- Keep batch writes chunked and transactional. Do not send a huge list in one statement by default.
|
|
463
|
+
- Avoid N+1 lookup loops. If a list view needs derived data from another table, write one XML query or fetch in batches.
|
|
464
|
+
- Keep result maps explicit for complex DTOs, enum columns, JSON columns, or fields with type handlers.
|
|
465
|
+
|
|
466
|
+
## Testing
|
|
467
|
+
|
|
468
|
+
Use mapper tests for XML SQL. Use a real database with Testcontainers when SQL depends on dialect behavior.
|
|
469
|
+
|
|
470
|
+
```java
|
|
471
|
+
@SpringBootTest
|
|
472
|
+
@Testcontainers
|
|
473
|
+
class UserMapperIT {
|
|
474
|
+
|
|
475
|
+
@Container
|
|
476
|
+
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.4");
|
|
477
|
+
|
|
478
|
+
@Autowired
|
|
479
|
+
private UserMapper userMapper;
|
|
480
|
+
|
|
481
|
+
@Test
|
|
482
|
+
void selectOrderSummaries_filtersByTenant() {
|
|
483
|
+
var criteria = new UserOrderSearchCriteria(
|
|
484
|
+
1L,
|
|
485
|
+
null,
|
|
486
|
+
null,
|
|
487
|
+
null,
|
|
488
|
+
null,
|
|
489
|
+
1,
|
|
490
|
+
20
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
var page = Page.<UserOrderSummary>of(1, 20);
|
|
494
|
+
var result = userMapper.selectOrderSummaries(page, criteria);
|
|
495
|
+
|
|
496
|
+
assertThat(result.getRecords())
|
|
497
|
+
.allSatisfy(row -> assertThat(row.userId()).isNotNull());
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
Test at least these cases:
|
|
503
|
+
|
|
504
|
+
- Simple wrapper query includes tenant / ownership filters.
|
|
505
|
+
- XML query works with all optional criteria absent.
|
|
506
|
+
- XML query works with each optional criterion present.
|
|
507
|
+
- Pagination returns stable ordering.
|
|
508
|
+
- Logical delete rows are hidden.
|
|
509
|
+
- Update operations check affected row count.
|
|
510
|
+
- Optimistic lock conflict is handled.
|
|
511
|
+
|
|
512
|
+
## Database Migrations
|
|
513
|
+
|
|
514
|
+
Keep table definitions aligned with entity annotations.
|
|
515
|
+
|
|
516
|
+
```sql
|
|
517
|
+
CREATE TABLE sys_user (
|
|
518
|
+
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|
519
|
+
tenant_id BIGINT NOT NULL,
|
|
520
|
+
email VARCHAR(100) NOT NULL,
|
|
521
|
+
username VARCHAR(100) NOT NULL,
|
|
522
|
+
status VARCHAR(32) NOT NULL,
|
|
523
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
524
|
+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
525
|
+
deleted TINYINT NOT NULL DEFAULT 0,
|
|
526
|
+
version INT NOT NULL DEFAULT 0,
|
|
527
|
+
UNIQUE KEY uk_user_tenant_email (tenant_id, email),
|
|
528
|
+
KEY idx_user_tenant_status_created (tenant_id, status, created_at),
|
|
529
|
+
KEY idx_user_username (username)
|
|
530
|
+
);
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
Do not rely on MyBatis-Plus annotations as a replacement for Flyway/Liquibase migrations. The database schema is still the source of truth for production.
|
|
534
|
+
|
|
535
|
+
## Code Generator
|
|
536
|
+
|
|
537
|
+
Generated code is a starting point, not final code.
|
|
538
|
+
|
|
539
|
+
- Rename generated entities to match the project suffix (`DO`, `Entity`, or existing convention).
|
|
540
|
+
- Remove generated controllers unless they match the API design.
|
|
541
|
+
- Replace field injection with constructor injection.
|
|
542
|
+
- Replace broad `@Data` if the project prefers `@Getter` / `@Setter`.
|
|
543
|
+
- Move complex generated SQL into reviewed XML files.
|
|
544
|
+
- Add DTO mapping instead of returning persistence objects directly.
|
|
545
|
+
|
|
546
|
+
## MUST DO
|
|
547
|
+
|
|
548
|
+
| Rule | Correct Pattern |
|
|
549
|
+
|------|-----------------|
|
|
550
|
+
| Simple single-table query | `BaseMapper` + `LambdaQueryWrapper` |
|
|
551
|
+
| Complex join query | Mapper method + XML |
|
|
552
|
+
| Runtime values in SQL | `#{param}` |
|
|
553
|
+
| Dynamic identifiers | Whitelist before using `${}` |
|
|
554
|
+
| API response | Map `DO` to DTO/record |
|
|
555
|
+
| Pagination | `Page<T>` with max page size |
|
|
556
|
+
| Transactions | Service layer only |
|
|
557
|
+
| Audit fields | `FieldFill` + `MetaObjectHandler` |
|
|
558
|
+
| Safety plugin | `BlockAttackInnerInterceptor` |
|
|
559
|
+
| Custom XML | Mapper test with real DB dialect when needed |
|
|
560
|
+
|
|
561
|
+
## MUST NOT DO
|
|
562
|
+
|
|
563
|
+
- Do not write XML for simple CRUD that MP already supports.
|
|
564
|
+
- Do not force joins, reports, or aggregation queries into wrapper chains.
|
|
565
|
+
- Do not inject mappers into controllers.
|
|
566
|
+
- Do not return `DO` objects from REST APIs.
|
|
567
|
+
- Do not concatenate SQL with user input.
|
|
568
|
+
- Do not pass user input into `${}`, `last(...)`, or raw SQL fragments.
|
|
569
|
+
- Do not run unbounded list queries from API endpoints.
|
|
570
|
+
- Do not ignore affected row counts on important updates/deletes.
|
|
571
|
+
- Do not rely on auto-fill when calling `update(Wrapper<T>)` without an entity.
|
|
572
|
+
- Do not add global plugins or generator templates before the project needs them.
|
|
573
|
+
|
|
574
|
+
## Quick Reference
|
|
575
|
+
|
|
576
|
+
| API / Annotation | Purpose |
|
|
577
|
+
|------------------|---------|
|
|
578
|
+
| `BaseMapper<T>` | Built-in CRUD mapper |
|
|
579
|
+
| `Wrappers.lambdaQuery` | Type-safe query conditions |
|
|
580
|
+
| `Wrappers.lambdaUpdate` | Type-safe update conditions |
|
|
581
|
+
| `Page<T>` / `IPage<T>` | MyBatis-Plus pagination |
|
|
582
|
+
| `@TableName` | Table mapping |
|
|
583
|
+
| `@TableId` | Primary key mapping |
|
|
584
|
+
| `@TableField` | Field mapping, fill, type handler, non-table fields |
|
|
585
|
+
| `FieldFill` | Insert/update auto-fill strategy |
|
|
586
|
+
| `MetaObjectHandler` | Auto-fill implementation |
|
|
587
|
+
| `@TableLogic` | Logical delete field |
|
|
588
|
+
| `@Version` | Optimistic lock field |
|
|
589
|
+
| `MybatisPlusInterceptor` | Plugin container |
|
|
590
|
+
| `PaginationInnerInterceptor` | Pagination plugin |
|
|
591
|
+
| `OptimisticLockerInnerInterceptor` | Optimistic lock plugin |
|
|
592
|
+
| `BlockAttackInnerInterceptor` | Blocks full-table update/delete |
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: spring-boot-engineer_zn
|
|
3
|
+
description: 中文版 Java / Spring Boot 工程规约与代码 Review 标准。覆盖 Jakarta Validation 参数校验、空安全工具类、常量与枚举、Lombok/POJO、异常与日志、安全、MySQL/ORM、并发、单元测试、工程分层与 Review 清单。当用中文进行 Java/Spring Boot 代码生成、改造或 Review 时加载。
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
version: "1.0.0"
|
|
7
|
+
domain: backend
|
|
8
|
+
triggers: Java 代码规约, Spring Boot 规约, Jakarta Validation, Lombok, MyBatis, 阿里巴巴 Java 开发手册, 代码 Review, 工程分层
|
|
9
|
+
role: specialist
|
|
10
|
+
scope: standards
|
|
11
|
+
output-format: code
|
|
12
|
+
related-skills: spring-boot-engineer, java-architect
|
|
13
|
+
language: zh-CN
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Spring Boot 工程规约(中文)
|
|
17
|
+
|
|
18
|
+
## 适用范围
|
|
19
|
+
|
|
20
|
+
本 skill 以《Java 开发手册》为基准,适用于 Java / Spring / Spring Boot 项目的代码生成、修改和 Review,强制遵守命名、格式、异常日志、安全、数据库、分层与设计规约。
|
|
21
|
+
|
|
22
|
+
加载时机:当用中文进行 Java / Spring Boot 代码生成、改造或 Review,且需要超出纯编码风格(见 `docs/rules/java_zn/coding-style.md`)的工程规约时,加载本 skill。生成代码时优先遵守项目现有约定;项目无明确约定时按本 skill 执行。
|
|
23
|
+
|
|
24
|
+
## 参考指南
|
|
25
|
+
|
|
26
|
+
按上下文加载对应参考文件:
|
|
27
|
+
|
|
28
|
+
| 主题 | 参考文件 | 何时加载 |
|
|
29
|
+
|------|----------|----------|
|
|
30
|
+
| DTO 与参数校验 | `references/validation.md` | Jakarta Validation 注解、`@Valid`/`@Validated`、跨字段与业务校验 |
|
|
31
|
+
| OOP / POJO / Lombok | `references/pojo-lombok.md` | POJO 设计、Lombok 注解组合、Builder、`@Data` 取舍 |
|
|
32
|
+
| 异常与日志 | `references/exception-logging.md` | 异常处理完整规则、SLF4J 日志、脱敏 |
|
|
33
|
+
| 安全 | `references/security.md` | 参数校验、权限、脱敏、SQL 注入、CSRF、幂等防刷 |
|
|
34
|
+
| MySQL 与 ORM | `references/persistence.md` | 表结构规约、MyBatis、分页、事务 |
|
|
35
|
+
| 并发 | `references/concurrency.md` | 线程池、`ThreadLocal`、锁、并发更新 |
|
|
36
|
+
| 单元测试 | `references/testing.md` | 测试编写、覆盖范围、独立可重复 |
|
|
37
|
+
| 工程结构与设计 | `references/architecture.md` | 重复代码抽象、分层依赖、单一职责、设计原则 |
|
|
38
|
+
|
|
39
|
+
## 核心规约
|
|
40
|
+
|
|
41
|
+
以下为高频内联规约,完整迁移自原规则。
|
|
42
|
+
|
|
43
|
+
### 值判断与工具类使用
|
|
44
|
+
|
|
45
|
+
- 字符串、包装类型、对象、集合、数组等值判断,应优先使用空安全工具类或 JDK 工具方法,避免直接使用 `==` / `!=` 做相等判断。
|
|
46
|
+
- 字符串空白判断优先使用 `org.apache.commons.lang3.StringUtils.isBlank` / `isNotBlank`,或项目已有的 `org.springframework.util.StringUtils.hasText`。
|
|
47
|
+
- 字符串相等优先使用 `org.apache.commons.lang3.StringUtils.equals` / `equalsIgnoreCase`。
|
|
48
|
+
- 对象相等优先使用 `java.util.Objects.equals`。
|
|
49
|
+
- Boolean 包装类型判断优先使用 `org.apache.commons.lang3.BooleanUtils.isTrue` / `isFalse`。
|
|
50
|
+
- 集合或 Map 判空优先使用 `org.springframework.util.CollectionUtils.isEmpty`。
|
|
51
|
+
- 数组、对象整体判空可使用 `org.springframework.util.ObjectUtils.isEmpty`。
|
|
52
|
+
- 枚举相等在明确非空语义下可以使用 `EnumConstant == value`;如果枚举变量可能为 null,优先使用 `Objects.equals(value, EnumConstant)` 或将常量放左侧。
|
|
53
|
+
- 基本类型大小比较仍使用 `>`、`<`、`>=`、`<=`。
|
|
54
|
+
|
|
55
|
+
禁止写法:
|
|
56
|
+
|
|
57
|
+
```java
|
|
58
|
+
if (userName == "") {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (status == 1) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (enabled == true) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
推荐写法:
|
|
72
|
+
|
|
73
|
+
```java
|
|
74
|
+
if (StringUtils.isBlank(userName)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (Objects.equals(status, ENABLED_STATUS)) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (BooleanUtils.isTrue(enabled)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (CollectionUtils.isEmpty(userList)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 常量与枚举
|
|
92
|
+
|
|
93
|
+
- 禁止魔法值直接出现在代码中,应定义为常量或枚举。
|
|
94
|
+
- `Long` 字面量使用大写 `L`,例如 `1L`。
|
|
95
|
+
- 固定范围的状态、类型、渠道、来源等优先使用枚举。
|
|
96
|
+
- 常量按功能归类,不要维护一个“大而全”的常量类。
|
|
97
|
+
- 枚举类建议以 `Enum` 结尾,枚举项全大写并使用下划线分隔。
|
|
98
|
+
|
|
99
|
+
### DTO 与参数校验要点
|
|
100
|
+
|
|
101
|
+
- DTO 字段校验优先使用 `jakarta.validation` 注解,Controller 入参用 `@Valid` / `@Validated` 触发,嵌套对象与集合元素用 `@Valid` 级联。
|
|
102
|
+
- 禁止使用 `javax.validation` 包;Spring Boot 3+ 统一 Jakarta Validation。
|
|
103
|
+
- 简单字段校验交给 DTO 注解,不在 Service 中重复写 `if`;跨字段、依赖外部数据、权限幂等等复杂校验才用代码校验。
|
|
104
|
+
|
|
105
|
+
> 完整规则与示例见 `references/validation.md`。
|
|
106
|
+
|
|
107
|
+
### Lombok 与 POJO 要点
|
|
108
|
+
|
|
109
|
+
- 创建 DO / DTO / VO / BO / Query / Command 等数据承载对象时,优先使用 Lombok 注解生成 getter、setter、构造器、`toString`、Builder 等样板代码。
|
|
110
|
+
- 字段较多或可选参数较多时优先 `@Builder` + `Xxx.builder().field(value).build()`;框架反射创建对象时保留无参构造器。
|
|
111
|
+
- 谨慎使用 `@Data`:涉及继承、实体、缓存 key、集合元素去重或需精确控制 `equals` / `hashCode` 的类,改用更明确的 `@Getter`、`@Setter`、`@ToString`、`@EqualsAndHashCode`。
|
|
112
|
+
|
|
113
|
+
> 完整规则与示例见 `references/pojo-lombok.md`。
|
|
114
|
+
|
|
115
|
+
## Review 检查清单
|
|
116
|
+
|
|
117
|
+
Review Java 代码时至少检查:
|
|
118
|
+
|
|
119
|
+
- DTO 字段校验是否优先使用 `jakarta.validation` 注解。
|
|
120
|
+
- 是否误用了 `javax.validation`。
|
|
121
|
+
- 字符串、包装类型、对象、集合判断是否使用空安全工具类。
|
|
122
|
+
- 是否存在 `==` 比较字符串、包装类型或业务值对象。
|
|
123
|
+
- 数据承载对象是否优先使用 Lombok 注解,字段较多的对象创建是否优先使用 Builder。
|
|
124
|
+
- 是否出现魔法值。
|
|
125
|
+
- 同类或跨类重复代码是否出现 3 次及以上。
|
|
126
|
+
- 是否存在过深嵌套、超长方法或复杂条件表达式。
|
|
127
|
+
- 异常是否被吞掉,日志是否重复打印或泄露敏感信息。
|
|
128
|
+
- SQL 是否显式列字段、使用参数绑定并命中必要索引。
|
|
129
|
+
- 新增核心逻辑是否有单元测试覆盖。
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# 工程结构与设计
|
|
2
|
+
|
|
3
|
+
## 重复代码与抽象
|
|
4
|
+
|
|
5
|
+
- 同一段或高度相似代码出现 3 次及以上,必须评估抽取。
|
|
6
|
+
- 简单业务复用优先抽为 `private` 方法或领域服务方法。
|
|
7
|
+
- 跨类复用优先抽为公共组件、工具类、模板方法或策略类。
|
|
8
|
+
- 跨层、横切关注点优先考虑 AOP、Filter、Interceptor、注解驱动组件,例如:
|
|
9
|
+
- 登录态、权限、租户、审计、幂等、限流、防重放。
|
|
10
|
+
- 统一日志、埋点、异常转换、接口耗时统计。
|
|
11
|
+
- 重复的通用参数校验逻辑。
|
|
12
|
+
- DTO 简单字段校验重复出现时,不要抽 `checkXxx` 方法,应优先回到 DTO 上加 Jakarta Validation 注解。
|
|
13
|
+
|
|
14
|
+
## 工程结构与设计
|
|
15
|
+
|
|
16
|
+
- 上层可以依赖下层,下层不要反向依赖上层。
|
|
17
|
+
- Web 层负责协议适配、基础参数校验和转发,不承载复杂业务。
|
|
18
|
+
- Service / Manager 层承载业务编排和领域逻辑。
|
|
19
|
+
- DAO 层只处理持久化,不写业务逻辑。
|
|
20
|
+
- 类应遵守单一职责。
|
|
21
|
+
- 优先组合 / 聚合,谨慎使用继承。
|
|
22
|
+
- 依赖抽象而非具体实现,保持扩展开放、修改封闭。
|
|
23
|
+
- 状态超过 3 个、对象协作复杂、调用链涉及对象超过 3 个时,应考虑补充状态图、时序图或类图辅助设计。
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# 并发规则
|
|
2
|
+
|
|
3
|
+
- 线程资源必须通过线程池提供,不允许随意 `new Thread`。
|
|
4
|
+
- 线程池不使用 `Executors` 快捷方法,应通过 `ThreadPoolExecutor` 明确核心参数。
|
|
5
|
+
- 线程和线程池必须有可识别的业务名称。
|
|
6
|
+
- `ThreadLocal` 使用后必须在 `finally` 中清理。
|
|
7
|
+
- 多资源加锁必须保持一致顺序,避免死锁。
|
|
8
|
+
- 并发更新同一记录必须考虑锁、版本号、幂等或唯一约束。
|
|
9
|
+
- `SimpleDateFormat` 不得作为共享静态变量;优先使用 `java.time`。
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# 异常处理与日志
|
|
2
|
+
|
|
3
|
+
## 异常处理
|
|
4
|
+
|
|
5
|
+
- 异常不得用于流程控制。
|
|
6
|
+
- 能通过预检查避免的 RuntimeException,不应依赖 catch 兜底。
|
|
7
|
+
- catch 后必须处理、转换或记录,不允许空 catch。
|
|
8
|
+
- 事务代码中 catch 后如果需要回滚,必须显式处理回滚或继续抛出异常。
|
|
9
|
+
- `finally` 中禁止 `return`。
|
|
10
|
+
- 流和资源优先使用 `try-with-resources`。
|
|
11
|
+
- 对外接口使用统一错误码或统一响应结构;应用内部优先通过明确的业务异常表达失败。
|
|
12
|
+
- 方法返回值允许为 null 时,必须通过注释、Optional 或调用约定说明清楚。
|
|
13
|
+
|
|
14
|
+
## 日志规则
|
|
15
|
+
|
|
16
|
+
- 使用 SLF4J 等统一日志门面,不直接依赖具体日志实现 API。
|
|
17
|
+
- 日志变量拼接使用占位符,不使用字符串拼接。
|
|
18
|
+
- `trace` / `debug` / 大量 `info` 日志输出前必须判断日志级别。
|
|
19
|
+
- 不要重复打印同一个异常;上层已记录时下层不要重复记录完整堆栈。
|
|
20
|
+
- 异常日志必须包含现场信息和异常堆栈。
|
|
21
|
+
- 生产环境禁止输出敏感数据,用户敏感信息必须脱敏。
|
|
22
|
+
|
|
23
|
+
示例:
|
|
24
|
+
|
|
25
|
+
```java
|
|
26
|
+
if (log.isDebugEnabled()) {
|
|
27
|
+
log.debug("Create user request, orgId={}, userName={}", orgId, userName);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
log.error("Create user failed, orgId={}, userName={}", orgId, userName, exception);
|
|
31
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# MySQL 与 ORM
|
|
2
|
+
|
|
3
|
+
- 表名、字段名使用小写字母、数字和下划线,禁止保留字。
|
|
4
|
+
- 表名不使用复数。
|
|
5
|
+
- 表必须包含 `id`、`create_time`、`update_time` 等基础字段,除非项目规范另有说明。
|
|
6
|
+
- 表达是与否的数据库字段使用 `is_xxx`,Java POJO 字段不要使用 `is` 前缀,并在映射中处理。
|
|
7
|
+
- 小数类型使用 `decimal`,禁止用 `float` / `double` 存储精确金额。
|
|
8
|
+
- 具备唯一业务语义的字段必须建立唯一索引。
|
|
9
|
+
- 查询字段必须显式列出,禁止 `select *`。
|
|
10
|
+
- MyBatis 参数使用 `#{}`,禁止 `${}` 拼接用户输入。
|
|
11
|
+
- 分页查询时如果 `count` 为 0,应直接返回,不继续执行分页查询。
|
|
12
|
+
- 更新记录时必须维护更新时间字段。
|
|
13
|
+
- `@Transactional` 不要滥用,事务范围应尽量小且语义明确。
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# OOP 与 POJO
|
|
2
|
+
|
|
3
|
+
## OOP 与 POJO 通用规则
|
|
4
|
+
|
|
5
|
+
- 所有覆写方法必须添加 `@Override`。
|
|
6
|
+
- 禁止使用过时类或过时方法。
|
|
7
|
+
- DO / DTO / VO 等 POJO 字段必须使用包装类型,局部变量可使用基本类型。
|
|
8
|
+
- POJO 类必须提供可读的 `toString`,避免遗漏关键字段;优先使用 Lombok `@ToString` 生成。
|
|
9
|
+
- POJO 布尔字段不要以 `is` 开头,避免序列化和反序列化歧义。
|
|
10
|
+
- 构造方法中禁止写业务逻辑;初始化逻辑放到明确的初始化方法或工厂方法。
|
|
11
|
+
- `BigDecimal` 禁止使用 `new BigDecimal(double)`,应使用字符串或 `BigDecimal.valueOf`。
|
|
12
|
+
- 循环体内字符串拼接优先使用 `StringBuilder`。
|
|
13
|
+
- 访问控制从严,能 `private` 不 `protected`,能包内可见不 `public`。
|
|
14
|
+
|
|
15
|
+
## Lombok 使用规则
|
|
16
|
+
|
|
17
|
+
- 创建 DO / DTO / VO / BO / Query / Command 等数据承载对象时,优先使用 Lombok 注解生成 getter、setter、构造器、`toString`、Builder 等样板代码。
|
|
18
|
+
- 常用组合:
|
|
19
|
+
- 可变 DTO / VO:`@Getter`、`@Setter`、`@ToString`、`@NoArgsConstructor`
|
|
20
|
+
- 需要全参构造:增加 `@AllArgsConstructor`
|
|
21
|
+
- 字段较多或可选参数较多:增加 `@Builder`,创建对象时优先使用 `Xxx.builder().field(value).build()`
|
|
22
|
+
- 继承结构下的 Builder:使用 `@SuperBuilder`
|
|
23
|
+
- 不可变值对象:优先使用 `@Value` 或 `@Getter` + `final` 字段,按项目约定选择
|
|
24
|
+
- Jackson、MyBatis、JPA 等框架需要反射创建对象时,应保留无参构造器,例如 `@NoArgsConstructor`。
|
|
25
|
+
- DTO 字段不要为了 Lombok `@Builder.Default` 设置默认值;确有业务默认值时,应在业务层、工厂方法或明确的初始化流程中处理。
|
|
26
|
+
- 谨慎使用 `@Data`:纯 DTO / VO 可按项目约定使用;涉及继承、实体、缓存 key、集合元素去重或需要精确控制 `equals` / `hashCode` 的类,应改用更明确的 `@Getter`、`@Setter`、`@ToString`、`@EqualsAndHashCode`。
|
|
27
|
+
|
|
28
|
+
推荐写法:
|
|
29
|
+
|
|
30
|
+
```java
|
|
31
|
+
@Getter
|
|
32
|
+
@Setter
|
|
33
|
+
@ToString
|
|
34
|
+
@NoArgsConstructor
|
|
35
|
+
@AllArgsConstructor
|
|
36
|
+
@Builder
|
|
37
|
+
public class UserDTO {
|
|
38
|
+
|
|
39
|
+
private Long id;
|
|
40
|
+
|
|
41
|
+
private String userName;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
UserDTO user = UserDTO.builder()
|
|
45
|
+
.id(userId)
|
|
46
|
+
.userName(userName)
|
|
47
|
+
.build();
|
|
48
|
+
```
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# DTO 与参数校验
|
|
2
|
+
|
|
3
|
+
## Jakarta Validation 优先
|
|
4
|
+
|
|
5
|
+
- DTO 字段非空、长度、范围、格式等校验,必须优先使用 `jakarta.validation` 包下的注解。
|
|
6
|
+
- 禁止使用 `javax.validation` 包;Spring Boot 3+ 项目统一使用 Jakarta Validation。
|
|
7
|
+
- Controller 入参 DTO 使用 `@Valid` 或 `@Validated` 触发校验。
|
|
8
|
+
- 嵌套对象、集合元素需要级联校验时使用 `@Valid`。
|
|
9
|
+
- DTO 字段不要设置默认值,避免框架反序列化和业务语义混淆。
|
|
10
|
+
|
|
11
|
+
常用注解优先级:
|
|
12
|
+
|
|
13
|
+
```java
|
|
14
|
+
import jakarta.validation.Valid;
|
|
15
|
+
import jakarta.validation.constraints.AssertTrue;
|
|
16
|
+
import jakarta.validation.constraints.Email;
|
|
17
|
+
import jakarta.validation.constraints.Max;
|
|
18
|
+
import jakarta.validation.constraints.Min;
|
|
19
|
+
import jakarta.validation.constraints.NotBlank;
|
|
20
|
+
import jakarta.validation.constraints.NotEmpty;
|
|
21
|
+
import jakarta.validation.constraints.NotNull;
|
|
22
|
+
import jakarta.validation.constraints.Pattern;
|
|
23
|
+
import jakarta.validation.constraints.Positive;
|
|
24
|
+
import jakarta.validation.constraints.Size;
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
推荐写法:
|
|
28
|
+
|
|
29
|
+
```java
|
|
30
|
+
public class CreateUserDTO {
|
|
31
|
+
|
|
32
|
+
@NotBlank(message = "用户名不能为空")
|
|
33
|
+
@Size(max = 64, message = "用户名长度不能超过64")
|
|
34
|
+
private String userName;
|
|
35
|
+
|
|
36
|
+
@NotNull(message = "组织ID不能为空")
|
|
37
|
+
@Positive(message = "组织ID必须为正数")
|
|
38
|
+
private Long orgId;
|
|
39
|
+
|
|
40
|
+
@Email(message = "邮箱格式不正确")
|
|
41
|
+
private String email;
|
|
42
|
+
|
|
43
|
+
@Valid
|
|
44
|
+
@NotEmpty(message = "明细不能为空")
|
|
45
|
+
private List<CreateUserItemDTO> items;
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Controller 示例:
|
|
50
|
+
|
|
51
|
+
```java
|
|
52
|
+
@PostMapping("/users")
|
|
53
|
+
public Result<Void> createUser(@Valid @RequestBody CreateUserDTO request) {
|
|
54
|
+
userService.createUser(request);
|
|
55
|
+
return Result.success();
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 代码校验只作为补充
|
|
60
|
+
|
|
61
|
+
- 简单字段校验不得在 Service 中重复写 `if` 判断,应交给 DTO 注解。
|
|
62
|
+
- 以下场景可以使用代码校验:
|
|
63
|
+
- 跨字段关系,例如 `startTime` 必须早于 `endTime`。
|
|
64
|
+
- 依赖数据库、缓存、外部服务的业务校验。
|
|
65
|
+
- 权限、幂等、防重放、防刷等上下文相关校验。
|
|
66
|
+
- 注解表达成本过高或会显著降低可读性的复杂条件。
|
|
67
|
+
- 跨字段规则优先考虑 `@AssertTrue`、自定义约束注解或类级别 Validator;仍不适合时才写业务代码校验。
|
|
68
|
+
- 方法参数校验可在类上使用 `@Validated`,并在 public 方法参数上使用 Jakarta Validation 注解。
|
|
69
|
+
|
|
70
|
+
示例:
|
|
71
|
+
|
|
72
|
+
```java
|
|
73
|
+
@AssertTrue(message = "开始时间必须早于结束时间")
|
|
74
|
+
public boolean isTimeRangeValid() {
|
|
75
|
+
if (startTime == null || endTime == null) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return startTime.isBefore(endTime);
|
|
79
|
+
}
|
|
80
|
+
```
|