微信H5自主开发文档

298 阅读3分钟

微信H5商户号申请

1):登录微信商户平台:pay.weixin.qq.com/

进入之后:产品中心---》我的产品---》支付产品---》H5支付。进行开通H5支付

如果已经开通H5支付,直接进入:产品中心---》开发配置---》H5支付域名配置。新增新的H5支付域名即可

2):申请H5支付需要指定的支付域名的备案信息

查看备案信息的地址(icp/ip地址/域名信息备案管理系统):beian.miit.gov.cn/?wm=5317_00…

进入以后:ICP备案查询---》输入一级域名进行查询。以及域名就是xxx.com,而xxxx.xxx.com是二级域名

截图一级域名的备案即可

特别注意:申请的支付域名一定要有相关的对应商城,且商城地址可以直接访问,商城要求:有首页,首页必须要有商品和价格,点击商品要有商品信息以及价格,还需要有客服信息

H5支付核心文件和信息

登录微信商户平台:pay.weixin.qq.com/

1):微信证书文件:账户中心---》API安全---》管理证书。点击进去以后,找到最新的证书,复制证书序列号以及证书文件(apiclient_key.pem是证书秘钥文件)

2):微信商户V3秘钥:账户中心---》API安全---》设置APIv3秘钥,设置完成以后保存下来

3):商户号和商户号的key

代码开发步骤

调起H5微信支付的代码

1):导入依赖

        <!--微信支付V3-->
        <dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-apache-httpclient</artifactId>
            <version>0.4.7</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
        </dependency>

2):逻辑代码

订单实体类:

package com.rrb.common.module.payment.model.pojo;
import java.math.*;
import java.util.Date;
import java.sql.Timestamp;
import org.beetl.sql.core.annotatoin.Table;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
/*
* 
* gen by beetlsql 2023-06-14
*/
@Table(name="pyt_web_order")
@Getter
@Setter
@ApiModel(value = "PytWebOrder", description = "")
public class PytWebOrder  {

	/*
	订单号
	*/
	@ApiModelProperty(value = "订单号")
	private String id;
	/*
	回调后第三方处理状态
	*/
	@ApiModelProperty(value = "回调后第三方处理状态")
	private Integer extraStatus;
	/*
	0未删除、1已删除
	*/
	@ApiModelProperty(value = "0未删除、1已删除")
	private Integer isDeleted;
	/*
	支付类型id
	*/
	@ApiModelProperty(value = "支付类型id")
	private Integer payTypeId;
	/*
	状态:0未支付、1已支付、2已取消、3已过期、4退款中、 5已退款、6退款失败、7订单关闭
	*/
	@ApiModelProperty(value = "状态:0未支付、1已支付、2已取消、3已过期、4退款中、 5已退款、6退款失败、7订单关闭")
	private Integer status;
	/*
	收支类别:0支出 、1收入
	*/
	@ApiModelProperty(value = "收支类别:0支出 、1收入")
	private Integer type;
	/*
	实际支付金额(元)
	*/
	@ApiModelProperty(value = "实际支付金额(元)")
	private BigDecimal actualAmount;
	/*
	折扣(元)
	*/
	@ApiModelProperty(value = "折扣(元)")
	private BigDecimal disAmount;
	/*
	额外信息,JSON格式
	*/
	@ApiModelProperty(value = "额外信息,JSON格式")
	private String extraInfo;
	/*
	订单说明
	*/
	@ApiModelProperty(value = "订单说明")
	private String note;
	/*
	日期、商家号来生成
	*/
	@ApiModelProperty(value = "日期、商家号来生成")
	private String orderNo;
	/*
	原价(元)
	*/
	@ApiModelProperty(value = "原价(元)")
	private BigDecimal oriAmount;
	/*
	支付项目id
	*/
	@ApiModelProperty(value = "支付项目id")
	private Long payItemId;
	/*
	第三方接口返回值
	*/
	@ApiModelProperty(value = "第三方接口返回值")
	private String returnInfo;
	/*
	学校id
	*/
	@ApiModelProperty(value = "学校id")
	private Long schoolId;
	/*
	学生id
	*/
	@ApiModelProperty(value = "学生id")
	private Long studentId;
	/*
	用户id
	*/
	@ApiModelProperty(value = "用户id")
	private Long userId;
	/*
	创建时间
	*/
	@ApiModelProperty(value = "创建时间")
	private Date createTime;
	/*
	修改时间
	*/
	@ApiModelProperty(value = "修改时间")
	private Date modifyTime;
	/*
	支付时间
	*/
	@ApiModelProperty(value = "支付时间")
	private Date payTime;
	/*
	交易时间
	*/
	@ApiModelProperty(value = "交易时间")
	private Date transTime;

