ssm微信公众号支付demo-以及配置过程

一、准备资料

  • 微信公众号服务号
  • 商户平台账号开通了微信支付
  • 公网域名(或内网穿透)
  • 服务器(或内网穿透)
  • 商户APPID
  • 商户API密钥APPSECRET
  • 商户号MCHID

二、公众号和商户平台配置

1、公众号配置

公众号登录后 -> 公众号设置 -> 功能设置:JS接口安全域名需要填写准备使用微信支付的域名
ssm微信公众号支付demo-以及配置过程_第1张图片
添加或修改
ssm微信公众号支付demo-以及配置过程_第2张图片
这里有4点要注意的:
1.需要填写域名或路径,不支持ip地址,端口号,短链域名,一般直接用域名就行了
2.域名需要经过备案
3.将其中下载的txt文件放在服务器域名映射的根目录,假如域名是:abc.de.com,那就要确保:abc.de.com/MP_verify_nT4jEtfHhaE7IceO.txt能访问到该txt文件。
公众号的设置就完成了

2、商户平台配置

首先进入:账户中心 -> 操作证书,在电脑安装了证书之后进入:API安全

  • 下载证书,这个证书是用于退款等操作的
  • API密钥设置,api密钥设置不能特意设置,得随意设置,一般设置有意义的字符串都不能支付,需要去生成随机32位随机码,地址:随机码

ssm微信公众号支付demo-以及配置过程_第3张图片

接着继续进入:产品中心 -> 开发配置 -> 支付配置 -> 公众号支付 -> 支付授权目录
这里需要注意的是:
   授权目录一定要填写到调起支付页面的上一级目录,加入支付页是:abc.de.com/demo/pay.jsp,那么需要填写:abc.de.com/demo/
ssm微信公众号支付demo-以及配置过程_第4张图片
商户平台的配置也完成了,接下来就是编码了

三、编写demo

先定义一部分常量:
WxConst.java

package com.demo.common;

/**
 * @author xyd
 * @version V1.0
 * @Package com.demo.common
 * @Description:
 * @date 2018/8/8 10:45
 */
public class WxConst {
    /**
     * acesstoken
     */
    public static String ACCESSTOKEN = "";
    /**
     * 公众号appId
     */
    public static String APPID = "";

    /**
     * 公众号secret
     */
    public static String SECRET = "";

    /**
     * 公众号jsapi ticket
     */
    public static String JSAPITICKET = "";

    /**
     * 签名类型
     */
    public enum SignType {
        MD5, HMACSHA256
    }

    /**
     * 商户支付密码
     */
    public static String PAYAPISECRET = "";

    /**
     * 商户号
     */
    public static String MCHID = "";

    /**
     * 商户密钥
     */
    public static String MCHKEY = "";

    /**
     * 签名字段名称
     */
    public static final String FIELD_SIGN = "sign";


    /**
     * 服务商退款证书路径
     */
    public static String CERTIFICATE_PATH = "/usr/local/cert/apiclient_cert.p12";

    /**
     * 下单地址
     */
    public static final String WXPAY_UNIFIEDORDER_GATEWAY = "https://api.mch.weixin.qq.com/pay/unifiedorder";
    /**
     * 退款地址
     */
    public static final String WXPAY_REFUND_GATEWAY = "https://api.mch.weixin.qq.com/secapi/pay/refund";

    /**
     * 下单回调地址
     */
    public static final String ORDER_NOTIFYURL = "";
    /**
     * 回复微信的消息
     */
    public static final String NOTIFY_RESPONSE_BODY = "\n" +
            "  \n" +
            "  \n" +
            "";

    /**
     * 失败
     */
    public static final String FAIL = "FAIL";
    /**
     * 成功
     */
    public static final String SUCCESS = "SUCCESS";
}

1、动态获取ACCESSTOKEN和JSAPITICKET

参考微信支付开发文档可知,需要先获取accesstoken和jsapiticket
JS-SDK文档:JS-SDK
微信统一下单文档:统一下单

首先注入依赖:
pom.xml



