大纲
一、流程总结
1.1 技术概览
1.2 官方业务流程时序图
1.3 支付流程
用户在请求支付的时候,服务端就要向微信支付发起统一下单
接口:这里就会填入商户内部订单的编号、付款金额、通知地址等,统一下单接口就会返回调起支付的必要参数。服务器就需要将这个参数返回给客户端。
微信服务器验证正确后会自动通知微信弹出输入密码框。
用户在输入了支付密码之后,微信支付系统将会根据你在统一下单接口时候填入的通知地址
,返回一些数据信息。服务端在处理了这些信息之后,就要返回微信支付系统需要的返回参数。
流程,如下所示:
1.4 退款流程
用户在发起退款申请,服务器就调起申请退款
接口,输入退款结果通知。
推荐使用异步通知进行处理:因为微信退款产生,并不是即时到帐,通常出现这个提示之后的几分钟之内,钱款就会到账。
二、支付注意事项
- 可以利用好统一下单接口的附加数据 attach,方便回调处理
- 在微信回调的时候,还需要验证信息:
- 得到的订单编号中能否查询相应的订单
- 该通知是否已经处理过
- 校验返回的订单金额是否与商户的订单金额一致
- 订单总金额,单位为分
- 当且仅当 return_code 与 result_code 为 SUCCESS 时,才是付款成功
- 回调地址:必须为外网可访问的url,不能携带参数。 公网域名必须为https
- 支付成功才会有异步支付结果通知下发,支付失败没有,回调中的
result_code
为fail:有可能是商户出现问题 - 支付金额,应该是根据订单编号查询数据库中的支付金额进行支付申请
三、退款注意事项
- 退款结果通知验证:
- 当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过;如果没有处理过再进行处理,如果处理过直接返回结果成功。
- 当且仅当 return_code 与 result_code 为 SUCCESS 时,才是退款成功
- “退款异常”:
当用户使用银行卡支付时,微信支付首先原路退款到银行卡,当银行卡状态不正常或银行卡错误时,微信支付会优先转退用户微信零钱,仅当用户微信零钱也注销,才会转入“退款异常”状态。此时可选择“其它方式退款”,手动录入用户的银行信息完成退款 - “退款关闭”:
余额不足等其他情况会导致退款关闭 - 退款金额与支付金额相似
四、代码
4.1 依赖导入
<dependency>
<groupId>com.github.javen205</groupId>
<artifactId>IJPay-WxPay</artifactId>
<version>2.8.0</version>
</dependency>
4.2 配置文件
wx:
miniApp:
appId:
appSecret:
pay:
mchId:
machSerialNumber:
mchSecret:
# 商户证书文件路径
# 请参考“商户证书”一节 https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=4_3
cert: apiclient_cert.p12
# 请参考“回调通知注意事项” https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=23_8&index=6
doMain:
android:
appId:
appSecret:
ios:
appId:
appSecret:
4.3 配置类
@ConfigurationProperties(prefix = "wx")
@Component
@Data
public class WxProperties {
private MiniApp miniApp;
private Pay pay;
private Android android;
private Ios ios;
@Data
public static class MiniApp{
private String appId;
private String appSecret;
}
@Data
public static class Pay{
private String mchId;
private String machSerialNumber;
private String mchSecret;
private String cert;
private String doMain;
}
@Data
public static class Ios{
private String appId;
private String appSecret;
}
@Data
public static class Android{
private String appId;
private String appSecret;
}
}
4.4 业务层
1. 微信支付
1) 用户统一下单
/**
* 付款订单的预支付会话标识
* <p> 此接口对应的是 V2 的版本
* 1. 检查订单是否能够付款
* 2. 处理微信商户平台返回的参数
* 3. 设置订单付款状态
*/
@Transactional(rollbackFor = Exception.class)
public Object prepay(Integer originType) {
UnifiedOrderModel.UnifiedOrderModelBuilder unifiedOrderModelBuilder = UnifiedOrderModel.builder();
// 商户号
unifiedOrderModelBuilder.mch_id(wxProperties.getPay().getMchId());
// 商品描述: 订单编号
unifiedOrderModelBuilder.body("");
// 订单编号:订单中获取
unifiedOrderModelBuilder.out_trade_no("");
// 价格单位:(分) 整数
unifiedOrderModelBuilder.total_fee("");
// 附加数据
unifiedOrderModelBuilder.attach("");
// 随机字符串
unifiedOrderModelBuilder.nonce_str(WxPayKit.generateStr());
//支付的ip地址
unifiedOrderModelBuilder.spbill_create_ip(ServletUtil.getClientIP(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest()));
// 支付回调地址
unifiedOrderModelBuilder.notify_url(wxProperties.getPay().getDoMain() + "/other/wx/notifyUrl");
/*
* 订单有效期为15min。用户在14min59s唤起支付,在支付界面停留了2s(此时,已经过了交易期限),不能够让用户在进行微信支付
* 就需要设置交易的开始时间以及结束时间
*/
DateTime orderCreateTime = DateUtil.date();
// 交易开始时间
unifiedOrderModelBuilder.time_start(DateUtil.format(orderCreateTime, DatePattern.PURE_DATETIME_PATTERN));
// 交易结束时间 : 15分钟
unifiedOrderModelBuilder.time_expire(DateUtil.format(DateUtil.offsetMinute(orderCreateTime, 15), DatePattern.PURE_DATETIME_PATTERN));
if (originType == 0) {
// 小程序appId
unifiedOrderModelBuilder.appid(wxProperties.getMiniApp().getAppId());
// 小程序中的openId(ios以及android是没有的)
unifiedOrderModelBuilder.openid("");
// 交易方式
unifiedOrderModelBuilder.trade_type(TradeType.JSAPI.getTradeType());
} else if (originType == 1) {
// 安卓appId
unifiedOrderModelBuilder.appid(wxProperties.getAndroid().getAppId());
// 交易方式
unifiedOrderModelBuilder.trade_type(TradeType.APP.getTradeType());
} else if (originType == 2) {
// ios的appId
unifiedOrderModelBuilder.appid(wxProperties.getIos().getAppId());
// 交易方式
unifiedOrderModelBuilder.trade_type(TradeType.APP.getTradeType());
}
Map<String, String> requestParams = unifiedOrderModelBuilder.build().createSign(wxProperties.getPay().getMchSecret(), SignType.MD5);
logger.info("统一下单请求参数:" + requestParams);
// 向微信发送统一下单请求
String resultXml = WxPayApi.pushOrder(false, requestParams);
logger.info("统一下单请求参数:" + resultXml);
// 将获取结果集xml变为Map
Map<String, String> resultMap = WxPayKit.xmlToMap(resultXml);
String returnCode = resultMap.get("return_code");
String returnMsg = resultMap.get("return_msg");
// 检查是否连接到微信支付
if (!WxPayKit.codeIsOk(returnCode)) {
return returnMsg;
}
// 检查交易是否成功
String resultCode = resultMap.get("result_code");
if (!WxPayKit.codeIsOk(resultCode)) {
return returnMsg;
}
// 以下字段在 return_code 和 result_code 都为 SUCCESS 的时候有返回
Map<String, String> returnParams = null;
if (originType == 0) {
returnParams = WxPayKit.miniAppPrepayIdCreateSign(wxProperties.getMiniApp().getAppId(), resultMap.get("prepay_id"), wxProperties.getPay().getMchSecret(), SignType.MD5);
logger.info("小程序调起支付的参数: " + returnParams);
} else if (originType == 1) {
returnParams = WxPayKit.appPrepayIdCreateSign(wxProperties.getAndroid().getAppId(), wxProperties.getPay().getMchId(), resultMap.get("prepay_id"), wxProperties.getPay().getMchSecret(), SignType.MD5);
logger.info("Android调起支付的参数: " + returnParams);
} else if (originType == 2) {
returnParams = WxPayKit.appPrepayIdCreateSign(wxProperties.getIos().getAppId(), wxProperties.getPay().getMchId(), resultMap.get("prepay_id"), wxProperties.getPay().getMchSecret(), SignType.MD5);
logger.info("IOS调起支付的参数: " + returnParams);
}
/*
* 数据库处理:设置状态为未支付、设置支付方式等业务处理
*/
return returnParams;
}
2) 支付回调通知:Post
public Object payNotify(HttpServletRequest request, HttpServletResponse response) {
Map<String, String> analysisMap = WxPayKit.xmlToMap(HttpKit.readData(request));
logger.info("支付结果通知:" + analysisMap);
// 验证支付
if (WxPayKit.verifyNotify(analysisMap, wxProperties.getPay().getMchSecret(), SignType.MD5)) {
// 与微信通讯是否成功
if (WxPayKit.codeIsOk(analysisMap.get("return_code"))) {
Map<String, String> resultParams = new HashMap<>();
resultParams.put("return_code", "SUCCESS");
resultParams.put("return_msg", "OK");
//支付成功
if (WxPayKit.codeIsOk(analysisMap.get("result_code"))) {
// !得到的订单编号中能否查询相应的订单 !
// !校验返回的订单金额是否与商户的订单金额一致 !
// !该通知是否已经处理过 !
/*
* 数据库的业务处理
*/
return WxPayKit.toXml(resultParams);
} else {
// 交易失败
// ! 用户输错密码 银行卡余额不足 不走微信支付结果通知接口的失败 !
log.info("微信支付交易失败");
return WxPayKit.toXml(resultParams);
}
}else {
log.info("微信回调:微信通讯失败: {}", analysisMap.get("return_msg"));
}
}
log.info("非法微信回调");
return null;
}
2. 微信退款
1) 用户申请退款
/**
* @param orderNo 订单编号
* @param originType 订单来源
*/
public Object refund(String orderNo,Integer originType) {
/*
* 查看能否搜寻到该订单
*/
/*
* 查看订单是否能够退款
*/
// 微信退款
String appid = null;
// 0.小程序 1.安卓 2.iOS
if (originType == 0) {
appid = wxProperties.getMiniApp().getAppId();
} else if (originType == 1) {
appid = wxProperties.getAndroid().getAppId();
} else {
appid = wxProperties.getIos().getAppId();
}
Map<String, String> params = RefundModel.builder()
.appid(appid)
.mch_id(wxProperties.getPay().getMchId())
.nonce_str(WxPayKit.generateStr())
.out_trade_no(orderNo)
.out_refund_no("R" + orderNo.substring(1))
.total_fee("")
.refund_fee("")
.notify_url(wxProperties.getPay().getDoMain() + "/wx/refundUrl")
.build()
.createSign(wxProperties.getPay().getMchSecret(), SignType.MD5);
Map<String, String> resultMap = null;
try {
resultMap = WxPayKit.xmlToMap(WxPayApi.orderRefund(false, params, new ClassPathResource("apiclient_cert.p12").getURL().getPath(), wxProperties.getPay().getMchId()));
} catch (IOException e) {
return new Exception("微信调起支付退款异常" + e.getMessage());
}
String returnCode = resultMap.get("return_code");
String returnMsg = resultMap.get("return_msg");
if (!WxPayKit.codeIsOk(returnCode)) {
log.info("微信退款失败:{}", returnMsg);
return "微信退款失败";
}
log.info("微信退款成功");
return "微信退款成功";
}
2) 退款回调通知
public String refundUrl(HttpServletRequest request){
String xmlMsg = HttpKit.readData(request);
Map<String, String> params = WxPayKit.xmlToMap(xmlMsg);
log.info("退款通知:{}", params);
String returnCode = params.get("return_code");
if (WxPayKit.codeIsOk(returnCode)) {
String reqInfo = params.get("req_info");
Map<String, String> decryptData = WxPayKit.xmlToMap(WxPayKit.decryptData(reqInfo, wxProperties.getPay().getMchSecret()));
log.info("退款通知解密后的数据:{}", decryptData);
Map<String, String> xml = new HashMap<String, String>(2);
xml.put("return_code", "SUCCESS");
xml.put("return_msg", "OK");
// 退款是否成功
if (WxPayKit.codeIsOk(decryptData.get("refund_status"))) {
/*
* 当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,
* 如果没有处理过再进行处理,如果处理过直接返回结果成功。
*/
return WxPayKit.toXml(xml);
}else {
if (("CHANGE".equals(decryptData.get("refund_status")))) {
/*
* “退款异常”:当用户使用银行卡支付时,当银行卡状态不正常或银行卡错误时,又退回用户微信零钱失败
*/
log.info("微信退款发生退款异常");
} else {
/*
* “退款关闭”:余额不足或者其他情况
*/
log.info("微信退款发生退款关闭异常");
}
return WxPayKit.toXml(xml);
}
}
log.info("微信退款回调错误:{}", params.get("return_msg"));
return null;
}
3. 查询订单
/**
* 微信支付订单的查询
*
* @param orderNo 订单号
* @param originType 订单来源 0.小程序 1.安卓 2.iOS
*/
public Map<String, String> orderQuery(String orderNo, Integer originType) {
String appid = null;
// 0.小程序 1.安卓 2.iOS
if (originType == 0) {
appid = wxProperties.getMiniApp().getAppId();
} else if (originType == 1) {
appid = wxProperties.getAndroid().getAppId();
} else {
appid = wxProperties.getIos().getAppId();
}
Map<String, String> params = OrderQueryModel.builder()
.appid(appid)
.mch_id(wxProperties.getPay().getMchId())
.out_trade_no(orderNo)
.nonce_str(WxPayKit.generateStr())
.build()
.createSign(wxProperties.getPay().getMchSecret(), SignType.MD5);
Map<String, String> resultMap = WxPayKit.xmlToMap(WxPayApi.orderQuery(params));
String return_code = resultMap.get("return_code");
String return_msg = resultMap.get("return_msg");
if (!WxPayKit.codeIsOk(return_code)) {
log.info("查询微信支付订单失败:{}", return_msg);
}
log.info("查询微信支付订单:{}", resultMap);
return resultMap;
}
4. 查询退款
/**
* 微信退款订单的查询
*
* @param orderNo 订单号
* @param originType 订单来源 0.小程序 1.安卓 2.iOS
*/
public Map<String, String> refundQuery(String orderNo, Integer originType) {
String appid = null;
// 0.小程序 1.安卓 2.iOS
if (originType == 0) {
appid = wxProperties.getMiniApp().getAppId();
} else if (originType == 1) {
appid = wxProperties.getAndroid().getAppId();
} else {
appid = wxProperties.getIos().getAppId();
}
Map<String, String> params = RefundQueryModel.builder()
.appid(appid)
.mch_id(wxProperties.getPay().getMchId())
.out_trade_no(orderNo)
.nonce_str(WxPayKit.generateStr())
.build()
.createSign(wxProperties.getPay().getMchSecret(), SignType.MD5);
Map<String, String> resultMap = WxPayKit.xmlToMap(WxPayApi.orderRefundQuery(false, params));
String returnCode = resultMap.get("return_code");
String returnMsg = resultMap.get("return_msg");
if (!WxPayKit.codeIsOk(returnCode)) {
log.info("查询退款信息失败:{}", returnMsg);
}
log.info("查询退款信息:{}", resultMap);
return resultMap;
}