package com.ruoyi.common.utils.sms;
|
|
import com.alibaba.fastjson.JSON;
|
import com.alibaba.fastjson.JSONArray;
|
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.io.BufferedReader;
|
import java.io.IOException;
|
import java.io.InputStream;
|
import java.io.InputStreamReader;
|
import java.io.OutputStream;
|
import java.net.HttpURLConnection;
|
import java.net.URL;
|
import java.net.URLEncoder;
|
import java.nio.charset.StandardCharsets;
|
import java.util.Base64;
|
import java.util.Date;
|
import java.util.HashMap;
|
import java.util.Map;
|
import java.util.concurrent.ConcurrentHashMap;
|
|
/**
|
* 短信发送工具类
|
*
|
* @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;
|
}
|
|
/**
|
* 丽水微信 app-api 前缀
|
*/
|
private static final String LS_WX_APP_PREFIX = "/app-api";
|
|
/**
|
* 丽水微信开放接口前缀
|
*/
|
private static final String LS_WX_API_PREFIX = LS_WX_APP_PREFIX + "/cross/open-api";
|
|
/**
|
* 获取访问令牌
|
*/
|
private static final String LS_WX_TOKEN_PATH = "/token";
|
|
/**
|
* 获取消息模板
|
*/
|
private static final String LS_WX_TEMPLATE_PATH = "/getTemplate";
|
|
/**
|
* 发送消息
|
*/
|
private static final String LS_WX_SEND_PATH = "/sendMessage";
|
|
/**
|
* 访问令牌请求头
|
*/
|
private static final String LS_WX_TOKEN_HEADER = "openApiToken";
|
|
/**
|
* 令牌缓存(key:baseUrl|appId)
|
*/
|
private static final Map<String, LsWxToken> LS_WX_TOKEN_CACHE = new ConcurrentHashMap<>();
|
|
/**
|
* 令牌提前刷新的秒数,避免临界过期
|
*/
|
private static final long LS_WX_TOKEN_REFRESH_AHEAD_SECONDS = 60L;
|
|
/**
|
* 丽水微信接口配置
|
*/
|
@Data
|
public static class LsWxConfig {
|
/**
|
* 应用名称
|
*/
|
private String appName;
|
/**
|
* 接口根地址,如 http://127.0.0.1:8080
|
* <p>
|
* 可以只填 scheme://host:port,也可以填到 /app-api 或 /app-api/cross/open-api。
|
*/
|
private String baseUrl;
|
/**
|
* 应用ID
|
*/
|
private String appId;
|
/**
|
* 应用密钥
|
*/
|
private String appSecret;
|
/**
|
* 默认消息模板ID,消息里未指定时使用
|
*/
|
private Long templateId;
|
/**
|
* 默认消息模板名称,未配置模板ID时通过 getTemplate 按名称查找
|
*/
|
private String templateName;
|
/**
|
* 连接超时,单位:毫秒
|
*/
|
private Integer connectTimeout = 5000;
|
/**
|
* 读取超时,单位:毫秒
|
*/
|
private Integer readTimeout = 10000;
|
|
/**
|
* 丽水默认配置
|
*/
|
public static LsWxConfig defaultLishui() {
|
LsWxConfig config = new LsWxConfig();
|
config.setAppName("患者随访");
|
config.setBaseUrl("https://wmini.lsfby.com.cn/app-api");
|
config.setAppId("kpzEHgZSteCO4lz74");
|
config.setAppSecret("HKUDVMJSVTKQADIXV7XXRLNBQM8HNNL7");
|
// 消息模板:诊后随访提醒(未指定模板ID时按名称从 getTemplate 查询)
|
config.setTemplateName("诊后随访提醒");
|
return config;
|
}
|
|
/**
|
* 根据配置内容(json)构建,未配置的字段沿用默认值
|
*
|
* @param configJson 形如
|
* {"appName":"患者随访","baseUrl":"https://wmini.lsfby.com.cn/app-api","appId":"...","appSecret":"...","templateId":16,"templateName":"诊后随访提醒"}
|
*/
|
public static LsWxConfig of(String configJson) {
|
LsWxConfig config = defaultLishui();
|
if (StringUtils.isEmpty(configJson)) {
|
return config;
|
}
|
JSONObject json;
|
try {
|
json = JSON.parseObject(configJson);
|
} catch (Exception e) {
|
log.error("【丽水微信】配置解析失败,使用默认配置,配置内容:{}", configJson, e);
|
return config;
|
}
|
if (json == null) {
|
return config;
|
}
|
if (StringUtils.isNotEmpty(json.getString("appName"))) {
|
config.setAppName(json.getString("appName"));
|
}
|
if (StringUtils.isNotEmpty(json.getString("baseUrl"))) {
|
config.setBaseUrl(json.getString("baseUrl"));
|
} else if (StringUtils.isNotEmpty(json.getString("url"))) {
|
config.setBaseUrl(json.getString("url"));
|
} else if (StringUtils.isNotEmpty(json.getString("address"))) {
|
config.setBaseUrl(json.getString("address"));
|
}
|
if (StringUtils.isNotEmpty(json.getString("appId"))) {
|
config.setAppId(json.getString("appId"));
|
}
|
if (StringUtils.isNotEmpty(json.getString("appSecret"))) {
|
config.setAppSecret(json.getString("appSecret"));
|
}
|
if (json.containsKey("templateId")) {
|
config.setTemplateId(json.getLong("templateId"));
|
} else if (json.containsKey("id")) {
|
config.setTemplateId(json.getLong("id"));
|
}
|
if (StringUtils.isNotEmpty(json.getString("templateName"))) {
|
config.setTemplateName(json.getString("templateName"));
|
} else if (StringUtils.isNotEmpty(json.getString("tmpName"))) {
|
config.setTemplateName(json.getString("tmpName"));
|
}
|
if (json.containsKey("connectTimeout")) {
|
config.setConnectTimeout(json.getInteger("connectTimeout"));
|
}
|
if (json.containsKey("readTimeout")) {
|
config.setReadTimeout(json.getInteger("readTimeout"));
|
}
|
return config;
|
}
|
|
/**
|
* 关键参数是否齐全
|
*/
|
public boolean isValid() {
|
return StringUtils.isNotEmpty(baseUrl) && StringUtils.isNotEmpty(appId)
|
&& StringUtils.isNotEmpty(appSecret);
|
}
|
|
/**
|
* 拼接开放接口地址
|
*/
|
private String url(String path) {
|
String prefix = baseUrl == null ? "" : baseUrl.trim();
|
while (prefix.endsWith("/")) {
|
prefix = prefix.substring(0, prefix.length() - 1);
|
}
|
if (prefix.endsWith(LS_WX_API_PREFIX)) {
|
return prefix + path;
|
}
|
if (prefix.endsWith(LS_WX_APP_PREFIX)) {
|
return prefix + "/cross/open-api" + path;
|
}
|
return prefix + LS_WX_API_PREFIX + path;
|
}
|
}
|
|
/**
|
* 丽水微信消息参数,字段与 sendMessage 接口一致
|
*/
|
@Data
|
public static class LsWxMessage {
|
/**
|
* 消息模板ID,为空时使用配置中的模板
|
*/
|
private Long id;
|
/**
|
* 患者编号
|
*/
|
private String patientId;
|
/**
|
* 患者姓名
|
*/
|
private String patientName;
|
/**
|
* 证件号
|
*/
|
private String idCard;
|
/**
|
* 手机号码
|
*/
|
private String mobile;
|
/**
|
* 就诊卡号
|
*/
|
private String medCardNo;
|
/**
|
* 通知标题
|
*/
|
private String titleName;
|
/**
|
* 通知内容
|
*/
|
private String content;
|
/**
|
* 链接地址
|
*/
|
private String url;
|
}
|
|
/**
|
* 访问令牌缓存对象
|
*/
|
private static class LsWxToken {
|
private final String token;
|
private final long expireAt;
|
|
private LsWxToken(String token, long expireAt) {
|
this.token = token;
|
this.expireAt = expireAt;
|
}
|
|
private String getToken() {
|
return token;
|
}
|
|
private long getExpireAt() {
|
return expireAt;
|
}
|
}
|
|
/**
|
* 丽水微信消息发送(先获取/复用访问令牌,再调用 sendMessage)
|
* <p>
|
* 接口流程:
|
* <ol>
|
* <li>GET /app-api/cross/open-api/token?appId=...&appSecret=... 获取 openApiToken</li>
|
* <li>POST /app-api/cross/open-api/sendMessage,请求头携带 openApiToken</li>
|
* </ol>
|
*
|
* @param config 丽水微信配置
|
* @param message 消息参数
|
* @return 接口原始响应;本地校验失败时返回 {"code":-1,"msg":"失败原因"}
|
*/
|
public static String sendChatLs(LsWxConfig config, LsWxMessage message) {
|
if (config == null || !config.isValid()) {
|
return lsWxFail("丽水微信配置不完整,请配置 baseUrl、appId、appSecret");
|
}
|
if (message == null) {
|
return lsWxFail("丽水微信消息参数不能为空");
|
}
|
if (StringUtils.isEmpty(message.getMobile()) && StringUtils.isEmpty(message.getIdCard())
|
&& StringUtils.isEmpty(message.getPatientId()) && StringUtils.isEmpty(message.getMedCardNo())) {
|
return lsWxFail("手机号码、证件号、患者编号、就诊卡号不能全部为空");
|
}
|
try {
|
String token = getLsWxToken(config);
|
if (StringUtils.isEmpty(token)) {
|
return lsWxFail("获取丽水微信访问令牌失败");
|
}
|
|
// 模板优先级:消息指定 > 配置指定 > 按模板名称查询
|
Long templateId = message.getId();
|
if (templateId == null) {
|
templateId = config.getTemplateId();
|
}
|
if (templateId == null) {
|
templateId = findLsWxTemplateId(config, token, config.getTemplateName());
|
if (templateId == null && StringUtils.isNotEmpty(config.getTemplateName())) {
|
// 可能是缓存的令牌已失效,刷新令牌后重试一次
|
LS_WX_TOKEN_CACHE.remove(tokenCacheKey(config));
|
token = getLsWxToken(config);
|
if (StringUtils.isNotEmpty(token)) {
|
templateId = findLsWxTemplateId(config, token, config.getTemplateName());
|
}
|
}
|
}
|
if (templateId == null) {
|
return lsWxFail("未找到丽水微信消息模板,请指定消息模板ID或模板名称");
|
}
|
|
JSONObject body = new JSONObject(true);
|
body.put("id", templateId);
|
body.put("patientId", nvl(message.getPatientId()));
|
body.put("patientName", nvl(message.getPatientName()));
|
body.put("idCard", nvl(message.getIdCard()));
|
body.put("mobile", message.getMobile());
|
body.put("medCardNo", nvl(message.getMedCardNo()));
|
body.put("titleName", nvl(message.getTitleName()));
|
body.put("content", nvl(message.getContent()));
|
body.put("url", nvl(message.getUrl()));
|
String requestBody = body.toJSONString();
|
|
Map<String, String> headers = new HashMap<>(2);
|
headers.put(LS_WX_TOKEN_HEADER, token);
|
String sendUrl = config.url(LS_WX_SEND_PATH);
|
log.info("【丽水微信】发送消息,地址:{},参数:{}", sendUrl, requestBody);
|
String resp = lsWxRequest("POST", sendUrl, requestBody, headers, config);
|
log.info("【丽水微信】发送结果:{}", resp);
|
|
// 令牌失效时清除缓存并重试一次
|
if (isLsWxTokenInvalid(resp)) {
|
LS_WX_TOKEN_CACHE.remove(tokenCacheKey(config));
|
token = getLsWxToken(config);
|
if (StringUtils.isNotEmpty(token)) {
|
headers.put(LS_WX_TOKEN_HEADER, token);
|
resp = lsWxRequest("POST", sendUrl, requestBody, headers, config);
|
log.info("【丽水微信】令牌失效重试结果:{}", resp);
|
}
|
}
|
return resp;
|
} catch (Exception e) {
|
log.error("【丽水微信】发送异常,手机号:{},原因:{}", message.getMobile(), e.getMessage(), e);
|
return lsWxFail("丽水微信发送异常:" + e.getMessage());
|
}
|
}
|
|
/**
|
* 丽水微信消息发送(常用随访参数)
|
*
|
* @param config 丽水微信配置
|
* @param mobile 手机号码
|
* @param idCard 证件号
|
* @param patientName 患者姓名
|
* @param titleName 通知标题
|
* @param content 通知内容
|
* @param url 链接地址
|
* @return 接口原始响应;本地校验失败时返回 {"code":-1,"msg":"失败原因"}
|
*/
|
public static String sendChatLs(LsWxConfig config, String mobile, String idCard, String patientName,
|
String titleName, String content, String url) {
|
LsWxMessage message = new LsWxMessage();
|
message.setMobile(mobile);
|
message.setIdCard(idCard);
|
message.setPatientName(patientName);
|
message.setTitleName(titleName);
|
message.setContent(content);
|
message.setUrl(url);
|
return sendChatLs(config, message);
|
}
|
|
/**
|
* 丽水微信消息发送(配置直接传 sys_config 中的 json)
|
*
|
* @param configJson 丽水微信配置 json
|
* @param mobile 手机号码
|
* @param idCard 证件号
|
* @param patientName 患者姓名
|
* @param titleName 通知标题
|
* @param content 通知内容
|
* @param url 链接地址
|
* @return 接口原始响应;本地校验失败时返回 {"code":-1,"msg":"失败原因"}
|
*/
|
public static String sendChatLs(String configJson, String mobile, String idCard, String patientName,
|
String titleName, String content, String url) {
|
return sendChatLs(LsWxConfig.of(configJson), mobile, idCard, patientName, titleName, content, url);
|
}
|
|
/**
|
* 获取丽水微信访问令牌,优先使用缓存
|
*/
|
private static String getLsWxToken(LsWxConfig config) throws IOException {
|
String cacheKey = tokenCacheKey(config);
|
long now = System.currentTimeMillis();
|
LsWxToken cached = LS_WX_TOKEN_CACHE.get(cacheKey);
|
if (cached != null && cached.getExpireAt() > now) {
|
return cached.getToken();
|
}
|
|
String url = config.url(LS_WX_TOKEN_PATH)
|
+ "?appId=" + encode(config.getAppId())
|
+ "&appSecret=" + encode(config.getAppSecret());
|
String resp = lsWxRequest("GET", url, null, null, config);
|
log.info("【丽水微信】获取token响应:{}", resp);
|
if (StringUtils.isEmpty(resp)) {
|
return null;
|
}
|
JSONObject json = JSON.parseObject(resp);
|
if (json == null || json.getIntValue("code") != 0) {
|
log.error("【丽水微信】获取token失败:{}", resp);
|
return null;
|
}
|
JSONObject data = json.getJSONObject("data");
|
String token = data == null ? null : data.getString("token");
|
if (StringUtils.isEmpty(token)) {
|
log.error("【丽水微信】获取token失败,响应中没有token:{}", resp);
|
return null;
|
}
|
|
long expiresIn = data.getLongValue("expiresIn");
|
if (expiresIn <= 0) {
|
expiresIn = 3000L;
|
}
|
long expireAt = now + Math.max(expiresIn - LS_WX_TOKEN_REFRESH_AHEAD_SECONDS, 1L) * 1000L;
|
LS_WX_TOKEN_CACHE.put(cacheKey, new LsWxToken(token, expireAt));
|
return token;
|
}
|
|
/**
|
* 根据模板名称查询模板ID
|
*/
|
private static Long findLsWxTemplateId(LsWxConfig config, String token, String templateName) throws IOException {
|
if (StringUtils.isEmpty(templateName)) {
|
return null;
|
}
|
String url = config.url(LS_WX_TEMPLATE_PATH) + "?appId=" + encode(config.getAppId());
|
Map<String, String> headers = new HashMap<>(2);
|
headers.put(LS_WX_TOKEN_HEADER, token);
|
String resp = lsWxRequest("GET", url, null, headers, config);
|
log.info("【丽水微信】获取模板响应:{}", resp);
|
if (StringUtils.isEmpty(resp)) {
|
return null;
|
}
|
JSONObject json = JSON.parseObject(resp);
|
if (json == null || json.getIntValue("code") != 0) {
|
log.error("【丽水微信】获取模板失败:{}", resp);
|
return null;
|
}
|
JSONArray data = json.getJSONArray("data");
|
if (data == null) {
|
return null;
|
}
|
for (int i = 0; i < data.size(); i++) {
|
JSONObject item = data.getJSONObject(i);
|
if (item != null && templateName.equals(item.getString("tmpName"))) {
|
return item.getLong("id");
|
}
|
}
|
log.error("【丽水微信】未找到模板:{}", templateName);
|
return null;
|
}
|
|
/**
|
* 判断响应是否属于令牌失效
|
*/
|
private static boolean isLsWxTokenInvalid(String resp) {
|
if (StringUtils.isEmpty(resp)) {
|
return false;
|
}
|
try {
|
JSONObject json = JSON.parseObject(resp);
|
if (json == null) {
|
return false;
|
}
|
int code = json.getIntValue("code");
|
if (code == 401 || code == 403) {
|
return true;
|
}
|
String msg = json.getString("msg");
|
if (StringUtils.isEmpty(msg)) {
|
return false;
|
}
|
String lower = msg.toLowerCase();
|
return (lower.contains("token") || msg.contains("令牌"))
|
&& (lower.contains("expire") || lower.contains("invalid")
|
|| msg.contains("失效") || msg.contains("过期"));
|
} catch (Exception e) {
|
return false;
|
}
|
}
|
|
/**
|
* 发送丽水微信 HTTP 请求
|
*/
|
private static String lsWxRequest(String method, String url, String body,
|
Map<String, String> headers, LsWxConfig config) throws IOException {
|
HttpURLConnection connection = null;
|
try {
|
connection = (HttpURLConnection) new URL(url).openConnection();
|
connection.setRequestMethod(method);
|
connection.setConnectTimeout(config.getConnectTimeout() == null ? 5000 : config.getConnectTimeout());
|
connection.setReadTimeout(config.getReadTimeout() == null ? 10000 : config.getReadTimeout());
|
connection.setRequestProperty("Accept", "*/*");
|
if (headers != null) {
|
for (Map.Entry<String, String> entry : headers.entrySet()) {
|
connection.setRequestProperty(entry.getKey(), entry.getValue());
|
}
|
}
|
if (body != null) {
|
connection.setDoOutput(true);
|
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
try (OutputStream os = connection.getOutputStream()) {
|
os.write(body.getBytes(StandardCharsets.UTF_8));
|
}
|
}
|
|
int status = connection.getResponseCode();
|
InputStream inputStream = status >= 200 && status < 300
|
? connection.getInputStream() : connection.getErrorStream();
|
String resp = readStream(inputStream);
|
if (status < 200 || status >= 300) {
|
log.warn("【丽水微信】请求返回非200,地址:{},状态码:{},响应:{}", url, status, resp);
|
}
|
return resp;
|
} finally {
|
if (connection != null) {
|
connection.disconnect();
|
}
|
}
|
}
|
|
/**
|
* 读取响应流
|
*/
|
private static String readStream(InputStream inputStream) throws IOException {
|
if (inputStream == null) {
|
return "";
|
}
|
StringBuilder result = new StringBuilder();
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
|
String line;
|
while ((line = reader.readLine()) != null) {
|
result.append(line);
|
}
|
}
|
return result.toString();
|
}
|
|
/**
|
* 令牌缓存key
|
*/
|
private static String tokenCacheKey(LsWxConfig config) {
|
return config.getBaseUrl() + "|" + config.getAppId();
|
}
|
|
/**
|
* URL参数编码
|
*/
|
private static String encode(String value) {
|
if (value == null) {
|
return "";
|
}
|
try {
|
return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
|
} catch (Exception e) {
|
return value;
|
}
|
}
|
|
/**
|
* null转空字符串
|
*/
|
private static String nvl(String value) {
|
return value == null ? "" : value;
|
}
|
|
/**
|
* 构造本地失败响应
|
*/
|
private static String lsWxFail(String msg) {
|
JSONObject json = new JSONObject(true);
|
json.put("code", -1);
|
json.put("msg", msg);
|
return json.toJSONString();
|
}
|
}
|