	/**
	 * 请求的IP地址
	 */
	private String ipPath;

}

代码实现基本逻辑:

    /**
     * H5支付测试
     *
     * @param pytWebOrder
     * @return
     */
    @Override
    public String doPaymentH5(PytWebOrder pytWebOrder) {
        // 创建订单信息
        // 生成订单号
        String orderId = WXSignH5Util.getUUID();
        System.out.println("orderId = " + orderId);

        // 生成订单流水号(日期和商户号拼接成)
        String orderNo = "NO:" + WeChatServiceEnvConstants.ZHXY_MCHID + System.currentTimeMillis();
        pytWebOrder.setId(orderId);
        pytWebOrder.setOrderNo(orderNo);
        pytWebOrder.setDisAmount(pytWebOrder.getOriAmount());
        pytWebOrder.setActualAmount(pytWebOrder.getOriAmount());
        pytWebOrderDao.insertOrder(pytWebOrder.getId(), pytWebOrder.getOrderNo(), pytWebOrder.getUserId(), pytWebOrder.getSchoolId(), pytWebOrder.getType(), pytWebOrder.getNote(), pytWebOrder.getPayItemId(), null, null, pytWebOrder.getPayTypeId(), pytWebOrder.getOriAmount(), pytWebOrder.getDisAmount(), pytWebOrder.getActualAmount(), pytWebOrder.getStatus(), pytWebOrder.getExtraInfo(), 0, new Date(), new Date(), pytWebOrder.getStudentId(), null);
        // 调用H5支付进行下单
        String wxh5Pay = WXH5Pay(pytWebOrder);

        return wxh5Pay;
    }

封装的调用微信H5支付的逻辑代码:

    /**
     * 微信支付----调用H5支付下单
     *
     * @param pytWebOrder
     * @return
     */
    public String WXH5Pay(PytWebOrder pytWebOrder) {
        log.info("调用微信支付开始=====================");
        // 取出拉起微信H5支付的地址
        String wechatH5Url = WeChatServiceEnvConstants.WECHAT_H5_URL;
        // 取出支付以后回调的地址
        String successCallBackUrl = rrbProperties.getZhxyWechatH5NotifyUrl();

        // 封装请求的参数
        Map<String, Object> param = new HashMap<>();// 总数据集合
        param.put("appid", WeChatServiceEnvConstants.ZHXY_APPID);
        param.put("mchid", WeChatServiceEnvConstants.ZHXY_MCHID);
        param.put("description", pytWebOrder.getNote());
        param.put("out_trade_no", pytWebOrder.getId());
        param.put("notify_url", successCallBackUrl);
        Map<String, Object> moneyParam = new HashMap<>();// 1金额数据集合
        moneyParam.put("total", pytWebOrder.getActualAmount().multiply(new BigDecimal(100)).intValue());
        moneyParam.put("currency", "CNY");
        param.put("amount", moneyParam);
        Map<String, Object> sceneParam = new HashMap<>();// 1场景信息
        Map<String, Object> hInfo = new HashMap<>();// 2H5场景信息
        sceneParam.put("payer_client_ip", pytWebOrder.getIpPath());
        hInfo.put("type", "Wap");
        sceneParam.put("h5_info", hInfo);
        param.put("scene_info", sceneParam);

        // 拼接参数信息----》参数信息进行签名加密----》放进请求头中
        // 请求方法(微信第三方的请求方式)
        String method = "POST";
        // 时间戳(微信签名限制时间戳只能精确到秒)
        long timestamp = System.currentTimeMillis() / 1000;
        // 随机字符串
        String nonceStr = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32).trim().toUpperCase();
        String signInfo = null;
        try {
            // 拼接参数信息(用来构造签名)----自己封装的方法
            String message = WXMessageInfo(new URL(wechatH5Url), method, timestamp, nonceStr, JSONUtil.toJsonStr(param));
            // 构造签名----自己封装的方法
            signInfo = wxSignInfo(message);
        } catch (Exception e) {
            log.info("微信支付---签名加密失败!!!");
            throw new APIException(ResponseCode.FAILED.getErrcode(), "微信支付---签名加密失败!!!");
//            e.printStackTrace();
        }

        // 拼接---设置请求头中的认证信息
        String header = "WECHATPAY2-SHA256-RSA2048 mchid=\"" + WeChatServiceEnvConstants.ZHXY_MCHID + "\",nonce_str=\"" + nonceStr + "\",signature=\"" + signInfo + "\",timestamp=\"" + timestamp + "\",serial_no=\"" + WeChatServiceEnvConstants.WECHAT_SERIAL_NO + "\"";

        // 发送微信第三方请求----H5下单
        log.info("调用微信支付的地址:{}", wechatH5Url);
        log.info("调用微信支付的参数信息:{}", JSONUtil.toJsonStr(param));
        String result = HttpUtil.createPost(wechatH5Url)
                .header("Content-Type", "application/json;chartset=utf-8")
                .header("Accept", "application/json")
                .header("Authorization", header)
                .body(JSONUtil.toJsonStr(param)).execute().body();

        log.info("调用微信支付结束=====================");
        log.info("调用微信支付返回结果:" + result);
        return result;

    }

