百家企业短信网关(背景及核心代码)-1-开源项目短信接口征集

1 背景: 小豆社保半夜短信费用完

小豆社保:是一家一站式人力资源SAAS服务云智慧平台,隶属于北京新琪科技有限公司, 说简单点就是解决工作变动无挂靠单位的人代缴社保的业务。
百家企业短信网关(背景及核心代码)-1-开源项目短信接口征集_第1张图片
阿亮: 小豆社保CTO, 原安邦集团系统架构师 。
百家企业短信网关(背景及核心代码)-1-开源项目短信接口征集_第2张图片
需求(半夜电话): 最近业务增长迅猛,我们公司预存的短信条数用完, 短信服务商半夜无法充值,怎么办?
是否可以同时对接几家短信公司的解决方案,这样有一个通道费用用完或万一出现故障,可以自动切换到其它通道。
解决方案(回复): 提供企业短信网关,最简单的报文接口即可完成对接,限制IP 调用 ,几行代码搞定, 问题解决。
为何企业短信网关能快速解决问题,以下将对什么叫企业短信网关 。

2 企业短信网关

  • 短信网关, 指同时对接移动、联通、电信的大网关, 根据短信号码自动识别属于哪家运营商。
    百家企业短信网关(背景及核心代码)-1-开源项目短信接口征集_第3张图片
  • 企业短信网关, 同一个企业可以同时对接多家短信服务商,比如助通科技、亿美软通、云掌通等.
    更多短信服务商参考《2021全网最全短信服务商排名》。
    区别: 前者(短信网关)是和移动、联通、电信(类似银行)直接对接,该系统一般由短信服务商建设。
    后者(企业短信网关)是和短信服务商(类似第三方支付,如易宝支付)对接,比如助通科技、亿美软通、云掌通等 。该系统由终端用户建设。

2.1企业短信网关架构

参考支付网关《最早的支付网关(滴滴支付)和最新的聚合支付设计架构》
企业只需要简单的配置多个接口的账户密码,
设置每个接口相应的流量比例, 路由功能将自动分配流量到不同的短信通道。

百家企业短信网关(背景及核心代码)-1-开源项目短信接口征集_第4张图片

2.2 路由及接口配置 (sms_config.ini)

{
     
	"route": {
     
		"izton": "30",
		"ztinfo":"50",
		"aliyun":"20",
		"emay":"0",
		"tzhl":"0",
		"monyun":"0"
	},
	"izton": {
     
		"smsUrl": "http://139.129.107.160:8085/sendsms.php",
		"userid": "",
		"password": "",
		"ext": "2033"
	},
	"emay": {
     
		"smsUrl": "http://www.btom.cn:8080",
		"appId": "",
		"secretKey": ""
	},
	"tzhl": {
     
		"smsUrl": "http://sms.tongzhouhl.com:9885/c123",
		"tzId": "",
		"tzPwd": ""
	},
	"aliyun": {
     
		"endpoint": "http://dysmsapi.aliyuncs.com",
		"accessKeyId": "",
		"accessKeySecret": ""
	},
	"ztinfo": {
     
		"smsUrl": "http://api.mix2.zthysms.com",
		"username": "",
		"password": "!"
	},
	"monyun": {
     
		"smsUrl": "http://api01.monyun.cn:7901",
		"userid": "",
		"password": ""
	}
}

2.3 路由及动态加载接口

package com.newxtc.sms;

import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alibaba.fastjson.JSONObject;
import com.newxtc.sms.cache.SpAccCache;
import com.newxtc.sms.entity.SmsRetMsg;

public class SmsProvider {
     
	private final static Logger logger = LoggerFactory.getLogger(SmsProvider.class);

	public static SmsApi getSmsApi(String spCode) {
     
		Map<String, String> configMap = SmsInit.getInstance().getConfig(spCode);
		return configMap != null ? new SmsClassLoad(configMap, spCode) : null;
	}

	public static SmsApi getTpSmsApi(String spCode) {
     
		Map<String, String> configMap = SmsInit.getInstance().getConfig(spCode);
		return configMap != null ? new SmsClassLoad(configMap, spCode) : null;		
	}

