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,371 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: error-handler
|
|
3
|
+
description: |
|
|
4
|
+
后端异常处理规范。包含 ServiceException 用法、全局异常处理器、参数校验、日志规范、错误码设计。
|
|
5
|
+
|
|
6
|
+
触发场景:
|
|
7
|
+
- 抛出业务异常(ServiceException)
|
|
8
|
+
- 全局异常处理器配置
|
|
9
|
+
- 参数校验异常处理
|
|
10
|
+
- 日志记录规范
|
|
11
|
+
- 错误码设计与国际化
|
|
12
|
+
- 事务异常处理
|
|
13
|
+
|
|
14
|
+
触发词:异常、ServiceException、throw、错误处理、全局异常、@Validated、参数校验、日志、log、错误码、事务、@Transactional、try-catch、异常捕获
|
|
15
|
+
|
|
16
|
+
注意:
|
|
17
|
+
- 如果是安全相关(认证授权、数据脱敏),请使用 security-guard。
|
|
18
|
+
- 如果是数据权限(@DataPermission),请使用 data-permission。
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# 后端异常处理指南
|
|
22
|
+
|
|
23
|
+
> 本项目是纯后端项目,本文档专注于 Java 后端异常处理规范。
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 快速索引
|
|
28
|
+
|
|
29
|
+
| 场景 | 推荐方式 |
|
|
30
|
+
|------|---------|
|
|
31
|
+
| 业务异常 | `throw new ServiceException("msg")` |
|
|
32
|
+
| 带参数异常 | `throw new ServiceException("用户 {} 不存在", userId)` |
|
|
33
|
+
| 带错误码 | `throw new ServiceException("msg", 200101)` |
|
|
34
|
+
| 参数校验 | `@Validated(AddGroup.class)` |
|
|
35
|
+
| 日志记录 | `log.error("msg: {}", e.getMessage(), e)` |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 1. 业务异常 - ServiceException
|
|
40
|
+
|
|
41
|
+
### 基本用法
|
|
42
|
+
|
|
43
|
+
```java
|
|
44
|
+
import org.dromara.common.core.exception.ServiceException;
|
|
45
|
+
|
|
46
|
+
// ✅ 基本用法:抛出业务异常
|
|
47
|
+
throw new ServiceException("用户不存在");
|
|
48
|
+
|
|
49
|
+
// ✅ 带占位符(支持 {} 占位符,使用 Hutool StrFormatter)
|
|
50
|
+
throw new ServiceException("用户 {} 不存在", userId);
|
|
51
|
+
throw new ServiceException("订单 {} 状态 {} 无法支付", orderId, status);
|
|
52
|
+
|
|
53
|
+
// ✅ 带错误码(第二个参数是 Integer code)
|
|
54
|
+
throw new ServiceException("用户不存在", 200101);
|
|
55
|
+
|
|
56
|
+
// ✅ 条件抛出(手动检查)
|
|
57
|
+
if (ObjectUtil.isNull(user)) {
|
|
58
|
+
throw new ServiceException("用户不存在");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ✅ 参数校验
|
|
62
|
+
if (StringUtils.isBlank(bo.getName())) {
|
|
63
|
+
throw new ServiceException("名称不能为空");
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### ServiceException 完整 API
|
|
68
|
+
|
|
69
|
+
> **🔴 重要**:ServiceException **没有静态工厂方法**(如 `of()`)和条件检查方法(如 `throwIf()`, `notNull()`),必须使用 `new` 关键字创建异常对象。
|
|
70
|
+
|
|
71
|
+
| 构造函数 | 说明 | 示例 |
|
|
72
|
+
|---------|------|------|
|
|
73
|
+
| `new ServiceException(String message)` | 只有错误消息 | `new ServiceException("操作失败")` |
|
|
74
|
+
| `new ServiceException(String message, Object... args)` | 带占位符参数 | `new ServiceException("用户{}不存在", userId)` |
|
|
75
|
+
| `new ServiceException(String message, Integer code)` | 带错误码 | `new ServiceException("用户不存在", 200101)` |
|
|
76
|
+
|
|
77
|
+
**链式调用方法**:
|
|
78
|
+
- `setMessage(String message)`: 设置错误消息
|
|
79
|
+
- `setDetailMessage(String detailMessage)`: 设置详细错误(用于内部调试)
|
|
80
|
+
|
|
81
|
+
**源码位置**: `ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/ServiceException.java`
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 2. 全局异常处理器
|
|
86
|
+
|
|
87
|
+
框架已提供全局异常处理器,自动捕获并处理各类异常。
|
|
88
|
+
|
|
89
|
+
**位置**: `ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/handler/GlobalExceptionHandler.java`
|
|
90
|
+
|
|
91
|
+
### 异常处理映射
|
|
92
|
+
|
|
93
|
+
> **注意**:所有异常的 HTTP 响应状态码均为 200,错误码通过 `R.code` 字段返回。
|
|
94
|
+
|
|
95
|
+
| 异常类型 | 处理方式 | R.code |
|
|
96
|
+
|---------|---------|--------|
|
|
97
|
+
| `ServiceException` | 返回业务错误信息 | 自定义 code 或 500 |
|
|
98
|
+
| `BindException` | 返回参数绑定错误 | 500 |
|
|
99
|
+
| `ConstraintViolationException` | 返回参数校验错误 | 500 |
|
|
100
|
+
| `MethodArgumentNotValidException` | 返回参数校验错误 | 500 |
|
|
101
|
+
| `HandlerMethodValidationException` | 返回 @Validated 校验错误 | 500 |
|
|
102
|
+
| `HttpRequestMethodNotSupportedException` | 请求方式不支持 | 405 |
|
|
103
|
+
| `NoHandlerFoundException` | 路由不存在 | 404 |
|
|
104
|
+
| `JsonParseException` | JSON 解析失败 | 400 |
|
|
105
|
+
| `HttpMessageNotReadableException` | 请求参数格式错误 | 400 |
|
|
106
|
+
| `ExpressionException` | SpEL 表达式解析失败 | 500 |
|
|
107
|
+
| `RuntimeException` / `Exception` | 系统错误 | 500 |
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 3. 参数校验
|
|
112
|
+
|
|
113
|
+
### 使用 @Validated 自动校验
|
|
114
|
+
|
|
115
|
+
```java
|
|
116
|
+
import org.dromara.common.core.validate.AddGroup;
|
|
117
|
+
import org.dromara.common.core.validate.EditGroup;
|
|
118
|
+
|
|
119
|
+
// Controller 层校验
|
|
120
|
+
@PostMapping
|
|
121
|
+
public R<Long> add(@Validated(AddGroup.class) @RequestBody XxxBo bo) {
|
|
122
|
+
// 参数校验失败会自动抛出异常
|
|
123
|
+
// 全局异常处理器会自动捕获并返回错误信息
|
|
124
|
+
return R.ok(xxxService.insert(bo));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@PutMapping
|
|
128
|
+
public R<Void> edit(@Validated(EditGroup.class) @RequestBody XxxBo bo) {
|
|
129
|
+
return toAjax(xxxService.update(bo));
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### BO 类校验注解
|
|
134
|
+
|
|
135
|
+
```java
|
|
136
|
+
public class XxxBo extends BaseEntity {
|
|
137
|
+
|
|
138
|
+
@NotNull(message = "ID不能为空", groups = { EditGroup.class })
|
|
139
|
+
private Long id;
|
|
140
|
+
|
|
141
|
+
@NotBlank(message = "名称不能为空", groups = { AddGroup.class, EditGroup.class })
|
|
142
|
+
@Size(max = 100, message = "名称长度不能超过100个字符")
|
|
143
|
+
private String name;
|
|
144
|
+
|
|
145
|
+
@Email(message = "邮箱格式不正确")
|
|
146
|
+
private String email;
|
|
147
|
+
|
|
148
|
+
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
|
149
|
+
private String phone;
|
|
150
|
+
|
|
151
|
+
@Min(value = 0, message = "数量不能小于0")
|
|
152
|
+
@Max(value = 9999, message = "数量不能大于9999")
|
|
153
|
+
private Integer count;
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 手动校验(Service 层)
|
|
158
|
+
|
|
159
|
+
```java
|
|
160
|
+
import org.dromara.common.core.utils.ValidatorUtils;
|
|
161
|
+
|
|
162
|
+
// 手动触发校验
|
|
163
|
+
ValidatorUtils.validate(bo, AddGroup.class);
|
|
164
|
+
ValidatorUtils.validate(bo, EditGroup.class);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## 4. 日志规范
|
|
170
|
+
|
|
171
|
+
### 日志级别
|
|
172
|
+
|
|
173
|
+
| 级别 | 使用场景 | 示例 |
|
|
174
|
+
|------|---------|------|
|
|
175
|
+
| ERROR | 系统错误、业务异常 | 数据库连接失败、第三方接口超时 |
|
|
176
|
+
| WARN | 警告信息、潜在问题 | 缓存未命中、重试操作 |
|
|
177
|
+
| INFO | 重要业务流程、操作记录 | 用户登录、订单创建 |
|
|
178
|
+
| DEBUG | 开发调试信息 | 方法入参、中间变量 |
|
|
179
|
+
| TRACE | 详细追踪信息 | 循环内部数据 |
|
|
180
|
+
|
|
181
|
+
### 日志最佳实践
|
|
182
|
+
|
|
183
|
+
```java
|
|
184
|
+
import lombok.extern.slf4j.Slf4j;
|
|
185
|
+
|
|
186
|
+
@Slf4j
|
|
187
|
+
@Service
|
|
188
|
+
public class XxxServiceImpl implements IXxxService {
|
|
189
|
+
|
|
190
|
+
// ✅ 好的:使用占位符(性能更好)
|
|
191
|
+
log.info("处理订单: {}, 状态: {}", orderId, status);
|
|
192
|
+
|
|
193
|
+
// ❌ 不好:字符串拼接(每次都会拼接,即使日志级别不输出)
|
|
194
|
+
log.info("处理订单: " + orderId + ", 状态: " + status);
|
|
195
|
+
|
|
196
|
+
// ✅ 好的:异常日志带堆栈(第三个参数传异常对象)
|
|
197
|
+
log.error("处理失败: {}", e.getMessage(), e);
|
|
198
|
+
|
|
199
|
+
// ❌ 不好:只记录消息,丢失堆栈
|
|
200
|
+
log.error("处理失败: {}", e.getMessage());
|
|
201
|
+
|
|
202
|
+
// ✅ 好的:判断日志级别(避免不必要的序列化开销)
|
|
203
|
+
if (log.isDebugEnabled()) {
|
|
204
|
+
log.debug("详细数据: {}", JsonUtils.toJsonString(data));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ✅ 好的:敏感信息脱敏
|
|
208
|
+
log.info("用户登录,手机: {}", DesensitizedUtil.mobilePhone(phone));
|
|
209
|
+
|
|
210
|
+
// ❌ 不好:记录敏感信息
|
|
211
|
+
log.info("用户登录,手机: {}", phone);
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Service 层日志示例
|
|
216
|
+
|
|
217
|
+
```java
|
|
218
|
+
@Slf4j
|
|
219
|
+
@RequiredArgsConstructor
|
|
220
|
+
@Service
|
|
221
|
+
public class SysUserServiceImpl implements ISysUserService {
|
|
222
|
+
|
|
223
|
+
private final SysUserMapper baseMapper;
|
|
224
|
+
|
|
225
|
+
@Override
|
|
226
|
+
@Transactional(rollbackFor = Exception.class)
|
|
227
|
+
public Long insertUser(SysUserBo bo) {
|
|
228
|
+
log.info("开始新增用户,用户名: {}", bo.getUserName());
|
|
229
|
+
|
|
230
|
+
// 1. 业务校验
|
|
231
|
+
SysUser existUser = baseMapper.selectUserByUserName(bo.getUserName());
|
|
232
|
+
if (ObjectUtil.isNotNull(existUser)) {
|
|
233
|
+
throw new ServiceException("用户名 {} 已存在", bo.getUserName());
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 2. 执行插入
|
|
237
|
+
SysUser user = MapstructUtils.convert(bo, SysUser.class);
|
|
238
|
+
baseMapper.insert(user);
|
|
239
|
+
|
|
240
|
+
log.info("新增用户成功,用户ID: {}, 用户名: {}", user.getUserId(), user.getUserName());
|
|
241
|
+
return user.getUserId();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
@Override
|
|
245
|
+
public SysUserVo selectUserById(Long userId) {
|
|
246
|
+
SysUser user = baseMapper.selectById(userId);
|
|
247
|
+
if (ObjectUtil.isNull(user)) {
|
|
248
|
+
throw new ServiceException("用户 {} 不存在", userId);
|
|
249
|
+
}
|
|
250
|
+
return MapstructUtils.convert(user, SysUserVo.class);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## 5. 错误码设计
|
|
258
|
+
|
|
259
|
+
### 错误码规范
|
|
260
|
+
|
|
261
|
+
```java
|
|
262
|
+
// 格式: 模块(2位) + 类型(2位) + 序号(2位)
|
|
263
|
+
// 模块: 10-系统, 20-用户, 30-订单, 40-商品
|
|
264
|
+
// 类型: 01-参数错误, 02-业务错误, 03-权限错误, 04-系统错误
|
|
265
|
+
|
|
266
|
+
public class ErrorCode {
|
|
267
|
+
// 通用错误
|
|
268
|
+
public static final int SUCCESS = 200;
|
|
269
|
+
public static final int ERROR = 500;
|
|
270
|
+
public static final int UNAUTHORIZED = 401;
|
|
271
|
+
public static final int FORBIDDEN = 403;
|
|
272
|
+
|
|
273
|
+
// 用户模块 20xxxx
|
|
274
|
+
public static final int USER_NOT_FOUND = 200201; // 用户不存在
|
|
275
|
+
public static final int USER_PASSWORD_ERROR = 200202; // 密码错误
|
|
276
|
+
public static final int USER_DISABLED = 200203; // 用户已禁用
|
|
277
|
+
|
|
278
|
+
// 订单模块 30xxxx
|
|
279
|
+
public static final int ORDER_NOT_FOUND = 300201; // 订单不存在
|
|
280
|
+
public static final int ORDER_STATUS_ERROR = 300202; // 订单状态错误
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 使用示例
|
|
284
|
+
throw new ServiceException("用户不存在", ErrorCode.USER_NOT_FOUND);
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### 错误消息国际化
|
|
288
|
+
|
|
289
|
+
```java
|
|
290
|
+
import org.dromara.common.core.utils.MessageUtils;
|
|
291
|
+
|
|
292
|
+
// 使用 MessageUtils.message() 获取国际化消息
|
|
293
|
+
throw new ServiceException(MessageUtils.message("user.not.exists"));
|
|
294
|
+
|
|
295
|
+
// 带参数的国际化消息
|
|
296
|
+
throw new ServiceException(MessageUtils.message("user.password.retry.limit.exceed", maxRetryCount));
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## 6. 用户友好提示
|
|
302
|
+
|
|
303
|
+
### 错误提示规范
|
|
304
|
+
|
|
305
|
+
```java
|
|
306
|
+
// ✅ 好的:用户友好提示
|
|
307
|
+
throw new ServiceException("订单已发货,无法取消");
|
|
308
|
+
throw new ServiceException("库存不足,请减少购买数量");
|
|
309
|
+
throw new ServiceException("验证码已过期,请重新获取");
|
|
310
|
+
throw new ServiceException("该用户名已被注册,请换一个试试");
|
|
311
|
+
|
|
312
|
+
// ❌ 不好:技术术语
|
|
313
|
+
throw new ServiceException("order.status.invalid");
|
|
314
|
+
throw new ServiceException("NullPointerException at line 123");
|
|
315
|
+
throw new ServiceException("数据库连接失败");
|
|
316
|
+
throw new ServiceException("Duplicate entry for key 'uk_username'");
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## 7. 事务异常处理
|
|
322
|
+
|
|
323
|
+
```java
|
|
324
|
+
@Transactional(rollbackFor = Exception.class) // 所有异常都回滚
|
|
325
|
+
public void batchOperation() {
|
|
326
|
+
// 业务逻辑
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ✅ 好的:指定回滚异常类型
|
|
330
|
+
@Transactional(rollbackFor = Exception.class)
|
|
331
|
+
|
|
332
|
+
// ❌ 不好:使用默认(只回滚 RuntimeException)
|
|
333
|
+
@Transactional
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## 错误处理检查清单
|
|
339
|
+
|
|
340
|
+
- [ ] 业务异常使用 `new ServiceException()`(不是 `ServiceException.of()`)
|
|
341
|
+
- [ ] 条件检查使用 `if + ObjectUtil.isNull()` 判断
|
|
342
|
+
- [ ] 参数校验使用 `@Validated(XxxGroup.class)`
|
|
343
|
+
- [ ] 事务方法添加 `@Transactional(rollbackFor = Exception.class)`
|
|
344
|
+
- [ ] 日志记录异常堆栈:`log.error("msg: {}", e.getMessage(), e)`
|
|
345
|
+
- [ ] 日志使用占位符 `{}`,不使用字符串拼接
|
|
346
|
+
- [ ] 敏感信息脱敏后再记录日志
|
|
347
|
+
- [ ] 重要操作记录 INFO 日志
|
|
348
|
+
- [ ] 错误提示使用用户友好语言
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## 快速对照表
|
|
353
|
+
|
|
354
|
+
| ❌ 错误写法 | ✅ 正确写法 |
|
|
355
|
+
|-----------|-----------|
|
|
356
|
+
| `throw ServiceException.of("msg")` | `throw new ServiceException("msg")` |
|
|
357
|
+
| `ServiceException.throwIf(cond, "msg")` | `if (cond) { throw new ServiceException("msg"); }` |
|
|
358
|
+
| `ServiceException.notNull(obj, "msg")` | `if (ObjectUtil.isNull(obj)) { throw new ServiceException("msg"); }` |
|
|
359
|
+
| `log.error("失败: " + e.getMessage())` | `log.error("失败: {}", e.getMessage(), e)` |
|
|
360
|
+
| `@Transactional` | `@Transactional(rollbackFor = Exception.class)` |
|
|
361
|
+
| `throw new ServiceException("DB error")` | `throw new ServiceException("数据保存失败,请重试")` |
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## 相关技能
|
|
366
|
+
|
|
367
|
+
| 需要了解 | 激活 Skill |
|
|
368
|
+
|---------|-----------|
|
|
369
|
+
| Java 异常规范 | `java-exception` |
|
|
370
|
+
| Service 层规范 | `java-service` |
|
|
371
|
+
| Controller 层规范 | `java-controller` |
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: file-oss-management
|
|
3
|
+
description: |
|
|
4
|
+
当需要进行文件上传、下载、存储管理时自动使用此 Skill。支持本地存储、阿里云OSS、腾讯云COS、七牛云、MinIO等。
|
|
5
|
+
|
|
6
|
+
触发场景:
|
|
7
|
+
- 文件上传下载
|
|
8
|
+
- 云存储配置
|
|
9
|
+
- 预签名URL生成
|
|
10
|
+
- 文件元数据管理
|
|
11
|
+
- OSS服务商切换
|
|
12
|
+
|
|
13
|
+
触发词:文件上传、OSS、云存储、MinIO、阿里云、腾讯云、七牛、图片上传、文件下载、预签名、presigned、OssClient、OssFactory
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# 文件与云存储指南
|
|
17
|
+
|
|
18
|
+
> 模块位置:`ruoyi-common/ruoyi-common-oss`
|
|
19
|
+
> 统一协议:基于 AWS S3 SDK v2,兼容所有 S3 协议云服务
|
|
20
|
+
|
|
21
|
+
## 核心类
|
|
22
|
+
|
|
23
|
+
| 类 | 说明 |
|
|
24
|
+
|----|------|
|
|
25
|
+
| `OssFactory` | 获取 OssClient 实例(只有2个方法) |
|
|
26
|
+
| `OssClient` | 统一操作入口(基于 AWS S3 SDK v2) |
|
|
27
|
+
| `UploadResult` | 上传结果(url, filename, eTag) |
|
|
28
|
+
| `ISysOssService` | OSS 文件管理服务接口 |
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 一、获取 OssClient
|
|
33
|
+
|
|
34
|
+
```java
|
|
35
|
+
import org.dromara.common.oss.factory.OssFactory;
|
|
36
|
+
import org.dromara.common.oss.core.OssClient;
|
|
37
|
+
|
|
38
|
+
OssClient client = OssFactory.instance(); // 默认配置
|
|
39
|
+
OssClient client = OssFactory.instance("aliyun"); // 指定配置(字符串,非枚举)
|
|
40
|
+
OssClient client = OssFactory.instance("minio");
|
|
41
|
+
|
|
42
|
+
// ❌ 不存在 OssType 枚举参数
|
|
43
|
+
OssClient client = OssFactory.instance(OssType.ALIYUN); // 编译错误!
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
> 内部使用 ConcurrentHashMap + ReentrantLock 双检锁缓存实例,支持多租户隔离。
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 二、文件上传
|
|
51
|
+
|
|
52
|
+
```java
|
|
53
|
+
import org.dromara.common.oss.entity.UploadResult;
|
|
54
|
+
|
|
55
|
+
// 1. 上传字节数组,自动生成路径(推荐)
|
|
56
|
+
UploadResult result = client.uploadSuffix(data, ".jpg", "image/jpeg");
|
|
57
|
+
|
|
58
|
+
// 2. 上传输入流,自动生成路径
|
|
59
|
+
UploadResult result = client.uploadSuffix(is, ".jpg", fileSize, "image/jpeg");
|
|
60
|
+
|
|
61
|
+
// 3. 上传 File 对象,自动生成路径
|
|
62
|
+
UploadResult result = client.uploadSuffix(file, ".jpg");
|
|
63
|
+
|
|
64
|
+
// 4. 上传到指定路径
|
|
65
|
+
UploadResult result = client.upload(file.toPath(), "avatar/user123.jpg", null, "image/jpeg");
|
|
66
|
+
|
|
67
|
+
// 5. 上传流到指定路径
|
|
68
|
+
UploadResult result = client.upload(is, "images/photo.jpg", fileSize, "image/jpeg");
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**方法签名:**
|
|
72
|
+
```java
|
|
73
|
+
UploadResult upload(Path filePath, String key, String md5Digest, String contentType)
|
|
74
|
+
UploadResult upload(InputStream inputStream, String key, Long length, String contentType)
|
|
75
|
+
UploadResult uploadSuffix(byte[] data, String suffix, String contentType)
|
|
76
|
+
UploadResult uploadSuffix(InputStream inputStream, String suffix, Long length, String contentType)
|
|
77
|
+
UploadResult uploadSuffix(File file, String suffix)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 三、UploadResult 字段
|
|
83
|
+
|
|
84
|
+
> 只有 3 个字段,使用 Lombok `@Builder`。
|
|
85
|
+
|
|
86
|
+
| 字段 | 类型 | 说明 |
|
|
87
|
+
|------|------|------|
|
|
88
|
+
| `url` | String | 文件访问URL |
|
|
89
|
+
| `filename` | String | 文件名/对象键(**小写 n**) |
|
|
90
|
+
| `eTag` | String | 文件校验标记 |
|
|
91
|
+
|
|
92
|
+
```java
|
|
93
|
+
String url = result.getUrl();
|
|
94
|
+
String filename = result.getFilename(); // ✅ 小写 'n'
|
|
95
|
+
// ❌ result.getFileName() / result.getFileSize() / result.getContentType() 不存在
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 四、文件下载
|
|
101
|
+
|
|
102
|
+
```java
|
|
103
|
+
// 下载到临时文件
|
|
104
|
+
Path tempFile = client.fileDownload("images/photo.jpg");
|
|
105
|
+
|
|
106
|
+
// 下载到输出流(推荐用于HTTP响应)
|
|
107
|
+
client.download("images/photo.jpg", out, contentLength -> {
|
|
108
|
+
response.setContentLengthLong(contentLength);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// 获取文件输入流(内部创建临时文件,使用后自动删除)
|
|
112
|
+
InputStream is = client.getObjectContent("images/photo.jpg");
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 五、文件删除与预签名URL
|
|
118
|
+
|
|
119
|
+
```java
|
|
120
|
+
// 删除
|
|
121
|
+
client.delete("images/photo.jpg");
|
|
122
|
+
// ❌ client.copyFile() / client.getFileMetadata() / client.listFiles() 不存在
|
|
123
|
+
|
|
124
|
+
// 下载预签名URL
|
|
125
|
+
String url = client.createPresignedGetUrl("images/photo.jpg", Duration.ofMinutes(60));
|
|
126
|
+
|
|
127
|
+
// 上传预签名URL(前端直传)
|
|
128
|
+
String url = client.createPresignedPutUrl("images/upload.jpg", Duration.ofHours(1), metadata);
|
|
129
|
+
// ❌ client.generatePresignedUrl() / client.generatePublicUrl() 不存在
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 六、OssClient 工具方法
|
|
135
|
+
|
|
136
|
+
```java
|
|
137
|
+
String baseUrl = client.getUrl(); // 基础URL
|
|
138
|
+
String endpoint = client.getEndpoint(); // 终端点URL
|
|
139
|
+
String domain = client.getDomain(); // 自定义域名
|
|
140
|
+
String configKey = client.getConfigKey(); // 配置键
|
|
141
|
+
AccessPolicyType policy = client.getAccessPolicy(); // 桶权限(PUBLIC/PRIVATE)
|
|
142
|
+
String path = client.getPath("", ".jpg"); // 生成对象键路径
|
|
143
|
+
String relative = client.removeBaseUrl(fullUrl); // 获取相对路径
|
|
144
|
+
boolean same = client.checkPropertiesSame(props); // 配置是否相同
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## 七、Controller 接口(SysOssController)
|
|
150
|
+
|
|
151
|
+
| 操作 | HTTP | 路径 | 权限 |
|
|
152
|
+
|------|------|------|------|
|
|
153
|
+
| 查询列表 | GET | `/resource/oss/list` | `system:oss:list` |
|
|
154
|
+
| 批量查询 | GET | `/resource/oss/listByIds/{ossIds}` | `system:oss:query` |
|
|
155
|
+
| 上传文件 | POST | `/resource/oss/upload` | `system:oss:upload` |
|
|
156
|
+
| 下载文件 | GET | `/resource/oss/download/{ossId}` | `system:oss:download` |
|
|
157
|
+
| 删除文件 | DELETE | `/resource/oss/{ossIds}` | `system:oss:remove` |
|
|
158
|
+
|
|
159
|
+
**上传接口规范:**
|
|
160
|
+
```java
|
|
161
|
+
// 必须使用 @RequestPart("file"),必须指定 consumes
|
|
162
|
+
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
|
163
|
+
public R<SysOssUploadVo> upload(@RequestPart("file") MultipartFile file) {
|
|
164
|
+
SysOssVo oss = ossService.upload(file);
|
|
165
|
+
SysOssUploadVo uploadVo = new SysOssUploadVo();
|
|
166
|
+
uploadVo.setUrl(oss.getUrl());
|
|
167
|
+
uploadVo.setFileName(oss.getOriginalName());
|
|
168
|
+
uploadVo.setOssId(oss.getOssId().toString());
|
|
169
|
+
return R.ok(uploadVo);
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**SysOssUploadVo**:`url`(String) / `fileName`(String) / `ossId`(String)
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## 八、Service 层接口(ISysOssService)
|
|
178
|
+
|
|
179
|
+
```java
|
|
180
|
+
TableDataInfo<SysOssVo> queryPageList(SysOssBo sysOss, PageQuery pageQuery);
|
|
181
|
+
List<SysOssVo> listByIds(Collection<Long> ossIds);
|
|
182
|
+
@Cacheable(cacheNames = CacheNames.SYS_OSS, key = "#ossId")
|
|
183
|
+
SysOssVo getById(Long ossId);
|
|
184
|
+
SysOssVo upload(MultipartFile file);
|
|
185
|
+
SysOssVo upload(File file);
|
|
186
|
+
void download(Long ossId, HttpServletResponse response) throws IOException;
|
|
187
|
+
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
> 推荐通过 `ISysOssService.upload()` 上传,会自动保存数据库记录。
|
|
191
|
+
> 直接使用 `OssClient` 上传不会有数据库记录。
|
|
192
|
+
|
|
193
|
+
> 完整 Service 实现代码详见 [references/service-impl.md](references/service-impl.md)
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## 九、数据库实体
|
|
198
|
+
|
|
199
|
+
> 完整实体/VO/BO 定义详见 [references/entities.md](references/entities.md)
|
|
200
|
+
|
|
201
|
+
**SysOss 关键字段:**
|
|
202
|
+
|
|
203
|
+
| 字段 | 说明 |
|
|
204
|
+
|------|------|
|
|
205
|
+
| `ossId` | 主键 |
|
|
206
|
+
| `fileName` | OSS对象键 |
|
|
207
|
+
| `originalName` | 原始文件名 |
|
|
208
|
+
| `fileSuffix` | 后缀名 |
|
|
209
|
+
| `url` | 访问URL |
|
|
210
|
+
| `ext1` | 扩展字段(JSON,存储 SysOssExt) |
|
|
211
|
+
| `service` | 服务商标识 |
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## 十、配置(sys_oss_config 表)
|
|
216
|
+
|
|
217
|
+
| 字段 | 说明 |
|
|
218
|
+
|------|------|
|
|
219
|
+
| `config_key` | 配置标识(aliyun、minio等) |
|
|
220
|
+
| `access_key` / `secret_key` | 认证信息 |
|
|
221
|
+
| `bucket_name` | 存储桶 |
|
|
222
|
+
| `prefix` | 路径前缀 |
|
|
223
|
+
| `endpoint` | 服务端点 |
|
|
224
|
+
| `domain` | 自定义域名 |
|
|
225
|
+
| `is_https` | 是否HTTPS(Y/N) |
|
|
226
|
+
| `region` | 区域 |
|
|
227
|
+
| `access_policy` | 0-private, 1-public, 2-custom |
|
|
228
|
+
|
|
229
|
+
> 配置从 Redis 读取(`CacheNames.SYS_OSS_CONFIG`),私有桶文件自动生成 120 秒预签名URL。
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## 十一、快速对照表
|
|
234
|
+
|
|
235
|
+
| 错误 | 正确 |
|
|
236
|
+
|------|------|
|
|
237
|
+
| `OssFactory.instance(OssType.ALIYUN)` | `OssFactory.instance("aliyun")` |
|
|
238
|
+
| `result.getFileName()` | `result.getFilename()` |
|
|
239
|
+
| `result.getFileSize()` | 不存在 |
|
|
240
|
+
| `client.downloadToTempFile(path)` | `client.fileDownload(path)` |
|
|
241
|
+
| `client.generatePresignedUrl(...)` | `client.createPresignedGetUrl(...)` |
|
|
242
|
+
| `throw ServiceException.of("msg")` | `throw new ServiceException("msg")` |
|
|
243
|
+
| `client.copyFile/listFiles/getFileMetadata` | 不存在 |
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## 核心文件位置
|
|
248
|
+
|
|
249
|
+
| 类型 | 位置 |
|
|
250
|
+
|------|------|
|
|
251
|
+
| OssFactory | `ruoyi-common/ruoyi-common-oss/.../factory/OssFactory.java` |
|
|
252
|
+
| OssClient | `ruoyi-common/ruoyi-common-oss/.../core/OssClient.java` |
|
|
253
|
+
| UploadResult | `ruoyi-common/ruoyi-common-oss/.../entity/UploadResult.java` |
|
|
254
|
+
| SysOssController | `ruoyi-modules/ruoyi-system/.../controller/system/SysOssController.java` |
|
|
255
|
+
| SysOssServiceImpl | `ruoyi-modules/ruoyi-system/.../service/impl/SysOssServiceImpl.java` |
|