接到的任务,是关于小程序联合后端,简单的订单支付与退款的,因为是主体是出租性质,所以会有押金一定会退款,逻辑很简单,第一次接触到小程序支付功能,看微信的文档,下载复制论坛内各种代码,然后进行复刻分析了解。完成了支付,支付回调,退款申请三个对应的接口吧,退款回调目前先不考虑写,所以只是了解一下。
就经验而言,微信支付文档必须每一个所用到的地方都要具体了解,开发中,严格按照开发文档规范来完成。
微信小程序支付文档官方地址:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=7_3&index=1
做小程序后端,是与小程序前端做联合,刚开始不懂怎么对接的。后来接触深了,认识也就深刻了。我把认知写出来加深印象,错与对都有可能,如果有发现错误,请各位指正。
使用支付的步骤流程:
小程序端前端先登录获取openId,后面就是选择商品等等操作,在确认点击付款时,才开始使用到后端支付接口。
ps:下单请求付款是一个接口,支付回调又是一个接口,这个概念要清楚。可以在小程序端支付成功后直接使用查询订单接口查询订单来做到支付回调的事情,但是可能有延时,导致结果通知不及时,需要多次主动查询,效率降低,还会印象性能。
统一下单接口:
如果需要用到退款回调的话,就在请求微信退款API接口传参多一个退款回调地址,然后参照支付回调一样,在外网部署退款通知接口,在这个接口里进行业务处理。
目前也只是简单的运用了,很多复杂的情况还没用到。例如消费券,红包,折扣等等,同一订单多次退款。我这个应该只是非常基础了,复制出代码,改改基本能套用其他简单模式吧。
闲话:
写完这个任务后,对于后端spring boot和mybatis和Swagger2框架,有了基础的了解,试了很多错,后来负责这个小程序和PC管理平台的后端,应该可以说入门了JAVA这门语言。
pom用的微信的sdk,好处是不用自己写发送请求,打包签名,解析签名等等功能,都已经集成好了,比较方便,不用我们自己去写解签,请求,签名,当然如果有兴趣可以去下载微信支付源码,我就下载看过,没深入,只是大致跟着仿写了功能还有调试过,最后没用到,全部用的集成的sdk。
源码的地址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=11_1 这个源码是JSAPI的,不是小程序的,当然本身就没什么区别。
pom.xml里的SDK如下:
com.github.wxpay
wxpay-sdk
0.0.3
先上微信支付实体类,放证书什么的,注意api秘钥。开发时把app密码还是什么搞上去了,在后面联调的时候出了错,查了一下午错误,才知道问题所在,感觉很多博客没有说到这个地方,大概我也没注意到细节。说实话,appId,商户ID,app密码,api秘钥真的很容易混淆吧?
package com...modal;
import com.github.wxpay.sdk.WXPayConfig;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
// 直接实现sdk的WXPayConfig ,后面很多方法不用复写
public class WeChatPayConfigUtils implements WXPayConfig {
// 证书字符流
private byte[] certData;
private final String appId = "wx12345";
private final String mchId = "12345";
private final String key = "12345";
// 初始化退款、撤销时的商户证书
public WeChatPayConfigUtils() throws Exception {
// 从微信商户平台下载的安全证书存放的目录 .p12结尾的证书
String certPath = "/你自己要下载/apiclient_cert.p12";
File file = new File(certPath);
InputStream certStream = new FileInputStream(file);
this.certData = new byte[(int) file.length()];
certStream.read(this.certData);
certStream.close();
}
@Override
public String getAppID() {
// 这个是AppID(小程序ID),注意不能外泄
return appId;
}
// 商户id
@Override
public String getMchID() {
return mchId;
}
// api密钥
@Override
public String getKey() {
return key;
}
@Override
public InputStream getCertStream() {
return new ByteArrayInputStream(this.certData);
}
// 设置网络连接超时的时间
@Override
public int getHttpConnectTimeoutMs() {
return 8000;
}
// 设置网络读取超时的时间
@Override
public int getHttpReadTimeoutMs() {
return 10000;
}
}
因为我的 业务逻辑 是 先客户在小程序先下单并付款后,根据回调结果分析 才保存进数据库,所以运用了redis,应该只是最基础的保存和取出,期间先转化成JSON再保存。
关于订单下单后对象转成JSON然后保存到redis这段,我就不写了,涉及到公司业务,转换方法我复制出来,应该比较基础。将数据转换成JSON格式保存进redis,然后取出来后转化成订单类,再插入数据库。
JSON转换方法:
/**
* Object转成JSON数据
*/
public String toJson(Object object) {
if (object instanceof Integer || object instanceof Long || object instanceof Float ||
object instanceof Double || object instanceof Boolean || object instanceof String) {
return String.valueOf(object);
}
return JSON.toJSONString(object);
}
/**
* JSON数据,转成Object
*/
public T fromJson(String json, Class clazz) {
return JSON.parseObject(json, clazz);
}
下单接口就不放出来了,有支付请求,解析回调信息,回调通知,退款请求。写接口简单调用service的方法即可。
处理微信支付的ServiceImpl:
package com...service.impl;
import com.github.wxpay.sdk.WXPay;
import com.github.wxpay.sdk.WXPayConstants;
import io.swagger.annotations.ApiParam;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestParam;
import com.github.wxpay.sdk.WXPayUtil;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Service
public class WeChatPayServiceImpl implements WeChatPayService {
private Logger logger = LoggerFactory.getLogger(WeChatPayServiceImpl.class);
// 支付回调地址
private final String PAY_NOTIFY_URL = "https://要在外网确定";
// 交易类型(小程序固定JSAPI)
private final String trade_type = "JSAPI";
// 没有这个实体的,自己需要写,包含了上面的toJson方法和对redis存取方法
@Autowired
private RedisUtil redisUtil;
// 微信端需要的数据,需要返回给微信的数据
// timeStamp 时间戳从1970年1月1日00:00:00至今的秒数,即当前的时间
// nonceStr 随机字符串,长度为32个字符以下。
// package 统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=*
// signType 签名类型,默认为MD5,支持HMAC-SHA256和MD5。注意此处需与统一下单的签名类型一致
// paySign{ 小程序ID appId 时间戳 timeStamp 随机串 nonceStr 数据包 package 签名方式 signType }
@Override
public Map doUnifiedOrder(
@RequestParam("attach") @ApiParam(value = "附加数据(自定义)") String attach,
@RequestParam("model") @ApiParam(value = "订单信息(传入)") 订单实体 model,
@RequestParam("total") @ApiParam(value = "金额") BigDecimal total
) throws Exception {
Map fail = new HashMap<>();
Map data = new HashMap<>();
String nonceStr = WXPayUtil.generateNonceStr();
WeChatPayConfigUtils wxUtil = new WeChatPayConfigUtils();
WXPay wxpay = new WXPay(wxUtil);
data.put("appid", wxUtil.getAppID()); // 公众账号ID
data.put("mch_id", wxUtil.getMchID()); // 商户号
data.put("nonce_str", nonceStr); // 随机字符串
data.put("sign_type","MD5"); // 签名类型
data.put("body", "商品描述"); // 商品描述
data.put("out_trade_no", "租床订单号"); // 商户订单号(租床订单号)
data.put("fee_type", "CNY"); // 标价币种(人命币)
data.put("total_fee", total.toString().split("\\.")[0]); // 标价金额(订单总金额)
data.put("spbill_create_ip", "192.168.1.1"); // 终端IP
data.put("notify_url", PAY_NOTIFY_URL); // 回调地址
data.put("trade_type", trade_type); // 交易类型
data.put("openid", "12345"); // 公众账号ID(根据微信登录得到的)
data.put("attach", attach); // 附加数据
// 签名注释掉是因为,unifiedOrder里有自动签名的方法
//data.put("sign", MD5Util.getPaySign(data)); // 签名
try {
// 直接调用微信接口下单API的方法去请求 WXPay方法内不需要自己处理xml和map之间的转化,接口里面包含了签名的方法和签名验证的方法
Map resp = wxpay.unifiedOrder(data, wxUtil.getHttpConnectTimeoutMs(),wxUtil.getHttpReadTimeoutMs());
return resp;
// 因同事前端已经有针对解析方法,所以不需要在此进行解析数据,直接返回微信回复结果
// 如果前端不解析,需要后端解析方法,则把下面的注释去掉。
// String returnCode = (String) resp.get("return_code");
// String returnMsg = (String) resp.get("return_msg");
//
// // 以下字段在return_code 和result_code都为SUCCESS的时候有返回 trade_type prepay_id
// if ("SUCCESS".equals(returnCode)) {
// String resultCode = (String) resp.get("result_code");
// String errCodeDes = (String) resp.get("err_code_des");
//
// System.out.print("err_code_des返回: " + errCodeDes);
// // trade_type 这个肯定固定是JSAPI,但是prepay_id这个 预支付会话标识 会在后续2小时有效
// if ("SUCCESS".equals(resultCode)) {
// Long timeStamp = System.currentTimeMillis() / 1000;
// 此处appId,I字母大写,严格按照开发文档,如果不行请自行修改。
// md5map.put("appId", wxUtil.getAppID());
// md5map.put("timeStamp", timeStamp + "");
// md5map.put("nonceStr", nonceStr);
// md5map.put("package", "prepay_id=" + resp.get("prepay_id"));
// md5map.put("signType", "MD5");
// // 将md5Map再次签名,得出这个paySign的值,然后和其他的所需项返回给微信端
// String sign = WXPayUtil.generateSignature(md5map, wxUtil.getKey(), WXPayConstants.SignType.MD5);
// System.out.println("生成的签名paySign : "+ sign);
// // 将五个值返回给微信端
// Map returnMap = new HashMap<>();
// returnMap.put("timeStamp", timeStamp + "");
// returnMap.put("nonceStr", nonceStr);
// returnMap.put("package", "prepay_id=" + resp.get("prepay_id"));
// returnMap.put("signType", "MD5");
// returnMap.put("paySign", sign);
// return returnMap;
// }
// } else {
// logger.info("订单号:{},错误信息:{}", 订单实体.getId(), returnMsg);
// }
} catch (Exception e) {
logger.info(e.getMessage());
return fail;
}
}
// 支付回调运用的实体方法,但是这个是先要根据getNotifyStr解析后再运行此方法,具体的接口我会在下面发出
@Override
public String payBack(String notifyData) {
WeChatPayConfigUtils config = null;
try {
config = new WeChatPayConfigUtils();
} catch (Exception e) {
e.printStackTrace();
}
WXPay wxpay = new WXPay(config);
String returnXml = "";
try {
Map notifyMap = WXPayUtil.xmlToMap(notifyData);
// 直接运用sdk的方法解析签名,失败则返回给接口,然后接口会返回给微信
if (!wxpay.isPayResultNotifySignatureValid(notifyMap)) {
return "" + " " + " " + " ";
}
// 注意看微信支付文档,如果错误,只会有return_code和return_msg返回,其他栏位看情况才会有返回的。
String returnCode = notifyMap.get("return_code");
String orderId = notifyMap.get("out_trade_no");
if ("SUCCESS".equals(returnCode)) {
// 如下则是业务逻辑了,如果存在订单ID,则把存在redis的数据搞出来保存到数据库
if (null != orderId) {
String jsonOrder = redisUtil.get("wx_orderId_" + orderId);
redisUtil.delete("wx_orderId_" + orderId);
if (null == jsonOrder) {
logger.info("微信手机支付回调失败订单号:{}", orderId);
return "" + " " + " " + " ";
}
// 此处注意,如果是先保存订单至数据库,后面根据回调更改状态的,注意需要判断已经申请退款才接收到本次回调
/* 业务代码,取出redis里订单数据保存至数据库 */
logger.info("微信手机支付回调成功订单号:{}", orderId);
returnXml = "" + " " + " " + " ";
}
if (null == orderId) {
logger.info("微信手机支付回调失败订单号:{}", orderId);
returnXml = "" + " " + " " + " ";
}
}
} catch (Exception e) {
logger.error("手机支付回调通知失败",e);
returnXml = "" + " " + " " + " ";
}
return returnXml;
}
// 小程序ID appid
// 商户号 mch_id
// 随机字符串 nonce_str
// 签名 sign
// 签名类型 sign_type
// 微信订单号 transaction_id (二选一)
// 商户订单号 out_trade_no (二选一)
// 商户退款单号 out_refund_no
// 订单金额 total_fee
// 退款金额 refund_fee
// 退款结果通知url notify_url(可选)
/**
* 申请退款
* @Param orderId
* @return "SUCCESS" or other
*/
@Override
public Map refund(String orderId) throws Exception {
Map data = new HashMap<>();
Map retrun_map = new HashMap<>();
System.err.println("进入微信退款申请");
// 订单实体
Order order = OrderService.getOrderById(orderId);
// 微信金额单位是分所以乘100,并取整
String totalFee = order.getTotal().toString().split("\\.")[0];
// 计算退款金额
String refundFee = OrderService.getBalance(order).toString().split("\\.")[0];
WeChatPayConfigUtils wxUtil = new WeChatPayConfigUtils();
WXPay wxpay = new WXPay(wxUtil);
data.put("appid", wxUtil.getAppID());
data.put("mch_id", wxUtil.getMchID());
data.put("nonce_str", WXPayUtil.generateNonceStr());
data.put("sign_type", "MD5");
data.put("out_refund_no", order.getId());
data.put("transaction_id", order.getTransactionId());
data.put("total_fee", totalFee);
data.put("refund_fee", refundFee);
data.put("sign", WXPayUtil.generateSignature(data, wxUtil.getKey(), WXPayConstants.SignType.MD5));
Map resp = wxpay.refund(data);
// 返回状态码
String return_code = resp.get("return_code");
// 返回信息
String return_msg = resp.get("return_msg");
retrun_map.put("return_code", return_code);
retrun_map.put("return_msg", return_msg);
System.out.println(return_code);
if ("SUCCESS".equals(return_code)) {
// 业务结果
String result_code = resp.get("result_code");
retrun_map.put("result_code", result_code);
// 错误代码描述,后面用到,所以用string保存
String err_code_des = resp.get("err_code_des");
retrun_map.put("err_code_des", err_code_des);
System.out.println(result_code);
if ("SUCCESS".equals(result_code)) {
/* 业务代码,关闭订单 */
return retrun_map;
} else {
logger.info("订单号:{}错误信息:{}", orderId ,err_code_des);
return retrun_map;
}
} else {
logger.info("订单号:{}错误信息:{}", orderId, return_msg);
return retrun_map;
}
}
// 解析微信回调信息HttpServletRequest
@Override
public String getNotifyStr(HttpServletRequest request) {
String notifyData = "";
try {
try (InputStream is = request.getInputStream()) {
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
StringBuilder sb = new StringBuilder();
String line;
while (null != (line = reader.readLine())) {
sb.append(line).append("\n");
}
notifyData = sb.toString();
} catch (IOException e) {
e.printStackTrace();
}
}catch (Exception e){
logger.error("获取回调数据异常:" + e.getMessage());
}
return notifyData;
}
}
支付回调接口,运用service层的解析与执行业务操作方法。注意此接口需要外网可访问,不能携带参数
@Autowired
private WeChatPayService wxPayService ;
@ApiOperation(value = "支付回调, notes = "支付回调")
@PostMapping(value = "/payBackAndInsert")
public void payBack(HttpServletRequest request, HttpServletResponse response) throws Exception {
String notifyData = wxPayService.getNotifyStr(request);
System.out.println("微信回调:" + notifyData);
String result = wxPayService.payBack(notifyData);
response.getWriter().write(result);
}
完事儿了,写的是支付接口,其实我负责了半个后端的工作,因为除了支付还有商品管理,位置管理,操作日志,加上之前套过来用的公司管理,用户管理,角色管理,权限管理等等,后续修改持续两个月。我下载了大概有五份代码,都是关于微信支付或者支付宝支付的,大体大同小异,只是侧重某一方面,业务处理不同,最基础的大概就这三个了,统一下单,支付回调,退款可能都没必要存在。