微信支付JSAPI
JSAPI支付介绍:
JSAPI支付是用户在微信中打开商户的H5页面,商户在H5页面通过调用微信支付提供的JSAPI接口调起微信支付模块完成支付。
应用场景有:
◆ 用户在微信公众账号内进入商家公众号,打开某个主页面,完成支付。
◆ 用户的好友在朋友圈、聊天窗口等分享商家页面连接,用户点击链接打开商家页面,完成支付。
◆ 将商户页面转换成二维码,用户扫描二维码后在微信浏览器中打开页面后完成支付。
这里使用将商户页面转换成二维码,用户扫描二维码后在微信浏览器中打开页面后完成支付。
一.JSAPI支付前期准备
JSAPI支付需要准备 :公众号和商户号
1.微信支付需要进行申请
2.公众号平台- 网页授权 和JS授权
网页授权
JS授权
3.微信支付商户平台-支付授权目录
**ps:出现添加后页面没有反应。**
1.添加支付目录后,按流程正确输入操作密码后。界面会跳回来,这个时候发现并没有添加成功。
2.接下来不要刷新网页,也不要点击别的模块。再添加一次刚才添加的目录。
3.这个时候,你会发现不用输入操作密码了,然后,目录也添加成功了
支付授权目录
ps:最好选择为 https//
4. 关联账号
需关联商户平台,如果是公众号支付就关联公众号,小程序则关联小程序。
二、获取主要参数
微信商户:
Appid:xxxx;
mch_id:xx
APi:xxx;
公众号:
Appid: xx
AppSecret:xx
1、商户号平台参数
商户号Appid的获取方法:微信支付商户后台(pay.weixin.qq.com) -> 【账户中心】->【账户设置-商户信息】->微信支付商户号。
微信支付商户号mch_id
旧版本-商户密匙API:在微信支付商户平台(pay.weixin.qq.com),进入【账户中心】->【账户设置-API安全】->【设置密钥】,自助设置32位API密钥即可。如下图所示:
*注意:请事先将需设置的密钥用文档记录,设置成功后不支持查看,只支持修改重设。
新版本-申请api证书和设置apiv2的key
APIv2 中,调用微信支付安全级别较高的接口(如:退款、企业红包、企业付款)需要API证书。
ps:商户证书说明
2.公众号参数
Appid:xx
AppSecret:xx
ps:注意设置服务器白名单。
3.开通JSAPI支付
配置完API密钥后,请记得在【产品中心 – JSAPI支付】,开通JSAPI支付。
ed:微信支付开通完成。
三、支付和退款
微信支付-》微信支付回调-》验证回调的签名支付信息-》支付成功
微信退款-》微信退款回调-》解密回调信息-》退款成功
1.获取用户的openid
这里使用的是:网页授权获取用户openid
具体而言,网页授权流程分为四步:
- 引导用户进入授权页面同意授权,获取code
- 通过code换取网页授权access_token(与基础支持中的access_token不同)
- 如果需要,开发者可以刷新网页授权access_token,避免过期
- 通过网页授权access_token和openid获取用户基本信息(支持UnionID机制)
网站链接转二维码地址:https://cli.im/
urlEncode 处理地址:http://www.jsons.cn/urlencode/
通过地址生成的二维码用微信扫码进入,后台就获取到了openId。
2022年3月2日 再次使用发现上面回调地址中如果携带了state字符,在后台接收时必须对应接收才可获取数据。如:public ModelAndView queryFee(String code,string state) { …}
后台代码:
//TODO 回调接口
@RequestMapping("/gzh")
public class GzhHtmlController {
@RequestMapping(value = "/queryFee")
public ModelAndView queryFee(String code) {
ModelAndView mv = new ModelAndView();
String openid=openidUtil.getOpenid(code);
mv.addObject("openid",openid);
mv.setViewName("gzh/index");
return mv;
}
}
getOpenid方法:
/**
*
* @param appid 公众号appid
* @param appSecret 公众号appsecret
* @param code
* @return
*/
public String getOpenid(String appid, String appSecret, String code) {
try {
RestTemplate restTemplate = new RestTemplate();
String url = String.format(
"https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code",
appid, appSecret, code);
String res = restTemplate.getForObject(url, String.class);
ObjectMapper mapper = new ObjectMapper();
@SuppressWarnings("rawtypes")
HashMap m = mapper.readValue(res, HashMap.class);
String openid = (String) m.get("openid");
return openid;
} catch (Exception e) {
e.printStackTrace();
return null;
}
2.支付代码
工具类WXPayUtil是官方的sdk包中,下载导入。
参考:https://blog.csdn.net/daotiao0199/article/details/85284038
2.1 准备好11个参数:
appid:商家平台ID。在微信的平台上有
body:商品描述。
mch_id:商户ID。在微信的平台上有
nonce_str:随机字符串,UUID就好了。
openid:用户标识。因为这边是用户已经登录成功了。所以在session中就能拿到。
out_trade_no:商户订单号
spbill_create_ip:终端IP。这个可以从请求头中拿到
total_fee:支付金额。单位是分。
trade_type:交易类型。这里我填JSAPI
notify_url:通知地址。就是用户支付成功之后,微信访问你的哪个接口,跟你传递支付成功的相关信息。
生成的签名:
sign:签名。这个签名它是由上面的10个参数计算得出的。
2.2 支付代码
wxpay类也是微信的sdk
private final Logger log = LoggerFactory.getLogger(CreatePaymentController.class);
/**
* @Description 微信浏览器内微信支付/公众号支付(JSAPI)
* @param request
* @param code
* @return Map
*/
@RequestMapping(value = "orders")
public Map<String, String> orders() {
try {
Map<String, String> paraMap= new HashMap<String, String> ;
String openId = "用户的openid";
paraMap.put("appid", AuthUtil.APPID); // 商家平台ID
paraMap.put("body", "纯情小店铺-薯条"); // 商家名称-销售商品类目、String(128)
paraMap.put("mch_id", AuthUtil.MCHID); // 商户ID
paraMap.put("nonce_str", WXPayUtil.generateNonceStr()); // UUID
paraMap.put("openid", openId);
paraMap.put("out_trade_no", UUID.randomUUID().toString().replaceAll("-", ""));// 订单号,每次都不同
paraMap.put("total_fee", "1"); // 支付金额,单位分
paraMap.put("trade_type", "JSAPI"); // 支付类型
paraMap.put("notify_url", "用户支付完成后,你想微信调你的哪个接口");// 此路径是微信服务器调用支付结果通知路径随意写
String sign = WXPayUtil.generateSignature(paraMap, AuthUtil.PATERNERKEY);
paraMap.put("sign", sign);
//业务处理 生成商户订单
Payment payment =xxx;
// 统一下单
Map<String, String> result= wxpay.unifiedOrder(paraMap);
System.out.println("result为:" + result.toString());
// 判断是否成功 并返回给前端,前端调用微信的组件进行支付
Map<String, String> data = new HashMap<>();
if (result.get("return_code").equals("SUCCESS")) {
data.put("appId", result.get("appid"));
data.put("timeStamp", WXPayUtil.getCurrentTimestamp() + "");
data.put("nonceStr", WXPayUtil.generateNonceStr());
data.put("package", "prepay_id=" + result.get("prepay_id"));
data.put("signType", "HMACSHA256");
data.put("paySign", WXPayUtil.generateSignature(data, config.getKey(), SignType.HMACSHA256));
// 返回SUCCESS,注意只有签名之后才能设置
data.put("returnCode", "SUCCESS");
data.put("outTradeNo", payment.getOutTradeNo());
} else {
data.put("returnCode", "FAIL");
data.put("returnMsg", "创建微信订单失败,错误为:" + result.get("return_msg"));
}
log.debug(data.toString());
return data;
}
/**
* @Title: callBack
* @Description: 支付完成的回调函数
* @param:
* @return:
*/
@RequestMapping("/notify")
public String callBack(HttpServletRequest request, HttpServletResponse response) {
// System.out.println("微信支付成功,微信发送的callback信息,请注意修改订单信息");
InputStream is = null;
try {
is = request.getInputStream();// 获取请求的流信息(这里是微信发的xml格式所有只能使用流来读)
String xml = WXPayUtil.InputStream2String(is);
Map<String, String> notifyMap = WXPayUtil.xmlToMap(xml);// 将微信发的xml转map
System.out.println("微信返回给回调函数的信息为:"+xml);
if (notifyMap.get("result_code").equals("SUCCESS")) {
String ordersSn = notifyMap.get("out_trade_no");// 商户订单号
String amountpaid = notifyMap.get("total_fee");// 实际支付的订单金额:单位 分
BigDecimal amountPay = (new BigDecimal(amountpaid).divide(new BigDecimal("100"))).setScale(2);// 将分转换成元-实际支付金额:元
/*
* 以下是自己的业务处理------仅做参考 更新order对应字段/已支付金额/状态码
*/
System.out.println("===notify===回调方法已经被调!!!");
}
// 告诉微信服务器收到信息了,不要在调用回调action了========这里很重要回复微信服务器信息用流发送一个xml即可
response.getWriter().write("<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
3.退款代码
/**
* 根据支付记录,进行预退款
* @param outRefundNo parkingpay系统中的退款订单号
* @param paymentId 该支付记录的id
* @param refundFee 退款金额
* @return
* @throws Exception
*/
@RequestMapping("/doRefund")
@Transactional
// TODO 增加一个调用安全控制
public Map<String, Object> Refund(String outRefundNo, Long paymentId, BigDecimal refundFee) throws Exception {
Map<String, Object> resp = new HashMap<>();
resp.put("returnCode", "SUCCESS");
log.debug("paymentId:{}", paymentId);
// 检查payment是否存在
Payment payment = paymentMapper.selectByPrimaryKey(paymentId); // 根据系统内支付ID查询
if (payment == null) {
log.debug("无效支付订单号");
resp.put("returnCode", "FAIL");
resp.put("returnMsg", "无效支付订单号");
return resp;
}
// 检查payment状态是否为支付成功
if (!payment.getStatus().equals(PaymentStatus.SUCCESS)) {
log.debug("订单状态:{} 不正确", payment.getStatus());
resp.put("returnCode", "FAIL");
resp.put("returnMsg", "订单状态不正确");
return resp;
}
// 检查是否存在退款订单
Refund refund = refundMapper.selectByOutRefundNo(outRefundNo);
if (refund == null) {
log.debug("退款申请不存在");
resp.put("returnCode", "FAIL");
resp.put("returnMsg", "退款申请不存在");
return resp;
}
// 检查退款订单状态是否为申请退款
if (!refund.getStatus().equals(RefundStatus.CREATED)) {
log.debug("退款申请状态:{} 不正确", refund.getStatus());
resp.put("returnCode", "FAIL");
resp.put("returnMsg", "退款申请状态不正确");
return resp;
}
// 判断退款订单金额与付款金额大小
if (refundFee.compareTo(payment.getFee()) == 1) {
log.debug("申请退款:{} > 实际缴纳:{}", refundFee, payment.getFee());
resp.put("returnCode", "FAIL");
resp.put("returnMsg", "退款申请金额大于已付金额");
return resp;
}
log.debug("准备退款参数");
// 必要参数
Map<String, String> parameters = new HashMap<>();
parameters.put("appid", appid); // 微信小程序appid
parameters.put("mch_id", mch_id); // 微信支付的商户号
parameters.put("nonce_str", WXPayUtil.generateNonceStr()); // 随机字符串
parameters.put("out_trade_no", payment.getOutTradeNo()); // 商户订单号
parameters.put("out_refund_no", refund.getOutRefundNo()); // 商户退款单号
parameters.put("total_fee", payment.getFee().multiply(BigDecimal.valueOf(100)).intValue() + ""); // 订单金额
parameters.put("refund_fee", refundFee.multiply(BigDecimal.valueOf(100)).intValue() + ""); // 退款金额。由于可以分批退款,所以退款金额<=订单金额
// 非必要参数
parameters.put("sign_type", "HMACSHA256"); // 签名类型 默认MD5
parameters.put("refund_fee_type", "CNY"); // 退款货币种类 默认CNY 可以不填
parameters.put("notify_url", notifyUrl); // 回调url
// 根据以上数据生成sign签名
parameters.put("sign", WXPayUtil.generateSignature(parameters, key, SignType.HMACSHA256));// 工具类的方法,key就是平台上获取的
log.debug("退款数据:{}", parameters.toString());
// 向微信发起退款申请并获取 response
WXPay wxPay = new WXPay(smallConfig);
log.debug("调用退款");
Map<String, String> returnMap;
try {
returnMap = wxPay.refund(parameters);
} catch (Exception e) {
e.printStackTrace();
throw e;
}
log.debug("退款申请返回:{}" + returnMap);
// retrunMap 为微信返回的 response body 中的 data
if ("SUCCESS".equals(returnMap.get("return_code")) && "SUCCESS".equals(returnMap.get("result_code"))) {
log.debug("更新退款申请(id:{}),状态为退款中", refund.getId());
// 修改退费申请的状态与实际退费金额
refund.setStatus(RefundStatus.REFUNDING);
refundMapper.updateByPrimaryKeySelective(refund);
resp.put("returnCode", "SUCCESS");
resp.put("returnMsg", "退款成功");
return resp;
} else if ("SUCCESS".equals(returnMap.get("return_code")) && "FAIL".equals(returnMap.get("result_code"))) {
// 请求发出,但微信端正常拒绝(接口调用正常,但业务不对:退款金额大于支付金额等)
log.debug("退款失败(接口调用正常)");
log.debug("失败原因:{}", returnMap.get("err_code_des"));
resp.put("returnCode", "FAIL");
resp.put("returnMsg", "退款失败,原因:" + returnMap.get("err_code_des"));
return resp;
} else {
// 请求错误(接口调用失败:签名失败、参数格式错误等)
// 失败关闭退款订单-删除 防止重复退款请求
log.debug("退款失败(接口调用出错)");
resp.put("returnCode", "FAIL");
resp.put("returnMsg", "退款失败,原因:" + returnMap.get("return_msg"));
return resp;
}
}
/**
* 退款回调
*
* @param request
* @return
* @throws Exception
*/
@RequestMapping("/notify")
public String finishPaymentOrder(HttpServletRequest request) throws Exception {
Map<String, String> respData = new HashMap<>();
String resXml = IOUtils.toString(request.getInputStream());
log.debug("退款返回:{}", resXml);
Map<String, String> map = WXPayUtil.xmlToMap(resXml);
log.debug("退款回调:{}" + map);
// 判断退款是否成功!
if ("SUCCESS".equals(map.get("return_code"))) {
log.debug("退款成功");
String req_info = map.get("req_info"); // 获取加密信息
// 解密 req_info
String decoded = AESUtil.decryptData(req_info);
log.debug("解密后信息为:{}", decoded);
Map<String, String> mapInfo = WXPayUtil.xmlToMap(decoded);
String outRefundNo = mapInfo.get("out_refund_no"); // 微信退款订单号
String refund_status = mapInfo.get("refund_status"); // 退款状态
String refundFee = mapInfo.get("refund_fee"); // 实际退款金额
Refund refund = refundMapper.selectByOutRefundNo(outRefundNo);
// 退款状态判断
if ("SUCCESS".equals(refund_status)) {
BigDecimal refundFeeDecimal = BigDecimal.valueOf(Long.valueOf(refundFee))
.divide(BigDecimal.valueOf(100));
// 保存实际退款金额
refund.setRefundFee(refundFeeDecimal);
refund.setStatus(RefundStatus.SUCCESS);
} else {
log.debug("退款失败");
refund.setStatus(RefundStatus.FAIL);
}
log.debug("更新refund");
refundMapper.updateByPrimaryKey(refund);
} else {
log.debug("退款失败,原因:{}", map.get("return_msg"));
// 除了日志还能干吗?
}
// 若执行到这里必然给微信返回成功
// 微信就不在会继续调用此接口
respData.put("return_code", "SUCCESS");
respData.put("return_msg", "OK");
log.debug("支付回调处理完毕");
return WXPayUtil.mapToXml(respData);
}
退款回调解密:
AESUtil.java
package cn.edu.cqu.paymentservice.utils.jmutil;
import cn.edu.cqu.paymentservice.utils.wxpay.WXPayUtil;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.util.Map;
/**
* @author jing.huang
* @function
* @date 2018年1月10日
* @version
*/
public class AESUtil {
/**
* 密钥算法
*/
private static final String ALGORITHM = "AES";
/**
* 加解密算法/工作模式/填充方式
*/
private static final String ALGORITHM_MODE_PADDING = "AES/ECB/PKCS5Padding";
/**
* 生成key
*/
private static SecretKeySpec key = new SecretKeySpec(
MD5.MD5Encode("api密匙").toLowerCase().getBytes(), ALGORITHM);
/**
* AES加密
*
* @param data
* @return
* @throws Exception
*/
public static String encryptData(String data) throws Exception {
// 创建密码器
Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING);
// 初始化
cipher.init(Cipher.ENCRYPT_MODE, key);
return Base64Util.encode(cipher.doFinal(data.getBytes()));
}
/**
* AES解密
*
* @param base64Data
* @return
* @throws Exception
*/
public static String decryptData(String base64Data) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING);
cipher.init(Cipher.DECRYPT_MODE, key);
return new String(cipher.doFinal(Base64Util.decode(base64Data)));
}
public static void main(String[] args) throws Exception {
String A = "<xml><return_code>SUCCESS</return_code><appid><![CDATA[wx6ee8f89d58e108ad]]></appid><mch_id><![CDATA[1389309602]]></mch_id><nonce_str><![CDATA[6c1060a9bce50ae1dbb99185a3addc2a]]></nonce_str><req_info><![CDATA[5HOIoMIqBE6kfuOI96rWcq7egyPUHd0wPscqU2ADA/TKchAZIbUQQbGGPbLvaBq2q3p4oyoowc53seV/I+F977v+IA8C7BAExbftBVIhuKo9C3ii6j8gDmkB3fuc31lLdcQoQqiZWO46voDH+Uc6P4+Q3chOqlZlKITQZ+4YxCe898UBCymgQNEO9LNW3gR6s1cIykfMjgeGjUJ89ugdI3tAIs9nEXvWQmzFSKLurFTn6Juj5B6lf4TwpgKzIZxS6zAM5G3opUU0JeoBwMUyUet49w3yoR+HkvPZr1SWiO5ufXYF41b1IzQh9TBL70byL9/o1s+rxTefOY4JRvbMkN139xoXBe1OWH0cJdux56J1XwV4bdiuk0Eh+FxGtABBzEJxZi5w5nara5VRRka+t+B2MSI0fyOwcYZGJEtMKkQlC1MvST6ht45S/midObIY49uCOuq92VuC/HrdmM88Ge881h4T4ccM4ViLTFW2NrKYxFIDFEp3iwhMWY1FpX7A5pIf1i+FLPUlnp9fvqMurG1gYmc61Etvz6HHlohrS+4uItlCLDkJ/O3ZMHFffmta6J2sxOhMOMq3mkkd8LGYan7InznOWQY/XiAxTO/Txxe3s2RJrbBn+BrmQa3Y2Ps2q5BcxMstH0Ln5rV3qG43cpI2zmVtfL0SFBAKn2IB4AdyaiARp8fwPM+PIt7jd215MDTOw5ac4ZppZ6GoS7jUORIO6DVyBlUwfkkIT8fyDutVtp2NZv4X+9rEpp9nH9hgAUU4OdbFVicv3TgLaNJjzpqIu0114c6mVwVavB6ah37+Qyg2PTvTPcGL3+vqXkfW7jnqc/hRBXAQBITyhAIiZnUV8XsU3pFNvDithFqAOBJiQok0JfdPO8uQSIinxuOyTZsnqBpG/1qSeYrzmQKWJda+TKsoBwlsHG+3oXLITyZ416vQJX6Bvof/5tpofuqekVl6GepRkz6ADkwEet/A7Jsp7+KrQwMyF4cWVra69loNJhpnji4qVTHMbPuyStLfngjzmZdA/aL5qvFYRnAIhOEm9WARwyw9QDY01hdleedm6yghd9FRZam7i9cJIA5q]]></req_info></xml>\n";
Map<String, String> map = WXPayUtil.xmlToMap(A); //微信sdk工具方法
String req_info = map.get("req_info");
// 解密 req_info
String B = AESUtil.decryptData(req_info);
System.out.println(B);
Map<String, String> mapInfo = WXPayUtil.xmlToMap(B);
System.out.println(mapInfo.get("refund_status"));
// 加密B
// System.out.println(encryptData(B));
}
}
Base64Util.java
package cn.edu.cqu.paymentservice.utils.jmutil;
import java.util.Base64;
/**
* @author jing.huang
* @function jdk8支持
* @date 2018年1月10日
* @version
*/
public class Base64Util {
public static byte[] decode(String encodedText){
final Base64.Decoder decoder = Base64.getDecoder();
return decoder.decode(encodedText);
}
public static String encode(byte[] data){
final Base64.Encoder encoder = Base64.getEncoder();
return encoder.encodeToString(data);
}
}
MD5.java
package cn.edu.cqu.paymentservice.utils.jmutil;
import java.security.MessageDigest;
public class MD5 {
private final static String[] hexDigits = {"0", "1", "2", "3", "4", "5", "6", "7",
"8", "9", "a", "b", "c", "d", "e", "f"};
/**
* 转换字节数组为16进制字串
* @param b 字节数组
* @return 16进制字串
*/
public static String byteArrayToHexString(byte[] b) {
StringBuilder resultSb = new StringBuilder();
for (byte aB : b) {
resultSb.append(byteToHexString(aB));
}
return resultSb.toString();
}
/**
* 转换byte到16进制
* @param b 要转换的byte
* @return 16进制格式
*/
private static String byteToHexString(byte b) {
int n = b;
if (n < 0) {
n = 256 + n;
}
int d1 = n / 16;
int d2 = n % 16;
return hexDigits[d1] + hexDigits[d2];
}
/**
* MD5编码
* @param origin 原始字符串
* @return 经过MD5加密之后的结果
*/
public static String MD5Encode(String origin) {
String resultString = null;
try {
resultString = origin;
MessageDigest md = MessageDigest.getInstance("MD5");
resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
} catch (Exception e) {
e.printStackTrace();
}
return resultString;
}
}
ps:2021.07.19更新
以上支付是公众号网页版支付。
如需小程序支付