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 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 接口配置。 *

* 注意: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 普通短信发送(一对一/多对一模式) *

* 接口:POST {url},报文为 json 的 base64 编码。 *

* * @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; } }