封装的拼接参数的方法:

    /**
     * 拼接参数
     *
     * @param url
     * @param body
     * @return
     */
    public String WXMessageInfo(URL url, String method, long timestamp, String nonceStr, String body) {
//        第二步,获取请求的绝对URL,并去除域名部分得到参与签名的URL。如果请求中有查询参数,URL末尾应附加有'?'和对应的查询字符串。
        String canonicalUrl = url.getPath();
        if (url.getQuery() != null) {
            canonicalUrl += "?" + url.getQuery();
        }
        System.out.println("开始拼接参数信息:--------------------");
        System.out.println("方法类型 = " + method);
        System.out.println("方法的地址 = " + canonicalUrl);
        System.out.println("时间戳 = " + timestamp);
        System.out.println("随机字符 = " + nonceStr);
        System.out.println("body请求体 = " + body);
        return method + "\n"
                + canonicalUrl + "\n"
                + timestamp + "\n"
                + nonceStr + "\n"
                + body + "\n";
    }

封装的构造签名的方法(用于放在请求头中):

    /**
     * 构造签名
     *
     * @return
     */
    private String wxSignInfo(String mess) throws Exception {

        // 去除秘钥中的\n转义字符
        String wxFileKeyInfo = WXSignH5Util.wxFileKeyInfo();
        String mchKey = wxFileKeyInfo.replaceAll("\n", "");

        // 对信息进行签名加密
        String sign = WXSignH5Util.sign(mess.getBytes(StandardCharsets.UTF_8), mchKey);

        System.out.println("最终加密的签名 = " + sign);
        log.info("微信H5支付最终签名信息:{}", sign);
        return sign;
    }

自己封装的微信签名加密和读取证书文件的工具类(WXSignH5Util):

package com.rrb.zhxy.base.payment.utils;

import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.core.io.ClassPathResource;

import java.io.IOException;
import java.nio.charset.Charset;
import java.security.PrivateKey;
import java.security.Signature;
import java.util.Base64;
import java.util.UUID;

@Slf4j
public class WXSignH5Util {


    /**
     * 将数据SHA256 with RSA签名加密----》对签名结果进行Base64编码得到签名值
     *
     * @param message
     * @param key
     * @return
     * @throws Exception
     */
    public static String sign(byte[] message, String key) throws Exception {
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initSign(getPrivateKey(key));
        signature.update(message);
        String encode = Base64.getEncoder().encodeToString(signature.sign());
        return encode;
    }

    public static PrivateKey getPrivateKey(String key) {
        PrivateKey privateKey = PemUtil.loadPrivateKey(key);
        return privateKey;
    }

