【微信支付】Java实现微信JSAPI支付流程

3,373 阅读8分钟

前言

微信登录网页授权与APP授权
微信JSAPI支付
微信APP支付
微信APP和JSAPI退款
支付宝手机网站支付
支付宝APP支付
支付宝退款
以上我都放到个人公众号,搜一搜:JAVA大贼船,文末有公众号二维码!觉得个人以后开发会用到的可以关注一下哦!少走点弯路…

官方文档

准备工作

公众号配置
  • 绑定域名(可在该域名下调用微信开放的JS接口,这里配置前端域名) 先登录微信公众平台进入“公众号设置”的“功能设置”里填写“JS接口安全域名”。

  • 填写接口配置信息(用于配合微信服务器认证)

    开发-基本配置,配置URL,Token和生成EncodingAESKey,配置完了这时候点击提交是不行的,需要后端写好接口与配合微信认证,如下代码。

    	@ApiOperation("微信认证地址")
    	@GetMapping("/baseCallback")
    	public String baseCallback(String signature,String timestamp,String nonce,String echostr) throws Exception {
    		return echostr;
    	}
    
  • 配置ip白名单

    获取普通accessToken时,请登录“微信公众平台-开发-基本配置”提前将服务器IP地址添加到IP白名单中,否则将无法调用成功

商户平台配置
  • 设置支付目录(配置了才能拉起微信收银台,配置前端域名,否则会报当前页面的Url未注册的错误)

    登录微信支付商户平台(pay.weixin.qq.com)-->产品中心-->开发配置,设置后一般5分钟内生效。

  • 支付授权目录校验说明

    1、如果支付授权目录设置为顶级域名(例如:www.weixin.com/ ),那么只校验顶级域名,不校验后缀;

    2、如果支付授权目录设置为多级目录,就会进行全匹配,例如设置支付授权目录为www.weixin.com/abc/123/,则实…

流程步骤

JSSDK使用步骤

这个文档主要看 1概述14微信支付,如下图;

主要步骤
  • 前端获取注入参数(即jssdk文档概述步骤三)

    前端支付前请求后台获取上图步骤三所需参数,并注入成功,前端最好配置全局,因为业务可能会扩展,其他接口会需要,如分享接口

  • 前端获取唤起微信收银台所需参数

    注入成功后->前端点击微信支付->后端向微信下单->下单成功后得到prepay_id->返回所需参数给前端->唤起收银台

后端代码实现

引入依赖
		<!-- 微信支付 -->
			<dependency>
				<groupId>com.github.wxpay</groupId>
				<artifactId>wxpay-sdk</artifactId>
				<version>0.0.3</version>
			</dependency>
配置参数

application.yml

# 微信相关配置
wx:
  #商户 ID(微信支付平台-账户中心-账户信息)
  MCH_ID: 
  # APP_ID(微信开放平台或公众号查找)
  H_APP_ID: 
  # 秘钥(微信开放平台或公众号查找)
  H_APP_SECRET: 
  # 消息加解密所用到的解密串(微信公众号-基本配置查找)
  H_ENCODINGAESKEY:
  # token(微信公众号-基本配置查找)
  H_TOKEN: 
  # 支付秘钥KEY(微信支付平台-账户中心-api安全-api秘钥)
  H_KEY: 
  # 支付商户证书所载目录(微信支付平台-账户中心-api安全-API证书)
  H_CERT_PATH: 
  #支付成功回调地址
  WX_CALLBACK_URL: 

YmlParament

@Component
@Data
public class YmlParament {
	/*微信相关字段*/
	@Value("${wx.H_APP_ID}")
	private String h_app_id;
	@Value("${wx.H_APP_SECRET}")
	private String h_app_secret;
	@Value("${wx.H_ENCODINGAESKEY}")
	private String h_encodingaeskey;
	@Value("${wx.H_TOKEN}")
	private String h_token;
	@Value("${wx.MCH_ID}")
	private String mch_id;
	@Value("${wx.H_KEY}")
	private String h_key;
	@Value("${wx.H_CERT_PATH}")
	private String h_cert_path;
    @Value("${wx.WX_CALLBACK_URL}")
	private String wx_callback_url;

获取前端config接口配置参数(查看jssdk文档附录1)

生成签名之前必须先了解一下jsapi_ticket,jsapi_ticket是公众号用于调用微信JS接口的临时票据。正常情况下,jsapi_ticket的有效期为7200秒,通过access_token来获取。由于获取jsapi_ticket的api调用次数非常有限,频繁刷新jsapi_ticket会导致api调用受限,影响自身业务,开发者必须在自己的服务全局缓存jsapi_ticket 。