<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0modelVersion>

  <groupId>com.demogroupId>
  <artifactId>wechatpaydemoartifactId>
  <version>1.0-SNAPSHOTversion>
  <packaging>warpackaging>

  <name>wechatpaydemo Maven Webappname>
  
  <url>http://www.example.comurl>

  <properties>
    
    <spring.version>4.3.3.RELEASEspring.version>
    
    <mybatis.version>3.3.1mybatis.version>
    
    <slf4j.version>1.7.7slf4j.version>

    <log4j.version>1.2.17log4j.version>
    <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
    <maven.compiler.source>1.8maven.compiler.source>
    <maven.compiler.target>1.8maven.compiler.target>
  properties>

  <dependencies>
    
    <dependency>
      <groupId>org.springframeworkgroupId>
      <artifactId>spring-coreartifactId>
      <version>${spring.version}version>
    dependency>

    <dependency>
      <groupId>org.springframeworkgroupId>
      <artifactId>spring-beansartifactId>
      <version>${spring.version}version>
    dependency>

    <dependency>
      <groupId>org.springframeworkgroupId>
      <artifactId>spring-webartifactId>
      <version>${spring.version}version>
    dependency>

    <dependency>
      <groupId>org.springframeworkgroupId>
      <artifactId>spring-aspectsartifactId>
      <version>${spring.version}version>
    dependency>


    <dependency>
      <groupId>org.springframeworkgroupId>
      <artifactId>spring-contextartifactId>
      <version>${spring.version}version>
    dependency>


    <dependency>
      <groupId>org.springframeworkgroupId>
      <artifactId>spring-txartifactId>
      <version>${spring.version}version>
    dependency>

    <dependency>
      <groupId>org.springframeworkgroupId>
      <artifactId>spring-jdbcartifactId>
      <version>${spring.version}version>
    dependency>

    <dependency>
      <groupId>org.springframeworkgroupId>
      <artifactId>spring-webmvcartifactId>
      <version>${spring.version}version>
    dependency>
    <dependency>
      <groupId>org.springframeworkgroupId>
      <artifactId>spring-aopartifactId>
      <version>${spring.version}version>
    dependency>

    <dependency>
      <groupId>org.springframeworkgroupId>
      <artifactId>spring-context-supportartifactId>
      <version>${spring.version}version>
    dependency>

    <dependency>
      <groupId>org.springframeworkgroupId>
      <artifactId>spring-testartifactId>
      <version>${spring.version}version>
    dependency>

    
    
    <dependency>
      <groupId>log4jgroupId>
      <artifactId>log4jartifactId>
      <version>${log4j.version}version>
    dependency>
    <dependency>
      <groupId>org.slf4jgroupId>
      <artifactId>slf4j-apiartifactId>
      <version>${slf4j.version}version>
    dependency>
    <dependency>
      <groupId>org.slf4jgroupId>
      <artifactId>slf4j-log4j12artifactId>
      <version>${slf4j.version}version>
    dependency>

    
    

    <dependency>
      <groupId>com.fasterxml.jackson.coregroupId>
      <artifactId>jackson-annotationsartifactId>
      <version>2.5.0version>
    dependency>

    
    <dependency>
      <groupId>com.fasterxml.jackson.coregroupId>
      <artifactId>jackson-coreartifactId>
      <version>2.5.0version>
    dependency>

    
    <dependency>
      <groupId>com.fasterxml.jackson.coregroupId>
      <artifactId>jackson-databindartifactId>
      <version>2.5.0version>
    dependency>

    <dependency>
      <groupId>com.alibabagroupId>
      <artifactId>fastjsonartifactId>
      <version>1.2.12version>
    dependency>

    <dependency>
      <groupId>javax.servletgroupId>
      <artifactId>javax.servlet-apiartifactId>
      <version>3.1.0version>
      <scope>providedscope>
    dependency>

    
    <dependency>
      <groupId>org.apache.httpcomponentsgroupId>
      <artifactId>httpclientartifactId>
      <version>4.5.5version>
    dependency>

    
    <dependency>
      <groupId>org.hibernategroupId>
      <artifactId>hibernate-validatorartifactId>
      <version>5.1.3.Finalversion>
    dependency>
    
    <dependency>
      <groupId>javax.validationgroupId>
      <artifactId>validation-apiartifactId>
      <version>1.1.0.Finalversion>
    dependency>
    
    <dependency>
      <groupId>com.thoughtworks.xstreamgroupId>
      <artifactId>xstreamartifactId>
      <version>1.4.10version>
    dependency>
    
    <dependency>
      <groupId>commons-langgroupId>
      <artifactId>commons-langartifactId>
      <version>2.5version>
    dependency>
    <dependency>
      <groupId>junitgroupId>
      <artifactId>junitartifactId>
      <version>4.11version>
      <scope>testscope>
    dependency>
  dependencies>

  <build>
    <finalName>wechatpaydemofinalName>
    <pluginManagement>
      <plugins>
        <plugin>
          <artifactId>maven-clean-pluginartifactId>
          <version>3.0.0version>
        plugin>
        
        <plugin>
          <artifactId>maven-resources-pluginartifactId>
          <version>3.0.2version>
        plugin>
        <plugin>
          <artifactId>maven-compiler-pluginartifactId>
          <version>3.7.0version>
        plugin>
        <plugin>
          <artifactId>maven-surefire-pluginartifactId>
          <version>2.20.1version>
        plugin>
        <plugin>
          <artifactId>maven-war-pluginartifactId>
          <version>3.2.0version>
        plugin>
        <plugin>
          <artifactId>maven-install-pluginartifactId>
          <version>2.5.2version>
        plugin>
        <plugin>
          <artifactId>maven-deploy-pluginartifactId>
          <version>2.8.2version>
        plugin>
      plugins>
    pluginManagement>
  build>
project>

使用spring的spring schedule定时任务来获取accesstoken和jsapiticket,将获取的代码定在service层
spring.xml配置如下:


