ai-engineering-init 1.6.0 → 1.8.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/.claude/agents/code-reviewer.md +3 -130
- package/.claude/hooks/skill-forced-eval.js +46 -60
- package/.claude/hooks/stop.js +24 -1
- package/.claude/settings.json +10 -1
- package/.claude/skills/api-development/SKILL.md +179 -130
- package/.claude/skills/architecture-design/SKILL.md +102 -212
- package/.claude/skills/backend-annotations/SKILL.md +166 -220
- package/.claude/skills/bug-detective/SKILL.md +225 -186
- package/.claude/skills/code-patterns/SKILL.md +127 -244
- package/.claude/skills/codex-code-review/SKILL.md +327 -0
- package/.claude/skills/collaborating-with-codex/SKILL.md +96 -113
- package/.claude/skills/crud-development/SKILL.md +226 -307
- package/.claude/skills/data-permission/SKILL.md +131 -202
- package/.claude/skills/database-ops/SKILL.md +158 -355
- package/.claude/skills/error-handler/SKILL.md +224 -285
- package/.claude/skills/file-oss-management/SKILL.md +174 -169
- package/.claude/skills/git-workflow/SKILL.md +123 -341
- package/.claude/skills/json-serialization/SKILL.md +121 -137
- package/.claude/skills/leniu-report-customization/SKILL.md +82 -2
- package/.claude/skills/leniu-report-standard-customization/SKILL.md +65 -2
- package/.claude/skills/loki-log-query/SKILL.md +400 -0
- package/.claude/skills/mysql-debug/SKILL.md +58 -22
- package/.claude/skills/performance-doctor/SKILL.md +83 -89
- package/.claude/skills/redis-cache/SKILL.md +134 -185
- package/.claude/skills/scheduled-jobs/SKILL.md +187 -224
- package/.claude/skills/security-guard/SKILL.md +168 -276
- package/.claude/skills/sms-mail/SKILL.md +266 -228
- package/.claude/skills/social-login/SKILL.md +257 -195
- package/.claude/skills/sync-back-merge/SKILL.md +66 -0
- package/.claude/skills/tenant-management/SKILL.md +172 -188
- package/.claude/skills/utils-toolkit/SKILL.md +214 -222
- package/.claude/skills/websocket-sse/SKILL.md +251 -172
- package/.claude/skills/workflow-engine/SKILL.md +178 -250
- package/.claude/skills/yunxiao-task-management/SKILL.md +489 -0
- package/.codex/skills/api-development/SKILL.md +179 -130
- package/.codex/skills/architecture-design/SKILL.md +102 -212
- package/.codex/skills/backend-annotations/SKILL.md +166 -220
- package/.codex/skills/bug-detective/SKILL.md +225 -186
- package/.codex/skills/code-patterns/SKILL.md +127 -244
- package/.codex/skills/collaborating-with-codex/SKILL.md +96 -113
- package/.codex/skills/crud-development/SKILL.md +226 -307
- package/.codex/skills/data-permission/SKILL.md +131 -202
- package/.codex/skills/database-ops/SKILL.md +158 -355
- package/.codex/skills/error-handler/SKILL.md +224 -285
- package/.codex/skills/file-oss-management/SKILL.md +174 -169
- package/.codex/skills/git-workflow/SKILL.md +123 -341
- package/.codex/skills/json-serialization/SKILL.md +121 -137
- package/.codex/skills/leniu-report-customization/SKILL.md +82 -2
- package/.codex/skills/leniu-report-standard-customization/SKILL.md +65 -2
- package/.codex/skills/loki-log-query/SKILL.md +400 -0
- package/.codex/skills/loki-log-query/environments.json +45 -0
- package/.codex/skills/mysql-debug/SKILL.md +58 -22
- package/.codex/skills/performance-doctor/SKILL.md +83 -89
- package/.codex/skills/redis-cache/SKILL.md +134 -185
- package/.codex/skills/scheduled-jobs/SKILL.md +187 -224
- package/.codex/skills/security-guard/SKILL.md +168 -276
- package/.codex/skills/skill-creator/LICENSE.txt +202 -0
- package/.codex/skills/skill-creator/SKILL.md +479 -0
- package/.codex/skills/skill-creator/agents/analyzer.md +274 -0
- package/.codex/skills/skill-creator/agents/comparator.md +202 -0
- package/.codex/skills/skill-creator/agents/grader.md +223 -0
- package/.codex/skills/skill-creator/assets/eval_review.html +146 -0
- package/.codex/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/.codex/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/.codex/skills/skill-creator/references/schemas.md +430 -0
- package/.codex/skills/skill-creator/scripts/__init__.py +0 -0
- package/.codex/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/.codex/skills/skill-creator/scripts/generate_report.py +326 -0
- package/.codex/skills/skill-creator/scripts/improve_description.py +248 -0
- package/.codex/skills/skill-creator/scripts/package_skill.py +136 -0
- package/.codex/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/.codex/skills/skill-creator/scripts/run_eval.py +310 -0
- package/.codex/skills/skill-creator/scripts/run_loop.py +332 -0
- package/.codex/skills/skill-creator/scripts/utils.py +47 -0
- package/.codex/skills/sms-mail/SKILL.md +266 -228
- package/.codex/skills/social-login/SKILL.md +257 -195
- package/.codex/skills/sync-back-merge/SKILL.md +66 -0
- package/.codex/skills/tenant-management/SKILL.md +172 -188
- package/.codex/skills/utils-toolkit/SKILL.md +214 -222
- package/.codex/skills/websocket-sse/SKILL.md +251 -172
- package/.codex/skills/workflow-engine/SKILL.md +178 -250
- package/.codex/skills/yunxiao-task-management/SKILL.md +489 -0
- package/.cursor/hooks/cursor-skill-eval.js +66 -6
- package/.cursor/hooks/stop.js +23 -1
- package/.cursor/skills/api-development/SKILL.md +179 -130
- package/.cursor/skills/architecture-design/SKILL.md +102 -212
- package/.cursor/skills/backend-annotations/SKILL.md +166 -220
- package/.cursor/skills/bug-detective/SKILL.md +225 -186
- package/.cursor/skills/code-patterns/SKILL.md +127 -244
- package/.cursor/skills/collaborating-with-codex/SKILL.md +96 -113
- package/.cursor/skills/crud-development/SKILL.md +226 -307
- package/.cursor/skills/data-permission/SKILL.md +131 -202
- package/.cursor/skills/database-ops/SKILL.md +158 -355
- package/.cursor/skills/error-handler/SKILL.md +224 -285
- package/.cursor/skills/file-oss-management/SKILL.md +174 -169
- package/.cursor/skills/git-workflow/SKILL.md +123 -341
- package/.cursor/skills/json-serialization/SKILL.md +121 -137
- package/.cursor/skills/leniu-report-customization/SKILL.md +82 -2
- package/.cursor/skills/leniu-report-standard-customization/SKILL.md +65 -2
- package/.cursor/skills/loki-log-query/SKILL.md +400 -0
- package/.cursor/skills/loki-log-query/environments.json +45 -0
- package/.cursor/skills/mysql-debug/SKILL.md +58 -22
- package/.cursor/skills/performance-doctor/SKILL.md +83 -89
- package/.cursor/skills/redis-cache/SKILL.md +134 -185
- package/.cursor/skills/scheduled-jobs/SKILL.md +187 -224
- package/.cursor/skills/security-guard/SKILL.md +168 -276
- package/.cursor/skills/skill-creator/LICENSE.txt +202 -0
- package/.cursor/skills/skill-creator/SKILL.md +479 -0
- package/.cursor/skills/skill-creator/agents/analyzer.md +274 -0
- package/.cursor/skills/skill-creator/agents/comparator.md +202 -0
- package/.cursor/skills/skill-creator/agents/grader.md +223 -0
- package/.cursor/skills/skill-creator/assets/eval_review.html +146 -0
- package/.cursor/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/.cursor/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/.cursor/skills/skill-creator/references/schemas.md +430 -0
- package/.cursor/skills/skill-creator/scripts/__init__.py +0 -0
- package/.cursor/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/.cursor/skills/skill-creator/scripts/generate_report.py +326 -0
- package/.cursor/skills/skill-creator/scripts/improve_description.py +248 -0
- package/.cursor/skills/skill-creator/scripts/package_skill.py +136 -0
- package/.cursor/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/.cursor/skills/skill-creator/scripts/run_eval.py +310 -0
- package/.cursor/skills/skill-creator/scripts/run_loop.py +332 -0
- package/.cursor/skills/skill-creator/scripts/utils.py +47 -0
- package/.cursor/skills/sms-mail/SKILL.md +266 -228
- package/.cursor/skills/social-login/SKILL.md +257 -195
- package/.cursor/skills/sync-back-merge/SKILL.md +66 -0
- package/.cursor/skills/tenant-management/SKILL.md +172 -188
- package/.cursor/skills/utils-toolkit/SKILL.md +214 -222
- package/.cursor/skills/websocket-sse/SKILL.md +251 -172
- package/.cursor/skills/workflow-engine/SKILL.md +178 -250
- package/.cursor/skills/yunxiao-task-management/SKILL.md +489 -0
- package/AGENTS.md +49 -540
- package/CLAUDE.md +73 -119
- package/README.md +37 -6
- package/bin/index.js +611 -25
- package/package.json +1 -1
- package/src/platform-map.json +4 -0
- package/src/skills/api-development/SKILL.md +179 -130
- package/src/skills/architecture-design/SKILL.md +102 -212
- package/src/skills/backend-annotations/SKILL.md +166 -220
- package/src/skills/bug-detective/SKILL.md +225 -186
- package/src/skills/code-patterns/SKILL.md +127 -244
- package/src/skills/codex-code-review/SKILL.md +261 -69
- package/src/skills/collaborating-with-codex/SKILL.md +96 -113
- package/src/skills/crud-development/SKILL.md +226 -307
- package/src/skills/data-permission/SKILL.md +131 -202
- package/src/skills/database-ops/SKILL.md +158 -355
- package/src/skills/error-handler/SKILL.md +224 -285
- package/src/skills/file-oss-management/SKILL.md +174 -169
- package/src/skills/git-workflow/SKILL.md +123 -341
- package/src/skills/json-serialization/SKILL.md +121 -137
- package/src/skills/leniu-report-customization/SKILL.md +82 -2
- package/src/skills/leniu-report-standard-customization/SKILL.md +65 -2
- package/src/skills/loki-log-query/SKILL.md +400 -0
- package/src/skills/loki-log-query/environments.json +45 -0
- package/src/skills/mysql-debug/SKILL.md +58 -22
- package/src/skills/performance-doctor/SKILL.md +83 -89
- package/src/skills/redis-cache/SKILL.md +134 -185
- package/src/skills/scheduled-jobs/SKILL.md +187 -224
- package/src/skills/security-guard/SKILL.md +168 -276
- package/src/skills/skill-creator/LICENSE.txt +202 -0
- package/src/skills/skill-creator/SKILL.md +479 -0
- package/src/skills/skill-creator/agents/analyzer.md +274 -0
- package/src/skills/skill-creator/agents/comparator.md +202 -0
- package/src/skills/skill-creator/agents/grader.md +223 -0
- package/src/skills/skill-creator/assets/eval_review.html +146 -0
- package/src/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/src/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/src/skills/skill-creator/references/schemas.md +430 -0
- package/src/skills/skill-creator/scripts/__init__.py +0 -0
- package/src/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/src/skills/skill-creator/scripts/generate_report.py +326 -0
- package/src/skills/skill-creator/scripts/improve_description.py +248 -0
- package/src/skills/skill-creator/scripts/package_skill.py +136 -0
- package/src/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/src/skills/skill-creator/scripts/run_eval.py +310 -0
- package/src/skills/skill-creator/scripts/run_loop.py +332 -0
- package/src/skills/skill-creator/scripts/utils.py +47 -0
- package/src/skills/sms-mail/SKILL.md +266 -228
- package/src/skills/social-login/SKILL.md +257 -195
- package/src/skills/sync-back-merge/SKILL.md +66 -0
- package/src/skills/tenant-management/SKILL.md +172 -188
- package/src/skills/utils-toolkit/SKILL.md +214 -222
- package/src/skills/websocket-sse/SKILL.md +251 -172
- package/src/skills/workflow-engine/SKILL.md +178 -250
- package/src/skills/yunxiao-task-management/SKILL.md +489 -0
|
@@ -1,239 +1,300 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sms-mail
|
|
3
3
|
description: |
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
通用短信邮件开发指南。涵盖抽象接口模式、多渠道切换策略、验证码流程、模板消息。
|
|
6
5
|
触发场景:
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
- 发送短信验证码
|
|
7
|
+
- 发送邮件通知(文本/HTML/附件)
|
|
8
|
+
- 配置短信服务商(阿里云/腾讯云等)
|
|
9
|
+
- 配置邮件服务器(SMTP)
|
|
10
|
+
- 实现多渠道消息通知
|
|
11
|
+
触发词:短信、邮件、SMS、验证码、通知、邮件发送、短信发送、SMTP、模板消息、多渠道
|
|
12
|
+
注意:如果项目有专属技能,优先使用专属版本。
|
|
14
13
|
---
|
|
15
14
|
|
|
16
15
|
# 短信与邮件开发指南
|
|
17
16
|
|
|
18
|
-
>
|
|
19
|
-
> 邮件模块:`ruoyi-common/ruoyi-common-mail`(Hutool JakartaMail)
|
|
17
|
+
> 通用模板。如果项目有专属技能,优先使用。
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
## 设计原则
|
|
22
20
|
|
|
23
|
-
|
|
21
|
+
1. **接口抽象**:定义统一的发送接口,具体渠道通过策略模式实现,方便切换供应商。
|
|
22
|
+
2. **异步发送**:发送操作应异步执行,不阻塞主业务流程。
|
|
23
|
+
3. **结果校验**:每次发送必须检查返回结果,失败需记录日志并决策是否重试。
|
|
24
|
+
4. **安全防护**:验证码需限频限量、设置过期时间,防止短信轰炸和暴力破解。
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
```yaml
|
|
28
|
-
sms:
|
|
29
|
-
config-type: yaml # yaml / 接口 / 数据库
|
|
30
|
-
blends:
|
|
31
|
-
config1:
|
|
32
|
-
supplier: alibaba
|
|
33
|
-
access-key-id: ${ALIYUN_SMS_ACCESS_KEY_ID}
|
|
34
|
-
access-key-secret: ${ALIYUN_SMS_ACCESS_KEY_SECRET}
|
|
35
|
-
signature: 若依框架
|
|
36
|
-
config2:
|
|
37
|
-
supplier: tencent
|
|
38
|
-
access-key-id: ${TENCENT_SMS_SECRET_ID}
|
|
39
|
-
access-key-secret: ${TENCENT_SMS_SECRET_KEY}
|
|
40
|
-
signature: 若依框架
|
|
41
|
-
sdk-app-id: "1400000000" # 腾讯云需要
|
|
42
|
-
```
|
|
26
|
+
---
|
|
43
27
|
|
|
44
|
-
|
|
45
|
-
> 限流配置详见 [references/sms-config.md](references/sms-config.md)
|
|
28
|
+
## 实现模式
|
|
46
29
|
|
|
47
|
-
###
|
|
30
|
+
### 一、抽象接口设计
|
|
48
31
|
|
|
49
32
|
```java
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// 获取短信实例
|
|
55
|
-
SmsBlend smsBlend = SmsFactory.getSmsBlend(); // 默认配置
|
|
56
|
-
SmsBlend smsBlend = SmsFactory.getSmsBlend("config1"); // 指定配置
|
|
57
|
-
|
|
58
|
-
// 发送模板短信
|
|
59
|
-
Map<String, String> params = new LinkedHashMap<>();
|
|
60
|
-
params.put("code", "123456");
|
|
61
|
-
params.put("time", "5");
|
|
62
|
-
SmsResponse response = smsBlend.sendMessage("13800138000", "SMS_123456789", params);
|
|
63
|
-
|
|
64
|
-
// 批量发送
|
|
65
|
-
List<String> phones = Arrays.asList("13800138001", "13800138002");
|
|
66
|
-
SmsResponse response = smsBlend.sendMessage(phones, "SMS_123456789", params);
|
|
67
|
-
|
|
68
|
-
// 检查结果
|
|
69
|
-
if (response.isSuccess()) {
|
|
70
|
-
log.info("发送成功,msgId: {}", response.getBizId());
|
|
71
|
-
} else {
|
|
72
|
-
log.error("发送失败:{}", response.getMessage());
|
|
33
|
+
// 统一短信发送接口
|
|
34
|
+
public interface SmsSender {
|
|
35
|
+
SmsResult send(String phone, String templateId, Map<String, String> params);
|
|
36
|
+
SmsResult batchSend(List<String> phones, String templateId, Map<String, String> params);
|
|
73
37
|
}
|
|
74
|
-
```
|
|
75
38
|
|
|
76
|
-
|
|
39
|
+
// 统一邮件发送接口
|
|
40
|
+
public interface MailSender {
|
|
41
|
+
String sendText(String to, String subject, String content);
|
|
42
|
+
String sendHtml(String to, String subject, String htmlContent);
|
|
43
|
+
String sendWithAttachment(String to, String subject, String content, File... files);
|
|
44
|
+
}
|
|
77
45
|
|
|
78
|
-
|
|
79
|
-
@
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
46
|
+
// 发送结果
|
|
47
|
+
@Data
|
|
48
|
+
public class SmsResult {
|
|
49
|
+
private boolean success;
|
|
50
|
+
private String messageId;
|
|
51
|
+
private String errorMessage;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
83
54
|
|
|
84
|
-
|
|
85
|
-
public R<Void> sendSmsCode(@RequestParam String phonenumber) {
|
|
86
|
-
String code = RandomUtil.randomNumbers(6);
|
|
55
|
+
### 二、短信服务
|
|
87
56
|
|
|
88
|
-
|
|
89
|
-
RedisUtils.setCacheObject("sms:code:" + phonenumber, code, Duration.ofMinutes(5));
|
|
57
|
+
#### 多渠道策略模式
|
|
90
58
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
59
|
+
```java
|
|
60
|
+
// 阿里云实现
|
|
61
|
+
@Service("aliyunSmsSender")
|
|
62
|
+
public class AliyunSmsSender implements SmsSender {
|
|
63
|
+
@Override
|
|
64
|
+
public SmsResult send(String phone, String templateId, Map<String, String> params) {
|
|
65
|
+
// 调用阿里云 SMS SDK
|
|
66
|
+
}
|
|
67
|
+
}
|
|
96
68
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
69
|
+
// 腾讯云实现
|
|
70
|
+
@Service("tencentSmsSender")
|
|
71
|
+
public class TencentSmsSender implements SmsSender {
|
|
72
|
+
@Override
|
|
73
|
+
public SmsResult send(String phone, String templateId, Map<String, String> params) {
|
|
74
|
+
// 调用腾讯云 SMS SDK
|
|
102
75
|
}
|
|
76
|
+
}
|
|
103
77
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
78
|
+
// 工厂/路由
|
|
79
|
+
@Service
|
|
80
|
+
public class SmsService {
|
|
81
|
+
|
|
82
|
+
@Autowired
|
|
83
|
+
private Map<String, SmsSender> senderMap;
|
|
109
84
|
|
|
110
|
-
|
|
111
|
-
|
|
85
|
+
@Value("${sms.default-channel:aliyunSmsSender}")
|
|
86
|
+
private String defaultChannel;
|
|
112
87
|
|
|
113
|
-
|
|
114
|
-
|
|
88
|
+
public SmsResult send(String phone, String templateId, Map<String, String> params) {
|
|
89
|
+
SmsSender sender = senderMap.get(defaultChannel);
|
|
90
|
+
if (sender == null) {
|
|
91
|
+
throw new [你的异常类]("短信渠道未配置: " + defaultChannel);
|
|
92
|
+
}
|
|
93
|
+
SmsResult result = sender.send(phone, templateId, params);
|
|
94
|
+
if (!result.isSuccess()) {
|
|
95
|
+
log.error("短信发送失败: phone={}, error={}", phone, result.getErrorMessage());
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
115
98
|
}
|
|
116
99
|
}
|
|
117
100
|
```
|
|
118
101
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
## 二、邮件开发(Hutool Mail)
|
|
122
|
-
|
|
123
|
-
### 2.1 配置
|
|
102
|
+
#### 配置示例
|
|
124
103
|
|
|
125
104
|
```yaml
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
connectionTimeout: 10000
|
|
105
|
+
sms:
|
|
106
|
+
default-channel: aliyunSmsSender
|
|
107
|
+
aliyun:
|
|
108
|
+
access-key-id: ${ALIYUN_SMS_KEY:}
|
|
109
|
+
access-key-secret: ${ALIYUN_SMS_SECRET:}
|
|
110
|
+
sign-name: [你的签名]
|
|
111
|
+
tencent:
|
|
112
|
+
secret-id: ${TENCENT_SMS_ID:}
|
|
113
|
+
secret-key: ${TENCENT_SMS_KEY:}
|
|
114
|
+
sdk-app-id: "1400000000"
|
|
115
|
+
sign-name: [你的签名]
|
|
138
116
|
```
|
|
139
117
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
### 2.2 核心 API
|
|
118
|
+
#### 验证码短信完整示例
|
|
143
119
|
|
|
144
120
|
```java
|
|
145
|
-
|
|
121
|
+
@RestController
|
|
122
|
+
@RequestMapping("/captcha")
|
|
123
|
+
public class CaptchaController {
|
|
146
124
|
|
|
147
|
-
|
|
148
|
-
|
|
125
|
+
@Autowired
|
|
126
|
+
private SmsService smsService;
|
|
149
127
|
|
|
150
|
-
|
|
151
|
-
|
|
128
|
+
@Autowired
|
|
129
|
+
private StringRedisTemplate redisTemplate;
|
|
152
130
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
131
|
+
@GetMapping("/sms")
|
|
132
|
+
public Result<?> sendSmsCode(@RequestParam String phone) {
|
|
133
|
+
// 1. 限频检查(60秒内只能发一次)
|
|
134
|
+
String limitKey = "sms:limit:" + phone;
|
|
135
|
+
if (Boolean.TRUE.equals(redisTemplate.hasKey(limitKey))) {
|
|
136
|
+
return Result.fail("请60秒后重试");
|
|
137
|
+
}
|
|
156
138
|
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
String msgId = MailUtils.sendHtml("to@example.com", "标题",
|
|
160
|
-
"<img src='cid:logo'/>", imageMap);
|
|
139
|
+
// 2. 生成验证码
|
|
140
|
+
String code = String.valueOf(ThreadLocalRandom.current().nextInt(100000, 999999));
|
|
161
141
|
|
|
162
|
-
//
|
|
163
|
-
String
|
|
142
|
+
// 3. 存入 Redis(5分钟有效)
|
|
143
|
+
String codeKey = "sms:code:" + phone;
|
|
144
|
+
redisTemplate.opsForValue().set(codeKey, code, 5, TimeUnit.MINUTES);
|
|
145
|
+
redisTemplate.opsForValue().set(limitKey, "1", 60, TimeUnit.SECONDS);
|
|
164
146
|
|
|
165
|
-
//
|
|
166
|
-
String
|
|
147
|
+
// 4. 发送短信
|
|
148
|
+
Map<String, String> params = new LinkedHashMap<>();
|
|
149
|
+
params.put("code", code);
|
|
150
|
+
SmsResult result = smsService.send(phone, "SMS_VERIFY_CODE", params);
|
|
167
151
|
|
|
168
|
-
|
|
169
|
-
|
|
152
|
+
if (!result.isSuccess()) {
|
|
153
|
+
redisTemplate.delete(codeKey);
|
|
154
|
+
return Result.fail("短信发送失败");
|
|
155
|
+
}
|
|
156
|
+
return Result.ok("验证码已发送");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@PostMapping("/verify")
|
|
160
|
+
public Result<Boolean> verify(@RequestParam String phone, @RequestParam String code) {
|
|
161
|
+
String codeKey = "sms:code:" + phone;
|
|
162
|
+
String cached = redisTemplate.opsForValue().get(codeKey);
|
|
163
|
+
|
|
164
|
+
if (cached == null) return Result.fail("验证码已过期");
|
|
165
|
+
if (!cached.equals(code)) return Result.fail("验证码错误");
|
|
166
|
+
|
|
167
|
+
redisTemplate.delete(codeKey);
|
|
168
|
+
return Result.ok(true);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
170
171
|
```
|
|
171
172
|
|
|
172
|
-
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
### 三、邮件服务
|
|
176
|
+
|
|
177
|
+
#### Spring Boot 原生邮件
|
|
173
178
|
|
|
174
179
|
```java
|
|
175
180
|
@Service
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
MailUtils.sendHtml(email, "【若依系统】验证码", htmlContent);
|
|
193
|
-
} catch (Exception e) {
|
|
194
|
-
log.error("邮件发送失败:{}", e.getMessage());
|
|
195
|
-
throw new ServiceException("邮件发送失败");
|
|
196
|
-
}
|
|
181
|
+
public class MailService {
|
|
182
|
+
|
|
183
|
+
@Autowired
|
|
184
|
+
private JavaMailSender mailSender;
|
|
185
|
+
|
|
186
|
+
@Value("${spring.mail.username}")
|
|
187
|
+
private String from;
|
|
188
|
+
|
|
189
|
+
// 文本邮件
|
|
190
|
+
public void sendText(String to, String subject, String content) {
|
|
191
|
+
SimpleMailMessage message = new SimpleMailMessage();
|
|
192
|
+
message.setFrom(from);
|
|
193
|
+
message.setTo(to);
|
|
194
|
+
message.setSubject(subject);
|
|
195
|
+
message.setText(content);
|
|
196
|
+
mailSender.send(message);
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
199
|
+
// HTML 邮件
|
|
200
|
+
public void sendHtml(String to, String subject, String htmlContent) throws MessagingException {
|
|
201
|
+
MimeMessage message = mailSender.createMimeMessage();
|
|
202
|
+
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
|
203
|
+
helper.setFrom(from);
|
|
204
|
+
helper.setTo(to);
|
|
205
|
+
helper.setSubject(subject);
|
|
206
|
+
helper.setText(htmlContent, true);
|
|
207
|
+
mailSender.send(message);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 带附件邮件
|
|
211
|
+
public void sendWithAttachment(String to, String subject, String content,
|
|
212
|
+
File... attachments) throws MessagingException {
|
|
213
|
+
MimeMessage message = mailSender.createMimeMessage();
|
|
214
|
+
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
|
215
|
+
helper.setFrom(from);
|
|
216
|
+
helper.setTo(to);
|
|
217
|
+
helper.setSubject(subject);
|
|
218
|
+
helper.setText(content, true);
|
|
219
|
+
for (File file : attachments) {
|
|
220
|
+
helper.addAttachment(file.getName(), file);
|
|
205
221
|
}
|
|
206
|
-
|
|
222
|
+
mailSender.send(message);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 群发
|
|
226
|
+
public void sendHtml(List<String> toList, String subject, String htmlContent)
|
|
227
|
+
throws MessagingException {
|
|
228
|
+
MimeMessage message = mailSender.createMimeMessage();
|
|
229
|
+
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
|
230
|
+
helper.setFrom(from);
|
|
231
|
+
helper.setTo(toList.toArray(new String[0]));
|
|
232
|
+
helper.setSubject(subject);
|
|
233
|
+
helper.setText(htmlContent, true);
|
|
234
|
+
mailSender.send(message);
|
|
207
235
|
}
|
|
208
236
|
}
|
|
209
237
|
```
|
|
210
238
|
|
|
239
|
+
#### 配置示例
|
|
240
|
+
|
|
241
|
+
```yaml
|
|
242
|
+
spring:
|
|
243
|
+
mail:
|
|
244
|
+
host: smtp.163.com
|
|
245
|
+
port: 465
|
|
246
|
+
username: system@example.com
|
|
247
|
+
password: ${MAIL_PASSWORD} # 授权码,非登录密码
|
|
248
|
+
properties:
|
|
249
|
+
mail:
|
|
250
|
+
smtp:
|
|
251
|
+
auth: true
|
|
252
|
+
ssl:
|
|
253
|
+
enable: true
|
|
254
|
+
timeout: 10000
|
|
255
|
+
connectiontimeout: 10000
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
常用 SMTP 服务器:
|
|
259
|
+
|
|
260
|
+
| 提供商 | Host | 端口(SSL) |
|
|
261
|
+
|--------|------|----------|
|
|
262
|
+
| 163 | smtp.163.com | 465 |
|
|
263
|
+
| QQ | smtp.qq.com | 465 |
|
|
264
|
+
| Gmail | smtp.gmail.com | 465 |
|
|
265
|
+
| 阿里企业邮箱 | smtp.qiye.aliyun.com | 465 |
|
|
266
|
+
|
|
211
267
|
---
|
|
212
268
|
|
|
213
|
-
|
|
269
|
+
### 四、多渠道消息通知
|
|
214
270
|
|
|
215
271
|
```java
|
|
216
272
|
@Service
|
|
217
|
-
@RequiredArgsConstructor
|
|
218
273
|
public class MessageService {
|
|
219
274
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
275
|
+
@Autowired
|
|
276
|
+
private SmsService smsService;
|
|
277
|
+
|
|
278
|
+
@Autowired
|
|
279
|
+
private MailService mailService;
|
|
280
|
+
|
|
281
|
+
// 可扩展:站内信、推送、企业微信等
|
|
282
|
+
public void sendNotification(List<String> channels, String subject,
|
|
283
|
+
String content, List<UserDTO> users) {
|
|
284
|
+
for (UserDTO user : users) {
|
|
223
285
|
try {
|
|
224
|
-
if (
|
|
225
|
-
Map<String, String> params =
|
|
226
|
-
|
|
227
|
-
SmsFactory.getSmsBlend("config1")
|
|
228
|
-
.sendMessage(user.getPhone(), "SMS_NOTIFY", params);
|
|
286
|
+
if (channels.contains("sms") && StringUtils.hasText(user.getPhone())) {
|
|
287
|
+
Map<String, String> params = Map.of("content", content);
|
|
288
|
+
smsService.send(user.getPhone(), "SMS_NOTIFY", params);
|
|
229
289
|
}
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
"<div style='padding:20px;'><h3>" + subject + "</h3><p>" +
|
|
290
|
+
if (channels.contains("email") && StringUtils.hasText(user.getEmail())) {
|
|
291
|
+
mailService.sendHtml(user.getEmail(), subject,
|
|
292
|
+
"<div style='padding:20px;'><h3>" + subject + "</h3><p>" + content + "</p></div>");
|
|
233
293
|
}
|
|
234
294
|
} catch (Exception e) {
|
|
235
|
-
log.error("
|
|
236
|
-
|
|
295
|
+
log.error("消息发送失败, channel={}, userId={}, error={}",
|
|
296
|
+
channels, user.getUserId(), e.getMessage());
|
|
297
|
+
// 不抛出,继续发送其他用户
|
|
237
298
|
}
|
|
238
299
|
}
|
|
239
300
|
}
|
|
@@ -242,67 +303,44 @@ public class MessageService {
|
|
|
242
303
|
|
|
243
304
|
---
|
|
244
305
|
|
|
245
|
-
##
|
|
306
|
+
## 选型建议
|
|
246
307
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
// ✅ params.put("code", "123456");
|
|
255
|
-
|
|
256
|
-
// ❌ 不检查发送结果
|
|
257
|
-
smsBlend.sendMessage(phone, templateId, params);
|
|
258
|
-
// ✅ SmsResponse response = smsBlend.sendMessage(...);
|
|
259
|
-
// if (!response.isSuccess()) throw new ServiceException("短信发送失败");
|
|
260
|
-
|
|
261
|
-
// ❌ 验证码无过期时间
|
|
262
|
-
RedisUtils.setCacheObject("sms:code:" + phone, code);
|
|
263
|
-
// ✅ RedisUtils.setCacheObject("sms:code:" + phone, code, Duration.ofMinutes(5));
|
|
264
|
-
```
|
|
308
|
+
| 维度 | 自研抽象层 | SMS4j | 云 SDK 直接调用 |
|
|
309
|
+
|------|----------|-------|----------------|
|
|
310
|
+
| 灵活性 | 最高 | 高 | 中 |
|
|
311
|
+
| 开发成本 | 中 | 低 | 低 |
|
|
312
|
+
| 多渠道切换 | 需自行实现 | 内置支持 | 需改代码 |
|
|
313
|
+
| 维护成本 | 中 | 低(社区维护) | 低 |
|
|
314
|
+
| 适用场景 | 定制需求高 | 通用 | 单一渠道 |
|
|
265
315
|
|
|
266
316
|
---
|
|
267
317
|
|
|
268
|
-
##
|
|
269
|
-
|
|
270
|
-
### 短信 API(SMS4j)
|
|
271
|
-
|
|
272
|
-
| 方法 | 说明 |
|
|
273
|
-
|------|------|
|
|
274
|
-
| `SmsFactory.getSmsBlend()` | 获取默认短信实例 |
|
|
275
|
-
| `SmsFactory.getSmsBlend("config1")` | 获取指定配置短信实例 |
|
|
276
|
-
| `smsBlend.sendMessage(phone, templateId, params)` | 发送模板短信 |
|
|
277
|
-
| `smsBlend.sendMessage(phones, templateId, params)` | 批量发送 |
|
|
278
|
-
| `response.isSuccess()` | 是否成功 |
|
|
279
|
-
| `response.getMessage()` | 错误信息 |
|
|
280
|
-
| `response.getBizId()` | 消息ID |
|
|
281
|
-
|
|
282
|
-
### 邮件 API(MailUtils)
|
|
283
|
-
|
|
284
|
-
| 方法 | 说明 |
|
|
285
|
-
|------|------|
|
|
286
|
-
| `MailUtils.sendText(to, subject, content)` | 文本邮件 |
|
|
287
|
-
| `MailUtils.sendHtml(to, subject, content)` | HTML邮件 |
|
|
288
|
-
| `MailUtils.sendText(to, subject, content, files...)` | 带附件文本邮件 |
|
|
289
|
-
| `MailUtils.sendHtml(to, subject, content, files...)` | 带附件HTML邮件 |
|
|
290
|
-
| `MailUtils.sendHtml(to, subject, content, imageMap)` | 带内嵌图片邮件 |
|
|
291
|
-
| `MailUtils.send(to, cc, bcc, subject, content, isHtml)` | 抄送/密送 |
|
|
292
|
-
| `MailUtils.sendHtml(tos, subject, content)` | 群发HTML邮件 |
|
|
293
|
-
| `MailUtils.getMailAccount()` | 获取邮件配置 |
|
|
318
|
+
## 常见错误
|
|
294
319
|
|
|
295
|
-
|
|
320
|
+
```java
|
|
321
|
+
// 1. 未配置就使用,NPE
|
|
322
|
+
SmsSender sender = senderMap.get("xxx");
|
|
323
|
+
sender.send(phone, template, params); // sender 为 null -> NPE
|
|
324
|
+
// 应先判空
|
|
325
|
+
|
|
326
|
+
// 2. 模板参数名与模板定义不匹配
|
|
327
|
+
params.put("verifyCode", "123456"); // 模板中变量名是 code
|
|
328
|
+
// 应确认模板变量名
|
|
329
|
+
|
|
330
|
+
// 3. 不检查发送结果
|
|
331
|
+
smsService.send(phone, templateId, params); // 发送可能失败
|
|
332
|
+
// 应检查 SmsResult.isSuccess()
|
|
296
333
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
334
|
+
// 4. 验证码无过期时间
|
|
335
|
+
redisTemplate.opsForValue().set("sms:code:" + phone, code); // 永不过期
|
|
336
|
+
// 应设置 5-10 分钟过期
|
|
337
|
+
|
|
338
|
+
// 5. 无限频控制(短信轰炸)
|
|
339
|
+
// 应限制:同号码 60秒 1 次、每天最多 10 次
|
|
340
|
+
|
|
341
|
+
// 6. 邮件密码用登录密码而非授权码
|
|
342
|
+
// 大多数邮件服务商需要使用"授权码"而非"登录密码"
|
|
343
|
+
|
|
344
|
+
// 7. 同步发送阻塞主线程
|
|
345
|
+
// 短信/邮件发送应使用 @Async 异步执行
|
|
346
|
+
```
|