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.
- package/.cursor/skills/bug-detective/SKILL.md +19 -19
- package/.cursor/skills/project-navigator/SKILL.md +164 -258
- package/README.md +20 -236
- package/bin/index.js +437 -7
- package/package.json +7 -1
- package/scripts/build-skills.js +180 -0
- package/src/platform-map.json +56 -0
- package/src/skills/add-skill/SKILL.md +488 -0
- package/src/skills/add-todo/SKILL.md +269 -0
- package/src/skills/api-development/SKILL.md +266 -0
- package/src/skills/architecture-design/SKILL.md +262 -0
- package/src/skills/backend-annotations/SKILL.md +302 -0
- package/src/skills/banana-image/CHANGELOG.md +37 -0
- package/src/skills/banana-image/README.md +146 -0
- package/src/skills/banana-image/SKILL.md +171 -0
- package/src/skills/banana-image/assets/logo.png +0 -0
- package/src/skills/banana-image/references/advanced-usage.md +189 -0
- package/src/skills/banana-image/scripts/apply_template.py +125 -0
- package/src/skills/banana-image/scripts/banana_image_exec.ts +412 -0
- package/src/skills/banana-image/scripts/batch_prep.py +82 -0
- package/src/skills/banana-image/scripts/package-lock.json +1437 -0
- package/src/skills/banana-image/scripts/package.json +18 -0
- package/src/skills/banana-image/scripts/requirements.txt +10 -0
- package/src/skills/banana-image/templates/poster.json +22 -0
- package/src/skills/banana-image/templates/product.json +17 -0
- package/src/skills/banana-image/templates/social.json +22 -0
- package/src/skills/banana-image/templates/thumbnail.json +17 -0
- package/src/skills/brainstorm/SKILL.md +216 -0
- package/src/skills/bug-detective/SKILL.md +256 -0
- package/src/skills/bug-detective/references/error-patterns.md +242 -0
- package/src/skills/check/SKILL.md +367 -0
- package/src/skills/code-patterns/SKILL.md +280 -0
- package/src/skills/code-patterns/references/leniu-code-patterns.md +87 -0
- package/src/skills/codex-code-review/SKILL.md +135 -0
- package/src/skills/collaborating-with-codex/SKILL.md +174 -0
- package/src/skills/collaborating-with-codex/scripts/codex_bridge.py +275 -0
- package/src/skills/collaborating-with-gemini/SKILL.md +194 -0
- package/src/skills/collaborating-with-gemini/scripts/gemini_bridge.py +275 -0
- package/src/skills/crud/SKILL.md +265 -0
- package/src/skills/crud-development/SKILL.md +409 -0
- package/src/skills/data-permission/SKILL.md +292 -0
- package/src/skills/data-permission/references/custom-data-scope.md +90 -0
- package/src/skills/database-ops/SKILL.md +407 -0
- package/src/skills/dev/SKILL.md +187 -0
- package/src/skills/error-handler/SKILL.md +371 -0
- package/src/skills/file-oss-management/SKILL.md +255 -0
- package/src/skills/file-oss-management/references/entities.md +105 -0
- package/src/skills/file-oss-management/references/service-impl.md +104 -0
- package/src/skills/git-workflow/SKILL.md +397 -0
- package/src/skills/init-docs/SKILL.md +194 -0
- package/src/skills/json-serialization/SKILL.md +357 -0
- package/src/skills/leniu-api-development/SKILL.md +319 -0
- package/src/skills/leniu-api-development/references/real-examples.md +273 -0
- package/src/skills/leniu-architecture-design/SKILL.md +383 -0
- package/src/skills/leniu-backend-annotations/SKILL.md +277 -0
- package/src/skills/leniu-brainstorm/SKILL.md +242 -0
- package/src/skills/leniu-brainstorm/references/business-scenarios.md +162 -0
- package/src/skills/leniu-code-patterns/SKILL.md +411 -0
- package/src/skills/leniu-crud-development/SKILL.md +404 -0
- package/src/skills/leniu-crud-development/references/templates.md +597 -0
- package/src/skills/leniu-customization-location/SKILL.md +410 -0
- package/src/skills/leniu-data-permission/SKILL.md +341 -0
- package/src/skills/leniu-database-ops/SKILL.md +426 -0
- package/src/skills/leniu-error-handler/SKILL.md +462 -0
- package/src/skills/leniu-java-amount-handling/SKILL.md +461 -0
- package/src/skills/leniu-java-code-style/SKILL.md +510 -0
- package/src/skills/leniu-java-concurrent/SKILL.md +400 -0
- package/src/skills/leniu-java-entity/SKILL.md +237 -0
- package/src/skills/leniu-java-entity/references/templates.md +237 -0
- package/src/skills/leniu-java-export/SKILL.md +570 -0
- package/src/skills/leniu-java-logging/SKILL.md +229 -0
- package/src/skills/leniu-java-logging/references/data-mask.md +46 -0
- package/src/skills/leniu-java-logging/references/logging-scenarios.md +113 -0
- package/src/skills/leniu-java-mq/SKILL.md +338 -0
- package/src/skills/leniu-java-mybatis/SKILL.md +267 -0
- package/src/skills/leniu-java-mybatis/references/report-mapper.md +88 -0
- package/src/skills/leniu-java-report-query-param/SKILL.md +291 -0
- package/src/skills/leniu-java-task/SKILL.md +367 -0
- package/src/skills/leniu-java-total-line/SKILL.md +196 -0
- package/src/skills/leniu-marketing-price-rule-customizer/SKILL.md +301 -0
- package/src/skills/leniu-marketing-recharge-rule-customizer/SKILL.md +285 -0
- package/src/skills/leniu-mealtime/SKILL.md +215 -0
- package/src/skills/leniu-redis-cache/SKILL.md +331 -0
- package/src/skills/leniu-report-customization/SKILL.md +335 -0
- package/src/skills/leniu-report-customization/references/table-fields.md +93 -0
- package/src/skills/leniu-report-standard-customization/SKILL.md +328 -0
- package/src/skills/leniu-report-standard-customization/references/analysis-module.md +64 -0
- package/src/skills/leniu-report-standard-customization/references/table-fields.md +113 -0
- package/src/skills/leniu-security-guard/SKILL.md +306 -0
- package/src/skills/leniu-utils-toolkit/SKILL.md +380 -0
- package/src/skills/mysql-debug/SKILL.md +364 -0
- package/src/skills/next/SKILL.md +137 -0
- package/src/skills/openspec-apply-change/SKILL.md +165 -0
- package/src/skills/openspec-archive-change/SKILL.md +122 -0
- package/src/skills/openspec-bulk-archive-change/SKILL.md +254 -0
- package/src/skills/openspec-continue-change/SKILL.md +126 -0
- package/src/skills/openspec-explore/SKILL.md +299 -0
- package/src/skills/openspec-ff-change/SKILL.md +109 -0
- package/src/skills/openspec-new-change/SKILL.md +82 -0
- package/src/skills/openspec-onboard/SKILL.md +414 -0
- package/src/skills/openspec-sync-specs/SKILL.md +146 -0
- package/src/skills/openspec-verify-change/SKILL.md +176 -0
- package/src/skills/performance-doctor/SKILL.md +303 -0
- package/src/skills/progress/SKILL.md +193 -0
- package/src/skills/project-navigator/SKILL.md +211 -0
- package/src/skills/redis-cache/SKILL.md +333 -0
- package/src/skills/redis-cache/references/listeners.md +23 -0
- package/src/skills/scheduled-jobs/SKILL.md +314 -0
- package/src/skills/security-guard/SKILL.md +353 -0
- package/src/skills/security-guard/references/encrypt-config.md +103 -0
- package/src/skills/security-guard/references/sensitive-strategies.md +42 -0
- package/src/skills/sms-mail/SKILL.md +308 -0
- package/src/skills/sms-mail/references/mail-config.md +88 -0
- package/src/skills/sms-mail/references/sms-config.md +74 -0
- package/src/skills/social-login/SKILL.md +266 -0
- package/src/skills/social-login/references/provider-configs.md +118 -0
- package/src/skills/start/SKILL.md +154 -0
- package/src/skills/store-pc/SKILL.md +366 -0
- package/src/skills/sync/SKILL.md +149 -0
- package/src/skills/task-tracker/SKILL.md +307 -0
- package/src/skills/tech-decision/SKILL.md +393 -0
- package/src/skills/tenant-management/SKILL.md +288 -0
- package/src/skills/tenant-management/references/tenant-scenarios.md +91 -0
- package/src/skills/test-development/SKILL.md +301 -0
- package/src/skills/test-development/references/parameterized-examples.md +119 -0
- package/src/skills/ui-pc/SKILL.md +438 -0
- package/src/skills/update-status/SKILL.md +159 -0
- package/src/skills/utils-toolkit/SKILL.md +362 -0
- package/src/skills/utils-toolkit/references/redis-utils-api.md +56 -0
- package/src/skills/websocket-sse/SKILL.md +271 -0
- package/src/skills/workflow-engine/SKILL.md +321 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: scheduled-jobs
|
|
3
|
+
description: |
|
|
4
|
+
定时任务开发指南。涵盖 @Scheduled、SnailJob 两种方案,支持分布式任务调度、失败重试、工作流编排。
|
|
5
|
+
|
|
6
|
+
触发场景:
|
|
7
|
+
- 每日数据汇总、定期清理等周期性任务
|
|
8
|
+
- 分布式复杂业务、失败重试、可视化管理(SnailJob)
|
|
9
|
+
- 任务分片、Map/MapReduce 分布式计算
|
|
10
|
+
- 广播任务(所有节点执行)
|
|
11
|
+
|
|
12
|
+
触发词:定时任务、SnailJob、任务调度、重试机制、工作流、@JobExecutor、@Scheduled、分布式任务、广播任务、分片任务、MapReduce
|
|
13
|
+
|
|
14
|
+
核心特性:
|
|
15
|
+
- @Scheduled:简单周期任务、框架内置
|
|
16
|
+
- SnailJob:分布式集群、可视化管理、失败重试、工作流编排
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# 定时任务开发指南
|
|
20
|
+
|
|
21
|
+
> 模块位置:`ruoyi-modules/ruoyi-job`
|
|
22
|
+
|
|
23
|
+
## 方案选择
|
|
24
|
+
|
|
25
|
+
| 场景 | 推荐 | 理由 |
|
|
26
|
+
|------|------|------|
|
|
27
|
+
| 简单周期任务(日报、清理) | `@Scheduled` | 内置、无依赖 |
|
|
28
|
+
| 分布式/需重试/需监控 | **SnailJob** | 可视化管理、完整重试 |
|
|
29
|
+
| 广播(所有节点执行) | **SnailJob** | 支持广播模式 |
|
|
30
|
+
| 海量数据分片 | **SnailJob** | 静态分片/Map/MapReduce |
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## SnailJob 配置
|
|
35
|
+
|
|
36
|
+
```yaml
|
|
37
|
+
# application-dev.yml
|
|
38
|
+
snail-job:
|
|
39
|
+
enabled: ${SNAIL_JOB_ENABLED:false}
|
|
40
|
+
group: ${app.id}
|
|
41
|
+
token: ${SNAIL_JOB_TOKEN:SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT}
|
|
42
|
+
server:
|
|
43
|
+
host: ${SNAIL_JOB_HOST:127.0.0.1}
|
|
44
|
+
port: ${SNAIL_JOB_PORT:17888}
|
|
45
|
+
namespace: ${spring.profiles.active}
|
|
46
|
+
port: 2${server.port}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```java
|
|
50
|
+
// ruoyi-common/ruoyi-common-job/.../config/SnailJobConfig.java
|
|
51
|
+
@AutoConfiguration
|
|
52
|
+
@ConditionalOnProperty(prefix = "snail-job", name = "enabled", havingValue = "true")
|
|
53
|
+
@EnableScheduling
|
|
54
|
+
@EnableSnailJob
|
|
55
|
+
public class SnailJobConfig {}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## SnailJob 任务类型
|
|
61
|
+
|
|
62
|
+
### 基础任务(注解方式,推荐)
|
|
63
|
+
|
|
64
|
+
> 源码:`ruoyi-job/.../snailjob/TestAnnoJobExecutor.java`
|
|
65
|
+
|
|
66
|
+
```java
|
|
67
|
+
import com.aizuda.snailjob.client.job.core.annotation.JobExecutor;
|
|
68
|
+
import com.aizuda.snailjob.client.job.core.dto.JobArgs;
|
|
69
|
+
import com.aizuda.snailjob.model.dto.ExecuteResult;
|
|
70
|
+
import com.aizuda.snailjob.common.log.SnailJobLog;
|
|
71
|
+
import com.aizuda.snailjob.common.core.util.JsonUtil;
|
|
72
|
+
|
|
73
|
+
@Component
|
|
74
|
+
@JobExecutor(name = "testJobExecutor")
|
|
75
|
+
public class TestAnnoJobExecutor {
|
|
76
|
+
|
|
77
|
+
public ExecuteResult jobExecute(JobArgs jobArgs) {
|
|
78
|
+
SnailJobLog.REMOTE.info("任务执行,参数: {}", JsonUtil.toJsonString(jobArgs));
|
|
79
|
+
String jobParams = jobArgs.getJobParams();
|
|
80
|
+
// 业务逻辑...
|
|
81
|
+
return ExecuteResult.success("执行成功");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 广播任务
|
|
87
|
+
|
|
88
|
+
> 源码:`ruoyi-job/.../snailjob/TestBroadcastJob.java`
|
|
89
|
+
|
|
90
|
+
所有节点都执行,适用于清理本地缓存等场景。
|
|
91
|
+
|
|
92
|
+
```java
|
|
93
|
+
@Component
|
|
94
|
+
@JobExecutor(name = "testBroadcastJob")
|
|
95
|
+
public class TestBroadcastJob {
|
|
96
|
+
|
|
97
|
+
public ExecuteResult jobExecute(JobArgs jobArgs) {
|
|
98
|
+
SnailJobLog.REMOTE.info("广播任务执行");
|
|
99
|
+
// 每个节点都会执行此方法
|
|
100
|
+
return ExecuteResult.success("广播任务执行成功");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 静态分片任务
|
|
106
|
+
|
|
107
|
+
> 源码:`ruoyi-job/.../snailjob/TestStaticShardingJob.java`
|
|
108
|
+
|
|
109
|
+
按固定规则分片,每个节点处理不同数据范围。
|
|
110
|
+
|
|
111
|
+
```java
|
|
112
|
+
@Component
|
|
113
|
+
@JobExecutor(name = "testStaticShardingJob")
|
|
114
|
+
public class TestStaticShardingJob {
|
|
115
|
+
|
|
116
|
+
// jobParams 格式:起始ID,结束ID(如:1,100000)
|
|
117
|
+
public ExecuteResult jobExecute(JobArgs jobArgs) {
|
|
118
|
+
String[] split = jobArgs.getJobParams().split(",");
|
|
119
|
+
Long fromId = Long.parseLong(split[0]);
|
|
120
|
+
Long toId = Long.parseLong(split[1]);
|
|
121
|
+
SnailJobLog.REMOTE.info("处理 ID 范围: {} - {}", fromId, toId);
|
|
122
|
+
// 处理该范围的数据
|
|
123
|
+
return ExecuteResult.success("分片任务完成");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**控制台分片配置**:分片0=`1,100000` / 分片1=`100001,200000` / 分片2=`200001,300000`
|
|
129
|
+
|
|
130
|
+
### Map 任务(动态分片)
|
|
131
|
+
|
|
132
|
+
> 源码:`ruoyi-job/.../snailjob/TestMapJobAnnotation.java`
|
|
133
|
+
|
|
134
|
+
运行时动态拆分数据,并行执行。
|
|
135
|
+
|
|
136
|
+
```java
|
|
137
|
+
import com.aizuda.snailjob.client.job.core.annotation.MapExecutor;
|
|
138
|
+
import com.aizuda.snailjob.client.job.core.dto.MapArgs;
|
|
139
|
+
import com.aizuda.snailjob.client.job.core.MapHandler;
|
|
140
|
+
|
|
141
|
+
@Component
|
|
142
|
+
@JobExecutor(name = "testMapJobAnnotation")
|
|
143
|
+
public class TestMapJobAnnotation {
|
|
144
|
+
|
|
145
|
+
@MapExecutor // 无 taskName = 入口方法
|
|
146
|
+
public ExecuteResult doJobMapExecute(MapArgs mapArgs, MapHandler mapHandler) {
|
|
147
|
+
List<List<Integer>> partition = IntStream.rangeClosed(1, 200)
|
|
148
|
+
.boxed()
|
|
149
|
+
.collect(Collectors.groupingBy(i -> (i - 1) / 50))
|
|
150
|
+
.values().stream().toList();
|
|
151
|
+
return mapHandler.doMap(partition, "doCalc");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@MapExecutor(taskName = "doCalc") // 子任务
|
|
155
|
+
public ExecuteResult doCalc(MapArgs mapArgs) {
|
|
156
|
+
List<Integer> sourceList = (List<Integer>) mapArgs.getMapResult();
|
|
157
|
+
int total = sourceList.stream().mapToInt(i -> i).sum();
|
|
158
|
+
return ExecuteResult.success(total);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### MapReduce 任务(分片 + 汇总)
|
|
164
|
+
|
|
165
|
+
> 源码:`ruoyi-job/.../snailjob/TestMapReduceAnnotation1.java`
|
|
166
|
+
|
|
167
|
+
在 Map 基础上增加 Reduce 汇总。
|
|
168
|
+
|
|
169
|
+
```java
|
|
170
|
+
import com.aizuda.snailjob.client.job.core.annotation.ReduceExecutor;
|
|
171
|
+
import com.aizuda.snailjob.client.job.core.dto.ReduceArgs;
|
|
172
|
+
|
|
173
|
+
@Component
|
|
174
|
+
@JobExecutor(name = "testMapReduceAnnotation1")
|
|
175
|
+
public class TestMapReduceAnnotation1 {
|
|
176
|
+
|
|
177
|
+
@MapExecutor
|
|
178
|
+
public ExecuteResult rootMapExecute(MapArgs mapArgs, MapHandler mapHandler) {
|
|
179
|
+
// 拆分数据(同 Map 任务)
|
|
180
|
+
return mapHandler.doMap(partition, "doCalc");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@MapExecutor(taskName = "doCalc")
|
|
184
|
+
public ExecuteResult doCalc(MapArgs mapArgs) {
|
|
185
|
+
List<Integer> sourceList = (List<Integer>) mapArgs.getMapResult();
|
|
186
|
+
return ExecuteResult.success(sourceList.stream().mapToInt(i -> i).sum());
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@ReduceExecutor
|
|
190
|
+
public ExecuteResult reduceExecute(ReduceArgs reduceArgs) {
|
|
191
|
+
List<?> mapResults = reduceArgs.getMapResult();
|
|
192
|
+
int total = mapResults.stream()
|
|
193
|
+
.mapToInt(i -> Integer.parseInt((String) i)).sum();
|
|
194
|
+
SnailJobLog.REMOTE.info("Reduce 汇总,最终总和: {}", total);
|
|
195
|
+
return ExecuteResult.success(total);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 执行模式对比
|
|
203
|
+
|
|
204
|
+
| 模式 | 特点 | 适用场景 |
|
|
205
|
+
|------|------|---------|
|
|
206
|
+
| **集群** | 多节点竞争,只有一个执行 | 订单处理、数据汇总 |
|
|
207
|
+
| **广播** | 所有节点都执行 | 清理缓存、刷新配置 |
|
|
208
|
+
| **静态分片** | 按固定规则分片 | 已知数据范围的批处理 |
|
|
209
|
+
| **Map** | 动态分片 | 运行时确定分片 |
|
|
210
|
+
| **MapReduce** | 动态分片 + 结果汇总 | 分布式计算(求和、统计) |
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## 日志工具
|
|
215
|
+
|
|
216
|
+
```java
|
|
217
|
+
import com.aizuda.snailjob.common.log.SnailJobLog;
|
|
218
|
+
|
|
219
|
+
SnailJobLog.LOCAL.info("本地日志: {}", msg); // 输出到控制台/日志文件
|
|
220
|
+
SnailJobLog.REMOTE.info("远程日志: {}", msg); // 上报到 SnailJob 控制台
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## 最佳实践
|
|
226
|
+
|
|
227
|
+
### 标准任务模板
|
|
228
|
+
|
|
229
|
+
```java
|
|
230
|
+
@Component
|
|
231
|
+
@JobExecutor(name = "orderCleanupJob")
|
|
232
|
+
public class OrderCleanupJob {
|
|
233
|
+
|
|
234
|
+
@Autowired
|
|
235
|
+
private IOrderService orderService;
|
|
236
|
+
|
|
237
|
+
public ExecuteResult jobExecute(JobArgs jobArgs) {
|
|
238
|
+
SnailJobLog.REMOTE.info("开始清理过期订单");
|
|
239
|
+
try {
|
|
240
|
+
String params = jobArgs.getJobParams();
|
|
241
|
+
int days = StringUtils.isBlank(params) ? 30 : Integer.parseInt(params);
|
|
242
|
+
int count = orderService.cleanupExpiredOrders(days);
|
|
243
|
+
SnailJobLog.REMOTE.info("清理完成,删除 {} 条", count);
|
|
244
|
+
return ExecuteResult.success("清理 " + count + " 条");
|
|
245
|
+
} catch (Exception e) {
|
|
246
|
+
SnailJobLog.REMOTE.error("清理失败: {}", e.getMessage());
|
|
247
|
+
throw e; // 抛出异常触发重试
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### 幂等性保证
|
|
254
|
+
|
|
255
|
+
```java
|
|
256
|
+
public ExecuteResult jobExecute(JobArgs jobArgs) {
|
|
257
|
+
String orderId = jobArgs.getJobParams();
|
|
258
|
+
if (paymentService.isSynced(orderId)) {
|
|
259
|
+
return ExecuteResult.success("已同步,跳过");
|
|
260
|
+
}
|
|
261
|
+
paymentService.sync(orderId);
|
|
262
|
+
return ExecuteResult.success("同步成功");
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### 错误处理
|
|
267
|
+
|
|
268
|
+
```java
|
|
269
|
+
// 抛出异常 -> SnailJob 自动重试
|
|
270
|
+
throw e;
|
|
271
|
+
|
|
272
|
+
// 返回 failure -> 不会触发重试(慎用)
|
|
273
|
+
return ExecuteResult.failure("失败");
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## 控制台配置
|
|
279
|
+
|
|
280
|
+
1. **任务管理** -> **新增任务**
|
|
281
|
+
2. 任务名称与 `@JobExecutor(name)` 一致
|
|
282
|
+
3. 任务类型:集群/广播/静态分片/Map/MapReduce
|
|
283
|
+
4. 触发类型:CRON / 固定频率
|
|
284
|
+
|
|
285
|
+
### 重试策略
|
|
286
|
+
|
|
287
|
+
| 策略 | 说明 | 场景 |
|
|
288
|
+
|------|------|------|
|
|
289
|
+
| 固定间隔 | 每次间隔相同 | 网络抖动 |
|
|
290
|
+
| 指数退避 | 间隔逐倍增加 | 服务恢复中 |
|
|
291
|
+
| CRON | 按表达式重试 | 定点重试 |
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## 常见问题
|
|
296
|
+
|
|
297
|
+
| 问题 | 排查步骤 |
|
|
298
|
+
|------|---------|
|
|
299
|
+
| 任务不执行 | 1.检查 `snail-job.enabled: true` 2.`@JobExecutor(name)` 与控制台一致 3.SnailJob 服务是否启动 |
|
|
300
|
+
| @Scheduled vs SnailJob | 简单/<100个任务 -> @Scheduled;需重试/监控/分布式 -> SnailJob |
|
|
301
|
+
| 查看任务日志 | 本地 -> 应用日志;远程 -> SnailJob 控制台 -> 任务实例 -> 查看日志 |
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## 核心文件位置
|
|
306
|
+
|
|
307
|
+
| 文件 | 路径 |
|
|
308
|
+
|------|------|
|
|
309
|
+
| 注解方式 | `ruoyi-modules/ruoyi-job/.../snailjob/TestAnnoJobExecutor.java` |
|
|
310
|
+
| 类方式 | `ruoyi-modules/ruoyi-job/.../snailjob/TestClassJobExecutor.java` |
|
|
311
|
+
| 广播任务 | `ruoyi-modules/ruoyi-job/.../snailjob/TestBroadcastJob.java` |
|
|
312
|
+
| 静态分片 | `ruoyi-modules/ruoyi-job/.../snailjob/TestStaticShardingJob.java` |
|
|
313
|
+
| Map 任务 | `ruoyi-modules/ruoyi-job/.../snailjob/TestMapJobAnnotation.java` |
|
|
314
|
+
| MapReduce | `ruoyi-modules/ruoyi-job/.../snailjob/TestMapReduceAnnotation1.java` |
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: security-guard
|
|
3
|
+
description: |
|
|
4
|
+
后端安全开发规范。包含 Sa-Token 认证授权、数据脱敏、数据加密、接口安全、漏洞防护。
|
|
5
|
+
|
|
6
|
+
触发场景:
|
|
7
|
+
- Sa-Token 权限控制配置
|
|
8
|
+
- 登录认证、Token 管理
|
|
9
|
+
- 数据脱敏处理(@Sensitive)
|
|
10
|
+
- 数据加密处理(@EncryptField、@ApiEncrypt)
|
|
11
|
+
- 接口限流(@RateLimiter)
|
|
12
|
+
- 防重复提交(@RepeatSubmit)
|
|
13
|
+
- XSS/SQL注入防护
|
|
14
|
+
|
|
15
|
+
触发词:安全、Sa-Token、@SaCheckPermission、@SaCheckLogin、@SaCheckRole、登录认证、Token、数据脱敏、@Sensitive、加密解密、@EncryptField、@ApiEncrypt、限流、@RateLimiter、防重复、@RepeatSubmit、XSS、SQL注入、漏洞防护、敏感数据、LoginHelper
|
|
16
|
+
|
|
17
|
+
注意:
|
|
18
|
+
- 如需行级数据权限(@DataPermission、部门隔离),请使用 data-permission。
|
|
19
|
+
- 如果是设计异常处理机制(try-catch、错误码),请使用 error-handler。
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# 后端安全开发指南
|
|
23
|
+
|
|
24
|
+
> 本项目是纯后端项目,本文档专注于 Java 后端安全规范。
|
|
25
|
+
|
|
26
|
+
## 1. Sa-Token 认证授权
|
|
27
|
+
|
|
28
|
+
### 1.1 权限注解
|
|
29
|
+
|
|
30
|
+
```java
|
|
31
|
+
import cn.dev33.satoken.annotation.*;
|
|
32
|
+
|
|
33
|
+
@SaCheckLogin // 登录校验
|
|
34
|
+
@SaCheckPermission("system:user:add") // 权限校验
|
|
35
|
+
@SaCheckRole("admin") // 角色校验
|
|
36
|
+
@SaCheckSafe // 二级认证(敏感操作)
|
|
37
|
+
|
|
38
|
+
// 多权限(满足其一 / 全部满足)
|
|
39
|
+
@SaCheckPermission(value = {"system:user:add", "system:user:update"}, mode = SaMode.OR)
|
|
40
|
+
@SaCheckPermission(value = {"system:user:add", "system:user:update"}, mode = SaMode.AND)
|
|
41
|
+
|
|
42
|
+
// 多角色(满足其一)
|
|
43
|
+
@SaCheckRole(value = {"admin", "editor"}, mode = SaMode.OR)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 1.2 LoginHelper 工具类
|
|
47
|
+
|
|
48
|
+
> 位置:`ruoyi-common-satoken/.../utils/LoginHelper.java`
|
|
49
|
+
|
|
50
|
+
```java
|
|
51
|
+
import org.dromara.common.satoken.utils.LoginHelper;
|
|
52
|
+
|
|
53
|
+
// 用户信息
|
|
54
|
+
LoginUser user = LoginHelper.getLoginUser();
|
|
55
|
+
Long userId = LoginHelper.getUserId();
|
|
56
|
+
String name = LoginHelper.getUsername();
|
|
57
|
+
String tenant = LoginHelper.getTenantId();
|
|
58
|
+
Long deptId = LoginHelper.getDeptId();
|
|
59
|
+
|
|
60
|
+
// 管理员判断
|
|
61
|
+
LoginHelper.isSuperAdmin(); // userId = 1
|
|
62
|
+
LoginHelper.isTenantAdmin(); // 租户管理员
|
|
63
|
+
LoginHelper.isLogin(); // 是否已登录
|
|
64
|
+
|
|
65
|
+
// 用户类型 & 登录
|
|
66
|
+
UserType type = LoginHelper.getUserType();
|
|
67
|
+
LoginHelper.login(loginUser, loginParameter);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 1.3 角色与权限常量
|
|
71
|
+
|
|
72
|
+
| 常量 | 值 | 说明 |
|
|
73
|
+
|------|-----|------|
|
|
74
|
+
| 超级管理员角色 | `superadmin` | 拥有所有权限 |
|
|
75
|
+
| 租户管理员角色 | `admin` | 租户内所有权限 |
|
|
76
|
+
| 通配符权限 | `*:*:*` | 所有权限标识 |
|
|
77
|
+
| 超级管理员ID | `1L` | 系统超管用户ID |
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 2. 数据脱敏(@Sensitive)
|
|
82
|
+
|
|
83
|
+
> 位置:`ruoyi-common-sensitive/.../`
|
|
84
|
+
> 完整 17 种策略详见 `references/sensitive-strategies.md`
|
|
85
|
+
|
|
86
|
+
### 基本用法
|
|
87
|
+
|
|
88
|
+
```java
|
|
89
|
+
import org.dromara.common.sensitive.annotation.Sensitive;
|
|
90
|
+
import org.dromara.common.sensitive.core.SensitiveStrategy;
|
|
91
|
+
|
|
92
|
+
public class UserVo {
|
|
93
|
+
@Sensitive(strategy = SensitiveStrategy.PHONE) // 138****8888
|
|
94
|
+
private String phone;
|
|
95
|
+
|
|
96
|
+
@Sensitive(strategy = SensitiveStrategy.ID_CARD) // 110***********1234
|
|
97
|
+
private String idCard;
|
|
98
|
+
|
|
99
|
+
@Sensitive(strategy = SensitiveStrategy.EMAIL) // t**@example.com
|
|
100
|
+
private String email;
|
|
101
|
+
|
|
102
|
+
@Sensitive(strategy = SensitiveStrategy.BANK_CARD) // 6222***********1234
|
|
103
|
+
private String bankCard;
|
|
104
|
+
|
|
105
|
+
@Sensitive(strategy = SensitiveStrategy.CHINESE_NAME) // 张*
|
|
106
|
+
private String realName;
|
|
107
|
+
|
|
108
|
+
@Sensitive(strategy = SensitiveStrategy.PASSWORD) // ******
|
|
109
|
+
private String password;
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 基于角色/权限的脱敏控制
|
|
114
|
+
|
|
115
|
+
```java
|
|
116
|
+
// admin 角色可查看原数据,其他用户看脱敏数据
|
|
117
|
+
@Sensitive(strategy = SensitiveStrategy.ID_CARD, roleKey = {"admin"})
|
|
118
|
+
private String idCard;
|
|
119
|
+
|
|
120
|
+
// 需要权限才能看原数据
|
|
121
|
+
@Sensitive(strategy = SensitiveStrategy.PHONE, perms = {"system:user:detail"})
|
|
122
|
+
private String phone;
|
|
123
|
+
|
|
124
|
+
// roleKey 和 perms 是 OR 关系
|
|
125
|
+
@Sensitive(strategy = SensitiveStrategy.BANK_CARD,
|
|
126
|
+
roleKey = {"admin"}, perms = {"finance:account:query"})
|
|
127
|
+
private String bankCard;
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 日志脱敏
|
|
131
|
+
|
|
132
|
+
```java
|
|
133
|
+
// NG: log.info("手机号: {}", phone);
|
|
134
|
+
// OK:
|
|
135
|
+
log.info("手机号: {}", DesensitizedUtil.mobilePhone(phone));
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 3. 数据加密(@EncryptField / @ApiEncrypt)
|
|
141
|
+
|
|
142
|
+
> 位置:`ruoyi-common-encrypt/.../`
|
|
143
|
+
> 完整加密配置和工具类详见 `references/encrypt-config.md`
|
|
144
|
+
|
|
145
|
+
### 支持算法
|
|
146
|
+
|
|
147
|
+
| 算法 | 类型 | 密钥要求 |
|
|
148
|
+
|------|------|---------|
|
|
149
|
+
| BASE64 | 编码 | 无 |
|
|
150
|
+
| AES | 对称加密 | 16/24/32 位 |
|
|
151
|
+
| RSA | 非对称加密 | 公钥/私钥 |
|
|
152
|
+
| SM2 | 国密非对称 | 公钥/私钥 |
|
|
153
|
+
| SM4 | 国密对称 | 16 位 |
|
|
154
|
+
|
|
155
|
+
### 字段级加密
|
|
156
|
+
|
|
157
|
+
```java
|
|
158
|
+
import org.dromara.common.encrypt.annotation.EncryptField;
|
|
159
|
+
import org.dromara.common.encrypt.enumd.AlgorithmType;
|
|
160
|
+
|
|
161
|
+
public class User {
|
|
162
|
+
@EncryptField // 默认(全局配置)
|
|
163
|
+
private String password;
|
|
164
|
+
|
|
165
|
+
@EncryptField(algorithm = AlgorithmType.AES) // AES
|
|
166
|
+
private String idCard;
|
|
167
|
+
|
|
168
|
+
@EncryptField(algorithm = AlgorithmType.SM4) // SM4 国密
|
|
169
|
+
private String phone;
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### API 级加密
|
|
174
|
+
|
|
175
|
+
```java
|
|
176
|
+
import org.dromara.common.encrypt.annotation.ApiEncrypt;
|
|
177
|
+
|
|
178
|
+
@ApiEncrypt // 请求体自动解密
|
|
179
|
+
@PostMapping("/addUser")
|
|
180
|
+
public R<Long> addUser(@RequestBody UserBo bo) { }
|
|
181
|
+
|
|
182
|
+
@ApiEncrypt(response = true) // 请求解密 + 响应加密
|
|
183
|
+
@PostMapping("/updateUser")
|
|
184
|
+
public R<Void> updateUser(@RequestBody UserBo bo) { }
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 4. 接口限流(@RateLimiter)
|
|
190
|
+
|
|
191
|
+
> 位置:`ruoyi-common-ratelimiter/.../`
|
|
192
|
+
|
|
193
|
+
```java
|
|
194
|
+
import org.dromara.common.ratelimiter.annotation.RateLimiter;
|
|
195
|
+
import org.dromara.common.ratelimiter.enums.LimitType;
|
|
196
|
+
|
|
197
|
+
// 全局限流:60秒内最多100次
|
|
198
|
+
@RateLimiter(time = 60, count = 100)
|
|
199
|
+
|
|
200
|
+
// IP 限流:每个 IP 每分钟最多10次
|
|
201
|
+
@RateLimiter(time = 60, count = 10, limitType = LimitType.IP)
|
|
202
|
+
|
|
203
|
+
// 动态 key(SpEL)
|
|
204
|
+
@RateLimiter(key = "#userId", time = 60, count = 5)
|
|
205
|
+
|
|
206
|
+
// 自定义错误消息
|
|
207
|
+
@RateLimiter(time = 60, count = 10, message = "访问过于频繁,请稍后再试")
|
|
208
|
+
|
|
209
|
+
// 集群限流
|
|
210
|
+
@RateLimiter(time = 60, count = 1000, limitType = LimitType.CLUSTER)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### 推荐配置
|
|
214
|
+
|
|
215
|
+
| 场景 | time | count | limitType |
|
|
216
|
+
|------|------|-------|-----------|
|
|
217
|
+
| 登录接口 | 60 | 5-10 | IP |
|
|
218
|
+
| 验证码 | 60 | 3 | IP |
|
|
219
|
+
| 查询接口 | 60 | 100-1000 | DEFAULT |
|
|
220
|
+
| 写入接口 | 60 | 10-50 | DEFAULT |
|
|
221
|
+
| 敏感操作 | 60 | 1-5 | IP |
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## 5. 防重复提交(@RepeatSubmit)
|
|
226
|
+
|
|
227
|
+
> 位置:`ruoyi-common-idempotent/.../`
|
|
228
|
+
|
|
229
|
+
```java
|
|
230
|
+
import org.dromara.common.idempotent.annotation.RepeatSubmit;
|
|
231
|
+
|
|
232
|
+
@RepeatSubmit() // 默认 5 秒
|
|
233
|
+
@RepeatSubmit(interval = 10, timeUnit = TimeUnit.SECONDS) // 10 秒
|
|
234
|
+
@RepeatSubmit(interval = 5000, message = "请勿重复提交订单") // 自定义消息
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
| 场景 | 推荐间隔 |
|
|
238
|
+
|------|---------|
|
|
239
|
+
| 普通表单 | 3-5 秒 |
|
|
240
|
+
| 订单创建 | 10 秒 |
|
|
241
|
+
| 支付操作 | 30 秒 |
|
|
242
|
+
| 文件上传 | 10 秒 |
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## 6. 数据权限(@DataPermission)
|
|
247
|
+
|
|
248
|
+
> **完整指南请使用 `data-permission` 技能**
|
|
249
|
+
|
|
250
|
+
```java
|
|
251
|
+
@DataPermission({
|
|
252
|
+
@DataColumn(key = "deptName", value = "create_dept"),
|
|
253
|
+
@DataColumn(key = "userName", value = "create_by")
|
|
254
|
+
})
|
|
255
|
+
public TableDataInfo<OrderVo> pageWithPermission(OrderBo bo, PageQuery pageQuery) { }
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
权限类型:全部数据 | 本部门 | 本部门及以下 | 仅本人 | 自定义
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## 7. 输入校验
|
|
263
|
+
|
|
264
|
+
```java
|
|
265
|
+
import org.dromara.common.core.validate.AddGroup;
|
|
266
|
+
import org.dromara.common.core.validate.EditGroup;
|
|
267
|
+
|
|
268
|
+
public class UserBo {
|
|
269
|
+
@NotNull(message = "ID不能为空", groups = { EditGroup.class })
|
|
270
|
+
private Long id;
|
|
271
|
+
|
|
272
|
+
@NotBlank(message = "用户名不能为空", groups = { AddGroup.class, EditGroup.class })
|
|
273
|
+
@Size(min = 2, max = 20)
|
|
274
|
+
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "只能包含字母数字下划线")
|
|
275
|
+
private String username;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Controller 分组校验
|
|
279
|
+
@PostMapping public R<Long> add(@Validated(AddGroup.class) @RequestBody UserBo bo) { }
|
|
280
|
+
@PutMapping public R<Void> update(@Validated(EditGroup.class) @RequestBody UserBo bo) { }
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## 8. 常见漏洞防护
|
|
286
|
+
|
|
287
|
+
### SQL 注入
|
|
288
|
+
|
|
289
|
+
```java
|
|
290
|
+
// NG: "SELECT * FROM user WHERE name = '" + name + "'"
|
|
291
|
+
// NG: @Select("SELECT * FROM user WHERE name = '${name}'")
|
|
292
|
+
// OK: MyBatis-Plus LambdaQueryWrapper
|
|
293
|
+
// OK: @Select("SELECT * FROM user WHERE name = #{name}")
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### 越权访问
|
|
297
|
+
|
|
298
|
+
```java
|
|
299
|
+
@Override
|
|
300
|
+
public OrderVo selectById(Long id) {
|
|
301
|
+
Order order = baseMapper.selectById(id);
|
|
302
|
+
if (ObjectUtil.isNull(order)) {
|
|
303
|
+
throw new ServiceException("订单不存在");
|
|
304
|
+
}
|
|
305
|
+
if (!LoginHelper.isSuperAdmin() && !order.getUserId().equals(LoginHelper.getUserId())) {
|
|
306
|
+
throw new ServiceException("无权访问此订单");
|
|
307
|
+
}
|
|
308
|
+
return MapstructUtils.convert(order, OrderVo.class);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 批量操作同样校验归属
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### 敏感信息泄露
|
|
315
|
+
|
|
316
|
+
```java
|
|
317
|
+
// NG: return userDao.getById(id); // 包含密码等
|
|
318
|
+
// OK: return MapstructUtils.convert(user, UserVo.class); // VO 过滤敏感字段
|
|
319
|
+
// OK: 使用 @Sensitive 自动脱敏
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## 9. 安全检查清单
|
|
325
|
+
|
|
326
|
+
### 代码审查
|
|
327
|
+
|
|
328
|
+
- [ ] 用户输入经过 `@NotBlank`/`@Size`/`@Pattern` 校验
|
|
329
|
+
- [ ] SQL 使用 MyBatis-Plus 或参数化查询(#{})
|
|
330
|
+
- [ ] 敏感字段使用 `@Sensitive` 脱敏
|
|
331
|
+
- [ ] 需加密字段使用 `@EncryptField`
|
|
332
|
+
- [ ] Controller 添加 `@SaCheckPermission`
|
|
333
|
+
- [ ] 敏感操作添加 `@RepeatSubmit`
|
|
334
|
+
- [ ] 高频接口添加 `@RateLimiter`
|
|
335
|
+
- [ ] 批量操作校验数据归属(防越权)
|
|
336
|
+
- [ ] 文件上传校验类型/大小/扩展名
|
|
337
|
+
- [ ] 日志中无敏感信息(或已脱敏)
|
|
338
|
+
|
|
339
|
+
### 配置 & 部署
|
|
340
|
+
|
|
341
|
+
- [ ] 生产关闭调试模式
|
|
342
|
+
- [ ] 敏感配置已加密或使用环境变量
|
|
343
|
+
- [ ] Token 有效期合理(2-24h)
|
|
344
|
+
- [ ] CORS 不使用 `*`
|
|
345
|
+
- [ ] 启用 HTTPS、安全响应头
|
|
346
|
+
- [ ] 错误页不泄露堆栈、数据库/Redis 端口不对外
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## 注意事项
|
|
351
|
+
|
|
352
|
+
- leniu-tengyun-core 项目请使用 `leniu-security-guard` skill
|
|
353
|
+
- leniu 使用自研 secure 模块,注解和工具类与 RuoYi-Vue-Plus 不同
|