<beans
        xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:tx="http://www.springframework.org/schema/tx"
        xmlns:aop="http://www.springframework.org/schema/aop"
        xmlns:context="http://www.springframework.org/schema/context"
        xmlns:task="http://www.springframework.org/schema/task"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-3.2.xsd
    http://www.springframework.org/schema/tx
    http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
    http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
    http://www.springframework.org/schema/task
   http://www.springframework.org/schema/task/spring-task.xsd ">


    <context:component-scan base-package="com.demo.service"/>

    <task:scheduled-tasks>
        
        <task:scheduled ref="timerService" initial-delay="2000" method="getAccessTokenAndTicket" fixed-delay="7200000" />
    task:scheduled-tasks>

beans>

TimerService.java

package com.demo.service;

import com.demo.common.WxConst;
import com.demo.util.WXUtil;
import org.springframework.stereotype.Service;

/**
 * @author xyd
 * @version V1.0
 * @Package com.demo.service
 * @Description:
 * @date 2018/8/8 10:53
 */
@Service
public class TimerService {

    /**
     * 自动获取accessToken和jsapiticket
     * @throws Exception
     */
    public void getAccessTokenAndTicket() throws Exception {
        System.out.println("开始获取token和ticket");
        WxConst.ACCESSTOKEN = WXUtil.getAccess_token(WxConst.APPID,WxConst.SECRET);
        WxConst.JSAPITICKET = WXUtil.getJsapiTicket(WxConst.ACCESSTOKEN);
    }
}

WXUtil.java

package com.demo.util;

import com.alibaba.fastjson.JSON;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import java.util.Map;

/**
 * @author xyd
 * @version V1.0
 * @Package com.demo.WXUtil
 * @Description:
 * @date 2018/8/8 10:53
 */
public class WXUtil {

    /**
     * 两个小时的过期 JsapiTicket
     * @param AccessToken
     * @return
     * @throws Exception
     */
    public static String getJsapiTicket(String AccessToken)throws Exception{
        HttpClient httpclient =  HttpClients.createDefault();
        String smsUrl="https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=" + AccessToken + "&type=jsapi";
        HttpGet httpGet = new HttpGet(smsUrl);
        String strResult = "";

        HttpResponse response = httpclient.execute(httpGet);
        if (response.getStatusLine().getStatusCode() == 200) {
                    /*读返回数据*/
            strResult = EntityUtils.toString(response
                    .getEntity());
        }
        Map resultMap = (Map) JSON.parse(strResult);
        return (String)resultMap.get("ticket");
    }

    /**
     * 两个小时的过期 ACCESSTOKEN
     * @param appid
     * @param secret
     * @return
     * @throws Exception
     */
    public static String getAccess_token(String  appid, String secret) throws Exception{
        HttpClient httpclient =  HttpClients.createDefault();
        String smsUrl="https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appid + "&SECRET=" + secret;
        HttpGet httpGet = new HttpGet(smsUrl);
        String strResult = "";

        HttpResponse response = httpclient.execute(httpGet);
        if (response.getStatusLine().getStatusCode() == 200) {
                    /*读返回数据*/
            strResult = EntityUtils.toString(response
                    .getEntity());
        }
        Map resultMap = (Map) JSON.parse(strResult);
        String AccessToken = (String)resultMap.get("access_token");
        System.out.println(AccessToken);
        return AccessToken;
    }


}

spring-mvc.xml


<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.2.xsd">

    <mvc:annotation-driven />
    <context:component-scan base-package="com.demo.controller" />
    
    <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping" />

    
    <bean class="org.springframework.web.servlet.view.BeanNameViewResolver"
          >
        <property name="order" value="10">property>

    bean>

    
    <bean id="exceptionJson"
          class="org.springframework.web.servlet.view.json.MappingJackson2JsonView">
    bean>

    
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/" />
        <property name="suffix" value=".jsp" />
    bean>

    
    <bean
            class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor">
    bean>

beans>

目前目录结构如下:
ssm微信公众号支付demo-以及配置过程_第5张图片
先测试一下是否获取到accesstoken和jsapiticket
DemoController.java

package com.demo.controller;

import com.demo.common.WxConst;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author xyd
 * @version V1.0
 * @Package com.demo.controller
 * @Description:
 * @date 2018/8/6 17:19
 */
@RestController("demo")
@RequestMapping("/wx")
public class DemoController {

    @GetMapping("/get")
    public String get(){
        System.out.println(WxConst.ACCESSTOKEN);
        System.out.println(WxConst.JSAPITICKET);
        return "token: " + WxConst.ACCESSTOKEN + "\n ticket: " + WxConst.JSAPITICKET;
    }


}

结果:
页面输出:
这里写图片描述
控制台输出:
这里写图片描述
问题:
有些人可能获取不到ticket和token,可能需要在公众号设置ip白名单:
ssm微信公众号支付demo-以及配置过程_第6张图片

2、获取发起支付需要的参数

1.统一下单

