这里记录一个例子,工厂模式的理论就不扯淡了。
遇到的问题:支付方式有很多种,比如微信支付 支付宝支付 银联支付 等等。我们在在实现的时候发现他么的流程上是相似的,以及每个方式都有大量的个性配置,在实例化时需要加载他们,以及为了清晰的讲调用方和实现方进行分离,就有来下面的小设计。
以银联为例,(通过测试)。
共用接口:
public interface CommonPay { /** * <pre> * 功能描述: * 支付预处理:生成支付URL或提供给支付平台的数据 * * @param param 预支付参数 * @return 支付URL或提供给支付平台的数据 * @throws Exception */ <T> PrepayDto<T> prePay(PrepayParam param) throws Exception; }
工厂类:
public class CommonPayFactory implements InitializingBean { /** 银联支付 */ private CommonPay unionPay; @Override public void afterPropertiesSet() throws Exception { if (unionPay == null) { throw new RuntimeException("未配置任何支付实例"); } } public CommonPay getUnionPay() { return unionPay; } public void setUnionPay(CommonPay unionPay) { this.unionPay = unionPay; } }
我们利用spring,将bean初始化信息配置好:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="unionPayConfig" class="com.xxxxxx.cashier.web.union.UnionPayConfig"> <property name="payUrl" value="${pay.unionpay.payUrl}" /> <property name="queryUrl" value="${pay.unionpay.queryUrl}" /> <property name="merId" value="${pay.unionpay.merId}" /> <property name="signPfxFile" value="${pay.unionpay.signPfxFile}" /> <property name="signPfxPwd" value="${pay.unionpay.signPfxPwd}" /> <property name="verifySignCertFile" value="${pay.unionpay.verifySignCertFile}" /> <property name="payNotifyFrontUrl" value="${pay.unionpay.payNotifyFrontUrl}" /> <property name="payNotifyUrl" value="${pay.unionpay.payNotifyUrl}" /> </bean> <!-- 通用支付工厂实例 --> <bean id="commonPayFactory" class="com.xxxxx.cashier.service.pay.CommonPayFactory"> <property name="unionPay"> <bean class="com.xxxxxx.cashier.service.pay.UnionPay"> <property name="UnionPayConfig" ref="unionPayConfig" /> </bean> </property> </bean> </beans>
如此在项目启动时,银联实现会根据配置信息实例化,工厂类会自动加载入银联实例以备获取。
以下是银联实现:
public class UnionPay implements CommonPay { /** Logger */ private static final Logger LOGGER = LoggerFactory.getLogger(UnionPay.class); /** 日期时间格式 */ private static final String DATE_FORMAT_YYYYMMDDHHMMSS = "yyyyMMddHHmmss"; /** 银联支付配置 */ private UnionPayConfig unionPayConfig; @Override public <T> PrepayDto<T> prePay(PrepayParam param) throws Exception { PrepayDto<String> prepayDto = new PrepayDto<>(); // 构造预下单签名参数 Map<String, String> params = new TreeMap<>(); /** 银联全渠道系统,产品参数,除了encoding自行选择外其他不需修改 */ // 版本号,全渠道默认值 params.put("version", UnionPayConfig.VERSION); // 字符集编码,可以使用UTF-8,GBK两种方式 params.put("encoding", UnionPayConfig.CHARSET); // 签名方法,只支持 01:RSA方式证书加密 params.put("signMethod", "01"); // 交易类型 ,01:消费 params.put("txnType", "01"); // 交易子类型, 01:自助消费 params.put("txnSubType", "01"); // 业务类型,B2C网关支付,手机wap支付 params.put("bizType", "000201"); // 渠道类型,这个字段区分B2C网关支付和手机wap支付;07:PC,平板 08:手机 params.put("channelType", "07"); /** 商户接入参数 */ // 商户号码,请改成自己申请的正式商户号或者open上注册得来的777测试商户号 params.put("merId", unionPayConfig.getMerId()); // 接入类型,0:直连商户 params.put("accessType", "0"); // 商户订单号,8-40位数字字母,不能含“-”或“_”,可以自行定制规则 params.put("orderId", param.getOutTradeNo()); // 订单发送时间,取系统时间,格式为YYYYMMDDhhmmss,必须取当前时间,否则会报txnTime无效 SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT_YYYYMMDDHHMMSS); Date orderAddTime = new Date(); String txtTime = sdf.format(orderAddTime); params.put("txnTime", txtTime); // 账号 // 1、 后台类消费交易时上送全卡号或卡号后4位 // 2、 跨行收单且收单机构收集银行卡信息时上送、 // 3、前台类交易可通过配置后返回,卡号可选上送 params.put("accNo", param.getCardNumber()); // 交易币种(境内商户一般是156 人民币) params.put("currencyCode", "156"); // 交易金额,单位分,不要带小数点 params.put("txnAmt", param.getTotalFee()); // 请求方保留域,如需使用请启用即可;透传字段(可以实现商户自定义参数的追踪)本交易的后台通知, // 对本交易的交易状态查询交易、对账文件中均会原样返回,商户可以按需上传,长度为1-1024个字节 params.put("reserved", "{cardNumberLock=1}"); // TODO // 前台通知地址 (需设置为外网能访问 http https均可),支付成功后的页面 点击“返回商户”按钮的时候将异步通知报文post到该地址 // 如果想要实现过几秒中自动跳转回商户页面权限,需联系银联业务申请开通自动返回商户权限 params.put("frontUrl", unionPayConfig.getPayNotifyFrontUrl()); // 后台通知地址(需设置为【外网】能访问 http https均可),支付成功后银联会自动将异步通知报文post到商户上送的该地址,失败的交易银联不会发送后台通知 // 注意:1.需设置为外网能访问,否则收不到通知 2.http https均可 // 3.收单后台通知后需要10秒内返回http200或302状态码 // 4.如果银联通知服务器发送通知后10秒内未收到返回状态码或者应答码非http200,那么银联会间隔一段时间再次发送。总共发送5次,每次的间隔时间为0,1,2,4分钟。 // 5.后台通知地址如果上送了带有?的参数,例如:http://abc/web?a=b&c=d 在后台通知处理程序验证签名之前需要编写逻辑将这些字段去掉再验签,否则将会验签失败 params.put("backUrl", unionPayConfig.getPayNotifyUrl()); // 对请求参数拼接后进行消息摘要(SHA-1),然后用商户私钥进行签名 sign(params); // 生成表单HTML文档 String html = createAutoFormHtml(unionPayConfig.getPayUrl(), params, UnionPayConfig.CHARSET); prepayDto.setOrderId(param.getOrderId()); prepayDto.setOrderPaymentMethod(1);//TODO prepayDto.setOutTradeNo(param.getOutTradeNo()); prepayDto.setResp(html); return (PrepayDto<T>) prepayDto; } private void sign(Map<String, String> params) { // 证书ID 填写签名私钥证书的Serial Number,该值可通过银联提供的SDK获取 params.put("certId", unionPayConfig.getCertId()); // 对请求参数进行签名 String reqStr = PayUtil.mapToQueryStr(params, UnionPayConfig.CHARSET, false, true); // 将Map信息转换成key1=value1&key2=value2的形式 byte[] signDigest = SHA1Util.sha1X16(reqStr, UnionPayConfig.CHARSET); // SHA-1消息摘要 byte[] byteSign = RSA.signBySoft(unionPayConfig.getPrivateKey(), signDigest); // 用商户私钥签名 String sign = com.xiaoka.freework.utils.encrypt.Base64.encode(byteSign); // Base64编码 // 报文摘要的签名 params.put("signature", sign); } private static String createAutoFormHtml(String reqUrl, Map<String, String> hiddens, String encoding) { StringBuilder sb = new StringBuilder(); sb.append("<html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=").append(encoding).append("\"/></head><body>"); sb.append("<form id = \"pay_form\" action=\"").append(reqUrl).append("\" method=\"post\">"); if (null != hiddens && 0 != hiddens.size()) { Set<Map.Entry<String, String>> set = hiddens.entrySet(); for (Map.Entry<String, String> ey : set) { String key = ey.getKey(); String value = ey.getValue(); sb.append("<input type=\"hidden\" name=\"").append(key).append("\" id=\"") .append(key).append("\" value=\"").append(value).append("\"/>"); } } sb.append("</form>"); sb.append("</body>"); sb.append("<script type=\"text/javascript\">"); sb.append("document.all.pay_form.submit();"); sb.append("</script>"); sb.append("</html>"); return sb.toString(); } public UnionPayConfig getUnionPayConfig() { return unionPayConfig; } public void setUnionPayConfig(UnionPayConfig unionPayConfig) { this.unionPayConfig = unionPayConfig; } }
调用方只需获取银联实例调用方法即可:
CommonPay unionPay = commonPayFactory.getUnionPay(); PrepayParam prepayParam = new PrepayParam(); prepayParam.setOrderId(orderId); prepayParam.setOutTradeNo(tradeNo); prepayParam.setCardNumber(cardNo); prepayParam.setTotalFee(amount.toString()); PrepayDto prepayDto = null; try { prepayDto = unionPay.prePay(prepayParam); } catch (Exception e) { logger.error("unionPay.prePay has error", e); }
耐心点~