  • Application 开启计划任务,用于定时刷新
@EnableScheduling//开启计划任务 表达式参考http://cron.qqe2.com/
  • 配置全局缓存

WxParament

/**
	 * 获取ACCESS_TOKEN
	 * 刷新规则:
	 * 1、启动设置该值
	 * 2、一小时刷新一次
	 */
	public static String ACCESS_TOKEN;
	/**
	 * 获取JSAPI_TICKET
	 * 刷新规则:
	 * 1、启动设置该值
	 * 2、一小时刷新一次
	 */
	public static String JSAPI_TICKET;
  • 定时刷新

InitServiceImpl

	@Autowired
	private YmlParament ymlParament;

	@PostConstruct
	@Override
	public void init() {
		initAccessToken();
		initJsapiTicket();
	}

	/**
	 * 初始化AccessToken 一小时执行一次
	 *cron表达式可在线生成:https://cron.qqe2.com/
	 */
	@Scheduled(cron = "0 0 0/1 * * ?")
	@Override
	public void initAccessToken() {
		try {
			WxParament.ACCESS_TOKEN = GetToken.getAccessToken(ymlParament.getH_app_id(), ymlParament.getH_app_secret());
		} catch (Exception e) {
			log.error("<====initAccessToken初始化失败!==>" + e);
			e.printStackTrace();
		}
		log.info("<====初始化initAccessToken成功,值为==>" + WxParament.ACCESS_TOKEN);
	}

	/**
	 * 初始化JSAPI_TICKET 一小时执行一次
	 */
	@Scheduled(cron = "0 0 0/1 * * ?")
	@Override
	public void initJsapiTicket() {
		try {
			log.info("<====正在刷新 JSAPI_TICKET ==>");
			WxParament.JSAPI_TICKET = GetToken.getTicket(ymlParament.getH_app_id(), ymlParament.getH_app_secret(),
					WxParament.ACCESS_TOKEN);
		} catch (Exception e) {
			log.error("<====initJsapiTicket初始化失败!==>" + e);
			e.printStackTrace();
		}
		log.info("<====刷新 JSAPI_TICKET 成功,值为 ==>" + WxParament.JSAPI_TICKET);
	}

GetToken

/**
	 * ~~~~~~~~~ 第一步 ~~~~~~~~~
	 * 有效期两个小时,注意缓存 获取access_token,记得配置ip白名单
	 * 参考以下文档获取access_token(有效期7200秒,开发者必须在自己的服务全局缓存access_token):
	 * https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html
	 * @param appid
	 * @param secret
	 * @throws Exception 
	 */
	public static String getAccessToken(String appid, String secret) throws Exception {
		String res=HttpUtil.get("https://api.weixin.qq.com/cgi-bin/token",
				"grant_type=client_credential&appid=" + appid + "&secret=" + secret);
			String access_token=JSON.parseObject(res).getString("access_token");
			if(IsNull.isNull(access_token)) {
			throw new Exception(res);
		}
		 return access_token;
	}

	/**
	 * ~~~~~~~~~ 第二步 ~~~~~~~~~
	 * 有效期两个小时,注意缓存 用第一步拿到的access_token 采用http
	 * GET方式请求获得jsapi_ticket(有效期7200秒,开发者必须在自己的服务全局缓存jsapi_ticket):
	 * https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi
	 * @param appid
	 * @param secret
	 * @param accessToken 如果 accessToken 传null 则重新获取 access_token,如果accessToken有值,则直接获取Ticket
	 * @throws Exception
	 */
	public static String getTicket(String appid, String secret,String accessToken) throws Exception {
		String token=IsNull.isNull(accessToken)?getAccessToken(appid, secret):accessToken;
		String res=HttpUtil.get("https://api.weixin.qq.com/cgi-bin/ticket/getticket",
				"access_token=" + token + "&type=jsapi");
		String ticket=JSON.parseObject(res).getString("ticket");
		if(IsNull.isNull(ticket)) {
			throw new Exception(res);
		}
		return ticket;
	}
	