查看开发文档第一步是经过统一下单获取prepay_id。
文档:统一下单
大致流程:

  • 1.用户购买某个商品时,先将商品信息传至后台
  • 2.后台拿到后去数据库获取该商品的信息和价格
  • 3.将发起统一下单需要的信息使用java Bean来填充
  • 4.按照微信签名算法将信息签名后存入sign字段中(重点)
  • 5.将javaBean转化成微信服务器需要的xml格式数据
  • 6.通过HttpClient发起xml格式的Post请求
  • 7.解析返回的数据

第1和第2步在demo中没有逻辑,所以跳过。
第3步:将发起统一下单需要的信息使用java Bean来填充
定义统一下单的bean:
WxPayUnifiedorderRequest.java

package com.demo.bean;
import com.thoughtworks.xstream.annotations.XStreamAlias;

/**
 * @author xyd
 * @version V1.0
 * @Package com.demo.bean
 * @Description:
 * @date 2018/8/8 12:45
 */
 // xstream标记,后面使用xstream包转成xml格式数据
@XStreamAlias("xml")
public class WxPayUnifiedorderRequest {
    //公众账号ID 必填
    private String appid;
    //商户号 必填
    // 转化成xml需要改变的名称
    @XStreamAlias("mch_id")
    private String mchId;
    //设备号
    @XStreamAlias("device_info")
    private String deviceInfo;
    //随机字符串 必填
    @XStreamAlias("nonce_str")
    private String nonceStr;
    //签名 必填
    private String sign;
    //签名类型
    private String sign_type;

    //商品描述 必填
    private String body;
    //商品详情
    private String detail;
    //附加数据
    private String attach;
    //商户订单号 必填
    @XStreamAlias("out_trade_no")
    private String outTradeNo;

    //标价币种
    @XStreamAlias("fee_type")
    private String feeType;
    //标价金额 必填
    @XStreamAlias("total_fee")
    private Integer totalFee;
    //终端IP 必填
    @XStreamAlias("spbill_create_ip")
    private String spbillCreateIp;

    //交易起始时间
    @XStreamAlias("time_start")
    private String timeStart;
    //交易结束时间
    @XStreamAlias("time_expire")
    private String timeExpire;

    //订单优惠标记
    @XStreamAlias("goods_tag")
    private String goodsTag;

    //通知地址      必填
    @XStreamAlias("notify_url")
    private String notifyUrl;

    //交易类型  必填
    @XStreamAlias("trade_type")
    private String tradeType;

    //商品ID
    @XStreamAlias("product_id")
    private String productId;

    //指定支付方式
    @XStreamAlias("limit_pay")
    private String limitPay;

    //用户标识
    private String openid;

    //场景信息
    @XStreamAlias("scene_info")
    private String sceneInfo;



    public String getAppid() {
        return appid;
    }

/**
     * 公众账号ID
     * 必填: 是
     * @param appid
     */

    public void setAppid(String appid) {
        this.appid = appid;
    }

    public String getMchId() {
        return mchId;
    }

/**
     * 商户号
     *  必填: 是
     * @param mchId
     */

    public void setMchId(String mchId) {
        this.mchId = mchId;
    }


    public String getDeviceInfo() {
        return deviceInfo;
    }

/**
     * 设备号
     * 必填: 否
     * @param deviceInfo
     */

    public void setDeviceInfo(String deviceInfo) {
        this.deviceInfo = deviceInfo;
    }

    public String getNonceStr() {
        return nonceStr;
    }

/**
     * 随机字符串
     * 必填: 是
     * @param nonceStr
     */

    public void setNonceStr(String nonceStr) {
        this.nonceStr = nonceStr;
    }

    public String getSign() {
        return sign;
    }

/**
     * 签名
     * 必填: 是
     * @param sign
     */

    public void setSign(String sign) {
        this.sign = sign;
    }

    public String getAttach() {
        return attach;
    }

/**
     * 附加数据
     * 必填: 否
     * @param attach
     */

    public void setAttach(String attach) {
        this.attach = attach;
    }

    public String getBody() {
        return body;
    }

/**
     * 商品描述
     * 必填: 是
     * @param body
     */

    public void setBody(String body) {
        this.body = body;
    }

    public String getDetail() {
        return detail;
    }


/**
     * 商品详情
     * 必填: 否
     * @param detail
     */

    public void setDetail(String detail) {
        this.detail = detail;
    }

    public String getNotifyUrl() {
        return notifyUrl;
    }

    public void setNotifyUrl(String notifyUrl) {
        this.notifyUrl = notifyUrl;
    }

    public String getOpenid() {
        return openid;
    }

    public void setOpenid(String openid) {
        this.openid = openid;
    }

    public String getOutTradeNo() {
        return outTradeNo;
    }

/**
     * 商户订单号
     * 必填: 是
     * @param outTradeNo
     */

    public void setOutTradeNo(String outTradeNo) {
        this.outTradeNo = outTradeNo;
    }

    public String getSpbillCreateIp() {
        return spbillCreateIp;
    }

