陈昶聿
2 天以前 933846091c0b8f06158fcab23cfa0b59e8514f9b
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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
package com.ruoyi.common.utils.sms;
 
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.annotation.JSONField;
import com.ruoyi.common.utils.HttpUtil;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.common.utils.sign.Md5Utils;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
 
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
 
/**
 * 短信发送工具类
 *
 * @author ruoyi
 */
@Slf4j
public final class SmsUtils {
 
    private SmsUtils() {
    }
 
    /**
     * 云MAS 每批次最多手机号数量
     */
    private static final int YUNMAS_MAX_MOBILES = 5000;
 
    /**
     * 云MAS 返回码说明
     */
    private static final Map<String, String> YUNMAS_RSPCOD_DESC = new HashMap<>();
 
    static {
        YUNMAS_RSPCOD_DESC.put("success", "数据验证通过");
        YUNMAS_RSPCOD_DESC.put("IllegalMac", "mac校验不通过");
        YUNMAS_RSPCOD_DESC.put("IllegalSignId", "无效的签名编码");
        YUNMAS_RSPCOD_DESC.put("InvalidMessage", "非法消息,请求数据解析失败");
        YUNMAS_RSPCOD_DESC.put("InvalidUsrOrPwd", "非法用户名/密码");
        YUNMAS_RSPCOD_DESC.put("NoSignId", "未匹配到对应的签名信息");
        YUNMAS_RSPCOD_DESC.put("TooManyMobiles", "手机号数量超限(>5000)");
    }
 
    /**
     * 云MAS 接口配置。
     * <p>
     * 注意:secretKey 只参与 mac 计算,不能放进请求报文。
     */
    @Data
    public static class YunMasConfig {
        /**
         * 接口地址
         */
        private String url;
        /**
         * 企业名称
         */
        private String ecName;
        /**
         * 接口账号用户名
         */
        private String apId;
        /**
         * 密钥(仅参与 mac 计算)
         */
        private String secretKey;
        /**
         * 签名编码
         */
        private String sign;
        /**
         * 扩展码:精确匹配填空字符串;模糊匹配可填空字符串或自定义扩展码(与服务代码合计不超过20位)
         */
        private String addSerial;
 
        public String getAddSerial() {
            return addSerial == null ? "" : addSerial;
        }
 
        /**
         * 丽水妇幼默认配置(未在 sys_config 中配置时使用)
         */
        public static YunMasConfig defaultLishui() {
            YunMasConfig config = new YunMasConfig();
            config.setUrl("http://9.208.5.35:1992/sms/norsubmit");
            config.setEcName("丽水市妇幼保健院");
            config.setApId("dxjkkf");
            config.setSecretKey("1q2w3e4r5t6y");
            config.setSign("D9gKbwdt7");
            config.setAddSerial("");
            return config;
        }
 
        /**
         * 根据配置内容(json)构建,未配置的字段沿用默认值
         *
         * @param configJson 形如 {"url":"...","ecName":"...","apId":"...","secretKey":"...","sign":"...","addSerial":""}
         */
        public static YunMasConfig of(String configJson) {
            YunMasConfig config = defaultLishui();
            if (StringUtils.isEmpty(configJson)) {
                return config;
            }
            JSONObject json;
            try {
                json = JSON.parseObject(configJson);
            } catch (Exception e) {
                log.error("【云MAS】配置解析失败,使用默认配置,配置内容:{}", configJson, e);
                return config;
            }
            if (json == null) {
                return config;
            }
            if (StringUtils.isNotEmpty(json.getString("url"))) {
                config.setUrl(json.getString("url"));
            }
            if (StringUtils.isNotEmpty(json.getString("ecName"))) {
                config.setEcName(json.getString("ecName"));
            }
            if (StringUtils.isNotEmpty(json.getString("apId"))) {
                config.setApId(json.getString("apId"));
            }
            if (StringUtils.isNotEmpty(json.getString("secretKey"))) {
                config.setSecretKey(json.getString("secretKey"));
            }
            if (StringUtils.isNotEmpty(json.getString("sign"))) {
                config.setSign(json.getString("sign"));
            }
            if (json.containsKey("addSerial")) {
                config.setAddSerial(json.getString("addSerial"));
            }
            return config;
        }
 