	public static String sendTpSms(String phone, String templateId, String templateJson) {
     
		if (phone == null || templateId == null) {
     
			logger.error("sendTpSms() phone=" + phone + "|templateId=" + templateId);
		}
		String routeType = "route";
		Map<String, String> configMap = SmsInit.getInstance().getConfig(routeType);
		Set<String> key = configMap != null ? configMap.keySet() : null;
		if (key != null && key.size() > 0) {
     
			// 通道个数
			String spCode = null;
			SmsRetMsg retMsg = null;
			SmsApi smsImpl = null;
			try {
     
				boolean isJson = templateJson != null && templateJson.contains("{") && templateJson.contains("}") && templateJson.contains(":");
				@SuppressWarnings("unchecked")
				Map<String, String> paramMap = isJson ? JSONObject.parseObject(templateJson, Map.class) : null;
				for (int i = 0; i <= 1; i++) {
     
					spCode = SmsProvider.getRoute(configMap, null, routeType);
					smsImpl = isTpSp(spCode) ? getTpSmsApi(spCode) : getSmsApi(spCode);
					retMsg = smsImpl != null ? smsImpl.sendTemplateSms(phone, templateId, paramMap) : null;
					Integer ret = (retMsg != null) ? retMsg.getRet() : null;
					if (ret != null && ret.equals(0)) {
     
						break;
					} else {
     
						if (configMap.size() == 1)
							spCode = null;
						logger.error("first ret=" + ret);
						continue;
					}
				}
				if (retMsg != null) {
     
					JSONObject json = new JSONObject();
					json.put("ret", retMsg.getRet());
					json.put("msg", retMsg.getMsg());
					json.put("spCode", spCode);
					String message = json.toJSONString();
					return message;
				} else {
     
					logger.error("spCode=" + spCode + "|retMsg=" + retMsg);
					return null;
				}
			} catch (Exception e) {
     
				logger.error("spCode=" + spCode + "|e=" + e.toString());
				for (StackTraceElement elment : e.getStackTrace()) {
     
					logger.error(elment.toString());
				}
				return null;
			}
		} else {
     
			logger.error("no exist smsProvider");
			return null;
		}
	}

	private static boolean isTpSp(String spCode) {
     
		Map<String, String> configMap = SmsInit.getInstance().getConfig("route_msg");
		return !configMap.containsKey(spCode);
	}

	public static String sendSms(String phone, String msg) {
     
		String routeType = "route_msg";
		Map<String, String> configMap = SmsInit.getInstance().getConfig(routeType);
		Set<String> key = configMap != null ? configMap.keySet() : null;
		if (key != null && key.size() > 0) {
     
			// 通道个数
			String spCode = null;
			SmsRetMsg retMsg = null;
			SmsApi smsImpl = null;
			spCode = SmsProvider.getRoute(configMap, null, routeType);
			smsImpl = getSmsApi(spCode);
			retMsg = smsImpl != null ? smsImpl.sendSms(phone, msg) : null;
			if (retMsg.getRet() != 0) {
     
				logger.error("first retMsg=" + retMsg);
				spCode = SmsProvider.getRoute(configMap, spCode, routeType);
				smsImpl = getSmsApi(spCode);
				retMsg = smsImpl != null ? smsImpl.sendSms(phone, msg) : null;
			}
			if (retMsg != null) {
     
				JSONObject json = new JSONObject();
				json.put("ret", retMsg.getRet());
				json.put("msg", retMsg.getMsg());
				json.put("spCode", spCode);
				String message = json.toJSONString();
				return message;
			} else {
     
				logger.error("no exist smsProvider");
				return null;
			}
		} else {
     
			logger.error("no exist smsProvider");
			return null;
		}
	}