    /**
     * 获取微信证书文件中的公钥
     *
     * @return
     */
    public static String wxFileKeyInfo() {
        String fileInfo = null;
        try {
//            File file = ResourceUtils.getFile("classpath:files/apiclient_key.pem");
//            System.out.println("file = " + file);
//            fileInfo = FileUtils.readFileToString(file, StandardCharsets.UTF_8);
//            System.out.println("fileInfo = " + fileInfo);
            // 证书放的位置在项目:resources/files/apiclient_key.pem
            ClassPathResource resource = new ClassPathResource("files/apiclient_key.pem");
            fileInfo = IOUtils.toString(resource.getInputStream(), Charset.defaultCharset());
        } catch (IOException e) {
            System.out.println("获取文件信息异常!!!");
            log.info("获取文件信息异常!!!");
//            throw new APIException(ResponseCode.FAILED.getErrcode(), "获取微信文件信息异常!!!");
            log.info("报错信息:{}", e.getMessage());
            e.printStackTrace();

        }
        return fileInfo;
    }

    /**
     * 获取生成的订单号码
     *
     * @return
     */
    public static String getUUID() {
        String uuid = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32).trim();
        return uuid;
    }


}

回调的代码

1):基本代码实现

回调的实体类

package com.rrb.common.module.payment.model;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class WeChatPayEntity {

    @JsonProperty("id")
    private String id;

    @JsonProperty("create_time")
    private String create_time;

    @JsonProperty("resource_type")
    private String resource_type;

    @JsonProperty("event_type")
    private String event_type;

    @JsonProperty("summary")
    private String summary;

    @JsonProperty("resource")
    private WeChatRequestEntities weChatRequestEntities;

}

package com.rrb.common.module.payment.model;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class WeChatRequestEntities {

    @JsonProperty("original_type")
    private String original_type;

    @JsonProperty("algorithm")
    private String algorithm;

    @JsonProperty("ciphertext")
    private String ciphertext;

    @JsonProperty("associated_data")
    private String associated_data;

    @JsonProperty("nonce")
    private String nonce;


}