        /**
         * 关键参数是否齐全
         */
        public boolean isValid() {
            return StringUtils.isNotEmpty(url) && StringUtils.isNotEmpty(ecName)
                    && StringUtils.isNotEmpty(apId) && StringUtils.isNotEmpty(secretKey)
                    && StringUtils.isNotEmpty(sign);
        }
    }
 
    /**
     * 短信发送结果
     */
    @Data
    public static class SmsResult {
        /**
         * 平台返回的状态码(success 表示校验通过)
         */
        private String rspcod;
        /**
         * 消息批次号,失败时为空
         */
        private String msgGroup;
        /**
         * 平台返回的数据校验结果
         */
        private boolean success;
        /**
         * 本地异常/失败描述,便于排查
         */
        private String message;
 
        /**
         * 是否发送成功(平台校验通过)
         */
        @JSONField(serialize = false, deserialize = false)
        public boolean isOk() {
            return success && "success".equals(rspcod);
        }
 
        public static SmsResult fail(String message) {
            SmsResult result = new SmsResult();
            result.setSuccess(false);
            result.setMessage(message);
            return result;
        }
    }
 
    /**
     * 杭州市一医院吴山院区我短信接口
     *
     * @param url      短信URL
     * @param userName 短信帐号
     * @param passWord 短信密码
     * @param phone    手机号组合如 13500000001,13500000002
     * @param content  短信内容
     * @return 发送结果
     */
    public static String sendSms(String url, String userName, String passWord, String phone, String content) {
        long timestamp = new Date().getTime();
        String sign = Md5Utils.hash(userName + timestamp + Md5Utils.hash(passWord));
        content = "【杭州市肿瘤医院】" + content;
        String jsonMsg = "{" +
                "\"userName\": \"" + userName + "\"," +
                "\"content\": \"" + content + "\"," +
                "\"phoneList\": [" + phone + "]," +
                "\"timestamp\": " + timestamp + "," +
                "\"sign\": \"" + sign + "\"" +
                "}";
        Boolean sendMsg = HttpUtil.postJsonRequest(url, jsonMsg);
        return sendMsg.toString();
 
    }
 
 
    /**
     * 杭州市一医院湖滨院区我短信接口
     *
     * @param url      短信URL
     * @param userName 短信帐号
     * @param passWord 短信密码
     * @param phone    手机号组合如 13500000001,13500000002
     * @param content  短信内容
     * @return 发送结果
     */
    public static String sendSms1(String url, String userName, String passWord, String phone, String content) {
        long timestamp = new Date().getTime();
        String sign = Md5Utils.hash(userName + timestamp + Md5Utils.hash(passWord));
        content = "【杭州市第一人民医院】" + content;
        String jsonMsg = "{" +
                "\"userName\": \"" + userName + "\"," +
                "\"content\": \"" + content + "\"," +
                "\"phoneList\": [" + phone + "]," +
                "\"timestamp\": " + timestamp + "," +
                "\"sign\": \"" + sign + "\"" +
                "}";
        Boolean sendMsg = HttpUtil.postJsonRequest(url, jsonMsg);
        return sendMsg.toString();
 
    }
 
