ai-engineering-init 1.4.3 → 1.6.0

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.
Files changed (131) hide show
  1. package/.cursor/skills/bug-detective/SKILL.md +19 -19
  2. package/.cursor/skills/project-navigator/SKILL.md +164 -258
  3. package/README.md +20 -236
  4. package/bin/index.js +437 -7
  5. package/package.json +7 -1
  6. package/scripts/build-skills.js +180 -0
  7. package/src/platform-map.json +56 -0
  8. package/src/skills/add-skill/SKILL.md +488 -0
  9. package/src/skills/add-todo/SKILL.md +269 -0
  10. package/src/skills/api-development/SKILL.md +266 -0
  11. package/src/skills/architecture-design/SKILL.md +262 -0
  12. package/src/skills/backend-annotations/SKILL.md +302 -0
  13. package/src/skills/banana-image/CHANGELOG.md +37 -0
  14. package/src/skills/banana-image/README.md +146 -0
  15. package/src/skills/banana-image/SKILL.md +171 -0
  16. package/src/skills/banana-image/assets/logo.png +0 -0
  17. package/src/skills/banana-image/references/advanced-usage.md +189 -0
  18. package/src/skills/banana-image/scripts/apply_template.py +125 -0
  19. package/src/skills/banana-image/scripts/banana_image_exec.ts +412 -0
  20. package/src/skills/banana-image/scripts/batch_prep.py +82 -0
  21. package/src/skills/banana-image/scripts/package-lock.json +1437 -0
  22. package/src/skills/banana-image/scripts/package.json +18 -0
  23. package/src/skills/banana-image/scripts/requirements.txt +10 -0
  24. package/src/skills/banana-image/templates/poster.json +22 -0
  25. package/src/skills/banana-image/templates/product.json +17 -0
  26. package/src/skills/banana-image/templates/social.json +22 -0
  27. package/src/skills/banana-image/templates/thumbnail.json +17 -0
  28. package/src/skills/brainstorm/SKILL.md +216 -0
  29. package/src/skills/bug-detective/SKILL.md +256 -0
  30. package/src/skills/bug-detective/references/error-patterns.md +242 -0
  31. package/src/skills/check/SKILL.md +367 -0
  32. package/src/skills/code-patterns/SKILL.md +280 -0
  33. package/src/skills/code-patterns/references/leniu-code-patterns.md +87 -0
  34. package/src/skills/codex-code-review/SKILL.md +135 -0
  35. package/src/skills/collaborating-with-codex/SKILL.md +174 -0
  36. package/src/skills/collaborating-with-codex/scripts/codex_bridge.py +275 -0
  37. package/src/skills/collaborating-with-gemini/SKILL.md +194 -0
  38. package/src/skills/collaborating-with-gemini/scripts/gemini_bridge.py +275 -0
  39. package/src/skills/crud/SKILL.md +265 -0
  40. package/src/skills/crud-development/SKILL.md +409 -0
  41. package/src/skills/data-permission/SKILL.md +292 -0
  42. package/src/skills/data-permission/references/custom-data-scope.md +90 -0
  43. package/src/skills/database-ops/SKILL.md +407 -0
  44. package/src/skills/dev/SKILL.md +187 -0
  45. package/src/skills/error-handler/SKILL.md +371 -0
  46. package/src/skills/file-oss-management/SKILL.md +255 -0
  47. package/src/skills/file-oss-management/references/entities.md +105 -0
  48. package/src/skills/file-oss-management/references/service-impl.md +104 -0
  49. package/src/skills/git-workflow/SKILL.md +397 -0
  50. package/src/skills/init-docs/SKILL.md +194 -0
  51. package/src/skills/json-serialization/SKILL.md +357 -0
  52. package/src/skills/leniu-api-development/SKILL.md +319 -0
  53. package/src/skills/leniu-api-development/references/real-examples.md +273 -0
  54. package/src/skills/leniu-architecture-design/SKILL.md +383 -0
  55. package/src/skills/leniu-backend-annotations/SKILL.md +277 -0
  56. package/src/skills/leniu-brainstorm/SKILL.md +242 -0
  57. package/src/skills/leniu-brainstorm/references/business-scenarios.md +162 -0
  58. package/src/skills/leniu-code-patterns/SKILL.md +411 -0
  59. package/src/skills/leniu-crud-development/SKILL.md +404 -0
  60. package/src/skills/leniu-crud-development/references/templates.md +597 -0
  61. package/src/skills/leniu-customization-location/SKILL.md +410 -0
  62. package/src/skills/leniu-data-permission/SKILL.md +341 -0
  63. package/src/skills/leniu-database-ops/SKILL.md +426 -0
  64. package/src/skills/leniu-error-handler/SKILL.md +462 -0
  65. package/src/skills/leniu-java-amount-handling/SKILL.md +461 -0
  66. package/src/skills/leniu-java-code-style/SKILL.md +510 -0
  67. package/src/skills/leniu-java-concurrent/SKILL.md +400 -0
  68. package/src/skills/leniu-java-entity/SKILL.md +237 -0
  69. package/src/skills/leniu-java-entity/references/templates.md +237 -0
  70. package/src/skills/leniu-java-export/SKILL.md +570 -0
  71. package/src/skills/leniu-java-logging/SKILL.md +229 -0
  72. package/src/skills/leniu-java-logging/references/data-mask.md +46 -0
  73. package/src/skills/leniu-java-logging/references/logging-scenarios.md +113 -0
  74. package/src/skills/leniu-java-mq/SKILL.md +338 -0
  75. package/src/skills/leniu-java-mybatis/SKILL.md +267 -0
  76. package/src/skills/leniu-java-mybatis/references/report-mapper.md +88 -0
  77. package/src/skills/leniu-java-report-query-param/SKILL.md +291 -0
  78. package/src/skills/leniu-java-task/SKILL.md +367 -0
  79. package/src/skills/leniu-java-total-line/SKILL.md +196 -0
  80. package/src/skills/leniu-marketing-price-rule-customizer/SKILL.md +301 -0
  81. package/src/skills/leniu-marketing-recharge-rule-customizer/SKILL.md +285 -0
  82. package/src/skills/leniu-mealtime/SKILL.md +215 -0
  83. package/src/skills/leniu-redis-cache/SKILL.md +331 -0
  84. package/src/skills/leniu-report-customization/SKILL.md +335 -0
  85. package/src/skills/leniu-report-customization/references/table-fields.md +93 -0
  86. package/src/skills/leniu-report-standard-customization/SKILL.md +328 -0
  87. package/src/skills/leniu-report-standard-customization/references/analysis-module.md +64 -0
  88. package/src/skills/leniu-report-standard-customization/references/table-fields.md +113 -0
  89. package/src/skills/leniu-security-guard/SKILL.md +306 -0
  90. package/src/skills/leniu-utils-toolkit/SKILL.md +380 -0
  91. package/src/skills/mysql-debug/SKILL.md +364 -0
  92. package/src/skills/next/SKILL.md +137 -0
  93. package/src/skills/openspec-apply-change/SKILL.md +165 -0
  94. package/src/skills/openspec-archive-change/SKILL.md +122 -0
  95. package/src/skills/openspec-bulk-archive-change/SKILL.md +254 -0
  96. package/src/skills/openspec-continue-change/SKILL.md +126 -0
  97. package/src/skills/openspec-explore/SKILL.md +299 -0
  98. package/src/skills/openspec-ff-change/SKILL.md +109 -0
  99. package/src/skills/openspec-new-change/SKILL.md +82 -0
  100. package/src/skills/openspec-onboard/SKILL.md +414 -0
  101. package/src/skills/openspec-sync-specs/SKILL.md +146 -0
  102. package/src/skills/openspec-verify-change/SKILL.md +176 -0
  103. package/src/skills/performance-doctor/SKILL.md +303 -0
  104. package/src/skills/progress/SKILL.md +193 -0
  105. package/src/skills/project-navigator/SKILL.md +211 -0
  106. package/src/skills/redis-cache/SKILL.md +333 -0
  107. package/src/skills/redis-cache/references/listeners.md +23 -0
  108. package/src/skills/scheduled-jobs/SKILL.md +314 -0
  109. package/src/skills/security-guard/SKILL.md +353 -0
  110. package/src/skills/security-guard/references/encrypt-config.md +103 -0
  111. package/src/skills/security-guard/references/sensitive-strategies.md +42 -0
  112. package/src/skills/sms-mail/SKILL.md +308 -0
  113. package/src/skills/sms-mail/references/mail-config.md +88 -0
  114. package/src/skills/sms-mail/references/sms-config.md +74 -0
  115. package/src/skills/social-login/SKILL.md +266 -0
  116. package/src/skills/social-login/references/provider-configs.md +118 -0
  117. package/src/skills/start/SKILL.md +154 -0
  118. package/src/skills/store-pc/SKILL.md +366 -0
  119. package/src/skills/sync/SKILL.md +149 -0
  120. package/src/skills/task-tracker/SKILL.md +307 -0
  121. package/src/skills/tech-decision/SKILL.md +393 -0
  122. package/src/skills/tenant-management/SKILL.md +288 -0
  123. package/src/skills/tenant-management/references/tenant-scenarios.md +91 -0
  124. package/src/skills/test-development/SKILL.md +301 -0
  125. package/src/skills/test-development/references/parameterized-examples.md +119 -0
  126. package/src/skills/ui-pc/SKILL.md +438 -0
  127. package/src/skills/update-status/SKILL.md +159 -0
  128. package/src/skills/utils-toolkit/SKILL.md +362 -0
  129. package/src/skills/utils-toolkit/references/redis-utils-api.md +56 -0
  130. package/src/skills/websocket-sse/SKILL.md +271 -0
  131. package/src/skills/workflow-engine/SKILL.md +321 -0