回调解析数据代码:

    /**
     * H5支付回调测试
     *
     * @param weChatPayEntity
     * @return
     */
    @Override
    public WwChatResultInfo doPaymentH5CallBack(WeChatPayEntity weChatPayEntity) {
        log.info("微信回调的参数信息:{}",JSONUtil.toJsonStr(weChatPayEntity));
        log.info("微信H5支付回调开始----------------------");
        // 设置最终返回给微信的值----默认为失败
        WwChatResultInfo msg = new WwChatResultInfo();
        msg.setCode("FAIL");

        // 参数判断
        if (weChatPayEntity == null) {
            log.info("回调参数为空!!!");
            msg.setMessage("回调参数为空");
            return msg;
        }
        if (weChatPayEntity.getWeChatRequestEntities() == null) {
            log.info("回调参数核心信息为空!!!");
            msg.setMessage("回调参数核心信息为空");
            return msg;
        }
        String associated_data = weChatPayEntity.getWeChatRequestEntities().getAssociated_data();
        String nonce = weChatPayEntity.getWeChatRequestEntities().getNonce();
        String ciphertext = weChatPayEntity.getWeChatRequestEntities().getCiphertext();
        log.info("开始对微信H5支付的参数进行解密--------------------------");
        // 解密----使用自己封装的工具类进行解密
        String result = WXDecodeUtil.WxDecodeInfo(associated_data, nonce, ciphertext);
        if (StringUtils.isBlank(result)) {
            log.info("返回的解析值为空!!!");
            msg.setMessage("回调参数核心信息为空");
            return msg;
        }
        log.info("对微信H5支付的参数进行解密完成--------------------------");
        String orderNo = null;
        String wxOrderNo = null;
        String tradeState = null;
        String successTime = null;
        String openId = null;
        try {
            // 解析参数信息
            ObjectMapper objectMapper = new ObjectMapper();
            JSONObject readValue = objectMapper.readValue(result, JSONObject.class);
            /**
             * 取出解析结果中的指定的值
             * 小程序id(或者公众号)
             * 商户号id
             * 订单号(自己生成的)
             * 微信订单号
             * 支付类型(JSAPI:公众号支付,NATIVE:扫码支付,APP:APP支付,MICROPAY:付款码支付,MWEB:H5支付,FACEPAY:刷脸支付)
             * 支付状态(SUCCESS:支付成功,REFUND:转入退款,NOTPAY:未支付,CLOSED:已关闭,REVOKED:已撤销(付款码支付),USERPAYING:用户支付中(付款码支付),PAYERROR:支付失败(其他原因,如银行返回失败))
             * 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用,实际情况下只有支付完成状态才会返回该字段
             * 支付完成时间
             * 支付者信息
             * -----支付用户在直连商户appid下的唯一标识
             */
            String appid = (String) readValue.get("appid");
            String mchid = (String) readValue.get("mchid");
            orderNo = (String) readValue.get("out_trade_no");
            wxOrderNo = (String) readValue.get("transaction_id");
            String tradeType = (String) readValue.get("trade_type");
            tradeState = (String) readValue.get("trade_state");
            String attach = (String) readValue.get("attach");
            successTime = (String) readValue.get("success_time");
            Object payer = readValue.get("payer");
            JSONObject payInfo = objectMapper.readValue(JSONUtil.toJsonStr(payer), JSONObject.class);
            openId = (String) payInfo.get("openid");
        } catch (JsonProcessingException e) {
            log.info("微信回调参数解析失败:{}",result);
//            e.printStackTrace();
            msg.setMessage("微信回调参数解析失败");
            return msg;
        }

        // 查看指定的订单信息
        if (StringUtils.isBlank(orderNo)) {
            log.info("微信回调订单不可为空!!!");
            msg.setMessage("微信回调订单不可为空");
            return msg;
        }
        PytWebOrder single = pytWebOrderDao.single(orderNo);
        if (single == null) {
            log.info("支付订单不存在!!!");
            msg.setMessage("支付订单不存在");
            return msg;
        }
        // 判断支付状态
        if (!"SUCCESS".equals(tradeState)) {
            // 支付失败
            single.setReturnInfo(tradeState);
            pytWebOrderDao.upsert(single);
            log.info("支付失败:{}", tradeState);
            msg.setMessage("支付订单失败");
            return msg;
        }
        // 判断订单是否支付成功---填充相关的订单信息,更改数据库中的订单状态
        single.setStatus(1);
        single.setPayTime(cn.hutool.core.date.DateUtil.parse(successTime));
        single.setTransTime(new Date());
        single.setExtraStatus(1);
        single.setReturnInfo("|state|" + tradeState + "/|wxOrder|" + wxOrderNo + "/|user|" + openId);
        pytWebOrderDao.upsert(single);
        msg.setCode("SUCCESS");
        msg.setMessage(null);
        log.info("微信H5支付回调结束----------------------");
        return msg;
    }

封装的解析微信回调数据的工具类(WXDecodeUtil):

package com.rrb.zhxy.base.payment.utils;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

@Slf4j
public class WXDecodeUtil {

    /**
     * 解密微信订单数据
     *
     * @param associatedData
     * @param nonce
     * @param content
     * @return
     */
    public static String WxDecodeInfo(String associatedData, String nonce, String content) {

        String str = null;
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            // 微信商户号中的APIv3的秘钥
            String key = "ed013e618ea3c04e31ee0480ad2b7977";
            byte[] decode = Base64.getDecoder().decode(content);
            cipher.init(2, new SecretKeySpec(key.getBytes(), "AES"), new GCMParameterSpec(128, nonce.getBytes()));
            if (StringUtils.isNotEmpty(associatedData)) {
                cipher.updateAAD(associatedData.getBytes());
            }
            str = new String(cipher.doFinal(decode), StandardCharsets.UTF_8);
            log.info("回调解密完成的数据:{}",str);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return str;
    }

}

微信支付回调使用APIV3秘钥进行解析的注意点

使用JDk1.8的环境进行解密的时候,会报错

java.security.InvalidKeyException: Illegal key size

这个报错的原因是因为JRE中自带的“local_policy.jar ”和“US_export_policy.jar”是支持128位密钥的加密算法,而当我们要使用256位密钥算法的时候,已经超出它的范围,无法支持

解决方法:

​ 查看地址:developers.weixin.qq.com/community/d…

已经下载的安装包:

下载地址:JDK8: www.oracle.com/technetwork…