	/**
	 * 根据当前发送量,和目标比率对比,选择发送的通道
	 * 
	 * @return
	 */
	public static String getRoute(Map<String, String> configMap, String exinclude, String smsType) {
     
		// 1 计算的各通道实际发送比率
		// 配置分母值 Denominator
		long configSum = 0, cacheSum = 0;
		// 内存中实际的总署
		long cacheCount = 0;
		// 第一次遍历计算总数
		for (String spCode : configMap.keySet()) {
     
			configSum += Long.parseLong(configMap.get(spCode));
			cacheSum += SpAccCache.getInstance().get(spCode);
		}
		// 第二次遍历计算每个通道的比率
		double from, destn;// 目标比率
		String retSp = null;// 命中通道
		for (String spCode : configMap.keySet()) {
     
			retSp = spCode;
			if (exinclude != null && spCode.equals(exinclude))
				continue;
			long configSp = Long.parseLong(configMap.get(spCode));
			cacheCount = SpAccCache.getInstance().get(spCode);
			from = (cacheCount * 1.0) / cacheSum;
			destn = (configSp * 1.0) / configSum;
			if (from < destn) {
     
				logger.debug("getRoute() spCode=" + spCode + "|from=" + from + "|destn=" + destn);
				break;
			}
		}
		return retSp;
	}

	public static void main(String[] args) throws Exception {
     
		SmsInit.getInstance().init(null);
		String templateId = "1";
		String templateJson = "{\"code\":\"8988788\"}";
		for (int i = 0; i < 20; i++) {
     
			SmsProvider.sendTpSms("13718211912", templateId, templateJson);
			Thread.sleep(20000);
		}

		Map<String, AtomicLong> m = SpAccCache.getInstance().getAccMap();
		for (String spCode : m.keySet()) {
     
			Long l = m.get(spCode).get();
			System.out.println(spCode + "=" + l);
		}

	}

}

2.4 发送次数计数器

利用原子技术及内存结构, 分别为每一个通道计数

package com.newxtc.sms.cache;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

public class SpAccCache {
     

	// 内存 key: spCode : value : 计数器
	private static Map<String, AtomicLong> accMap = null;

	private static SpAccCache statisticsResultCache = null;

	public static SpAccCache getInstance() {
     
		if (statisticsResultCache == null) {
     
			statisticsResultCache = new SpAccCache();
			accMap = new ConcurrentHashMap<String, AtomicLong>();
		}
		return statisticsResultCache;
	}

	public Map<String, AtomicLong> getAccMap() {
     
		return accMap;
	}

	// 累加器
	public long inc(String spCode) {
     
		AtomicLong atomic = accMap.get(spCode);
		if (atomic == null) {
     
			atomic = new AtomicLong();
			accMap.put(spCode, atomic);
		}
		return atomic.incrementAndGet();
	}

	public long get(String spCode) {
     
		AtomicLong atomic = accMap.get(spCode);
		return atomic != null ? atomic.get() : 0;
	}

}

2.5 发送回执多线程异步获取

短信请求后, 短信服务商的回应只是说这笔请求已经收到,但并不是真正发送的结果,
获取的的方式有2种,

  1. 查询,一般可以查询最近一个时段的, 查询过的不再返回,
  2. 有些服务商也会主动通知,但主动通知需要暴露一个互联网端口,为了收取短信回执不值得的, 所以采用的比较少。

多线程延时队列,采用延时队列,可以按指定的时间执行任务。

package com.newxtc.sms.delay;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author RAF
 *
 */
public class DelayThread {
     
	// log
	private final static Logger logger = LoggerFactory.getLogger(DelayThread.class);

	private BlockingQueue<Runnable> taskQueue;
	private ThreadPoolExecutor pool;

	private int dispatchQueueSize = 10000;
	private int disatchThreadSize = 4;
	private static DelayThread instance = null;

	public synchronized static DelayThread getInstance() {
     
		if (instance == null) {
     
			instance = new DelayThread();
			System.out.println("DelayThread init ");
		}
		return instance;
	}

	/**
	 * 
	 * @param config_path
	 *            配置路径
	 * @param threadName
	 *            线程名称, 线程的配置文件为:配置路径 + 线程名称.ini
	 * @param taskEntityCls
	 *            任务实体类 , 必须实现ScanTaskApi 接口
	 * @param runCls
	 *            任务运行类, 必须实现 ThreadRunApi 接口
	 */

	public DelayThread() {
     
		try {
     
			taskQueue = new LinkedBlockingQueue<Runnable>(dispatchQueueSize);
			pool = new ThreadPoolExecutor(disatchThreadSize, disatchThreadSize * 2, 10 * 1000L, TimeUnit.MILLISECONDS, taskQueue, new ThreadPoolExecutor.AbortPolicy());
		} catch (Exception e) {
     
			logger.error(e.toString());
		}
	}

