接入第三方短信平台(以聚合數據 Juhe.cn 為例),實現短信驗證碼發送、模板管理、屏蔽詞檢測等功能,適用於用戶註冊、登錄驗證、支付確認等場景。
| 功能 | 接口地址 | 關鍵參數 |
|---|---|---|
| 發送短信 | https://v.juhe.cn/sms/send | mobile, tpl_id, tpl_value, key |
| 屏蔽詞檢測 | https://v.juhe.cn/sms/black | word, key |
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import net.sf.json.JSONObject;
public class JuheSmsClient {
private static final String CHARSET = "UTF-8";
private static final int CONNECT_TIMEOUT = 30000;
private static final int READ_TIMEOUT = 30000;
private static final String USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
// 配置中心讀取,勿硬編碼
private static final String APP_KEY = System.getenv("JUHE_SMS_APPKEY");
private static final String SEND_URL = "https://v.juhe.cn/sms/send";
private static final String BLACK_URL = "https://v.juhe.cn/sms/black";
/** 發送模板短信 */
public static SmsResult sendTemplateSms(String mobile, String tplId, Map tplValues) {
if (StringUtils.isBlank(APP_KEY)) {
return SmsResult.fail("APP_KEY 未配置");
}
Map params = new HashMap<>();
params.put("mobile", mobile);
params.put("tpl_id", tplId);
params.put("key", APP_KEY);
params.put("dtype", "json");
// 模板變量值需 URL 編碼
if (tplValues != null && !tplValues.isEmpty()) {
StringBuilder sb = new StringBuilder();
for (Map.Entry entry : tplValues.entrySet()) {
sb.append("#").append(entry.getKey()).append("#=")
.append(encode(entry.getValue())).append("&");
}
params.put("tpl_value", sb.substring(0, sb.length() - 1));
}
try {
String response = doPost(SEND_URL, params);
return parseResponse(response);
} catch (Exception e) {
return SmsResult.fail("請求異常: " + e.getMessage());
}
}
/** 屏蔽詞檢測 */
public static SmsResult checkBlackWord(String content) {
Map params = new HashMap<>();
params.put("word", content);
params.put("key", APP_KEY);
try {
String response = doGet(BLACK_URL, params);
return parseResponse(response);
} catch (Exception e) {
return SmsResult.fail("檢測異常: " + e.getMessage());
}
}
// ==================== 內部方法 ====================
private static String doGet(String url, Map params) throws IOException {
String query = buildQuery(params);
HttpURLConnection conn = createConnection(url + "?" + query, "GET");
return readResponse(conn);
}
private static String doPost(String url, Map params) throws IOException {
HttpURLConnection conn = createConnection(url, "POST");
conn.setDoOutput(true);
try (OutputStream os = conn.getOutputStream()) {
os.write(buildQuery(params).getBytes(StandardCharsets.UTF_8));
}
return readResponse(conn);
}
private static HttpURLConnection createConnection(String urlStr, String method) throws IOException {
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod(method);
conn.setRequestProperty("User-Agent", USER_AGENT);
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
conn.setConnectTimeout(CONNECT_TIMEOUT);
conn.setReadTimeout(READ_TIMEOUT);
conn.setUseCaches(false);
return conn;
}
private static String buildQuery(Map params) {
StringBuilder sb = new StringBuilder();
for (Map.Entry entry : params.entrySet()) {
if (sb.length() > 0) sb.append('&');
sb.append(entry.getKey()).append('=').append(encode(String.valueOf(entry.getValue())));
}
return sb.toString();
}
private static String encode(String value) {
try {
return URLEncoder.encode(value, CHARSET);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
private static String readResponse(HttpURLConnection conn) throws IOException {
try (InputStream is = conn.getResponseCode() == 200 ? conn.getInputStream() : conn.getErrorStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is, CHARSET))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) sb.append(line);
return sb.toString();
} finally {
conn.disconnect();
}
}
private static SmsResult parseResponse(String jsonStr) {
JSONObject json = JSONObject.fromObject(jsonStr);
int errorCode = json.getInt("error_code");
if (errorCode == 0) {
return SmsResult.success(json.get("result"));
}
return SmsResult.fail(errorCode + ": " + json.getString("reason"));
}
// ==================== 結果封裝 ====================
@Data
@AllArgsConstructor
public static class SmsResult {
private boolean success;
private Object data;
private String message;
public static SmsResult success(Object data) {
return new SmsResult(true, data, null);
}
public static SmsResult fail(String message) {
return new SmsResult(false, null, message);
}
}
}
@Service
public class SmsService {
private static final String VERIFY_TPL_ID = "123456"; // 平台申請的模板ID
/** 發送註冊驗證碼 */
public void sendRegisterCode(String mobile, String code) {
// 1. 限流檢查(Redis 計數器,每分鐘 1 條,每天 10 條)
if (!rateLimiter.tryAcquire("sms:register:" + mobile)) {
throw new BusinessException("發送過於頻繁,請稍後再試");
}
// 2. 生成 6 位數字驗證碼,存入 Redis(5 分鐘過期)
String verifyCode = String.format("%06d", new Random().nextInt(1000000));
redisTemplate.opsForValue().set("verify:code:" + mobile, verifyCode, 5, TimeUnit.MINUTES);
// 3. 調用第三方發送
Map tplValues = Map.of("code", verifyCode, "time", "5");
JuheSmsClient.SmsResult result = JuheSmsClient.sendTemplateSms(mobile, VERIFY_TPL_ID, tplValues);
if (!result.isSuccess()) {
// 發送失敗,刪除驗證碼,拋出異常
redisTemplate.delete("verify:code:" + mobile);
throw new BusinessException("短信發送失敗: " + result.getMessage());
}
}
/** 驗證碼校驗 */
public boolean verifyCode(String mobile, String inputCode) {
String cached = redisTemplate.opsForValue().get("verify:code:" + mobile);
return StringUtils.isNotBlank(cached) && cached.equals(inputCode);
}
}
@Async 或消息隊列(RocketMQ/Kafka)解耦,提升響應速度封裝穩健的 JuheSmsClient,配合業務層限流、緩存、異步、降級機制,即可構建企業級高可用短信服務。核心原則:參數編碼正確、超時控制合理、異常信息完整、結果封裝統一。