liusheng
2026-10-10 0afac42fb2e9d6e5291304dd0044ce3042b45d2b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
package com.ruoyi.message.sms.provider.xh;
 
import com.ruoyi.common.utils.HttpUtil;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.message.MessageChannel;
import com.ruoyi.message.SendResult;
import com.ruoyi.message.config.MessageProperties;
import com.ruoyi.message.config.XhsmsConfig;
import com.ruoyi.message.config.RootMessageProperties;
import com.ruoyi.message.sms.AbstractSmsSender;
import com.ruoyi.message.sms.SmsSendRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
 
import java.net.URLEncoder;
import java.util.LinkedHashMap;
import java.util.Map;
 
/**
 * 新华短信发送方({@code xh} 通道,通用短信网关)。
 * <p>
 * 对接特征:HTTP POST 表单 {@code action/account/password/mobile/content/extno/rt},无签名。
 * <p>
 * ⚠️ 现有三个入口对同一个网关的调用方式并不一致,且其中两个是坏的。为满足「按站点原样封装、
 * 不改线上行为」的要求,这里把三种模式都保留为独立方法,由调用方按原站点选择:
 * <ul>
 *   <li>{@link #sendJsonLegacy} —— 等价 {@code RyTask}:拼好表单参数却发送 {@code new Gson().toJson(req)}
 *       ({@code req} 是空 map,实际发出 {@code {}}),且把 {@code Boolean} 当 {@code String} 返回值,
 *       必然触发 NPE 被异常分支吞掉。结果:**新华站点长期任务短信实际发不出去**;</li>
 *   <li>{@link #sendForm} —— 等价 {@code RedisMqReceiver}:发送 URL 编码后的表单字符串(正确),
 *       但原实现忽略了返回值,代码里固定按成功处理;</li>
 *   <li>{@link #sendJson} —— 等价 {@code ServiceSubtaskServiceImpl.smsSubTask} / {@code SmsController}:
 *       发送 {@code Gson.toJson(map)} 的 JSON。</li>
 * </ul>
 * TODO 待确认:以上三种写法哪一种才是新华网关真正接受的报文格式(很可能是 {@link #sendForm}),
 * 确认后应统一为一种并删除其余两种。
 */
@Slf4j
@Component
public class XinHuaSmsSender extends AbstractSmsSender {
 
    /**
     * 渠道 key,与 {@code spring.profiles.active=xh} 对应
     */
    public static final String KEY = "xh";
 
    private final MessageProperties properties;
 
    public XinHuaSmsSender(MessageProperties properties) {
        this.properties = properties;
    }
 
    @Override
    public String key() {
        return KEY;
    }
 
    @Override
    public String description() {
        return "新华短信(通用网关,表单/JSON 两种报文)";
    }
 
    @Override
    protected SendResult doSend(SmsSendRequest request) {
        //默认按 RedisMqReceiver 的(正确)表单模式发送
        return sendForm(request);
    }
 
    /**
     * 模式一:表单字符串(等价 {@code RedisMqReceiver},推荐)
     */
    public SendResult sendForm(SmsSendRequest request) {
        XhsmsConfig cfg = properties.getSms().getXhsms();
        String encodedParams = buildEncodedParams(request, cfg);
        log.info("【{}】新华短信发送(表单),地址:{},参数:{}", KEY, cfg.getPath(), encodedParams);
        HttpUtils.sendPost(cfg.getPath(), encodedParams);
        //与线上行为一致:原实现未使用返回值,固定按成功处理
        return SendResult.success(KEY, MessageChannel.SMS, encodedParams);
    }
 
    /**
     * 模式二:JSON 报文(等价 {@code smsSubTask} / {@code SmsController})
     */
    public SendResult sendJson(SmsSendRequest request) {
        XhsmsConfig cfg = properties.getSms().getXhsms();
        Map<String, String> params = buildParams(request, cfg);
        String json = com.alibaba.fastjson.JSON.toJSONString(params);
        log.info("【{}】新华短信发送(JSON),地址:{},参数:{}", KEY, cfg.getPath(), json);
        Boolean ok = HttpUtil.postJsonRequest(cfg.getPath(), json);
        if (Boolean.TRUE.equals(ok)) {
            return SendResult.success(KEY, MessageChannel.SMS, json);
        }
        return SendResult.fail(KEY, MessageChannel.SMS, "短信发送失败", json);
    }
 
    /**
     * 模式三:兼容 {@code RyTask} 的旧实现(发送空 JSON,必然失败)。
     * <p>
     * 保留此方法是为了让 {@code RyTask} 的改造可以做到「行为一字不差」;确认网关格式后应删除。
     */
    public SendResult sendJsonLegacy(SmsSendRequest request) {
        XhsmsConfig cfg = properties.getSms().getXhsms();
        //原实现:Map<String,String> req = new HashMap<>(); ...(HttpUtils.sendPost(xhsmsPath, new Gson().toJson(req));
        //        Gson 序列化空 map 的结果即 "{}"
        String emptyJson = "{}";
        log.info("【{}】新华短信发送(RyTask 旧实现,发送空报文),地址:{}", KEY, cfg.getPath());
        Boolean result = HttpUtils.sendPost(cfg.getPath(), emptyJson);
        if (Boolean.TRUE.equals(result)) {
            return SendResult.success(KEY, MessageChannel.SMS, emptyJson);
        }
        return SendResult.fail(KEY, MessageChannel.SMS, "短信发送失败", emptyJson);
    }
 
    private Map<String, String> buildParams(SmsSendRequest request, XhsmsConfig cfg) {
        Map<String, String> params = new LinkedHashMap<>();
        params.put("action", "send");
        params.put("account", cfg.getAccount());
        params.put("password", cfg.getPwd());
        params.put("mobile", request.getPhone());
        params.put("content", request.getContent());
        params.put("extno", cfg.getExtno());
        params.put("rt", "json");
        return params;
    }
 
    private String buildEncodedParams(SmsSendRequest request, XhsmsConfig cfg) {
        StringBuilder encodedParams = new StringBuilder();
        for (Map.Entry<String, String> entry : buildParams(request, cfg).entrySet()) {
            if (encodedParams.length() > 0) {
                encodedParams.append("&");
            }
            encodedParams.append(encode(entry.getKey())).append("=").append(encode(entry.getValue()));
        }
        return encodedParams.toString();
    }
 
    private String encode(String value) {
        try {
            return URLEncoder.encode(value == null ? "" : value, "UTF-8");
        } catch (Exception e) {
            return value;
        }
    }
}