ai-engineering-init 1.7.0 → 1.10.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 (139) hide show
  1. package/.claude/agents/bug-analyzer.md +103 -0
  2. package/.claude/agents/code-reviewer.md +115 -5
  3. package/.claude/agents/image-reader.md +154 -0
  4. package/.claude/agents/loki-runner.md +80 -0
  5. package/.claude/agents/mysql-runner.md +81 -0
  6. package/.claude/agents/requirements-analyzer.md +162 -0
  7. package/.claude/agents/task-fetcher.md +75 -0
  8. package/.claude/commands/dev.md +29 -0
  9. package/.claude/commands/next.md +31 -1
  10. package/.claude/commands/progress.md +23 -1
  11. package/.claude/hooks/skill-forced-eval.js +46 -62
  12. package/.claude/settings.json +10 -1
  13. package/.claude/skills/api-development/SKILL.md +179 -130
  14. package/.claude/skills/architecture-design/SKILL.md +102 -212
  15. package/.claude/skills/backend-annotations/SKILL.md +166 -220
  16. package/.claude/skills/bug-detective/SKILL.md +225 -186
  17. package/.claude/skills/code-patterns/SKILL.md +127 -244
  18. package/.claude/skills/collaborating-with-codex/SKILL.md +96 -113
  19. package/.claude/skills/crud-development/SKILL.md +226 -307
  20. package/.claude/skills/data-permission/SKILL.md +131 -202
  21. package/.claude/skills/database-ops/SKILL.md +158 -355
  22. package/.claude/skills/error-handler/SKILL.md +224 -285
  23. package/.claude/skills/file-oss-management/SKILL.md +174 -169
  24. package/.claude/skills/git-workflow/SKILL.md +123 -341
  25. package/.claude/skills/json-serialization/SKILL.md +121 -137
  26. package/.claude/skills/performance-doctor/SKILL.md +83 -89
  27. package/.claude/skills/redis-cache/SKILL.md +134 -185
  28. package/.claude/skills/scheduled-jobs/SKILL.md +187 -224
  29. package/.claude/skills/security-guard/SKILL.md +168 -276
  30. package/.claude/skills/sms-mail/SKILL.md +266 -228
  31. package/.claude/skills/social-login/SKILL.md +257 -195
  32. package/.claude/skills/tenant-management/SKILL.md +172 -188
  33. package/.claude/skills/utils-toolkit/SKILL.md +214 -222
  34. package/.claude/skills/websocket-sse/SKILL.md +251 -172
  35. package/.claude/skills/workflow-engine/SKILL.md +178 -250
  36. package/.codex/skills/api-development/SKILL.md +179 -130
  37. package/.codex/skills/architecture-design/SKILL.md +102 -212
  38. package/.codex/skills/backend-annotations/SKILL.md +166 -220
  39. package/.codex/skills/bug-detective/SKILL.md +225 -186
  40. package/.codex/skills/code-patterns/SKILL.md +127 -244
  41. package/.codex/skills/collaborating-with-codex/SKILL.md +96 -113
  42. package/.codex/skills/crud-development/SKILL.md +226 -307
  43. package/.codex/skills/data-permission/SKILL.md +131 -202
  44. package/.codex/skills/database-ops/SKILL.md +158 -355
  45. package/.codex/skills/dev/SKILL.md +476 -131
  46. package/.codex/skills/error-handler/SKILL.md +224 -285
  47. package/.codex/skills/file-oss-management/SKILL.md +174 -169
  48. package/.codex/skills/git-workflow/SKILL.md +123 -341
  49. package/.codex/skills/json-serialization/SKILL.md +121 -137
  50. package/.codex/skills/next/SKILL.md +186 -42
  51. package/.codex/skills/performance-doctor/SKILL.md +83 -89
  52. package/.codex/skills/progress/SKILL.md +147 -76
  53. package/.codex/skills/redis-cache/SKILL.md +134 -185
  54. package/.codex/skills/scheduled-jobs/SKILL.md +187 -224
  55. package/.codex/skills/security-guard/SKILL.md +168 -276
  56. package/.codex/skills/sms-mail/SKILL.md +266 -228
  57. package/.codex/skills/social-login/SKILL.md +257 -195
  58. package/.codex/skills/tenant-management/SKILL.md +172 -188
  59. package/.codex/skills/utils-toolkit/SKILL.md +214 -222
  60. package/.codex/skills/websocket-sse/SKILL.md +251 -172
  61. package/.codex/skills/workflow-engine/SKILL.md +178 -250
  62. package/.cursor/agents/bug-analyzer.md +102 -0
  63. package/.cursor/agents/code-reviewer.md +80 -97
  64. package/.cursor/agents/image-reader.md +154 -0
  65. package/.cursor/agents/loki-runner.md +80 -0
  66. package/.cursor/agents/mysql-runner.md +81 -0
  67. package/.cursor/agents/project-manager.md +1 -1
  68. package/.cursor/agents/requirements-analyzer.md +141 -0
  69. package/.cursor/agents/task-fetcher.md +75 -0
  70. package/.cursor/hooks/cursor-skill-eval.js +66 -6
  71. package/.cursor/skills/api-development/SKILL.md +179 -130
  72. package/.cursor/skills/architecture-design/SKILL.md +102 -212
  73. package/.cursor/skills/backend-annotations/SKILL.md +166 -220
  74. package/.cursor/skills/bug-detective/SKILL.md +225 -186
  75. package/.cursor/skills/code-patterns/SKILL.md +127 -244
  76. package/.cursor/skills/collaborating-with-codex/SKILL.md +96 -113
  77. package/.cursor/skills/crud-development/SKILL.md +226 -307
  78. package/.cursor/skills/data-permission/SKILL.md +131 -202
  79. package/.cursor/skills/database-ops/SKILL.md +158 -355
  80. package/.cursor/skills/error-handler/SKILL.md +224 -285
  81. package/.cursor/skills/file-oss-management/SKILL.md +174 -169
  82. package/.cursor/skills/git-workflow/SKILL.md +123 -341
  83. package/.cursor/skills/json-serialization/SKILL.md +121 -137
  84. package/.cursor/skills/performance-doctor/SKILL.md +83 -89
  85. package/.cursor/skills/redis-cache/SKILL.md +134 -185
  86. package/.cursor/skills/scheduled-jobs/SKILL.md +187 -224
  87. package/.cursor/skills/security-guard/SKILL.md +168 -276
  88. package/.cursor/skills/sms-mail/SKILL.md +266 -228
  89. package/.cursor/skills/social-login/SKILL.md +257 -195
  90. package/.cursor/skills/tenant-management/SKILL.md +172 -188
  91. package/.cursor/skills/utils-toolkit/SKILL.md +214 -222
  92. package/.cursor/skills/websocket-sse/SKILL.md +251 -172
  93. package/.cursor/skills/workflow-engine/SKILL.md +178 -250
  94. package/AGENTS.md +117 -540
  95. package/CLAUDE.md +105 -117
  96. package/README.md +37 -6
  97. package/bin/index.js +5 -1
  98. package/package.json +1 -1
  99. package/src/skills/api-development/SKILL.md +179 -130
  100. package/src/skills/architecture-design/SKILL.md +102 -212
  101. package/src/skills/backend-annotations/SKILL.md +166 -220
  102. package/src/skills/bug-detective/SKILL.md +225 -186
  103. package/src/skills/code-patterns/SKILL.md +127 -244
  104. package/src/skills/collaborating-with-codex/SKILL.md +96 -113
  105. package/src/skills/crud-development/SKILL.md +226 -307
  106. package/src/skills/data-permission/SKILL.md +131 -202
  107. package/src/skills/database-ops/SKILL.md +158 -355
  108. package/src/skills/error-handler/SKILL.md +224 -285
  109. package/src/skills/file-oss-management/SKILL.md +174 -169
  110. package/src/skills/git-workflow/SKILL.md +123 -341
  111. package/src/skills/json-serialization/SKILL.md +121 -137
  112. package/src/skills/performance-doctor/SKILL.md +83 -89
  113. package/src/skills/redis-cache/SKILL.md +134 -185
  114. package/src/skills/scheduled-jobs/SKILL.md +187 -224
  115. package/src/skills/security-guard/SKILL.md +168 -276
  116. package/src/skills/sms-mail/SKILL.md +266 -228
  117. package/src/skills/social-login/SKILL.md +257 -195
  118. package/src/skills/tenant-management/SKILL.md +172 -188
  119. package/src/skills/utils-toolkit/SKILL.md +214 -222
  120. package/src/skills/websocket-sse/SKILL.md +251 -172
  121. package/src/skills/workflow-engine/SKILL.md +178 -250
  122. package/.claude/skills/skill-creator/LICENSE.txt +0 -202
  123. package/.claude/skills/skill-creator/SKILL.md +0 -479
  124. package/.claude/skills/skill-creator/agents/analyzer.md +0 -274
  125. package/.claude/skills/skill-creator/agents/comparator.md +0 -202
  126. package/.claude/skills/skill-creator/agents/grader.md +0 -223
  127. package/.claude/skills/skill-creator/assets/eval_review.html +0 -146
  128. package/.claude/skills/skill-creator/eval-viewer/generate_review.py +0 -471
  129. package/.claude/skills/skill-creator/eval-viewer/viewer.html +0 -1325
  130. package/.claude/skills/skill-creator/references/schemas.md +0 -430
  131. package/.claude/skills/skill-creator/scripts/__init__.py +0 -0
  132. package/.claude/skills/skill-creator/scripts/aggregate_benchmark.py +0 -401
  133. package/.claude/skills/skill-creator/scripts/generate_report.py +0 -326
  134. package/.claude/skills/skill-creator/scripts/improve_description.py +0 -248
  135. package/.claude/skills/skill-creator/scripts/package_skill.py +0 -136
  136. package/.claude/skills/skill-creator/scripts/quick_validate.py +0 -103
  137. package/.claude/skills/skill-creator/scripts/run_eval.py +0 -310
  138. package/.claude/skills/skill-creator/scripts/run_loop.py +0 -332
  139. 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
+ ```