java打造微信测试服务(可直接支付、退款版)

  • java搭建微信测试服务器(可支付、退款版)
      • 思路:
      • nginx配置(重要)
      • 增加微信配置*:
      • 下载微信支付sdk中,退款需要的证书文件并实现sdk接口:
        • 证书下载:(度娘解答⬇)
        • sdk方法实现
          • MyConfig:(实现WXPayConfig抽象类)
          • WXPayDomainSimpleImpl 实现 IWXPayDomain
        • 微信sdk的使用:
      • 扩展
        • nginx重写(对于使用同一个服务器的童鞋)
        • 安装dubbo-admin:
        • 关于zookeeper暴露服务问题:
        • 测试环境使用redis需要注意的问题:

java搭建微信测试服务器(可支付、退款版)

鉴于最近业务需求的新增,作为一个微信网上商城,微信支付等功能不能单单靠内网穿透和测试公众号进行调试,也没有一个相对类似的环境可以更好的测试这类功能,自己便着手搭建了测试用的一套服务(最主要的还是实用,搭建成功跟生产环境支付没有任何区别,测试更加理想 (●ˇ∀ˇ●)),妈妈再也不用担心我怎么线上测试支付啦。

首先列举一下测试环境需要什么东西:
**必须的东西: **

  • nginx(好东西,请求转发、负载均衡什么的。没怎么研究就感觉很厉害的样子)
  • jdk
  • tomcat或者其他
  • 公众号(且注册关联了商户的)、微信支付sdk(微信自己的,用着还算方便)
  • 建立测试数据库
  • **非必须的东西(跟架构有关)⬇ **
  • zookeeper 、redis 、dubbo-admin(用于查看zookeeper服务的一个工具)

思路:

为了重用之前公众号的域名,之前的想法就是页面:
www.aaa.com/view/index.html 转化成
www.aaa.com/test/view/index.html;
并且其中的请求:
www.aaa.com/web名/controller名/接口 转化成
www.aaa.com/test_ web名/controller名/接口

其中之所以页面没有web名是因为用nginx做了静态页面分离。好处就百度吧=-= 我暂时也不大懂。注意上面url的test!然后这样做既可以满足使用同一个微信的appid、安全域名等一系列东西,又不会影响到线上的程序。岂不美哉?
想法有了就可以一步一步做了,我这边使用的是zookeeper代理服务,为了不重复service一个是换一台服务器装,一个是再装一个。目前因为服务器负载比较大,配置又不高,我就拿自己个人的服务器安装了jdk,zookeeper,redis,nginx,tomcat。这边的nginx通过生产环境的nginx又多做了一层代理。又可以重新搭建一下环境,熟悉一下。

nginx配置(重要)

其实最主要的环节就是这块nginx的配置。因为这边分了两个服务器的nginx,所以有两套,看懂的话用一个服务器的nginx和多个代理转发也能实现。

整体重要的配置:

#微信服务(生产环境服务)
    upstream weixin_server{		
        #ip_hash;		
	server 127.0.0.1:8080 max_fails=2 fail_timeout=5s;
    }
#微信web测试服务
    upstream test_weixin_server{	
				server 120.xx.xx.247:80 max_fails=2 fail_timeout=5s;
    }
    server {
        listen       80;
        server_name  www.aaa.com;
	 #这边使用正则拦截对应的请求,代理到我的服务器nginx处理,也可以直接代理到服务器下的测试程序中(可以代理到不同的端口),这个是微信后台
     location ~ ^/test_gsswe/{
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
				proxy_set_header X-Forwarded-Proto $scheme;	
        proxy_pass http://test_weixin_server;		
     }
     #生产环境后台
     location ~ ^/gsswe/{
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
				proxy_set_header X-Forwarded-Proto $scheme;	
        proxy_pass http://weixin_server;		
     }
     #测试用页面,转发到测试服务器/转发到本地后台对应端口
     location ~ ^/test/wefruitmall/ {
     		proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
				proxy_set_header X-Forwarded-Proto $scheme;	
        proxy_pass http://test_weixin_server_web;
     }
    }

增加微信配置*:

配置IP白名单(我这里用的另外一台服务器处理微信页面,所以得配置白名单)
配置商户支付授权目录(重要,支付用的测试的显然需要增加一个)
支付安全路径

下载微信支付sdk中,退款需要的证书文件并实现sdk接口:

证书下载:(度娘解答⬇)

商户平台,找到交易中心,下载cert证书,下载的时候会提示你密码之类的,密码就是你的商户平台密码哦。
java打造微信测试服务(可直接支付、退款版)_第1张图片

sdk方法实现

