现状
- 每种业务均有可能大于1个渠道。
- 每个渠道均有一些限制条件,例如:省份屏蔽,且不可及时预知。
- 每个业务的渠道,可能会有人工定制优先级的需求。
目的
通过手机号码归属地找到最合适的通道,提高用户的消费转化率。
省份
1、河北省 2、山西省 3、辽宁省 4、吉林省 5、黑龙江省 6、江苏省 7、浙江省 8、安徽省 9、福建省 10、江西省 11、山东省 12、河南省 13、湖北省 14、湖南省 15、广东省 16、海南省 17、四川省 18、贵州省 19、云南省 20、陕西省 21、甘肃省 22、青海省 23、台湾省
OO流程
如果 count(sp_id) = 0
1.返回不支持该类型
**如果count(sp_id) > 1 **
- 按照优先级顺序轮询available_id,调用口扣款接口,轮询次数 <N ,调用扣费接口,记录available_id,create_time,status,如果有成功扣款,则返回扣款成功,如果均失败,则返回不支持该类型。
自动隔离机制
- 在 x min内,该available_id 失败次数 > N,把该available_id的in_use置为0
- 在 x min内,该available_id 接口超时次数 > M ,把该available_id的in_use置为0
手动隔离机制
- 业务调整等其他原因,人工把该available_id 的in_use置为0
自动恢复机制
- 每隔一个小时,让一部分流量切到available_id状态为in_use = 0上,如果 x min available_id 的成功次数大于 N次,则把该availe_id的in_use置为1。
手动恢复机制
不需要,人工干预恢复,人工只能干预隔离。
通过spark实时统计,把满足规则的available_id 通过mq 给监控程序,让监控程序处理。
HJ流程
直接轮询所有可能性渠道。
特点
- 不是所有的渠道都提供专门的手机号是否能下单的接口。
- 事后,标记省份屏蔽的订单,来提高订单率,属于被动型策略,每个时期,省份屏蔽的名单也在变化,不利于以后维护统计。
- 轮询次数多的话,影响效率。
区别
- 事前防御VS事后防御。
- 前者轮询是固定的值,是为了拿真实的一部分流量嗅探接口的可用性;后者随着可用渠道接口增加,轮询数加大。
是否支持省份屏蔽告知功能
渠道 | 是否支持 | 现在情况 | 错误码 |
---|---|---|---|
优易付 | 支持 | 预下单 | 20940 |
空中网 | 不支持 | 目前内部通知 | 待提供相应的接口 |
小沃 | 支持 | 错误码待定 | 暂无 |
赞成 | 不支持 | 目前没有屏蔽省份 | 错误码待确认 |
爱动漫 | 不支持 | 待提供 | 暂无 |
翼支付 | 支持 | 提供 | 010061 |
渠道列表
- 移动包月 S1
优易付 AS
- 移动点播 S2
优易付 AT
空中网 H1
- 联通包月 S3
小沃 UP
- 联通点播 S4
赞成 LT
- 电信包月 S5
爱动漫 TP
优易付 A4
翼支付 Y1
- 电信点播 S6
优易付 AR
翼支付 Y2
方案
方案一:统一处理法。
- 设置几个支付类型 S1 S2 S3 S4 S5 S6 含义见上面。
- 抽象新的command 结构为:verifyParams doRequest saveOrder dealTemplate
- verifyParams 校验公共参数 doRequest 调用工具类得到请求结果以及实际的paytype saveOrder 根据实际的类型,插入实际的渠道表 dealTemplate 根据实际类型处理
工具类 :
方法:getRealPayType
输入:公共请求参数 commonparam包含paytype
输出:请求结果
{"status":1,"code":"success200","msg":"成功","realpaytype":"AS"}
{"status":0,"code":"错误码","msg":"失败","realpaytype":"AR"}
逻辑:
- 统一校验参数。
- 根据paytype,查找可用的url,按优先级顺序轮询请求url,只有当请求失败且错误码为省被屏蔽的情况下,才轮询。
注意点:
- 工具类方法并发同步控制。
- 请求第三方接口超时处理。
表设计:
paytype
-
移动包月 S1
-
移动点播 S2
-
联通包月 S3
-
联通点播 S4
-
电信包月 S5
-
电信点播 S6
sp
id | sp_name | url | in_use | create_time | update_time |
---|---|---|---|---|---|
1 | 优易付 | http://113.31.25.56:23000/sdkfee/api2/create_order | 1 | 2016-06-24 19:25:33 | 2016-06-24 19:25:33 |
2 | 空中网 | http://113.31.25.56:23000/sdkfee/api2/create_order | 1 | 2016-06-24 19:25:33 | 2016-06-24 19:25:33 |
3 | 小沃 | http://113.31.25.56:23000/sdkfee/api2/create_order | 1 | 2016-06-24 19:25:33 | 2016-06-24 19:25:33 |
4 | 赞成 | http://113.31.25.56:23000/sdkfee/api2/create_order | 1 | 2016-06-24 19:25:33 | 2016-06-24 19:25:33 |
5 | 爱动漫 | http://113.31.25.56:23000/sdkfee/api2/create_order | 1 | 2016-06-24 19:25:33 | 2016-06-24 19:25:33 |
6 | 翼支付 | http://113.31.25.56:23000/sdkfee/api2/create_order | 1 | 2016-06-24 19:25:33 | 2016-06-24 19:25:33 |
available_sp
id | sp_id | sp_name | paytype | province_code | priority | in_use | mode | create_time | update_time |
---|---|---|---|---|---|---|---|---|---|
1 | 1 | 优易付 | S1 | 110000 | 1 | 1 | Manual | 2016-06-24 19:25:33 | 2016-06-24 19:25:33 |
2 | 1 | 优易付 | S2 | 120000 | 1 | 1 | Manual | 2016-06-24 19:25:33 | 2016-06-24 19:25:33 |
3 | 2 | 空中网 | S1 | 130000 | 2 | 1 | Manual | 2016-06-24 19:25:33 | 2016-06-24 19:25:33 |
总结:
- 改动量比较大,涉及到重构公共模板command,相对应的这些渠道都要重构。
- 公共参数不容易校验,不同第三方sp的接口定义不一样,有一些字段可用,有一些不可用。
- 难统一不同渠道的返回值,又需要重构定义,前端也要改变。
- 还需要配合监控程序去修改available_sp表,系统复杂度比较高。
方案二:提前预判法
- 建立一个分发Command,当虚拟类型S1,S2,S3,S4,S5,S6,过来的时候,先提前调用工具类,确定实际支付方式,然后分发到正确的Command,此时Command,不需要再去请求第三方接口,只用处理后续逻辑即可。
- command 结构: verifyParams doProcess saveOrder dealTemplate
总结:
- 与方案一相比,改动量较小一点,但是相关渠道也要重构。
方案三:事后处理法
- 根据策略选择(排除掉最近已经失败N次的且优先级最高的paytype,如果没有直接返回错误)的command。
- 每个渠道在请求第三方处理结果的时候,专门处理错误码为省不支持的,记录到表里。
表设计:
sp
|id|real_pay_type|priority|pay_type|create_time|update_time|
|--|--|--|--|--|--|--|--|--|--|--|
|1|AT|1|S2|2016-06-24 19:25:33|2016-06-24 19:25:33|
|2|H1|2|S2|2016-06-24 19:25:33|2016-06-24 19:25:33|
sp_fail
|id|province_code|real_pay_type|create_time|update_time|
|--|--|--|--|
|1|110000|AT|2016-06-24 19:25:33|2016-06-24 19:25:33|
|2|100000|H1|2016-06-24 19:25:33|2016-06-24 19:25:33|
总结:
- 改动量比较小,只需要在command前加一层分发,专门处理S1,S2 类型。
- 不需要额外的监控程序去更新可用规则表。
- 看似简单,实则包含自动隔离以及自动恢复机制,只需要控制最近时间的粒度即可。
方案四:产品流程法(建议)
- 给前端提供一个接口(入参:mobile,pay_type 返回real_pay_type),获得真实的支付类型,如果没有可用的支付类型,直接返回不支持,前端就不用调真正的下单接口了;如果有的话,则还是按以前的方式下单即可,可以考虑整合在发验证码接口里,或者点发验证码接口时候调用这个独立的接口。
- 获得真实的支付类型原理同方案三。
总结:
- 无论前端还是后台,改动量都最小,最适合,甚至为了减轻这个接口的压力,可以考虑缓存一段时间,前端,后端缓存都行。
方案五:等待您的新想法
思考
- 如果优先级程序自动根据规则排序的话,可以考虑实现一个优先级队列。
部分代码关于请求第三方原有处理
比较乱、杂,难以统一。
Map<String, String> result = ArSoftChannelUtils.getResultParams(response);
if ("0".equals(result.get("status"))) {
String orderid = result.get("orderid");
logger.info("xunleiPayId:{} get payed orderid that is {}",
request.getOrderId(), orderid);
channelData.setOrderId(orderid);
if (MONTHLY_TYPE.equals(getIsMonthly())) {
// PayOrder payOrder = payOrderDAO.getPayOrder(unitedPayRequest.getXunleiPayId());
// //包月请求建立签约请求
// createContactRequest(payOrder);
}
saveOrder(channelData);
} else {
String status = result.get("status");
String msg = result.get("msg");
Template errorTemplate = getErrorTemplate(request, status, msg);
channelData.setBackTemplate(errorTemplate);
}
ExtUniMonthPayResponse response = ExtUniMonthPayUtil.sendIdentifyingCode(payRequest);
// 解析沃商店下行短信接口响应
String status = response.getStatus();
String woorderid = response.getWoorderid();
Extunimonthpay extunimonthpay = new Extunimonthpay();
extunimonthpay.setOrderid(request.getXunleipayid());
extunimonthpay = facade.findExtunimonthpay(extunimonthpay);
if ("00000".equals(status)) {
if (null == woorderid || "".equals(woorderid)) {
LOG.error("orderid:{},get woorderid is empty!", orderid);
throw new Exception("woorderid is empty!");
}
// 短信下发成功,将woorderid保存入库
extunimonthpay.setWoorderid(response.getWoorderid());
facade.updateExtunimonthpay(extunimonthpay);
Template resTemplate = getSuccessTemplate(request, orderid);
channelData.setTemplate(resTemplate);
return;
} else {
// 短信下发失败,订单直接就置为失败了
extunimonthpay.setOrderstatus("F");// 设置为失败
extunimonthpay.setErrcode(status);
extunimonthpay.setErrmsg(ExtUniMonthPayResponseDesc.getDesc(status));
facade.updateExtunimonthpay(extunimonthpay);
updateBizorderStatus(request.getXunleipayid(), status, PayProxyFunctionConstant.PAY_STATUS_FAIL);// 支付失败
LOG.error("orderid:{},queryIdentifyingCode fail!status:{}", new Object[] { orderid, status });
Template template = getFailTemplate(request, PayproxyRtnCode.ERROR_UNDEFINE_ERROR, ExtUniMonthPayResponseDesc.getDesc(status));
channelData.setBackTemplate(template);
return;
}
String resp = HttpGetAndPostSender.sendPost(ExtumpAuthorityUtil.getUrl(), params.toString());
LOG.info("=============response is[{}]=============", resp);
LOG.info(resp);
resp = resp.replace(" ", "");//去空格
LOG.info(resp);
LOG.info("=============response is[{}]=============", resp);
if(resp.contains("OK")) {
Map<String,Object> map = new TreeMap<String,Object>();
map.put("pid", ExtumpAuthorityUtil.getPid());
map.put("svcid", ExtumpAuthorityUtil.getSvcid());
map.put("paymentUser", mobile);
map.put("sign", ExtunionmobilepayUtil.createToken(map,ExtumpAuthorityUtil.getKey()));
String response = HttpClientUtil.doGet(ExtumpAuthorityUtil.getFee_url(), map, null);
LOG.info("=============fee response is[{}]=============", response);
Map<String,String> result = ExtunionmobilepayUtil.json2map(response);
LOG.info("=============get params [{}]============="+result.toString());
String resultCode = result.get("resultCode");
String outTradeNo = result.get("outTradeNo");
extunionmobilepayChannelData.setOutTradeNo(outTradeNo);
LOG.info("=============resultCode is[{}]============="+resultCode);
if(!"0".equals(resultCode)) {
if(resultCode == null) {
String errorCode = result.get("errorCode");
String errorDesc = result.get("errorDesc");
if(errorCode != null) {
throw new ExtumpException(errorDesc, errorCode);
} else {
throw new ExtumpException("未知错误", ExtumpResCode.RTN99.getCode());
}
} else {
String resultDescription = result.get("resultDescription");
throw new ExtumpException(resultDescription, resultCode);
}
}
} else {
// 没有通过梦网的号码检测
String errMsg = "according to WowCheck,mobile[" + mobile + "] was not allowed to order!Errmg is :[" + resp + "]";
throw new ExtumpException(errMsg, ExtumpResCode.RTN1004.getCode());
}
String mobile = data.getOrderInfo().getMobile();
String rescode = data.getOrderInfo().getResCode();
if (rescode.equals(MobileResCode.SUCCESS)) {// 请求爱动漫下发短信成功
Template resTemplate = getSuccessTemplate(request, orderid, mobile);
channelData.setTemplate(resTemplate);
return;
} else if (rescode.equals(MobileResCode.TOKEN_FAILED)) {// 获取token失败
LOG.error("orderId:{},resCode:{}", orderid,rescode);
Template template = getFailTemplate(request, TeleMonthlyResCode.SYSTEM_ERROR, "get token failed");
channelData.setBackTemplate(template);
return;
} else if (rescode.equals(MobileResCode.SMS_FAILED)) {// 获取token正常 但是下行短信出错
LOG.error("orderId:{},resCode:{}", orderid,rescode);
Template template = getFailTemplate(request, TeleMonthlyResCode.SYSTEM_ERROR, "sms captcher failed");
channelData.setBackTemplate(template);
return;
} else {// 其他未知错误
LOG.error("orderId:{},resCode:{}", orderid,rescode);
Template template = getFailTemplate(request, TeleMonthlyResCode.UNDEFINE_ERROR, "sms captcher failed unknown error");
channelData.setBackTemplate(template);
return;
}
评论区