Răsfoiți Sursa

接入塞邮平台实例,支持管理端配置
更改发送账号手机号位手机尾号

liyanbo 1 săptămână în urmă
părinte
comite
0022a8092f

+ 2 - 1
byzs-module-system/src/main/java/cn/iocoder/byzs/module/system/framework/sms/core/client/impl/SmsClientFactoryImpl.java

@@ -81,10 +81,11 @@ public class SmsClientFactoryImpl implements SmsClientFactory {
             case TENCENT: return new TencentSmsClient(properties);
             case HUAWEI: return  new HuaweiSmsClient(properties);
             case QINIU: return new QiniuSmsClient(properties);
+            case SUBMAIL: return new SubmailSmsClient(properties);
         }
         // 创建失败,错误日志 + 抛出异常
         log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties);
         throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", properties));
     }
 
-}
+}

+ 198 - 0
byzs-module-system/src/main/java/cn/iocoder/byzs/module/system/framework/sms/core/client/impl/SubmailSmsClient.java

@@ -0,0 +1,198 @@
+package cn.iocoder.byzs.module.system.framework.sms.core.client.impl;
+
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.digest.DigestUtil;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import cn.iocoder.byzs.framework.common.core.KeyValue;
+import cn.iocoder.byzs.framework.common.util.http.HttpUtils;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.TreeMap;
+import cn.iocoder.byzs.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
+import cn.iocoder.byzs.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
+import cn.iocoder.byzs.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
+import cn.iocoder.byzs.module.system.framework.sms.core.property.SmsChannelProperties;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+public class SubmailSmsClient extends AbstractSmsClient {
+
+    private static final String API_URL_XSEND = "https://api.mysubmail.com/message/xsend";
+    private static final String API_URL_QUERY = "https://api.mysubmail.com/message/query";
+    private static final String API_URL_TEMPLATE_GET = "https://api-v4.mysubmail.com/sms/template";
+    private static final String TIMESTAMP_URL = "https://api-v4.mysubmail.com/service/timestamp";
+    private static final String SIGN_TYPE_MD5 = "md5";
+    private static final String NORMAL = "normal";
+
+    public SubmailSmsClient(SmsChannelProperties properties) {
+        super(properties);
+        Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
+        Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
+    }
+
+    @Override
+    public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
+                                   List<KeyValue<String, Object>> templateParams) throws Throwable {
+        StringBuilder postData = new StringBuilder();
+        postData.append("appid=").append(URLEncoder.encode(properties.getApiKey(), StandardCharsets.UTF_8));
+        postData.append("&signature=").append(URLEncoder.encode(properties.getApiSecret(), StandardCharsets.UTF_8));
+        postData.append("&project=").append(URLEncoder.encode(apiTemplateId, StandardCharsets.UTF_8));
+        postData.append("&to=").append(URLEncoder.encode(mobile, StandardCharsets.UTF_8));
+
+        if (ObjectUtil.isNotEmpty(properties.getSignature())) {
+            postData.append("&sms_signature=").append(URLEncoder.encode(properties.getSignature(), StandardCharsets.UTF_8));
+        }
+
+        if (ObjectUtil.isNotEmpty(templateParams)) {
+            Map<String, Object> vars = new LinkedHashMap<>();
+            templateParams.forEach(param -> vars.put(param.getKey(), param.getValue()));
+            postData.append("&vars=").append(URLEncoder.encode(JSONUtil.toJsonStr(vars), StandardCharsets.UTF_8));
+        }
+
+        java.util.Map<String, String> headers = new LinkedHashMap<>();
+        headers.put("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
+        headers.put("Accept", "application/json");
+
+        String responseBody = HttpUtils.post(API_URL_XSEND, headers, postData.toString());
+        JSONObject response = JSONUtil.parseObj(responseBody);
+
+        String status = response.getStr("status");
+        if ("success".equals(status)) {
+            return new SmsSendRespDTO()
+                    .setSuccess(true)
+                    .setSerialNo(response.getStr("send_id"));
+        }
+        return new SmsSendRespDTO()
+                .setSuccess(false)
+                .setApiCode(response.getStr("code"))
+                .setApiMsg(response.getStr("msg"));
+    }
+
+    @Override
+    public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) throws Throwable {
+        List<SmsReceiveRespDTO> results = new ArrayList<>();
+        JSONObject status = JSONUtil.parseObj(text);
+
+        String statusStr = status.getStr("status");
+        if ("success".equals(statusStr)) {
+            JSONObject data = status.getJSONObject("data");
+            if (ObjectUtil.isNotEmpty(data)) {
+                SmsReceiveRespDTO result = new SmsReceiveRespDTO()
+                        .setSuccess("success".equals(data.getStr("status")))
+                        .setErrorMsg(data.getStr("status"))
+                        .setMobile(data.getStr("phone"))
+                        .setSerialNo(data.getStr("send_id"));
+                results.add(result);
+            }
+        }
+        return results;
+    }
+
+    @Override
+    public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
+        TreeMap<String, String> requestData = new TreeMap<>();
+        requestData.put("appid", properties.getApiKey());
+        requestData.put("template_id", apiTemplateId);
+
+        requestData = buildSignature(properties.getApiKey(), properties.getApiSecret(), NORMAL, requestData);
+
+        StringBuilder urlBuilder = new StringBuilder(API_URL_TEMPLATE_GET);
+        urlBuilder.append("?");
+        for (Map.Entry<String, String> entry : requestData.entrySet()) {
+            String key = entry.getKey();
+            String value = entry.getValue();
+            urlBuilder.append(key).append("=").append(URLEncoder.encode(value, StandardCharsets.UTF_8)).append("&");
+        }
+        String url = urlBuilder.substring(0, urlBuilder.length() - 1);
+
+        java.util.Map<String, String> headers = new LinkedHashMap<>();
+        headers.put("Accept", "application/json");
+
+        String responseBody;
+        try {
+            responseBody = HttpUtils.get(url, headers);
+        } catch (Exception e) {
+            log.warn("[getSmsTemplate][模板编号({}) 请求异常: {}]", apiTemplateId, e.getMessage());
+            return null;
+        }
+
+        if (ObjectUtil.isEmpty(responseBody)) {
+            log.warn("[getSmsTemplate][模板编号({}) 响应为空]", apiTemplateId);
+            return null;
+        }
+
+        String trimmedBody = responseBody.trim();
+        if (!trimmedBody.startsWith("{")) {
+            log.warn("[getSmsTemplate][模板编号({}) 响应不是有效的 JSON,响应内容: {}", apiTemplateId, trimmedBody);
+            return null;
+        }
+
+        JSONObject response;
+        try {
+            response = JSONUtil.parseObj(responseBody);
+        } catch (Exception e) {
+            log.warn("[getSmsTemplate][模板编号({}) 响应 JSON 解析失败,错误: {},响应内容: {}", apiTemplateId, e.getMessage(), responseBody);
+            return null;
+        }
+
+        if (response.containsKey("status") && "success".equals(response.getStr("status"))) {
+            JSONObject data = response.getJSONObject("template");
+            if (ObjectUtil.isNotEmpty(data)) {
+                return new SmsTemplateRespDTO()
+                        .setId(data.getStr("template_id"))
+                        .setContent(data.getStr("sms_content"))
+                        .setAuditStatus(Integer.valueOf(data.getStr("template_status")));
+            }
+        } else {
+            log.warn("[getSmsTemplate][模板编号({}) 查询失败,响应: {}", apiTemplateId, responseBody);
+        }
+        return null;
+    }
+
+    private String getTimestamp() {
+        try {
+            String responseBody = HttpUtils.get(TIMESTAMP_URL, new LinkedHashMap<>());
+            if (ObjectUtil.isNotEmpty(responseBody)) {
+                JSONObject json = JSONUtil.parseObj(responseBody);
+                return json.getStr("timestamp");
+            }
+        } catch (Exception e) {
+            log.warn("[getTimestamp][获取时间戳异常: {}]", e.getMessage());
+        }
+        return null;
+    }
+
+    private TreeMap<String, String> buildSignature(String appid, String appkey, String signType, TreeMap<String, String> requestData) {
+        String timestamp = getTimestamp();
+        if (StrUtil.isEmpty(timestamp)) {
+            log.warn("[buildSignature][获取时间戳失败]");
+            return requestData;
+        }
+        requestData.put("timestamp", timestamp);
+        requestData.put("sign_type", signType);
+
+        StringBuilder signStr = new StringBuilder();
+        signStr.append(appid).append(appkey);
+        for (Map.Entry<String, String> entry : requestData.entrySet()) {
+            signStr.append(entry.getKey()).append("=").append(entry.getValue());
+        }
+        signStr.append(appid).append(appkey);
+
+        String signature;
+        if (SIGN_TYPE_MD5.equals(signType)) {
+            signature = DigestUtil.md5Hex(signStr.toString(), StandardCharsets.UTF_8);
+        } else {
+            signature = appkey;
+        }
+        requestData.put("signature", signature);
+        return requestData;
+    }
+}

+ 3 - 1
byzs-module-system/src/main/java/cn/iocoder/byzs/module/system/service/invitecode/InviteCodeServiceImpl.java

@@ -415,7 +415,9 @@ public class InviteCodeServiceImpl implements InviteCodeService {
 
             // 构建短信模板参数
             Map<String, Object> templateParams = new HashMap<>();
-            templateParams.put("user", inviteCode.getOnlyPhone());
+            String phone = inviteCode.getOnlyPhone();
+            String phoneSuffix = phone != null && phone.length() >= 4 ? phone.substring(phone.length() - 4) : phone;
+            templateParams.put("user", phoneSuffix);
             templateParams.put("code", inviteCode.getCode());
             
             // 发送短信(使用短信模板编码,需要根据实际情况配置)