MyConfig:(实现WXPayConfig抽象类)
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class MyConfig extends WXPayConfig {
	private static final Logger log = LoggerFactory.getLogger(MyConfig.class);

	private byte[] certData;

	public MyConfig() {
		//这边就是读取证书文件,记得下载好的证书要跟这里对应,最好重命名以下证书
		try {
			String certPath = "/path/to/apiclient_cert.p12";
			File file = new File(certPath);
			InputStream certStream;
			certStream = new FileInputStream(file);
			this.certData = new byte[(int) file.length()];
			certStream.read(this.certData);
			certStream.close();
		} catch (FileNotFoundException e) {
			log.error("cert file no found", e);
		} catch (IOException e) {
			log.error("read cert file err", e);
		}
	}

	// #微信公众号AppId
	@Value("${wx.appid}")
	private String wxAppId;
	// #微信商户号
	@Value("${wx.mchid}")
	private String wxMchId;
	// #微信支付API密钥
	@Value("${wx.apikey}")
	private String wxPayApiKey;

	@Override
	public String getAppID() {
		return wxAppId;
	}

	@Override
	String getMchID() {
		return wxMchId;
	}

	@Override
	String getKey() {
		return wxPayApiKey;
	}

	@Override
	InputStream getCertStream() {
		ByteArrayInputStream certBis = new ByteArrayInputStream(this.certData);
		return certBis;
	}

	@Override
	IWXPayDomain getWXPayDomain() {
		return WXPayDomainSimpleImpl.instance();
	}

}
WXPayDomainSimpleImpl 实现 IWXPayDomain

