ai-engineering-init 1.7.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.
Files changed (118) hide show
  1. package/.claude/hooks/skill-forced-eval.js +46 -62
  2. package/.claude/settings.json +10 -1
  3. package/.claude/skills/api-development/SKILL.md +179 -130
  4. package/.claude/skills/architecture-design/SKILL.md +102 -212
  5. package/.claude/skills/backend-annotations/SKILL.md +166 -220
  6. package/.claude/skills/bug-detective/SKILL.md +225 -186
  7. package/.claude/skills/code-patterns/SKILL.md +127 -244
  8. package/.claude/skills/collaborating-with-codex/SKILL.md +96 -113
  9. package/.claude/skills/crud-development/SKILL.md +226 -307
  10. package/.claude/skills/data-permission/SKILL.md +131 -202
  11. package/.claude/skills/database-ops/SKILL.md +158 -355
  12. package/.claude/skills/error-handler/SKILL.md +224 -285
  13. package/.claude/skills/file-oss-management/SKILL.md +174 -169
  14. package/.claude/skills/git-workflow/SKILL.md +123 -341
  15. package/.claude/skills/json-serialization/SKILL.md +121 -137
  16. package/.claude/skills/performance-doctor/SKILL.md +83 -89
  17. package/.claude/skills/redis-cache/SKILL.md +134 -185
  18. package/.claude/skills/scheduled-jobs/SKILL.md +187 -224
  19. package/.claude/skills/security-guard/SKILL.md +168 -276
  20. package/.claude/skills/sms-mail/SKILL.md +266 -228
  21. package/.claude/skills/social-login/SKILL.md +257 -195
  22. package/.claude/skills/tenant-management/SKILL.md +172 -188
  23. package/.claude/skills/utils-toolkit/SKILL.md +214 -222
  24. package/.claude/skills/websocket-sse/SKILL.md +251 -172
  25. package/.claude/skills/workflow-engine/SKILL.md +178 -250
  26. package/.codex/skills/api-development/SKILL.md +179 -130
  27. package/.codex/skills/architecture-design/SKILL.md +102 -212
  28. package/.codex/skills/backend-annotations/SKILL.md +166 -220
  29. package/.codex/skills/bug-detective/SKILL.md +225 -186
  30. package/.codex/skills/code-patterns/SKILL.md +127 -244
  31. package/.codex/skills/collaborating-with-codex/SKILL.md +96 -113
  32. package/.codex/skills/crud-development/SKILL.md +226 -307
  33. package/.codex/skills/data-permission/SKILL.md +131 -202
  34. package/.codex/skills/database-ops/SKILL.md +158 -355
  35. package/.codex/skills/error-handler/SKILL.md +224 -285
  36. package/.codex/skills/file-oss-management/SKILL.md +174 -169
  37. package/.codex/skills/git-workflow/SKILL.md +123 -341
  38. package/.codex/skills/json-serialization/SKILL.md +121 -137
  39. package/.codex/skills/performance-doctor/SKILL.md +83 -89
  40. package/.codex/skills/redis-cache/SKILL.md +134 -185
  41. package/.codex/skills/scheduled-jobs/SKILL.md +187 -224
  42. package/.codex/skills/security-guard/SKILL.md +168 -276
  43. package/.codex/skills/sms-mail/SKILL.md +266 -228
  44. package/.codex/skills/social-login/SKILL.md +257 -195
  45. package/.codex/skills/tenant-management/SKILL.md +172 -188
  46. package/.codex/skills/utils-toolkit/SKILL.md +214 -222
  47. package/.codex/skills/websocket-sse/SKILL.md +251 -172
  48. package/.codex/skills/workflow-engine/SKILL.md +178 -250
  49. package/.cursor/hooks/cursor-skill-eval.js +66 -6
  50. package/.cursor/skills/api-development/SKILL.md +179 -130
  51. package/.cursor/skills/architecture-design/SKILL.md +102 -212
  52. package/.cursor/skills/backend-annotations/SKILL.md +166 -220
  53. package/.cursor/skills/bug-detective/SKILL.md +225 -186
  54. package/.cursor/skills/code-patterns/SKILL.md +127 -244
  55. package/.cursor/skills/collaborating-with-codex/SKILL.md +96 -113
  56. package/.cursor/skills/crud-development/SKILL.md +226 -307
  57. package/.cursor/skills/data-permission/SKILL.md +131 -202
  58. package/.cursor/skills/database-ops/SKILL.md +158 -355
  59. package/.cursor/skills/error-handler/SKILL.md +224 -285
  60. package/.cursor/skills/file-oss-management/SKILL.md +174 -169
  61. package/.cursor/skills/git-workflow/SKILL.md +123 -341
  62. package/.cursor/skills/json-serialization/SKILL.md +121 -137
  63. package/.cursor/skills/performance-doctor/SKILL.md +83 -89
  64. package/.cursor/skills/redis-cache/SKILL.md +134 -185
  65. package/.cursor/skills/scheduled-jobs/SKILL.md +187 -224
  66. package/.cursor/skills/security-guard/SKILL.md +168 -276
  67. package/.cursor/skills/sms-mail/SKILL.md +266 -228
  68. package/.cursor/skills/social-login/SKILL.md +257 -195
  69. package/.cursor/skills/tenant-management/SKILL.md +172 -188
  70. package/.cursor/skills/utils-toolkit/SKILL.md +214 -222
  71. package/.cursor/skills/websocket-sse/SKILL.md +251 -172
  72. package/.cursor/skills/workflow-engine/SKILL.md +178 -250
  73. package/AGENTS.md +49 -540
  74. package/CLAUDE.md +73 -119
  75. package/README.md +37 -6
  76. package/bin/index.js +5 -1
  77. package/package.json +1 -1
  78. package/src/skills/api-development/SKILL.md +179 -130
  79. package/src/skills/architecture-design/SKILL.md +102 -212
  80. package/src/skills/backend-annotations/SKILL.md +166 -220
  81. package/src/skills/bug-detective/SKILL.md +225 -186
  82. package/src/skills/code-patterns/SKILL.md +127 -244
  83. package/src/skills/collaborating-with-codex/SKILL.md +96 -113
  84. package/src/skills/crud-development/SKILL.md +226 -307
  85. package/src/skills/data-permission/SKILL.md +131 -202
  86. package/src/skills/database-ops/SKILL.md +158 -355
  87. package/src/skills/error-handler/SKILL.md +224 -285
  88. package/src/skills/file-oss-management/SKILL.md +174 -169
  89. package/src/skills/git-workflow/SKILL.md +123 -341
  90. package/src/skills/json-serialization/SKILL.md +121 -137
  91. package/src/skills/performance-doctor/SKILL.md +83 -89
  92. package/src/skills/redis-cache/SKILL.md +134 -185
  93. package/src/skills/scheduled-jobs/SKILL.md +187 -224
  94. package/src/skills/security-guard/SKILL.md +168 -276
  95. package/src/skills/sms-mail/SKILL.md +266 -228
  96. package/src/skills/social-login/SKILL.md +257 -195
  97. package/src/skills/tenant-management/SKILL.md +172 -188
  98. package/src/skills/utils-toolkit/SKILL.md +214 -222
  99. package/src/skills/websocket-sse/SKILL.md +251 -172
  100. package/src/skills/workflow-engine/SKILL.md +178 -250
  101. package/.claude/skills/skill-creator/LICENSE.txt +0 -202
  102. package/.claude/skills/skill-creator/SKILL.md +0 -479
  103. package/.claude/skills/skill-creator/agents/analyzer.md +0 -274
  104. package/.claude/skills/skill-creator/agents/comparator.md +0 -202
  105. package/.claude/skills/skill-creator/agents/grader.md +0 -223
  106. package/.claude/skills/skill-creator/assets/eval_review.html +0 -146
  107. package/.claude/skills/skill-creator/eval-viewer/generate_review.py +0 -471
  108. package/.claude/skills/skill-creator/eval-viewer/viewer.html +0 -1325
  109. package/.claude/skills/skill-creator/references/schemas.md +0 -430
  110. package/.claude/skills/skill-creator/scripts/__init__.py +0 -0
  111. package/.claude/skills/skill-creator/scripts/aggregate_benchmark.py +0 -401
  112. package/.claude/skills/skill-creator/scripts/generate_report.py +0 -326
  113. package/.claude/skills/skill-creator/scripts/improve_description.py +0 -248
  114. package/.claude/skills/skill-creator/scripts/package_skill.py +0 -136
  115. package/.claude/skills/skill-creator/scripts/quick_validate.py +0 -103
  116. package/.claude/skills/skill-creator/scripts/run_eval.py +0 -310
  117. package/.claude/skills/skill-creator/scripts/run_loop.py +0 -332
  118. package/.claude/skills/skill-creator/scripts/utils.py +0 -47