	public ThreadPoolExecutor getThreadPoolExecutor() {
     
		return this.pool;
	}

	public void addTask(DelayTask task) {
     
		try {
     
			pool.execute(task);
		} catch (RejectedExecutionException e) {
     
			logger.error(e.toString());
		} catch (Throwable e) {
     
			logger.error(e.toString());
		}
	}
}

执行任务

package com.newxtc.sms.delay;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.newxtc.sms.SmsApi;
import com.newxtc.sms.delay.cache.DelayCache;
import com.newxtc.sms.delay.entity.Delay;

public class DelayTask implements Runnable {
     
	private final static Logger logger = LoggerFactory.getLogger(DelayTask.class);
	private SmsApi smsApi = null;
	private String uniqId = null; // 支持根据具体单号来查询

	private int repeat = 0;

	public DelayTask(String uniqId, SmsApi smsApi) {
     
		this.uniqId = uniqId;
		this.smsApi = smsApi;
	}

	public String getUniqId() {
     
		return uniqId;
	}

	@Override
	public void run() {
     
		try {
     
			repeat++;
			if (smsApi == null) {
     
				logger.error("uniqId=" + uniqId + "|smsApi=" + smsApi);
				return;
			}
			int ret = smsApi.getReport();
			// 碰到错误返回,最多重试3次,避免系统性问题导致重复调用
			if (ret != 0 && repeat <= 3) {
     
				Delay delay = new Delay();
				delay.setTask(this);
				delay.setE(System.currentTimeMillis() + 10000);
				DelayCache.getInstance().put(delay);
				logger.debug("delayTask repeat=" + repeat + "|next time=" + delay.getE());
			}
		} catch (Throwable e) {
     
			logger.error("run Error", e);
			for (StackTraceElement ele : e.getStackTrace())
				logger.error(ele.toString());
		}
	}

}

2.6 短信调用测试

测试类
com.newxtc.sms.SmsProvider

public static void main(String[] args) throws Exception {
     
		SmsInit.getInstance().init(null);
		String templateId = "1";
		String templateJson = "{\"code\":\"8988788\"}";
		for (int i = 0; i < 20; i++) {
     
			SmsProvider.sendTpSms("13718211912", templateId, templateJson);
			Thread.sleep(20000);
		}

		Map<String, AtomicLong> m = SpAccCache.getInstance().getAccMap();
		for (String spCode : m.keySet()) {
     
			Long l = m.get(spCode).get();
			System.out.println(spCode + "=" + l);
		}

	}