@@ -0,0 +1,288 @@
1
+ ---
2
+ name: tenant-management
3
+ description: |
4
+ 多租户数据隔离开发指南。基于 MyBatis-Plus 租户插件,自动 SQL/Redis/Cache 隔离。
5
+
6
+ 触发场景:
7
+ - 为业务表添加租户隔离
8
+ - 临时忽略租户过滤查询全量数据
9
+ - 切换到其他租户执行操作
10
+ - 配置排除租户过滤的表
11
+ - 定时任务遍历所有租户
12
+
13
+ 触发词:多租户、租户隔离、TenantEntity、TenantHelper、tenantId、跨租户、ignore租户、动态租户、租户切换、SaaS、数据隔离
14
+ ---
15
+
16
+ # 多租户开发指南
17
+
18
+ > **模块**:`ruoyi-common-tenant` | **核心类**:`TenantHelper`、`TenantEntity`
19
+
20
+ ## 一、Entity 规范
21
+
22
+ ### 继承 TenantEntity
23
+
24
+ ```java
25
+ import org.dromara.common.tenant.core.TenantEntity;
26
+
27
+ // TenantEntity 继承关系:TenantEntity → BaseEntity
28
+ // TenantEntity 额外字段:tenantId
29
+ // BaseEntity 字段:createDept, createBy, createTime, updateBy, updateTime, params
30
+
31
+ @Data
32
+ @EqualsAndHashCode(callSuper = true)
33
+ @TableName("biz_order")
34
+ public class BizOrder extends TenantEntity {
35
+ @TableId(value = "id")
36
+ private Long id;
37
+ private String orderNo;
38
+ private String status;
39
+ }
40
+ ```
41
+
42
+ ### 数据库表必须含 tenant_id
43
+
44
+ ```sql
45
+ CREATE TABLE biz_order (
46
+ id BIGINT(20) NOT NULL COMMENT '主键ID',
47
+ tenant_id VARCHAR(20) DEFAULT '000000' COMMENT '租户ID', -- 必须有
48
+ order_no VARCHAR(64) NOT NULL COMMENT '订单号',
49
+ status CHAR(1) DEFAULT '0' COMMENT '状态',
50
+ create_dept BIGINT(20) DEFAULT NULL COMMENT '创建部门',
51
+ create_by BIGINT(20) DEFAULT NULL COMMENT '创建人',
52
+ create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
53
+ update_by BIGINT(20) DEFAULT NULL COMMENT '更新人',
54
+ update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
55
+ del_flag CHAR(1) DEFAULT '0' COMMENT '删除标志',
56
+ PRIMARY KEY (id),
57
+ KEY idx_tenant_id (tenant_id)
58
+ );
59
+ ```
60
+
61
+ ---
62
+
63
+ ## 二、TenantHelper 工具类
64
+
65
+ **位置**:`org.dromara.common.tenant.helper.TenantHelper`
66
+
67
+ ### API 速查
68
+
69
+ | 方法 | 说明 | 返回值 |
70
+ |------|------|--------|
71
+ | `isEnable()` | 租户功能是否启用 | boolean |
72
+ | `getTenantId()` | 获取当前租户ID(优先动态租户) | String |
73
+ | `ignore(Runnable)` | 忽略租户执行(无返回值) | void |
74
+ | `ignore(Supplier<T>)` | 忽略租户执行(有返回值) | T |
75
+ | `dynamic(tenantId, Runnable)` | 切换租户执行(无返回值) | void |
76
+ | `dynamic(tenantId, Supplier<T>)` | 切换租户执行(有返回值) | T |
77
+ | `setDynamic(tenantId)` | 手动设置动态租户(需配合 clearDynamic) | void |
78
+ | `setDynamic(tenantId, true)` | 设置全局动态租户(跨请求,存 Redis) | void |
79
+ | `clearDynamic()` | 清除动态租户 | void |
80
+
81
+ ### 核心用法
82
+
83
+ ```java
84
+ import org.dromara.common.tenant.helper.TenantHelper;
85
+
86
+ // 1. 忽略租户(查全量数据)
87
+ List<SysUser> allUsers = TenantHelper.ignore(() -> {
88
+ return userMapper.selectList(null); // 不追加 tenant_id 条件
89
+ });
90
+
91
+ // 2. 切换到指定租户(推荐用法)
92
+ TenantHelper.dynamic("000001", () -> {
93
+ userMapper.insert(user); // 使用租户 000001
94
+ });
95
+
96
+ // 3. 手动管理(复杂场景)
97
+ TenantHelper.setDynamic("000001");
98
+ try {
99
+ userMapper.insert(user);
100
+ } finally {
101
+ TenantHelper.clearDynamic(); // 必须清理!
102
+ }
103
+ ```
104
+
105
+ ---
106
+
107
+ ## 三、配置
108
+
109
+ ```yaml
110
+ # application.yml
111
+ tenant:
112
+ enable: true
113
+ excludes: # 不追加 tenant_id 条件的表
114
+ - sys_menu
115
+ - sys_dict_type
116
+ - sys_dict_data
117
+ - sys_oss_config
118
+ - sys_config
119
+ ```
120
+
121
+ **自动配置组件**(`tenant.enable=true` 时生效):
122
+
123
+ | 组件 | 功能 |
124
+ |------|------|
125
+ | `TenantLineInnerInterceptor` | SQL 自动追加租户条件 |
126
+ | `TenantKeyPrefixHandler` | Redis Key 自动添加租户前缀 |
127
+ | `TenantSpringCacheManager` | Spring Cache 租户隔离 |
128
+ | `TenantSaTokenDao` | Sa-Token 会话租户隔离 |
129
+
130
+ ---
131
+
132
+ ## 四、常见场景
133
+
134
+ ### 场景 1:管理员查看所有租户数据
135
+
136
+ ```java
137
+ @SaCheckRole("superadmin")
138
+ public List<SysUserVo> listAllTenantUsers() {
139
+ return TenantHelper.ignore(() -> userMapper.selectVoList(null));
140
+ }
141
+ ```
142
+
143
+ ### 场景 2:跨租户数据同步
144
+
145
+ ```java
146
+ public void syncConfigToAllTenants(SysConfig config) {
147
+ List<String> tenantIds = TenantHelper.ignore(() ->
148
+ tenantMapper.selectList(null).stream()
149
+ .map(SysTenant::getTenantId).collect(Collectors.toList())
150
+ );
151
+ for (String tenantId : tenantIds) {
152
+ TenantHelper.dynamic(tenantId, () -> {
153
+ SysConfig existing = configMapper.selectByKey(config.getConfigKey());
154
+ if (existing == null) configMapper.insert(config);
155
+ else configMapper.updateById(config);
156
+ });
157
+ }
158
+ }
159
+ ```
160
+
161
+ ### 场景 3:定时任务处理所有租户
162
+
163
+ > 详细示例见 `references/tenant-scenarios.md`
164
+
165
+ ```java
166
+ @Scheduled(cron = "0 0 2 * * ?")
167
+ public void cleanupExpiredOrders() {
168
+ List<SysTenant> tenants = TenantHelper.ignore(() ->
169
+ tenantMapper.selectList(Wrappers.<SysTenant>lambdaQuery().eq(SysTenant::getStatus, "0"))
170
+ );
171
+ for (SysTenant tenant : tenants) {
172
+ try {
173
+ TenantHelper.dynamic(tenant.getTenantId(), () -> {
174
+ orderMapper.delete(Wrappers.<Order>lambdaQuery()
175
+ .eq(Order::getStatus, "CANCELLED")
176
+ .lt(Order::getCreateTime, DateUtils.addDays(new Date(), -30)));
177
+ });
178
+ } catch (Exception e) {
179
+ log.error("清理租户 {} 订单失败: {}", tenant.getTenantId(), e.getMessage());
180
+ }
181
+ }
182
+ }
183
+ ```
184
+
185
+ ---
186
+
187
+ ## 五、Redis 缓存隔离
188
+
189
+ ```
190
+ 原始 Key: user:info:1001
191
+ 实际 Key: 000000:user:info:1001 (租户 000000)
192
+ 实际 Key: 000001:user:info:1001 (租户 000001)
193
+ ```
194
+
195
+ **全局 Key(不隔离)**:使用 `GlobalConstants.GLOBAL_REDIS_KEY` 前缀
196
+
197
+ ```java
198
+ import org.dromara.common.core.constant.GlobalConstants;
199
+
200
+ // 全局缓存(所有租户共享)
201
+ String globalKey = GlobalConstants.GLOBAL_REDIS_KEY + "config:site_name";
202
+ RedisUtils.setCacheObject(globalKey, "网站名称");
203
+
204
+ // 租户隔离缓存(自动添加前缀)
205
+ RedisUtils.setCacheObject("user:info:" + userId, userInfo);
206
+ ```
207
+
208
+ > `TenantHelper.ignore()` 中的 Redis 操作不添加租户前缀。
209
+
210
+ ---
211
+
212
+ ## 六、常见错误
213
+
214
+ ### 1. 忘记继承 TenantEntity
215
+
216
+ ```java
217
+ // ❌ 继承 BaseEntity,没有 tenantId
218
+ public class BizOrder extends BaseEntity { }
219
+
220
+ // ✅ 继承 TenantEntity
221
+ public class BizOrder extends TenantEntity { }
222
+ ```
223
+
224
+ ### 2. 数据库表缺少 tenant_id
225
+
226
+ ```sql
227
+ -- ❌ 表没有 tenant_id
228
+ CREATE TABLE biz_order (id BIGINT NOT NULL, order_no VARCHAR(64));
229
+
230
+ -- ✅ 添加 tenant_id
231
+ CREATE TABLE biz_order (id BIGINT NOT NULL, tenant_id VARCHAR(20) DEFAULT '000000', order_no VARCHAR(64));
232
+ ```
233
+
234
+ ### 3. setDynamic 后忘记 clearDynamic
235
+
236
+ ```java
237
+ // ❌ 租户ID泄漏到其他请求
238
+ TenantHelper.setDynamic("000001");
239
+ userMapper.insert(user);
240
+ // 忘记 clearDynamic()
241
+
242
+ // ✅ 推荐使用 dynamic() 方法(自动清理)
243
+ TenantHelper.dynamic("000001", () -> userMapper.insert(user));
244
+ ```
245
+
246
+ ### 4. 事务中切换租户
247
+
248
+ ```java
249
+ // ❌ 事务内切换租户导致数据错乱
250
+ @Transactional
251
+ public void wrongMethod() {
252
+ orderMapper.insert(order);
253
+ TenantHelper.dynamic("000001", () -> logMapper.insert(log));
254
+ }
255
+
256
+ // ✅ 使用独立事务
257
+ @Transactional(propagation = Propagation.REQUIRES_NEW)
258
+ public void saveLogToOtherTenant(String tenantId, Log log) {
259
+ TenantHelper.dynamic(tenantId, () -> logMapper.insert(log));
260
+ }
261
+ ```
262
+
263
+ ### 5. 排除表配置不当
264
+
265
+ ```yaml
266
+ # ❌ 业务表不应排除
267
+ tenant:
268
+ excludes:
269
+ - biz_order
270
+
271
+ # ✅ 只排除共享的系统表
272
+ tenant:
273
+ excludes:
274
+ - sys_menu
275
+ - sys_dict_type
276
+ ```
277
+
278
+ ---
279
+
280
+ ## 七、参考代码位置
281
+
282
+ | 类型 | 位置 |
283
+ |------|------|
284
+ | 租户基类 | `ruoyi-common/ruoyi-common-tenant/.../core/TenantEntity.java` |
285
+ | 租户助手 | `ruoyi-common/ruoyi-common-tenant/.../helper/TenantHelper.java` |
286
+ | 租户处理器 | `ruoyi-common/ruoyi-common-tenant/.../handle/PlusTenantLineHandler.java` |
287
+ | Redis Key 处理 | `ruoyi-common/ruoyi-common-tenant/.../handle/TenantKeyPrefixHandler.java` |
288
+ | 配置文件 | `ruoyi-admin/src/main/resources/application.yml` |
@@ -0,0 +1,91 @@
1
+ # 多租户扩展场景
2
+
3
+ ## 统计所有租户数据
4
+
5
+ ```java
6
+ public List<TenantOrderStats> getTenantOrderStats() {
7
+ return TenantHelper.ignore(() -> {
8
+ return orderMapper.selectMaps(
9
+ new QueryWrapper<Order>()
10
+ .select("tenant_id", "COUNT(*) as order_count", "SUM(amount) as total_amount")
11
+ .groupBy("tenant_id")
12
+ ).stream().map(map -> {
13
+ TenantOrderStats stats = new TenantOrderStats();
14
+ stats.setTenantId((String) map.get("tenant_id"));
15
+ stats.setOrderCount(((Number) map.get("order_count")).longValue());
16
+ stats.setTotalAmount((BigDecimal) map.get("total_amount"));
17
+ return stats;
18
+ }).collect(Collectors.toList());
19
+ });
20
+ }
21
+ ```
22
+
23
+ ## 定时任务完整模板
24
+
25
+ ```java
26
+ @Service
27
+ @RequiredArgsConstructor
28
+ public class OrderCleanupJob {
29
+
30
+ private final OrderMapper orderMapper;
31
+ private final SysTenantMapper tenantMapper;
32
+
33
+ @Scheduled(cron = "0 0 2 * * ?")
34
+ public void cleanupExpiredOrders() {
35
+ // 1. 获取所有启用的租户
36
+ List<SysTenant> tenants = TenantHelper.ignore(() -> {
37
+ return tenantMapper.selectList(
38
+ Wrappers.<SysTenant>lambdaQuery()
39
+ .eq(SysTenant::getStatus, "0")
40
+ );
41
+ });
42
+
43
+ // 2. 逐个租户处理
44
+ for (SysTenant tenant : tenants) {
45
+ try {
46
+ TenantHelper.dynamic(tenant.getTenantId(), () -> {
47
+ orderMapper.delete(
48
+ Wrappers.<Order>lambdaQuery()
49
+ .eq(Order::getStatus, "CANCELLED")
50
+ .lt(Order::getCreateTime, DateUtils.addDays(new Date(), -30))
51
+ );
52
+ });
53
+ } catch (Exception e) {
54
+ log.error("清理租户 {} 订单失败: {}", tenant.getTenantId(), e.getMessage());
55
+ }
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ ## 跨租户数据同步完整模板
62
+
63
+ ```java
64
+ @Service
65
+ @RequiredArgsConstructor
66
+ public class DataSyncServiceImpl implements IDataSyncService {
67
+
68
+ private final SysConfigMapper configMapper;
69
+ private final SysTenantMapper tenantMapper;
70
+
71
+ public void syncConfigToAllTenants(SysConfig config) {
72
+ List<String> tenantIds = TenantHelper.ignore(() -> {
73
+ return tenantMapper.selectList(null)
74
+ .stream()
75
+ .map(SysTenant::getTenantId)
76
+ .collect(Collectors.toList());
77
+ });
78
+
79
+ for (String tenantId : tenantIds) {
80
+ TenantHelper.dynamic(tenantId, () -> {
81
+ SysConfig existing = configMapper.selectByKey(config.getConfigKey());
82
+ if (existing == null) {
83
+ configMapper.insert(config);
84
+ } else {
85
+ configMapper.updateById(config);
86
+ }
87
+ });
88
+ }
89
+ }
90
+ }
91
+ ```
@@ -0,0 +1,301 @@
1
+ ---
2
+ name: test-development
3
+ description: |
4
+ 测试开发技能,编写单元测试、集成测试、Controller测试。基于 JUnit5 + Spring Boot Test + Mockito 标准测试框架。
5
+
6
+ 触发场景:
7
+ - 编写单元测试(工具类、枚举、POJO)
8
+ - 编写 Spring 集成测试(Service、Controller、Mapper)
9
+ - Mock 外部依赖
10
+ - 参数化测试
11
+ - 测试数据构造
12
+ - 测试覆盖率提升
13
+
14
+ 触发词:测试、单元测试、集成测试、@Test、JUnit5、JUnit、Mockito、Mock、断言、test、测试用例、测试覆盖率、测试数据、@SpringBootTest、@Mock、@MockBean、Assertions、测试类、测试方法、@ParameterizedTest、参数化测试、@BeforeEach、@AfterEach、@DisplayName
15
+
16
+ 注意:本项目使用标准的 JUnit5 + Spring Boot Test,没有自定义测试基类。
17
+ ---
18
+
19
+ # 测试开发规范
20
+
21
+ ## 测试分层策略
22
+
23
+ | 层次 | 测试类型 | 启动 Spring | 执行速度 |
24
+ |------|---------|------------|---------|
25
+ | 单元测试 | 工具类/枚举/POJO | 否 | < 1s |
26
+ | 集成测试 | Service/Controller/Mapper | 是(`@SpringBootTest`) | 5-10s |
27
+
28
+ ## 测试文件位置
29
+
30
+ ```
31
+ src/test/java/org/dromara/test/ # 通用测试
32
+ src/test/java/org/dromara/{模块}/ # 模块测试(与源码包路径一致)
33
+ ```
34
+
35
+ **重要**:`@SpringBootTest` 只能在 SpringBoot 主包下使用(需包含 main 方法和 yml 配置)。
36
+
37
+ ---
38
+
39
+ ## 1. 单元测试(纯 JUnit5,无 Spring)
40
+
41
+ ```java
42
+ package org.dromara.test;
43
+
44
+ import org.junit.jupiter.api.Assertions;
45
+ import org.junit.jupiter.api.DisplayName;
46
+ import org.junit.jupiter.api.Test;
47
+
48
+ @DisplayName("StringUtils 测试")
49
+ public class StringUtilsTest {
50
+
51
+ @Test
52
+ @DisplayName("测试 isBlank 方法")
53
+ public void testIsBlank() {
54
+ Assertions.assertTrue(StringUtils.isBlank(null));
55
+ Assertions.assertTrue(StringUtils.isBlank(""));
56
+ Assertions.assertFalse(StringUtils.isBlank("hello"));
57
+ }
58
+
59
+ @Test
60
+ @DisplayName("测试异常抛出")
61
+ public void testThrowsException() {
62
+ Assertions.assertThrows(NullPointerException.class, () -> {
63
+ String str = null;
64
+ str.length();
65
+ });
66
+ }
67
+ }
68
+ ```
69
+
70
+ ---
71
+
72
+ ## 2. Service 集成测试
73
+
74
+ ```java
75
+ @SpringBootTest
76
+ @Transactional // 测试后自动回滚,不污染数据库
77
+ @DisplayName("TestDemo Service 测试")
78
+ public class TestDemoServiceTest {
79
+
80
+ @Autowired
81
+ private ITestDemoService testDemoService;
82
+
83
+ @Test
84
+ @DisplayName("测试添加数据")
85
+ public void testAdd() {
86
+ // Arrange
87
+ TestDemoBo bo = new TestDemoBo();
88
+ bo.setDeptId(103L);
89
+ bo.setUserId(1L);
90
+ bo.setTestKey("测试数据");
91
+ bo.setValue("test_value");
92
+
93
+ // Act
94
+ Boolean result = testDemoService.insertByBo(bo);
95
+
96
+ // Assert
97
+ Assertions.assertTrue(result);
98
+ }
99
+
100
+ @Test
101
+ @DisplayName("测试查询详情")
102
+ public void testGetById() {
103
+ TestDemoBo bo = new TestDemoBo();
104
+ bo.setTestKey("测试查询");
105
+ bo.setValue("test_query");
106
+ testDemoService.insertByBo(bo);
107
+
108
+ TestDemoVo vo = testDemoService.queryById(bo.getId());
109
+ Assertions.assertNotNull(vo);
110
+ Assertions.assertEquals("测试查询", vo.getTestKey());
111
+ }
112
+ }
113
+ ```
114
+
115
+ ### Mock 外部依赖
116
+
117
+ ```java
118
+ @SpringBootTest
119
+ @DisplayName("订单服务测试")
120
+ public class OrderServiceTest {
121
+
122
+ @Autowired
123
+ private IOrderService orderService;
124
+
125
+ @MockBean
126
+ private IPaymentService paymentService;
127
+
128
+ @Test
129
+ @DisplayName("测试订单支付(Mock 支付服务)")
130
+ public void testPayOrder() {
131
+ Long orderId = 123L;
132
+ Mockito.when(paymentService.pay(orderId)).thenReturn(true);
133
+
134
+ Boolean success = orderService.payOrder(orderId);
135
+
136
+ Assertions.assertTrue(success);
137
+ Mockito.verify(paymentService, Mockito.times(1)).pay(orderId);
138
+ }
139
+ }
140
+ ```
141
+
142
+ ---
143
+
144
+ ## 3. Controller 测试
145
+
146
+ ```java
147
+ @SpringBootTest
148
+ @AutoConfigureMockMvc
149
+ @DisplayName("TestDemo Controller 测试")
150
+ public class TestDemoControllerTest {
151
+
152
+ @Autowired
153
+ private MockMvc mockMvc;
154
+
155
+ @Test
156
+ @DisplayName("测试分页查询")
157
+ public void testPageList() throws Exception {
158
+ mockMvc.perform(get("/demo/demo/list")
159
+ .param("pageNum", "1")
160
+ .param("pageSize", "10"))
161
+ .andExpect(status().isOk())
162
+ .andExpect(jsonPath("$.code").value(200));
163
+ }
164
+
165
+ @Test
166
+ @DisplayName("测试添加数据")
167
+ public void testAdd() throws Exception {
168
+ String requestBody = """
169
+ {
170
+ "deptId": 103,
171
+ "userId": 1,
172
+ "testKey": "测试数据",
173
+ "value": "test_value"
174
+ }
175
+ """;
176
+
177
+ mockMvc.perform(post("/demo/demo")
178
+ .contentType(MediaType.APPLICATION_JSON)
179
+ .content(requestBody))
180
+ .andExpect(status().isOk())
181
+ .andExpect(jsonPath("$.code").value(200));
182
+ }
183
+ }
184
+ ```
185
+
186
+ ---
187
+
188
+ ## 4. 参数化测试
189
+
190
+ ```java
191
+ @DisplayName("参数化测试")
192
+ public class ParamUnitTest {
193
+
194
+ @ParameterizedTest
195
+ @ValueSource(strings = {"t1", "t2", "t3"})
196
+ public void testValueSource(String str) {
197
+ Assertions.assertNotNull(str);
198
+ }
199
+
200
+ @ParameterizedTest
201
+ @EnumSource(UserType.class)
202
+ public void testEnumSource(UserType type) {
203
+ Assertions.assertNotNull(type);
204
+ }
205
+
206
+ @ParameterizedTest
207
+ @CsvSource({"1, Banner, 横幅广告", "2, Popup, 弹窗广告"})
208
+ public void testCsvData(String code, String name, String desc) {
209
+ Assertions.assertNotNull(code);
210
+ }
211
+
212
+ @ParameterizedTest
213
+ @MethodSource("getParam")
214
+ public void testMethodSource(String str) {
215
+ Assertions.assertNotNull(str);
216
+ }
217
+
218
+ public static Stream<String> getParam() {
219
+ return Stream.of("t1", "t2", "t3");
220
+ }
221
+ }
222
+ ```
223
+
224
+ > 更多参数化测试示例详见 `references/parameterized-examples.md`
225
+
226
+ ---
227
+
228
+ ## 5. 异常测试
229
+
230
+ ```java
231
+ @Test
232
+ @DisplayName("测试 key 为空抛出异常")
233
+ public void testAdd_ThrowsException() {
234
+ TestDemoBo bo = new TestDemoBo();
235
+ // 不设置必填字段
236
+
237
+ ServiceException exception = Assertions.assertThrows(
238
+ ServiceException.class,
239
+ () -> testDemoService.insertByBo(bo)
240
+ );
241
+ Assertions.assertTrue(exception.getMessage().contains("key键不能为空"));
242
+ }
243
+ ```
244
+
245
+ ---
246
+
247
+ ## 6. 测试标签(@Tag)
248
+
249
+ ```java
250
+ @Test @Tag("dev") public void testDev() { }
251
+ @Test @Tag("prod") public void testProd() { }
252
+ ```
253
+
254
+ ```bash
255
+ mvn test -Dgroups=dev # 运行 dev 标签
256
+ mvn test -DexcludedGroups=prod # 排除 prod 标签
257
+ ```
258
+
259
+ ---
260
+
261
+ ## 运行测试
262
+
263
+ ```bash
264
+ mvn test # 运行所有测试
265
+ mvn test -Dtest=AdServiceTest # 运行单个测试类
266
+ mvn test -Dtest=AdServiceTest#testAddAd # 运行单个方法
267
+ mvn clean install -DskipTests # 跳过测试
268
+ ```
269
+
270
+ ---
271
+
272
+ ## 开发检查清单
273
+
274
+ ### 命名规范
275
+
276
+ - [ ] 测试类:`{被测试类名}Test`
277
+ - [ ] 测试方法:`test{功能}`
278
+ - [ ] 使用 `@DisplayName` 添加中文描述
279
+ - [ ] 位于 `src/test/java`,包路径与源码一致
280
+
281
+ ### 注解选择
282
+
283
+ | 场景 | 注解组合 |
284
+ |------|---------|
285
+ | 工具类/枚举/POJO | 不加 `@SpringBootTest` |
286
+ | Service 测试 | `@SpringBootTest` + `@Transactional` |
287
+ | Controller 测试 | `@SpringBootTest` + `@AutoConfigureMockMvc` |
288
+ | Mock Spring Bean | `@MockBean` |
289
+ | Mock(纯单元测试) | `@Mock` + `@InjectMocks` |
290
+
291
+ ### 常见错误
292
+
293
+ | 错误 | 正确做法 |
294
+ |------|---------|
295
+ | 测试在 `src/main/java` | 放到 `src/test/java` |
296
+ | 缺少 `@DisplayName` | 必须添加描述 |
297
+ | 需要注入但没加 `@SpringBootTest` | 加上注解 |
298
+ | `@SpringBootTest` 在非主包下 | 确保在主类同包或子包 |
299
+ | Mock 后不验证调用 | `Mockito.verify()` |
300
+ | 测试方法相互依赖 | 每个测试独立 |
301
+ | Service 测试不加 `@Transactional` | 加上防止污染数据库 |