代理模式的理解:
- 在实际的生活中代理情况是无处不在的,找代理买东西,请朋友帮忙拿东西,在这个过程中引入了新的角色即"第三者"的角色来帮助你完成某一件事,这里的第三者我们称之为代理者
- 通过引用一个新的对象来实现对真实对象的一个替身,这种实现的机制就是代理模式,通过引用代理对象来访问真实对象就是代理模式的设计动机
image
代理模式的定义
- 代理模式就是给一个对象提供一个代理,并由==对象控制对原对象的引用==
代理模式应用的场景
- 当我们无法或者不想直接访问某一个对象,那么可以通过一个代理对象间接访问,同时为了保证客户端透明性,委托对象与代理对象需要同时实现相同接口
代理模式的结构(角色的划分)
- Subject:抽象角色,目标接口,声明真实对象和代理对象的共同接口
- RealSubject:真实对象(即目标对象),是我们最终要引用的对象
- Proxy:代理类.代理对象与真实对象实现相同的接口,所以它能够在任何时刻都能够代理真实对象.代理角色内部包含有对真实对象的引用,所以她可以操作真实的对象,同时也可以附加其他的操作,相当于对真实对象进行封装
- Client:客户端,直接调用代理
模式的实现
- 服务器支付的集成(微信支付 银联支付)
- 首先来整体介绍使用代理模式实现服务器端的设计步骤:
1.支付对象设计Charge(专门用于返回给客户端)
//付款对象
public class Charge {
// 定义支付错误码
public enum ChargeError {
// 支付凭证或者成功
credentialGetSuccess("0", "支付凭证获取成功"),
// 支付凭证或者失败
credentialGetError("1", "支付凭证获取错误"),
// 支付凭证请求错误
credentialRequestError("2", "支付凭证请求错误"),
// 支付签名成功
sineSuccess("10", "支付凭证请求错误"),
// 支付签名错误
sineError("11", "支付凭证请求错误");
private String code;
private String msg;
private ChargeError(String code, String msg) {
this.code = code;
this.msg = msg;
}
public String getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
// 解决共性问题:支付统一参数(20个参数不等)
// 付款ID
private String id;
// 支付创建时间
private Long created;
// 支付渠道(微信支付渠道、银联支付渠道、支付宝支付渠道、苹果支付渠道)
private String channel;
// 订单号(每一个支付渠道:订单号长度不相同)
private String orderNo;
// 客户端IP
private String clientIP;
// 订单的金额(单位:分)
private int amount;
// 商品标题
private String subject;
// 商品描述
private String body;
// 订单错误码
private String failCode;
// 订单错误信息
private String failMsg;
// 解决差异问题:客户端支付凭证
// 分析
// 微信支付(返回支付凭证:appid、partnerid、prepayid、package、noncestr、timestamp、sign)
// 银联支付(返回支付凭证:tn、mode)
// 支付宝支付(返回支付凭证:sign)
// Map类似于iOS中字典Dictionary
private Map<String, Object> credential;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Long getCreated() {
return created;
}
public void setCreated(Long created) {
this.created = created;
}
public String getOrderNo() {
return orderNo;
}
public void setOrderNo(String orderNo) {
this.orderNo = orderNo;
}
public String getClientIP() {
return clientIP;
}
public void setClientIP(String clientIP) {
this.clientIP = clientIP;
}
public int getAmount() {
return amount;
}
public void setAmount(int amount) {
this.amount = amount;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public String getFailCode() {
return failCode;
}
public void setFailCode(String failCode) {
this.failCode = failCode;
}
public String getFailMsg() {
return failMsg;
}
public void setFailMsg(String failMsg) {
this.failMsg = failMsg;
}
public Map<String, Object> getCredential() {
return credential;
}
public void setCredential(Map<String, Object> credential) {
this.credential = credential;
}
public String getChannel() {
return channel;
}
public void setChannel(String channel) {
this.channel = channel;
}
public void setFail(ChargeError error) {
this.setFailCode(error.getCode());
this.setFailMsg(error.getMsg());
}
}
- 从上面可以看出,微信支付和银联支付有一些共性参数和一些差异问题,对于差异问题可以直接使用Map集合来处理
2.支付框架中(代理模式的设计)
- 对代理模式角色的分析:
- 1.角色一(Subject):抽象角色(目标接口)-->支付的接口-->IPay
//代理模式:目标接口(类似于iOS中协议)
public interface IPay {
/**
* 支付
* @param req 请求
* @param resp 响应
* @param params 请求第三方支付服务器参数
* @return
*/
Charge pay(HttpServletRequest req, HttpServletResponse resp,Map<String,Object> params);
}
2.角色二(RealSubject):目标的对象
- 第一个实现类:目标对象-->银联支付-->UnionPay 实现接口<IPay>
- 第二个实现类:目标对象-->微信支付-->WxPay 实现接口<IPay>
银联UnionPay
//银联支付:被代理对象(目标对象)
public class UnionPay implements IPay {
@Override
public Charge pay(HttpServletRequest req, HttpServletResponse resp, Map<String, Object> params) {
String amount = params.get("amount").toString();
String channel = params.get("channel").toString();
// 商户号(注册银联账号,并且申请开通银联支付账号)
// 测试账号:777290058110097
String merId = "777290058110048";
Date date = new Date();
String format = new SimpleDateFormat("yyyyMMddHHmmss").format(date);
// 交易金额
String txnAmt = amount;
// 商户订单号
String orderId = format;
// 订单发送时间
String txnTime = format;
// 第一步:拼接订单参数列表
Map<String, String> contentData = new HashMap<String, String>();
contentData.put("version", DemoBase.version);
contentData.put("encoding", DemoBase.encoding);
// 签名方法(01: 表示采用RSA签名)
contentData.put("signMethod", SDKConfig.getConfig().getSignMethod());
// 交易类型 01:消费(支付)
// 取值:00:查询交易,01:消费,02:预授权,03:预授权完成,
// 04:退货,05:圈存,11:代收,12:代付,13:账单支付,14:转账(保留),
// 21:批量交易,22:批量查询,31:消费撤销,32:预授权撤销,33:预授权完成撤销,
// 71:余额查询,72:实名认证-建立绑定关系,73:账单查询,74:解除绑定关系,
// 75:查询绑定关系,77:发送短信验证码交易,78:开通查询交易,79:开通交易,
// 94:IC卡脚本通知 95:查询更新加密公钥证书
contentData.put("txnType", "01");
// 交易子类 01:消费
contentData.put("txnSubType", "01");
// 产品类型
// 依据实际业务场景填写(目前仅使用后 4 位,签名 2 位 默认为 00)
// 默认取值:000000 具体取值范围: 000201:B2C 网关支付
// 000301:认证支付 2.0 000302:评级支付 000401:代付 000501:代收
// 000601:账单支付 000801:跨行收单 000901:绑定支付 001001:订购 000202:B2B
// 填写000201
contentData.put("bizType", "000201");
// 05:语音 07:互联网 08:移动 16:数字机顶盒
// 渠道类型 08手机
contentData.put("channelType", "08");
/*** 商户接入参数 ***/
// 商户号码,请改成自己申请的商户号或者open上注册得来的777商户号测试
// 测试账号:777290058110048
contentData.put("merId", merId);
// 接入类型,商户接入填0 ,不需修改(0:直连商户, 1: 收单机构 2:平台商户)
contentData.put("accessType", "0");
// 商户订单号,8-40位数字字母,不能含“-”或“_”,可以自行定制规则
contentData.put("orderId", orderId);
// 订单发送时间,取系统时间,格式为YYYYMMDDhhmmss,必须取当前时间,否则会报txnTime无效
contentData.put("txnTime", txnTime);
// 账号类型 01:银行卡 02:存折 03:IC卡帐号类型(卡介质)
contentData.put("accType", "01");
// 交易金额 单位为分,不能带小数点(服务器规定金额单位,客户端统一传递)
// 一般情况:采用分为基本单位(一般支付服务器开发标准)
contentData.put("txnAmt", txnAmt);
// 境内商户固定 156 人民币
contentData.put("currencyCode", "156");
// 请求方保留域,
// 透传字段,查询、通知、对账文件中均会原样出现,如有需要请启用并修改自己希望透传的数据。
// 出现部分特殊字符时可能影响解析,请按下面建议的方式填写:
// 1. 如果能确定内容不会出现&={}[]"'等符号时,可以直接填写数据,建议的方法如下。
// contentData.put("reqReserved", "透传信息1|透传信息2|透传信息3");
// 2. 内容可能出现&={}[]"'符号时:
// 1) 如果需要对账文件里能显示,可将字符替换成全角&={}【】“‘字符(自己写代码,此处不演示);
// 2) 如果对账文件没有显示要求,可做一下base64(如下)。
// 注意控制数据长度,实际传输的数据长度不能超过1024位。
// 查询、通知等接口解析时使用new String(Base64.decodeBase64(reqReserved),
// DemoBase.encoding);解base64后再对数据做后续解析。
// contentData.put("reqReserved",
// Base64.encodeBase64String("任意格式的信息都可以".toString().getBytes(DemoBase.encoding)));
// 后台通知地址(需设置为外网能访问 http
// https均可),支付成功后银联会自动将异步通知报文post到商户上送的该地址,【支付失败的交易银联不会发送后台通知】
// 后台通知参数详见open.unionpay.com帮助中心 下载 产品接口规范 网关支付产品接口规范 消费交易 商户通知
// 注意:1.需设置为外网能访问,否则收不到通知 2.http https均可 3.收单后台通知后需要10秒内返回http200或302状态码
// 4.如果银联通知服务器发送通知后10秒内未收到返回状态码或者应答码非http200或302,那么银联会间隔一段时间再次发送。总共发送5次,银联后续间隔1、2、4、5
// 分钟后会再次通知。
// 5.后台通知地址如果上送了带有?的参数,例如:http://abc/web?a=b&c=d
// 在后台通知处理程序验证签名之前需要编写逻辑将这些字段去掉再验签,否则将会验签失败
contentData.put("backUrl", DemoBase.backUrl);
/** 对请求参数进行签名并发送http post请求,接收同步应答报文 **/
// 第二步:签名处理
// 报文中certId,signature的值是在signData方法中获取并自动赋值的,只要证书配置正确即可。
// 参数一:需要签名的参数集合
// 参数二:签名的字符编码
Map<String, String> reqData = AcpService.sign(contentData, DemoBase.encoding);
// 交易请求url从配置文件读取对应属性文件acp_sdk.properties中的
// acpsdk.backTransUrl
String requestAppUrl = SDKConfig.getConfig().getAppRequestUrl();
// 第三步:发起银联服务器请求(获取tn:支付凭证)
// 发送请求报文并接受同步应答(默认连接超时时间30秒,读取返回结果超时时间30秒);
// 这里调用signData之后,调用submitUrl之前不能对submitFromData中的键值对做任何修改,
// 如果修改会导致验签不通过
Map<String, String> rspData = AcpService.post(reqData, requestAppUrl, DemoBase.encoding);
// 创建支付凭证对象
Charge charge = new Charge();
charge.setAmount(Integer.valueOf(amount));
charge.setBody("爱学宝");
charge.setChannel(channel);
charge.setClientIP(req.getRemoteAddr());
charge.setCreated(date.getTime());
charge.setOrderNo(orderId);
if (!rspData.isEmpty()) {
// 数据库处理(保存订单信息)
// 判断签名验证是否通过
if (AcpService.validate(rspData, DemoBase.encoding)) {
LogUtil.writeLog("验证签名成功");
String respCode = rspData.get("respCode");
if (("00").equals(respCode)) {
// 状态码 = 00 说明获取到了支付凭证
// 成功,获取tn号
String tn = rspData.get("tn");
Map<String, Object> credential = new HashMap<String, Object>();
credential.put("tn", tn);
credential.put("mode", "01");
// TODO
// 将银联服务器返回数据--->Json数据格式+自己服务器属性
// 目前这个数据格式不是标准的json,所以说你直接解析,会报错
// 设置支付凭证
charge.setCredential(credential);
charge.setFail(ChargeError.sineSuccess);
} else {
// 其他应答码为失败请排查原因或做失败处理
charge.setFail(ChargeError.sineError);
}
} else {
LogUtil.writeErrorLog("验证签名失败");
charge.setFail(ChargeError.sineError);
}
} else {
charge.setFail(ChargeError.credentialRequestError);
// 未返回正确的http状态
LogUtil.writeErrorLog("未获取到返回报文或返回http状态码非200");
}
return charge;
}
}
微信WxPay
//微信支付:目标对象(被代理对象)
public class WxPay implements IPay {
// 首先:定义支付应用ID、商户ID等等...
// 应用ID
public static String APP_ID = ConstantUtil.APP_ID;
// 商户ID
public static String PARTNER_ID = ConstantUtil.PARTNER_ID;
// 商户好对应的密钥
public static String PARTNER_KEY = ConstantUtil.PARTNER_KEY;
// 统一下单的接口(微信支付服务器提供的)
public static String URL_UNIFIEDORDER = ConstantUtil.URL_UNIFIEDORDER;
@Override
public Charge pay(HttpServletRequest req, HttpServletResponse resp, Map<String, Object> params) {
String amount = params.get("amount").toString();
String channel = params.get("channel").toString();
// 第1步-签名订单信息
// 第一点:获取客户端传递过来的参数
// req:客户端请求
// resp:响应客户端请求
// 第二点:设置访问微信支付服务器请求数据类型
resp.reset();
resp.setHeader("ContentType", "text/xml");
// 第三点:创建请求微信支付服务器参数集合
// 做了请求封装
// 微信写好一个封装案例,你可以根据服务器需求,自己定义网络请求框架
PrepayIdRequestHandler handler = new PrepayIdRequestHandler(req, resp);
// 统一下单的接口(调用微信支付服务器需要的接口)--->公开的
handler.setGateUrl(ConstantUtil.URL_UNIFIEDORDER);
// 设置密钥
handler.setKey(PARTNER_KEY);
// 设置应用的ID
handler.setParameter("appid", APP_ID);
// 商户号
handler.setParameter("mch_id", PARTNER_ID);
// 随机字符串
handler.setParameter("nonce_str", WXUtil.getNonceStr());
// 商品描述(例如:天天爱消除-游戏充值)
handler.setParameter("body", "爱学宝-笔记本");
// 商户订单号(自己服务器生成订单号)
String out_trade_no = OrderUtils.getOrderNumber();
handler.setParameter("out_trade_no", out_trade_no);
// 总金额
handler.setParameter("total_fee", amount);
// e8ab8a8d4bc8be24ccac804344ef0ef3
// 终端IP(客户端IP)
handler.setParameter("spbill_create_ip", req.getRemoteAddr());
// 通知地址(微信服务器回调商户服务器页面)
handler.setParameter("notify_url", ConstantUtil.NOTIFY_URL);
// 交易类型
handler.setParameter("trade_type", "APP");
// 第四点:对我们订单信息进行签名
String sign = handler.createMD5Sign();
// 设置签名
// 问题一(排序:可能不会在最后,隐藏潜在危机)
// handler.setParameter("sign", sign);
// 不参与排序,并且将签名参数放置最后
handler.setSign(sign);
// 第2步-调用微信统一下单接口(目的:获取prepay_id)
// command+1
Charge charge = new Charge();
charge.setAmount(Integer.valueOf(amount));
charge.setBody("爱学宝");
charge.setChannel(channel);
charge.setClientIP(req.getRemoteAddr());
charge.setOrderNo(out_trade_no);
try {
Map paramsMap = handler.sendPrepay();
String prepay_id = (String) paramsMap.get("prepay_id");
if (prepay_id != null && !"".equals(prepay_id)) {
// 统一下单接口调用成功
// 第3步-处理接口返回信息进行二次签名
// 调用支付接口:https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_12&index=2
// 二次签名和一次签名,参数不一样(复用之前参数集合)
// 请求集合列表(情空:不影响新的参数签名)
// 微信服务器要求
handler.clear();
String noncestr = (String) paramsMap.get("nonce_str");
String timestamp = WXUtil.getTimeStamp();
charge.setCreated(Long.valueOf(timestamp));
// 密钥
handler.setKey(PARTNER_KEY);
// 设置应用的ID
handler.setParameter("appid", APP_ID);
// 预付单ID
handler.setParameter("prepayid", prepay_id);
// 扩展字段
handler.setParameter("package", "Sign=WXPay");
// 商户号
handler.setParameter("partnerid", PARTNER_ID);
// 随机字符串
handler.setParameter("noncestr", noncestr);
// 时间戳
handler.setParameter("timestamp", timestamp);
// 进行二次签名(签名参数不一样)
// 第一次签名:对订单信息签名,获取prepay_id
// 第二次签名:对支付信息进行签名
sign = handler.createMD5Sign();
// 第4步-将签名信息返回客户端(xml、json都可以)
// 采用Json解析(构建json--->返回客户端)
Map<String, Object> credential = new HashMap<String, Object>();
credential.put("appid", APP_ID);
credential.put("noncestr", noncestr);
credential.put("packageValue", "Sign=WXPay");
credential.put("partnerid", PARTNER_ID);
credential.put("prepayid", prepay_id);
credential.put("tradeType", (String) paramsMap.get("trade_type"));
credential.put("sign", sign);
credential.put("timestamp", timestamp);
charge.setCredential(credential);
charge.setFail(ChargeError.sineSuccess);
} else {
charge.setFail(ChargeError.sineError);
}
} catch (Exception e) {
e.printStackTrace();
charge.setFail(ChargeError.credentialRequestError);
}
return charge;
}
}
2.角色三(Proxy):代理对象--->PayEngine
//代理对象(同时也是一个工厂类)
public class PayEngine implements IPay {
private static PayEngine payEngine;
private Map<String, IPay> channelPay;
private PayEngine() {
registerPayChannel();
}
public static PayEngine getInstance() {
if (payEngine == null) {
payEngine = new PayEngine();
}
return payEngine;
}
private void registerPayChannel() {
// 注册支付渠道
channelPay = new HashMap<String, IPay>();
channelPay.put("wxpay", new WxPay());
channelPay.put("unionpay", new UnionPay());
}
@Override
public Charge pay(HttpServletRequest req, HttpServletResponse resp, Map<String, Object> params) {
// 根据支付的凭证调用具体的支付渠道
// if判断
String channel = params.get("channel").toString();
return channelPay.get(channel).pay(req, resp, params);
}
}
- 这个代理对象PayEngine这里是通过Map集合来持有目标对象的引用,在实现的接口方法中,根据传入的渠道来判断调用哪一个支付
4.角色四(Client):客户端
- 实现一个接口来调用
- 客户端-->接口-->PayServlet
//支付统一接口
public class PayServlet extends HttpServlet {
private static final long serialVersionUID = 6717470124590741929L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 第一步:将客户端数据转成json
try {
Map<String, Object> params = JsonReader.jsontoMap(req);
// 第二步:调用支付
// 返回支付凭证
Charge charge = PayEngine.getInstance().pay(req, resp, params);
// 第三步:返回给客户端
resp.getWriter().write(new Gson().toJson(charge));
} catch (JSONException e) {
e.printStackTrace();
}
}
}
网友评论