测试日志:
SmsConfig init success totalWeight=100
|–route={izton=30, ztinfo=50, aliyun=20}
|–key=izton
|–key=emay
|–key=tzhl
|–key=aliyun
|–key=ztinfo
|–key=monyun
|–route_msg={izton=30, ztinfo=50, aliyun=20}
templateSign=新昕科技
template=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|-1->您的验证码是:${code},有效期为1分钟。如非本人操作,可不用理会。
|-2->您购买的商品已支付成功,支付金额 p a y p r i c e 元 , 订 单 号 {pay_price}元,订单号 payprice{order_id},感谢您的光临!
|-3->亲爱的用户${nickname},您的商品 s t o r e n a m e , 订 单 号 {store_name},订单号 storename{order_id}已发货,请注意查收
|-4->亲,您的订单 o r d e r i d , 商 品 {order_id},商品 orderid,{store_name}已确认收货,感谢您的光临!
|-5-> a d m i n n a m e 管 理 员 , 您 有 一 笔 已 支 付 的 订 单 待 处 理 , 订 单 号 为 {admin_name}管理员,您有一笔已支付的订单待处理,订单号为 adminname{order_id}!
|-6-> a d m i n n a m e 管 理 员 , 您 有 一 笔 支 付 成 功 的 订 单 待 处 理 , 订 单 号 {admin_name}管理员,您有一笔支付成功的订单待处理,订单号 adminname,{order_id}!
|-7-> a d m i n n a m e 管 理 员 , 您 有 一 笔 退 款 订 单 待 处 理 , 订 单 号 {admin_name}管理员,您有一笔退款订单待处理,订单号 adminname,退{order_id}!
|-8-> a d m i n n a m e 管 理 员 , 您 有 一 笔 订 单 已 经 确 认 收 货 , 订 单 号 {admin_name}管理员,您有一笔订单已经确认收货,订单号 adminname,{order_id}!
|-9->您有未付款订单,订单号为: o r d e r i d , 商 品 数 量 有 限 , 请 及 时 付 款 。 ∣ − 10 − > 您 的 订 单 {order_id},商品数量有限,请及时付款。 |-10->您的订单 orderid10>{order_id},实际支付金额已被修改为${pay_price}
aliyun=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|-1->SMS_193517056
|-2->SMS_193506971
|-3->SMS_193521858
|-4->SMS_193521862
|-5->SMS_193516931
|-6->SMS_193511960
|-7->SMS_193511961
|-8->SMS_193516936
|-9->SMS_193506990
|-10->SMS_193516944
19:14:12.163 [main] ERROR com.newxtc.sms.impl.AliyunSmsImpl - sendTpSms() paramsMap={Format=JSON, SignName=新昕科技, SignatureMethod=HMAC-SHA1, TemplateCode=SMS_193517056, Signature=aBR/qWCZb8CqP709yDWdKEJ1i6Q=, Timestamp=2021-02-07T11:14:11Z, TemplateParam={“code”:“8988788”}, OutId=13718211912-1612696451861, AccessKeyId=, Action=SendSms, RegionId=cn-hangzhou, SignatureNonce=6236648b-937a-4cf1-bd42-2ddac5540d32, SignatureVersion=1.0, Version=2017-05-25, PhoneNumbers=13718211912}|response=
19:14:12.166 [main] ERROR com.newxtc.sms.SmsProvider - first ret=-99
19:14:12.166 [main] DEBUG com.newxtc.sms.SmsProvider - getRoute() spCode=izton|from=0.0|destn=0.3
19:14:12.167 [main] DEBUG com.newxtc.sms.impl.IztonSmsImpl - sendTpSms() {code=8988788}
19:14:12.201 [main] INFO com.newxtc.sms.impl.IztonSmsImpl - sendSms() spCode=izton|phone=13718211912|total=1|msg=【新昕科技】您的验证码是:8988788,有效期为1分钟。如非本人操作,可不用理会。
19:14:12.202 [main] ERROR com.newxtc.sms.SmsProvider - first ret=-1
19:14:32.202 [main] DEBUG com.newxtc.sms.SmsProvider - getRoute() spCode=ztinfo|from=0.0|destn=0.5
19:14:32.203 [main] DEBUG com.newxtc.sms.impl.ZtinfoSmsImpl - sendTpSms() {code=8988788}
19:14:32.335 [main] ERROR com.newxtc.sms.impl.ZtinfoSmsImpl - sendSms() jsonObj={“content”:"【新昕科技】您的验证码是:8988788,有效期为1分钟。如非本人操作,可不用理会。",“username”:"",“tKey”:“1612696472”,“extend”:“13718211912-1612696472”,“password”:“e7e2bc55be19eedae481fcb398c7c891”,“mobile”:“13718211912”}|response={“code”:4001,“msg”:“username wrong”,“msgId”:“161269646780396789761”,“contNum”:0}
19:14:32.336 [main] ERROR com.newxtc.sms.SmsProvider - first ret=4001

代码参考: 下载地址

3 短信接口征集

百家企业短信网关(背景及核心代码)-1-开源项目短信接口征集_第5张图片

目前我们的接口数不到10+,计划向短信服务商及技术大牛收集更多的短信接口,
如果您需要对接的短信服务商不在列表中,还请告诉我们哦
我们的目标是: 对接任何一家短信接口,不需要写代码,只需要配置参数
欢迎提供更多的短信接口,如果短信通道被采纳,我们将对评价排前的提供一定的奖品哦

相关阅读:
百家企业短信网关(背景及核心代码)-1-同时对接多家短信公司的开源免费代码
2021全网最全短信服务商排名(100余家短信商户对照)
最早的支付网关(滴滴支付)和最新的聚合支付设计架构
比 REG007 更好用的查询手机注册网站的神器

你可能感兴趣的:(网关)