(这个是百度上“借鉴”的 ε=ε=ε=( ̄▽ ̄)

import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.httpclient.ConnectTimeoutException;

/**
 * Created by blaketang on 2017/6/16.
 */
public class WXPayDomainSimpleImpl implements IWXPayDomain {
	private WXPayDomainSimpleImpl() {
	}

	private static class WxpayDomainHolder {
		private static IWXPayDomain holder = new WXPayDomainSimpleImpl();
	}

	public static IWXPayDomain instance() {
		return WxpayDomainHolder.holder;
	}

	public synchronized void report(final String domain, long elapsedTimeMillis, final Exception ex) {
		DomainStatics info = domainData.get(domain);
		if (info == null) {
			info = new DomainStatics(domain);
			domainData.put(domain, info);
		}

		if (ex == null) { // success
			if (info.succCount >= 2) { // continue succ, clear error count
				info.connectTimeoutCount = info.dnsErrorCount = info.otherErrorCount = 0;
			} else {
				++info.succCount;
			}
		} else if (ex instanceof ConnectTimeoutException) {
			info.succCount = info.dnsErrorCount = 0;
			++info.connectTimeoutCount;
		} else if (ex instanceof UnknownHostException) {
			info.succCount = 0;
			++info.dnsErrorCount;
		} else {
			info.succCount = 0;
			++info.otherErrorCount;
		}
	}

	public synchronized DomainInfo getDomain(final WXPayConfig config) {
		DomainStatics primaryDomain = domainData.get(WXPayConstants.DOMAIN_API);
		if (primaryDomain == null || primaryDomain.isGood()) {
			return new DomainInfo(WXPayConstants.DOMAIN_API, true);
		}

		long now = System.currentTimeMillis();
		if (switchToAlternateDomainTime == 0) { // first switch
			switchToAlternateDomainTime = now;
			return new DomainInfo(WXPayConstants.DOMAIN_API2, false);
		} else if (now - switchToAlternateDomainTime < MIN_SWITCH_PRIMARY_MSEC) {
			DomainStatics alternateDomain = domainData.get(WXPayConstants.DOMAIN_API2);
			if (alternateDomain == null || alternateDomain.isGood()
					|| alternateDomain.badCount() < primaryDomain.badCount()) {
				return new DomainInfo(WXPayConstants.DOMAIN_API2, false);
			} else {
				return new DomainInfo(WXPayConstants.DOMAIN_API, true);
			}
		} else { // force switch back
			switchToAlternateDomainTime = 0;
			primaryDomain.resetCount();
			DomainStatics alternateDomain = domainData.get(WXPayConstants.DOMAIN_API2);
			if (alternateDomain != null)
				alternateDomain.resetCount();
			return new DomainInfo(WXPayConstants.DOMAIN_API, true);
		}
	}

	static class DomainStatics {
		final String domain;
		int succCount = 0;
		int connectTimeoutCount = 0;
		int dnsErrorCount = 0;
		int otherErrorCount = 0;

		DomainStatics(String domain) {
			this.domain = domain;
		}

		void resetCount() {
			succCount = connectTimeoutCount = dnsErrorCount = otherErrorCount = 0;
		}

		boolean isGood() {
			return connectTimeoutCount <= 2 && dnsErrorCount <= 2;
		}

		int badCount() {
			return connectTimeoutCount + dnsErrorCount * 5 + otherErrorCount / 4;
		}
	}

	private final int MIN_SWITCH_PRIMARY_MSEC = 3 * 60 * 1000; // 3 minutes
	private long switchToAlternateDomainTime = 0;
	private Map domainData = new HashMap();
}

微信sdk的使用:

使用的时候只要对照着api放入必要的参数即可,我这边是前后端默认都是md5,所以稍稍把sdk的编码统一md5了。

/**
	 * 微信预订单生成
	 * 
	 * @param wxOrder
	 *            初始化基本数据
	 * @param wxNotifyUrl
	 *            如:/order/xxx.do 微信回调接口
	 * @return
	 * @throws Exception
	 */
	public String wechatPrepay(WxPayOrder wxOrder, String wxNotifyUrl) throws Exception {
		String settleId = wxOrder.getOutTradeNo();
		String ipAddr = wxOrder.getClientIp();
		String openId = wxOrder.getOpenid();
		String totalFee = wxOrder.getTotalFee();
		String body = wxOrder.getBody();

		WXPay pay = new WXPay(config, autoReport, useSandbox);
		SortedMap reqData = new TreeMap();
		reqData.put("attach", settleId);
		reqData.put("body", body);
		reqData.put("openid", openId);
		reqData.put("out_trade_no", settleId);
		reqData.put("spbill_create_ip", ipAddr);
		reqData.put("total_fee", totalFee);
		reqData.put("trade_type", "JSAPI");
		reqData.put("notify_url", concatChatServerUrl() + wxNotifyUrl);

		Map unifiedOrder = pay.unifiedOrder(reqData);
		String prepayId = unifiedOrder.get("prepay_id");
		if (StringUtils.isEmpty(prepayId)) {
			throw new Exception("微信生成预支付订单失败");
		}
		return prepayId;
	}

	/**
	 * 退款
	 * 
	 * @param settleId 支付id
	 * @param refundId 退款id
	 * @param totalMoney 订单总价
	 * @param refundMoney 退款价格
	 * @return
	 * @throws Exception
	 */
	public Map refund(String settleId, String refundId, double totalMoney, double refundMoney)
			throws Exception {
		return this.refund(settleId, refundId, totalMoney, refundMoney, null);
	}

扩展

nginx重写(对于使用同一个服务器的童鞋)

rewrite只能放在 server{}, location{}, if{}中,并且只能对域名后边的除去传递的参数外的字符串起作用。
这个如果不想改war包名的另一种处理方式:
www.aaa.com/web名/controller名/接口 转化成
www.aaa.com/test/web名/controller名/接口
具体配置:在location中加入重写地址,自己试过加在server中的,location没有尝试

#重写url将/test/去除,
rewrite ^(.*)/test/(.*) $1/$2 last;

#效果:将www.aaa.com/test/项目名/接口名?数据
#替换为www.aaa.com/项目名/接口名?数据
#如果应该能实现通过它转发到其他代理端口并保持代码、项目名不需要更改

安装dubbo-admin:

下载地址:https://github.com/apache/incubator-dubbo
最新版我不知道怎么装( ̄▽ ̄)" 用的2.5x版本。只要能用就好,哈哈。我这个直接放的windows下(为什么不装linux服务器?2G运存伤不起啊 =-=,maven都没法熟悉安装了,装了一半内存不够打不开dubbo-admin,尴尬。),进入dubbo-admin目录,用的maven命令打包
java打造微信测试服务(可直接支付、退款版)_第2张图片

mvn  package -Dmaven.skip.test=true

一切正常将在该文件夹的target内有对应的war包
java打造微信测试服务(可直接支付、退款版)_第3张图片
生成的war包可以直接放到tomcat中启动(这边修改了对应启动的端口8888)免得冲突。
可以把war包改成ROOT.war这样就不用输入项目名。
默认配置文件在WEB-INF的dubbo.properties里面。
java打造微信测试服务(可直接支付、退款版)_第4张图片

关于zookeeper暴露服务问题:

如果有三台不同网段的服务器ABC:A服务器在B中注册提供服务,C服务器消费。阿里云会出现一点问题。A提供出的服务会默认是内网ip时C获取不到要消费的服务。这时候只需要进入host文件 vim /etc/hosts 将内网ip改成公网ip重新注册服务即可。

测试环境使用redis需要注意的问题:

测试环境中,如果需要开启redis远程连接,需要配置以下:

#注释掉连接地址,才能开启远程访问 
	#//小知识:vim快速搜索文本(在命令模式输入 / 搜索内容,n下一个,shift+n上一个)
#bind 127.0.0.1
#保护模式关闭
protected-mode no
#配置密码(重要,如果没有加密码,很容易被黑客装上挖矿程序,我已中奖,希望小伙伴注意配置自己的密码 (ノ`Д)ノ   )
requirepass zhangxxxx

你可能感兴趣的:(微信开发教程)