    public void setSpbillCreateIp(String spbillCreateIp) {
        this.spbillCreateIp = spbillCreateIp;
    }

    public Integer getTotalFee() {
        return totalFee;
    }

    public void setTotalFee(Integer totalFee) {
        this.totalFee = totalFee;
    }

    public String getTradeType() {
        return tradeType;
    }

    public void setTradeType(String tradeType) {
        this.tradeType = tradeType;
    }

    public String getSign_type() {
        return sign_type;
    }

    public void setSign_type(String sign_type) {
        this.sign_type = sign_type;
    }

    public String getFeeType() {
        return feeType;
    }

    public void setFeeType(String feeType) {
        this.feeType = feeType;
    }

    public String getTimeStart() {
        return timeStart;
    }

    public void setTimeStart(String timeStart) {
        this.timeStart = timeStart;
    }

    public String getTimeExpire() {
        return timeExpire;
    }

    public void setTimeExpire(String timeExpire) {
        this.timeExpire = timeExpire;
    }

    public String getGoodsTag() {
        return goodsTag;
    }

    public void setGoodsTag(String goodsTag) {
        this.goodsTag = goodsTag;
    }

    public String getProductId() {
        return productId;
    }

    public void setProductId(String productId) {
        this.productId = productId;
    }

    public String getLimitPay() {
        return limitPay;
    }

    public void setLimitPay(String limitPay) {
        this.limitPay = limitPay;
    }

    public String getSceneInfo() {
        return sceneInfo;
    }

/**
     * 场景信息
     * 必填:是
     * 格式:{"h5_info": {"type":"Wap","wap_url": "https://pay.qq.com","wap_name": "腾讯充值"}}
     * @param sceneInfo
     */

    public void setSceneInfo(String sceneInfo) {
        this.sceneInfo = sceneInfo;
    }
}

在DemoController.java中填充信息:

@GetMapping("/order")
    public Map order() throws Exception {
        // 获取5分钟后的时间
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
        calendar.add(Calendar.MINUTE, 5);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
        String timeExpire = sdf.format(calendar.getTime());
        // 统一下单Bean
        WxPayUnifiedorderRequest wxRequest = new WxPayUnifiedorderRequest();
        wxRequest.setAppid(WxConst.APPID);
        wxRequest.setMchId(WxConst.MCHID);
        // 随机串
        wxRequest.setNonceStr(RandomUtil.getRandomStr());
        wxRequest.setBody("TEST");
        // 订单号
        wxRequest.setOutTradeNo(WXUtil.getOutTradeNo());
        // 订单价格
        wxRequest.setTotalFee(1);
        // ip,当发起的是微信公众号支付时,微信对ip不进行校验
        wxRequest.setSpbillCreateIp("8.8.8.8");
        // 异步通知
        wxRequest.setNotifyUrl(WxConst.ORDER_NOTIFYURL);
        wxRequest.setTradeType("JSAPI");
        // 微信OpenId
        wxRequest.setOpenid("oEjT4vy21gy6m96WlGkV6AvhgG-E");
        // 订单过期时间
        wxRequest.setTimeExpire(timeExpire);
        // 签名
        wxRequest.setSign(sign(xmlToMap(XmlUtil.toXMl(wxRequest)), WxConst.MCHKEY));
        // WxConst.JS_URL表示支付页面的地址
        Map jsMap = WxPayUtil.pay(wxRequest, WxConst.JS_URL);
        return jsMap;
    }

随机串的生成算法:
RandomUtil.java

package com.demo.util;

public class RandomUtil {
    // 参与算法的字符
  private static final String RANDOM_STR = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

  private static final java.util.Random RANDOM = new java.util.Random();

  public static String getRandomStr() {
    StringBuilder sb = new StringBuilder();
    // 从参与算法的字符中随机选出一个字符,循环16次,16为随机数
    for (int i = 0; i < 16; i++) {
      sb.append(RANDOM_STR.charAt(RANDOM.nextInt(RANDOM_STR.length())));
    }
    return sb.toString();
  }

}

WXUtil.getOutTradeNo() 来动态获取订单号:

/**
     *生成18位随机订单号
     * 时间+5位随机数
     *
     */
    public static String getOutTradeNo(){
        Date date=new Date();
        // 日期精确到秒加4位随机数
        DateFormat format=new SimpleDateFormat("yyyyMMddHHmmss");
        String time=format.format(date);
        String radom = String.valueOf(new Random(7).nextInt(10000));
        String outTradeNo = time + radom ;
        return outTradeNo;
    }

XmlUtil.toXMl(WxPayUnifiedorderRequest wxRequest);

工具类将bean转成xml