    /**
     * 中国移动云MAS 普通短信发送(一对一/多对一模式)
     * <p>
     * 接口:POST {url},报文为 json 的 base64 编码。
     * <ul>
     *     <li>mac 按 ecName、apId、secretKey、mobiles、content、sign、addSerial 顺序拼接后取 MD5(32位小写)</li>
     *     <li>secretKey 只参与 mac 计算,不放进请求报文</li>
     *     <li>签名由平台根据 sign 编码自动追加,content 中不要再手工拼【签名】</li>
     * </ul>
     *
     * @param config  云MAS 配置
     * @param mobiles 收信手机号,英文逗号分隔,如 13800138000,13800138001,每批不超过 5000 个
     * @param content 短信内容(mac 用最终内容计算,计算后不得再修改)
     * @return 发送结果,通过 {@link SmsResult#isOk()} 判断是否成功
     */
    public static SmsResult sendYunMasSms(YunMasConfig config, String mobiles, String content) {
        if (config == null || !config.isValid()) {
            return SmsResult.fail("云MAS配置不完整");
        }
        if (StringUtils.isEmpty(mobiles)) {
            return SmsResult.fail("收信手机号不能为空");
        }
        if (StringUtils.isEmpty(content)) {
            return SmsResult.fail("短信内容不能为空");
        }
        if (mobiles.split(",").length > YUNMAS_MAX_MOBILES) {
            return SmsResult.fail("手机号数量超限(>" + YUNMAS_MAX_MOBILES + ")");
        }
 
        String addSerial = config.getAddSerial();
        // 1. mac 必须用报文里最终发送的内容计算
        String mac = Md5Utils.hash(config.getEcName() + config.getApId() + config.getSecretKey()
                + mobiles + content + config.getSign() + addSerial);
 
        // 2. 用 json 组装报文,避免内容中的引号/反斜杠破坏报文;secretKey 不落报文
        JSONObject body = new JSONObject(true);
        body.put("ecName", config.getEcName());
        body.put("apId", config.getApId());
        body.put("mobiles", mobiles);
        body.put("content", content);
        body.put("sign", config.getSign());
        body.put("addSerial", addSerial);
        body.put("mac", mac);
 
        String requestBody = Base64.getEncoder()
                .encodeToString(body.toJSONString().getBytes(StandardCharsets.UTF_8));
 
        try {
            String resp = HttpUtil.postJsonRequestForString(config.getUrl(), requestBody);
            log.info("【云MAS】发送短信,手机号:{},响应:{}", mobiles, resp);
            if (StringUtils.isEmpty(resp)) {
                return SmsResult.fail("云MAS响应为空");
            }
            SmsResult result = JSON.parseObject(resp, SmsResult.class);
            if (result == null) {
                return SmsResult.fail("云MAS响应解析失败:" + resp);
            }
            if (!result.isOk()) {
                String desc = YUNMAS_RSPCOD_DESC.getOrDefault(result.getRspcod(), "未知错误");
                result.setMessage(desc);
                log.error("【云MAS】发送失败,手机号:{},rspcod:{}({}),响应:{}",
                        mobiles, result.getRspcod(), desc, resp);
            }
            return result;
        } catch (Exception e) {
            log.error("【云MAS】发送异常,手机号:{},原因:{}", mobiles, e.getMessage(), e);
            return SmsResult.fail("云MAS发送异常:" + e.getMessage());
        }
    }
 
    /**
     * 杭州市一医院吴山院区微信接口
     *
     * @param url    微信URL
     * @param phone  手机号 13500000001
     * @param idCard 身份证
     * @return 发送结果
     */
    public static String sendChat(String url, String phone, String idCard) {
        String urlTemp = "http://192.200.54.14:5003/sfjk/SendMessage?sfzh="
                + idCard
                + "&title=您好,邀请您填写出院调查表,请点击填写。&content=待随访&phone=" + phone
                + "&url=" + url + "&key=ff76f8904f5f32b5ee1739e8ea46e60g";
        String sendMsg = "";
        try {
            log.info("【sendChat】市一微信公众号请求参数:{}", urlTemp);
            sendMsg = HttpUtils.sendPost(urlTemp);
            log.info("【sendChat】市一微信公众号返回值:{}", sendMsg);
        } catch (Exception ex) {
            sendMsg = ex.getMessage();
            log.error(ex.getMessage());
        }
        return sendMsg;
    }
}