	/**
	 *
	 * jsapi_ticket是公众号用于调用微信JS接口的临时票据
	 * 前台统一js签名,参考说明如下
	 * 1、业务说明:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#3
	 * 2、签名算法:1-JS-SDK使用权限签名算法:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#62
	 * @param jsapi_ticket,必须传入,每次有效时间两小时,如果频繁获取会报错
	 * @param url url(当前支付页面的URL,不包含#及其后面部分)
	 * @return
	 */
	public static Map<String, String> getJsSignature(String jsapi_ticket, String url) {
		return sign(jsapi_ticket, url);
	}
	/*签名算法*/
	private static Map<String, String> sign(String jsapi_ticket, String url) {
		Map<String, String> ret = new HashMap<String, String>();
		String nonce_str = UUID.randomUUID().toString();;
		String timestamp = Long.toString(System.currentTimeMillis() / 1000);
		String signature = null;
		// 注意这里参数名必须全部小写,且必须有序
		String string1 = "jsapi_ticket=" + jsapi_ticket + "&noncestr=" + nonce_str + "&timestamp=" + timestamp + "&url=" + url;
		try {
			MessageDigest crypt = MessageDigest.getInstance("SHA-1");
			crypt.reset();
			crypt.update(string1.getBytes("UTF-8"));
			signature = byteToHex(crypt.digest());
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		ret.put("url", url);
		ret.put("jsapi_ticket", jsapi_ticket);
		ret.put("nonceStr", nonce_str);
		ret.put("timestamp", timestamp);
		ret.put("signature", signature);
		return ret;
	}

	private static String byteToHex(final byte[] hash) {
		Formatter formatter = new Formatter();
		for (byte b : hash) {
			formatter.format("%02x", b);
		}
		String result = formatter.toString();
		formatter.close();
		return result;
	}

HttpUtil

public static String get(String urlStr, Map<String, String> parameters) throws IOException {
		URL url = new URL(urlStr);
		HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
		httpURLConnection.setDoInput(true);
		httpURLConnection.setDoOutput(true); // 设置该连接是可以输出的
		httpURLConnection.setRequestMethod("GET"); // 设置请求方式
		httpURLConnection.setRequestProperty("charset", "utf-8");
		PrintWriter pw = new PrintWriter(new BufferedOutputStream(httpURLConnection.getOutputStream()));

		StringBuffer parameter = new StringBuffer();
		parameter.append("1=1");
		for (Entry<String, String> entry : parameters.entrySet()) {
			parameter.append("&" + entry.getKey() + "=" + entry.getValue());
		}
		pw.write(parameter.toString());// 向连接中写数据(相当于发送数据给服务器)
		pw.flush();
		pw.close();
    
		BufferedReader br = new BufferedReader(new InputStreamReader(httpURLConnection.getInputStream(), "utf-8"));
		String line = null;
		StringBuilder sb = new StringBuilder();
		while ((line = br.readLine()) != null) { // 读取数据
			sb.append(line + "\n");
	}
		br.close();
	return sb.toString();
	}

JacksonUtil

public class JacksonUtil {
    public static String parseString(String body, String field) {
        ObjectMapper mapper = new ObjectMapper();
        JsonNode node;
        try {
            node = mapper.readTree(body);
            JsonNode leaf = node.get(field);
            if (leaf != null) {
                return leaf.asText();
            }
        } catch (IOException e) {
        	e.printStackTrace();
        }
        return null;
    }
}
  • 微信支付初始化参数
	@ApiOperation("微信支付初始化参数")
	@PostMapping("getJsSignature")
	public R getJsSignature(@RequestBody String body){
        //(当前支付页面的URL,不包含#及其后面部分)
		String url = JacksonUtil.parseString(body, "url");
		if(IsNull.isNull(url)) {
			return R.error("参数不能为空!");
		}
		Map<String, Object> res = new HashMap<>();
		Map<String, String> jsSignature = GetToken.getJsSignature(WxParament.JSAPI_TICKET, url);
		res.put("appId", ymlParament.getH_app_id());
		res.put("timestamp",jsSignature.get("timestamp"));
		res.put("nonceStr",jsSignature.get("nonceStr"));
		res.put("signature",jsSignature.get("signature"));
		return R.ok(res);
	}
获取openid

可参考我上一篇推文:mp.weixin.qq.com/s/FrhpFTENj…

微信统一下单

微信统一下单接口文档:pay.weixin.qq.com/wiki/doc/ap…

  • 初始化微信支付配置
@Component
public class WxConfig {
   @Autowired
   private YmlParament ymlParament;
   
   /**
    * 初始化微信支付配置
    * @throws Exception 
    */
   @Bean(autowire = Autowire.BY_NAME,value = WxParament.H5_WX_PAY)
   public WXPay setH5WXPay() throws Exception {
      return new WXPay(new WxPayConfig(
            ymlParament.getH_cert_path(),
            ymlParament.getH_app_id(),
            ymlParament.getMch_id(),
            ymlParament.getH_key()));
   }
 }

WxPayConfig

public class WxPayConfig implements WXPayConfig {
	private byte[] certData;
	private String appID;
	private String mchID;
	private String key;

	public WxPayConfig(String certPath, String appID,String mchID,String key) throws Exception {
		File file = new File(certPath);
		InputStream certStream = new FileInputStream(file);
		this.certData = new byte[(int) file.length()];
		certStream.read(this.certData);
		certStream.close();
		this.appID = appID;
		this.mchID = mchID;
		this.key = key;
	}
}
  • 微信下单接口,关键代码(服务层)
	@Resource(name = WxParament.H5_WX_PAY)
	private WXPay wxH5Pay;

/* 微信统一下单 */
   private Map<String, String> wxUnifiedOrder(String orderNo, String orderFee, String requestIp, String openid) throws RuntimeException {
      Map<String, String> data = new HashMap<String, String>();
      data.put("nonce_str", WXPayUtil.generateNonceStr());
      data.put("body", "我来下单啦");
      data.put("out_trade_no", orderNo);
      data.put("sign_type", "MD5");
      data.put("total_fee", orderFee);
      data.put("spbill_create_ip", requestIp);
      data.put("openid", openid);
      data.put("notify_url",ymlParament.getWx_callback_url());
      data.put("trade_type", "JSAPI"); // 此处指定为JSAPI支付
      Map<String, String> wxOrderResult = WxPay.unifiedorder(data,wxH5Pay);
     if("FAIL".equals(wxOrderResult.get("return_code"))){
       throw new RuntimeException(wxOrderResult.get("return_msg"));
     }
        /* IsNull自定义,主要判断非空 */
      if (IsNull.isNull(wxOrderResult.get("prepay_id"))) {
         throw new RuntimeException("微信支付下单成功后,返回的prepay_id为空");
      }
      return wxOrderResult;

   }

	@Override
	public Map<String, String> insertWxH5Pay(String orderNo,String orderFee, String requestIp, String openid) {
		try {
			/* 微信下单 */
			Map<String, String> wxOrderResult = wxUnifiedOrder(orderNo, orderFee, requestIp, openid);
			Map<String, String> chooseWXPay = new HashMap<>();
            /* 这里我遇到一个大坑,这里的key值是要驼峰式写法,而APP支付却不用!!! */
			chooseWXPay.put("appId", ymlParament.getH_app_id());
			chooseWXPay.put("timeStamp", WxUtils.create_timestamp());
			chooseWXPay.put("nonceStr", wxOrderResult.get("nonce_str"));
			chooseWXPay.put("package", "prepay_id=" + wxOrderResult.get("prepay_id"));
			chooseWXPay.put("signType", "MD5");
			String signature = WXPayUtil.generateSignature(chooseWXPay, ymlParament.getH_key());
			chooseWXPay.put("paySign", signature);
			return chooseWXPay;
		} catch (Exception e) {
			e.printStackTrace();
			throw new RuntimeException(e.getMessage());
		}
	}
  • controller层(略)
微信支付成功回调
  • 关键代码
   @Resource(name = WxParament.H5_WX_PAY)
   private WXPay wxH5Pay;

   @ApiOperation("微信支付回调")
   @PostMapping("callback")
   public String callback(HttpServletRequest request) throws Exception {
   	try {
   		// 1、获取参数
   		Map<String, String> params = getMapByRequest(request);
   		log.info("微信回调回来啦!!!!" + params);
   		// 2、校验
           checkCallbackWxPay(params);
   		//业务逻辑
   		return wxPaySuccess();
   	} catch (Exception e) {
   		e.printStackTrace();
   		return wxPayFail(e.getMessage());
   	}
   }
   
/**
    * 获取微信过来的请求参数
    * @param request
    * @return
    * @throws Exception
    */
   private static Map<String, String> getMapByRequest(HttpServletRequest request) throws Exception{
   	InputStream inStream = request.getInputStream();
   	ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
   	byte[] buffer = new byte[1024];
   	int len = 0;
   	while ((len = inStream.read(buffer)) != -1) {
   		outSteam.write(buffer, 0, len);
   	}
   	Map<String, String> ret= WXPayUtil.xmlToMap(new String(outSteam.toByteArray(), "utf-8"));
   	outSteam.close();
   	inStream.close();
   	return ret;
   }	
   
   /*校验 */
   private void checkCallbackWxPay(Map<String, String> params)
   		throws Exception {
   	if ("FAIL".equals(params.get("result_code"))) {
   		throw new Exception("微信支付失败");
   	}
   	if (!checkWxCallbackSing(params, wxH5Pay)) {
   		throw new Exception("微信支付回调签名认证失败");
   	}
   	//校验金额
       //判断订单是否重复
       //....业务逻辑
   }
   
/**
    * 检查微信回调签名 https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_7
    * 
    * @param wp 传入:@Autowired WXPay
    */
   private static boolean checkWxCallbackSing(Map<String, String> notifyMap, WXPay wp) throws Exception {
   	return wp.isPayResultNotifySignatureValid(notifyMap);
   }

   /**
    * 微信支付返回参数结果封装
    */
   private static String wxPaySuccess() throws Exception {
   	Map<String, String> succResult = new HashMap<String, String>();
   	succResult.put("return_code", "SUCCESS");
   	succResult.put("return_msg", "OK");
   	return WXPayUtil.mapToXml(succResult);
   }
   /**
    * @param mess 错误消息提示
    * @return 微信返回错我的提示
    * @throws Exception
    */
   private static String wxPayFail(String mess) throws Exception {
   	Map<String, String> succResult = new HashMap<String, String>();
   	succResult.put("return_code", "FAIL");
   	succResult.put("return_msg", IsNull.isNull(mess)?"自定义异常错误!":mess);
   	return WXPayUtil.mapToXml(succResult);
   }
补充

如果不想验证签名,还有一种方式判断是否支付成功,就是调用微信查询订单接口查看是否支付成功

  • 关键代码
	/*调用微信查询订单接口*/
   			Map<String, String> orderqueryRes = orderquery(wXH5Pay,params.get("out_trade_no"));
   			/*交易成功*/
   			if (!"SUCCESS".equals(orderqueryRes.get("trade_state"))){
   				throw new Exception("<===微信支付失败====>订单号为【"+ params.get("out_trade_no")+ "】的订单");
   			}

   /**
    * 查询支付结果 https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_2
    */
   private static Map<String, String> orderquery(WXPay wxpay, String outTradeNo) throws Exception {
       Map<String, String> parament = new HashMap<String, String>();
   	parament.put("out_trade_no", outTradeNo);
   	return wxpay.orderQuery(parament);
   }

欢迎关注公众号哦~