private static XStream createxStream(){
        return  new XStream(new XppDriver(new NoNameCoder()) {

            @Override
            public HierarchicalStreamWriter createWriter(Writer out) {
                return new PrettyPrintWriter(out) {
                    // 对所有xml节点的转换都增加CDATA标记
                    boolean cdata = true;

                    @Override
                    @SuppressWarnings("rawtypes")
                    public void startNode(String name, Class clazz) {
                        super.startNode(name, clazz);
                    }

                    ////当对象属性带下划线时,XStream会转换成双下划线,
                    // 重写这个方法,不再像XppDriver那样调用nameCoder来进行编译,而是直接返回节点名称,避免双下划线出现
                    @Override
                    public String encodeNode(String name) {
                        return name;
                    }


                    @Override
                    protected void writeText(QuickWriter writer, String text) {
                        if (cdata) {
                            writer.write(");
                            writer.write(text);
                            writer.write("]]>");
                        } else {
                            writer.write(text);
                        }
                    }
                };
            }
        });
    }

    /**
     * 对象转xml
     * @param obj
     * @return
     */
    public static String toXMl(Object obj) {
        //使用注解设置别名必须在使用之前加上注解类才有作用
        XStream xStream = createxStream();
        xStream.processAnnotations(obj.getClass());
        return xStream.toXML(obj);
    }

XmlUtil.xmlToMap(String xmlString);

将xml类型数据转化成map,这里先将bean转xml再转map是想去掉多余的参数字段,因为后面需要加密

/**
     * XML格式字符串转换为Map
     *
     * @param strXML XML字符串
     * @return XML数据转换后的Map
     * @throws Exception
     */
    public static Map xmlToMap(String strXML) throws Exception {
        try {
            Map data = new HashMap();
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
            InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
            org.w3c.dom.Document doc = documentBuilder.parse(stream);
            doc.getDocumentElement().normalize();
            NodeList nodeList = doc.getDocumentElement().getChildNodes();
            for (int idx = 0; idx < nodeList.getLength(); ++idx) {
                Node node = nodeList.item(idx);
                if (node.getNodeType() == Node.ELEMENT_NODE) {
                    org.w3c.dom.Element element = (org.w3c.dom.Element) node;
                    data.put(element.getNodeName(), element.getTextContent());
                }
            }
            try {
                stream.close();
            } catch (Exception ex) {
                // do nothing
            }
            return data;
        } catch (Exception ex) {
            throw ex;
        }

    }

WxPaySignatureUtil.sign(Map params, String signKey);

微信签名介绍:
假设传送的参数如下:

appid: wxd930ea5d5a258f4f
mch_id: 10000100
device_info: 1000
body: test
nonce_str: ibuaiVcKdpRxkhJA

第一步:对参数按照key=value的格式,并按照参数名ASCII字典序排序如下:

stringA=”appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA”;

第二步:拼接API密钥:

stringSignTemp=stringA+”&key=192006250b4c09247ec02edce69f6a2d” //注:key为商户平台设置的密钥key
sign=MD5(stringSignTemp).toUpperCase()=”9A0A8659F005D6984697E2CA0A9CF3B7” //注:MD5签名方式
sign=hash_hmac(“sha256”,stringSignTemp,key).toUpperCase()=”6A9AE1657590FD6257D693A078E1C3E4BB6BA4DC30B23E0EE2496E54170DACD6” //注:HMAC-SHA256签名方式

最终得到最终发送的数据:

<xml>

<appid>wxd930ea5d5a258f4fappid>

<mch_id>10000100mch_id>

<device_info>1000device_info>

<body>testbody>

<nonce_str>ibuaiVcKdpRxkhJAnonce_str>

<sign>9A0A8659F005D6984697E2CA0A9CF3B7sign>

xml>

签名代码:

/**
     * 签名
     * MD5
     * @param params
     * @param signKey
     * @return
     */
    public static String sign(Map params, String signKey) {
        // 将Map中的字段都进行按字母顺序排序
        SortedMap sortedMap = new TreeMap<>(params);
        StringBuilder toSign = new StringBuilder();
        for (String key : sortedMap.keySet()) {
            String value = params.get(key);
            // 当值不为空并且sign字段和key字段不参与字段连接
            if (StringUtils.isNotEmpty(value) && !"sign".equals(key) && !"key".equals(key)) {
                toSign.append(key).append("=").append(value).append("&");
            }
        }
        // 最后加上key字段,内容是商户密钥
        toSign.append("key=").append(signKey);
        // 使用apache的DigestUtils来进行md5加密,返回加密后的字符串
        return DigestUtils.md5Hex(toSign.toString()).toUpperCase();
    }

将sign字段加入bean中后,使用

WxPayUtil.pay(WxPayUnifiedorderRequest wxRequest, String url);

来向微信服务器发起请求:

    /**
     * 微信支付
     * @param jsUrl
     * @return
     * @throws Exception
     */
    public static Map pay(WxPayUnifiedorderRequest wxRequest, String jsUrl) throws Exception{
        String xml = XmlUtil.toXMl(wxRequest);
        String result =  HttpClientUtil.post(WxConst.WXPAY_UNIFIEDORDER_GATEWAY,xml, "XML");
        Map map = handleReturnMessage( result, jsUrl);
        return map;
    }

2.封装发起支付需要的参数

有个难点就是需要处理微信返回的数据,如果数据合格就需要进行签名并提取JS-SDK中wx.config()中的字段
JS-SDK的签名算法:

  • 1.获取随机串noncestr,jsapiticket,时间戳timestamp,支付页面url
  • 2.对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1
  • 对string1进行sha1签名,得到signature

注意事项
1.签名用的noncestr和timestamp必须与wx.config中的nonceStr和timestamp相同。
2.签名用的url必须是调用JS接口页面的完整URL。
3.出于安全考虑,开发者必须在服务器端实现签名的逻辑。
4.最坑的一点,注意页面的noncestr和参与签名的nonceStr大小写区别。和后面部分调起支付的timestamp与参与签名的timeStamp大小写。

public static Map handleReturnMessage(String returnMessage,String jsUrl)

/**
     *  最后返回给前端的数据
     * @param returnMessage
     * @param jsUrl
     * @return
     * @throws Exception
     */
    public static Map<String, String> handleReturnMessage(String returnMessage,String jsUrl) throws  Exception{
        // 将微信返回的数据转成map
        Map returnMap =  xmlToMap(returnMessage);
        Map<String,String> resultMap = new HashMap<String, String>();
        // 获取微信的两种代码
        String return_code = (String)returnMap.get("return_code");
        String result_code = (String)returnMap.get("result_code");
        System.out.println(returnMap);
        // 当return_code返回失败时,将错误信息返回前端
        if(return_code.equals(WxConst.FAIL)){
            resultMap.put("error", (String)returnMap.get("return_msg"));
            return resultMap;
        }
        // result_code返回失败时,返回错误信息前端
        if(result_code.equals(WxConst.FAIL)){
            resultMap.put("error", (String)returnMap.get("err_code_des"));
            return resultMap;
        }

        String tradeType = (String)returnMap.get("trade_type");
        // 返回的公众号支付,进行JS-SDK的wx.config()中的字段获取
        if(tradeType.equals("JSAPI")){
            String perpayId = (String)returnMap.get("prepay_id");
            String appId = (String)returnMap.get("appid");
            String signType = "MD5";

            String jsapi_ticket = WxConst.JSAPITICKET;
            String nonceStr = UUID.randomUUID().toString();
            String timestamp = String.valueOf(WXUtil.getSecondTimestamp());
            if(jsUrl == null) {
                return null;
            };
            //微信签名
            Map<String, String> JSAPIMap = new HashMap<String,String>();
            JSAPIMap.put("jsapi_ticket",jsapi_ticket);
            JSAPIMap.put("noncestr",nonceStr);
            JSAPIMap.put("timestamp",timestamp);
            JSAPIMap.put("url",jsUrl);
            String signature = generateJSAPISignature( JSAPIMap);
            // 支付签名
            Map<String, String> JSAPIPayMap = new HashMap<String,String>();
            JSAPIPayMap.put("appId",appId);
            JSAPIPayMap.put("timeStamp",timestamp);
            JSAPIPayMap.put("nonceStr",nonceStr);
            JSAPIPayMap.put("package","prepay_id="+perpayId); //prepay_id=wx201710301549453bd02ae87d0526670466
            JSAPIPayMap.put("signType","MD5");
            String paySign = sign(JSAPIPayMap, WxConst.MCHKEY);

            //返回结果
            resultMap.put("appId",appId);
            resultMap.put("timestamp",timestamp);
            resultMap.put("nonceStr",nonceStr);
            resultMap.put("signature",signature);
            resultMap.put("package","prepay_id="+perpayId);
            resultMap.put("signType","MD5");
            resultMap.put("paySign",paySign);
            return resultMap;

        }
        if (tradeType.equals("MWEB")){//返回的是H5支付
            resultMap.put("mweb_url",(String)returnMap.get("mweb_url"));
            return resultMap;
        }
        return null;
    }

测试结果:
ssm微信公众号支付demo-以及配置过程_第7张图片
说明下单成功!

3、发起支付

这样后台完成了发起支付的操作了,前端需要将这些数据填充到JS-SDK中发起支付,我们写一个静态jsp页面进行测试:
pay.jsp

<html>
<body>
<meta charset="UTF-8">
<title>Titletitle>
<script type="text/javascript" src="http://res.wx.qq.com/open/js/jweixin-1.0.0.js">script>
<h2>Hello World!h2>
<script>
    wx.config({
        debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
        appId: 'wx*******ed032', // 必填,公众号的唯一标识
        timestamp: "1526020385", // 必填,生成签名的时间戳
        nonceStr: '03ac62ff-7ead-403c-984e-ced5fd8e12cc', // 必填,生成签名的随机串
        signature: 'c2724aefc4e1b20048a5934aed4834d761d77abb',// 必填,签名
        jsApiList: ['chooseWXPay'] // 必填,需要使用的JS接口列表
    });

    wx.ready(function(){
        alert("支付");
        wx.chooseWXPay({
            timestamp: '1526020385', // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
            nonceStr: '03ac62ff-7ead-403c-984e-ced5fd8e12cc', // 支付签名随机串,不长于 32 位
            package: 'prepay_id=wx111***081363443414418cb31285518214', // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=\*\*\*)
            signType: 'MD5', // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'
            paySign: 'FED7D5D14F5D04628FB82D41B5979BEC', // 支付签名
            success: function (res) {
// 支付成功后的回调函数
                alert("支付成功");
            },
            cencel:function(res){
                alert('cencel pay');
            },
            fail: function(res){
                alert('pay fail');
                alert(JSON.stringify(res));
            }
        });
        wx.error(function(res){
            alert(res.err_msg);
        });
        // config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。
    });
script>
<input type="button" value="支付1111" onclick="">
body>
html>

测试过程:

  1. 调用接口获取JS-SDK需要的数据
  2. 将数据手动输入页面
  3. 通过微信手机点击链接发起支付

结果:
ssm微信公众号支付demo-以及配置过程_第8张图片
ssm微信公众号支付demo-以及配置过程_第9张图片
ssm微信公众号支付demo-以及配置过程_第10张图片
ssm微信公众号支付demo-以及配置过程_第11张图片
ssm微信公众号支付demo-以及配置过程_第12张图片

4、异步通知

异步通知很简单,先看官方文档说明:
地址:异步通知
总结下来就一下几点:

  • 没有说是通过post还是get请求我们服务器,不过携带了很多数据应该就是post
  • 微信请求自己服务器后,不管异步通知是否正确,应该马上返回成功接收的通知
<xml>
  <return_code>return_code>
  <return_msg>return_msg>
xml>
  • 如果因为网络原因导致微信异步通知发送了多次,需要自行处理多次请求的业务逻辑
  • 对于异步请求的数据一定要进行签名验证方可确认用户已付款

首先,写一个springmvc方法进行接收微信的请求:

@PostMapping("/notify")
    public void notify(HttpServletRequest request, HttpServletResponse response){
        //异步通知
        Map returnMap =  WxPayUtil.asynNotify(request,response);
        System.out.println(returnMap);
    }

方法

public static Map asynNotify(HttpServletRequest request, HttpServletResponse response)

如下:

    /**
     * 异步通知 成功返回Map,失败返回null
     * @param request
     * @param response
     * @return
     */
    public static Map asynNotify(HttpServletRequest request, HttpServletResponse response){
        ServletInputStream sis = null;
        String xmlData = "";
        Map returnMap = null;
        try {
            //编码格式改成UTF-8
            response.setCharacterEncoding("UTF-8");
            response.setContentType("text/xml;charset=utf-8");
            response.setHeader("Cache-control", "no-cache");

            sis = request.getInputStream();
            // 取HTTP请求流长度
            int size = request.getContentLength();
            // 用于缓存每次读取的数据
            byte[] buffer = new byte[size];
            // 用于存放结果的数组
            byte[] xmldataByte = new byte[size];
            int count = 0;
            int rbyte = 0;
            // 循环读取
            while (count < size) {
                // 每次实际读取长度存于rbyte中
                rbyte = sis.read(buffer);
                for (int i = 0; i < rbyte; i++) {
                    xmldataByte[count + i] = buffer[i];
                }
                count += rbyte;
            }
            xmlData = new String(xmldataByte, "UTF-8");

            PrintWriter out = response.getWriter();
            out.println(WxConst.NOTIFY_RESPONSE_BODY);
            returnMap = xmlToMap(xmlData);
            String return_code = (String) returnMap.get("return_code");
            if (return_code.equals(WxConst.FAIL)) {//不成功返回结果为空
               return null;
            }


            //验签
            if (return_code.equals(WxConst.SUCCESS)){
                if(!WxPaySignatureUtil.verify(returnMap,WxConst.MCHKEY)){//验签失败
                    return null ;
                }
            }

        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return returnMap;
    }
    /**
     * 校验签名
     * @param params
     * @param signKey
     * @return
     */
    public static Boolean verify(Map params, String signKey) {
        String sign = sign(params, signKey);
        return sign.equals(params.get("sign"));
    }

测试结果:
查看日志看到回复的数据:

{
    "transaction_id": "4200000144201808085758930691",
    "nonce_str": "wLnrwFqTgBvp5FDe",
    "bank_type": "CFT",
    "openid": "oEjT4vy21gy6m96WlGkV6AvhgG - E",
    "sign": "77361 FE27A070271F7EA78E5767A87DA",
    "fee_type": "CNY",
    "mch_id": "1489877082",
    "cash_fee": 1,
    "out_trade_no": "201808081646224236",
    "appid": "wx4a********0ed032",
    "total_fee": 1,
    "trade_type": "JSAPI",
    "result_code": "SUCCESS",
    "time_end": "20180808164817",
    "is_subscribe": "Y",
    "return_code": "SUCCESS"
}

微信支付和异步通知已经ok了
码云:地址

你可能感兴趣的:(微信相关)