微信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…