@@ -1,239 +1,300 @@
1
1
  ---
2
2
  name: sms-mail
3
3
  description: |
4
- 当需要发送短信、邮件通知时自动使用此 Skill。
5
-
4
+ 通用短信邮件开发指南。涵盖抽象接口模式、多渠道切换策略、验证码流程、模板消息。
6
5
  触发场景:
7
- - 需要发送短信验证码
8
- - 需要发送邮件通知(文本/HTML
9
- - 需要配置短信服务商(阿里云/腾讯云/华为云等)
10
- - 需要配置邮件服务器(SMTP)
11
- - 需要实现多渠道消息通知
12
-
13
- 触发词:短信、邮件、SMS、验证码、通知、SMS4j、MailUtils、邮件发送、短信发送、SmsBlend、SmsFactory、SMTP、模板消息
6
+ - 发送短信验证码
7
+ - 发送邮件通知(文本/HTML/附件)
8
+ - 配置短信服务商(阿里云/腾讯云等)
9
+ - 配置邮件服务器(SMTP)
10
+ - 实现多渠道消息通知
11
+ 触发词:短信、邮件、SMS、验证码、通知、邮件发送、短信发送、SMTP、模板消息、多渠道
12
+ 注意:如果项目有专属技能,优先使用专属版本。
14
13
  ---
15
14
 
16
15
  # 短信与邮件开发指南
17
16
 
18
- > 短信模块:`ruoyi-common/ruoyi-common-sms`(SMS4j)
19
- > 邮件模块:`ruoyi-common/ruoyi-common-mail`(Hutool JakartaMail)
17
+ > 通用模板。如果项目有专属技能,优先使用。
20
18
 
21
- ---
19
+ ## 设计原则
22
20
 
23
- ## 一、短信开发(SMS4j)
21
+ 1. **接口抽象**:定义统一的发送接口,具体渠道通过策略模式实现,方便切换供应商。
22
+ 2. **异步发送**:发送操作应异步执行,不阻塞主业务流程。
23
+ 3. **结果校验**:每次发送必须检查返回结果,失败需记录日志并决策是否重试。
24
+ 4. **安全防护**:验证码需限频限量、设置过期时间,防止短信轰炸和暴力破解。
24
25
 
25
- ### 1.1 配置
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
- > 支持的服务商:alibaba / tencent / huawei / yunpian / unisms / jdcloud / cloopen / emay / ctyun
45
- > 限流配置详见 [references/sms-config.md](references/sms-config.md)
28
+ ## 实现模式
46
29
 
47
- ### 1.2 核心 API
30
+ ### 一、抽象接口设计
48
31
 
49
32
  ```java
50
- import org.dromara.sms4j.api.SmsBlend;
51
- import org.dromara.sms4j.api.entity.SmsResponse;
52
- import org.dromara.sms4j.core.factory.SmsFactory;
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
- ### 1.3 验证码短信示例
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
- ```java
79
- @RestController
80
- @RequestMapping("/captcha")
81
- @RequiredArgsConstructor
82
- public class CaptchaController {
46
+ // 发送结果
47
+ @Data
48
+ public class SmsResult {
49
+ private boolean success;
50
+ private String messageId;
51
+ private String errorMessage;
52
+ }
53
+ ```
83
54
 
84
- @GetMapping("/sms")
85
- public R<Void> sendSmsCode(@RequestParam String phonenumber) {
86
- String code = RandomUtil.randomNumbers(6);
55
+ ### 二、短信服务
87
56
 
88
- // 存入Redis(5分钟有效)
89
- RedisUtils.setCacheObject("sms:code:" + phonenumber, code, Duration.ofMinutes(5));
57
+ #### 多渠道策略模式
90
58
 
91
- // 发送短信
92
- Map<String, String> map = new LinkedHashMap<>();
93
- map.put("code", code);
94
- SmsBlend smsBlend = SmsFactory.getSmsBlend("config1");
95
- SmsResponse smsResponse = smsBlend.sendMessage(phonenumber, "SMS_123456789", map);
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
- if (!smsResponse.isSuccess()) {
98
- log.error("短信发送失败:{}", smsResponse.getMessage());
99
- return R.fail("短信发送失败");
100
- }
101
- return R.ok("验证码已发送");
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
- @PostMapping("/verify")
105
- public R<Boolean> verifySmsCode(@RequestParam String phonenumber,
106
- @RequestParam String code) {
107
- String cacheKey = "sms:code:" + phonenumber;
108
- String cachedCode = RedisUtils.getCacheObject(cacheKey);
78
+ // 工厂/路由
79
+ @Service
80
+ public class SmsService {
81
+
82
+ @Autowired
83
+ private Map<String, SmsSender> senderMap;
109
84
 
110
- if (cachedCode == null) return R.fail("验证码已过期");
111
- if (!cachedCode.equals(code)) return R.fail("验证码错误");
85
+ @Value("${sms.default-channel:aliyunSmsSender}")
86
+ private String defaultChannel;
112
87
 
113
- RedisUtils.deleteObject(cacheKey);
114
- return R.ok(true);
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
- mail:
127
- enabled: true
128
- host: smtp.163.com
129
- port: 465
130
- auth: true
131
- from: system@example.com
132
- user: system@example.com
133
- pass: ${MAIL_PASSWORD} # 授权码,非登录密码
134
- sslEnable: true
135
- starttlsEnable: false
136
- timeout: 10000
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
- > 常用 SMTP 服务器配置详见 [references/mail-config.md](references/mail-config.md)
141
-
142
- ### 2.2 核心 API
118
+ #### 验证码短信完整示例
143
119
 
144
120
  ```java
145
- import org.dromara.common.mail.utils.MailUtils;
121
+ @RestController
122
+ @RequestMapping("/captcha")
123
+ public class CaptchaController {
146
124
 
147
- // 发送文本邮件
148
- String msgId = MailUtils.sendText("to@example.com", "标题", "正文内容");
125
+ @Autowired
126
+ private SmsService smsService;
149
127
 
150
- // 发送HTML邮件
151
- String msgId = MailUtils.sendHtml("to@example.com", "标题", "<h1>内容</h1>");
128
+ @Autowired
129
+ private StringRedisTemplate redisTemplate;
152
130
 
153
- // 带附件
154
- String msgId = MailUtils.sendText("to@example.com", "标题", "正文", attachment);
155
- String msgId = MailUtils.sendHtml("to@example.com", "标题", htmlContent, attachment);
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
- Map<String, InputStream> imageMap = Map.of("logo", new FileInputStream("logo.png"));
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 msgId = MailUtils.sendHtml(List.of("a@x.com", "b@x.com"), "标题", htmlContent);
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 msgId = MailUtils.send("to@x.com", "cc@x.com", "bcc@x.com", "标题", "内容", true);
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
- MailAccount account = MailUtils.getMailAccount();
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
- ### 2.3 邮件验证码示例
173
+ ---
174
+
175
+ ### 三、邮件服务
176
+
177
+ #### Spring Boot 原生邮件
173
178
 
174
179
  ```java
175
180
  @Service
176
- @RequiredArgsConstructor
177
- public class EmailService {
178
-
179
- public void sendEmailCode(String email) {
180
- String code = RandomUtil.randomNumbers(6);
181
- RedisUtils.setCacheObject("email:code:" + email, code, Duration.ofMinutes(10));
182
-
183
- String htmlContent = String.format("""
184
- <div style="padding: 20px; background: #f5f5f5;">
185
- <h2>验证码</h2>
186
- <p>您的验证码是:<strong style="color: #1890ff; font-size: 24px;">%s</strong></p>
187
- <p>有效期10分钟,请勿泄露。</p>
188
- </div>
189
- """, code);
190
-
191
- try {
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
- public boolean verifyEmailCode(String email, String code) {
200
- String cacheKey = "email:code:" + email;
201
- String cachedCode = RedisUtils.getCacheObject(cacheKey);
202
- if (cachedCode != null && cachedCode.equals(code)) {
203
- RedisUtils.deleteObject(cacheKey);
204
- return true;
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
- return false;
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
- public void sendMessage(List<String> messageType, String subject,
221
- String message, List<UserDTO> userList) {
222
- for (UserDTO user : userList) {
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 (messageType.contains("sms") && StringUtils.isNotBlank(user.getPhone())) {
225
- Map<String, String> params = new LinkedHashMap<>();
226
- params.put("content", message);
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 (messageType.contains("email") && StringUtils.isNotBlank(user.getEmail())) {
231
- MailUtils.sendHtml(user.getEmail(), subject,
232
- "<div style='padding:20px;'><h3>" + subject + "</h3><p>" + message + "</p></div>");
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
- messageType, user.getUserId(), e.getMessage());
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
- ```java
248
- // ❌ 未配置就使用,getSmsBlend 返回 null → NPE
249
- SmsBlend smsBlend = SmsFactory.getSmsBlend("config1");
250
- smsBlend.sendMessage(...);
251
-
252
- // 模板参数名与模板定义不匹配
253
- params.put("verifyCode", "123456"); // 模板中是 ${code}
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
- ## 五、API 速查表
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
- | 短信配置 | `ruoyi-common/ruoyi-common-sms/.../config/SmsAutoConfiguration.java` |
302
- | 短信缓存 | `ruoyi-common/ruoyi-common-sms/.../core/dao/PlusSmsDao.java` |
303
- | 邮件工具类 | `ruoyi-common/ruoyi-common-mail/.../utils/MailUtils.java` |
304
- | 邮件配置 | `ruoyi-common/ruoyi-common-mail/.../config/MailConfig.java` |
305
- | 邮件属性 | `ruoyi-common/ruoyi-common-mail/.../config/properties/MailProperties.java` |
306
- | 验证码示例 | `ruoyi-admin/.../web/controller/CaptchaController.java` |
307
- | 短信演示 | `ruoyi-modules/ruoyi-demo/.../controller/SmsController.java` |
308
- | 工作流消息 | `ruoyi-modules/ruoyi-workflow/.../service/impl/FlwCommonServiceImpl.java` |
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
+ ```