前言
微信支付有两个版本:v2和v3版本。
【v2版本】微信支付最初版本,采用xml格式交互
【v3版本】遵循REST设计风格,采用json格式交互,更加简单易用
【v2文档】https://pay.weixin.qq.com/docs/merchant/products/jsapi-payment/introduction.html
【v3文档】https://pay.weixin.qq.com/doc/v3/merchant/4013070158
本文采用v2+v3混合使用。
接入
APP接入文档:https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_5_1.shtml
微信开放平台:https://open.weixin.qq.com
微信商户平台:https://pay.weixin.qq.com
配置
配置文件
# 服务号的应用ID
APP_ID = wx631abbc200000001
# 商户号
MCH_ID = 1700000001
# API密钥
API_KEY = TEST952op762K654YHJSK61F500001
# 商户API私钥路径
MCH_API_KEY_PATH = /apiclient_key.pem
# 商户证书序列号
MCH_SERIAL_NUMBER = 34280FAA686E09A8A3sd12sa5daw12d12asA
# 商户APIv3密钥
MCH_API_V3_KEY = TESTjG3uY5dJ9tS3tT7oN5lP8s4d51wd
配置类
package com.jiang.pay;
import org.apache.commons.configuration2.PropertiesConfiguration;
import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder;
import org.apache.commons.configuration2.builder.fluent.Parameters;
import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
import org.apache.commons.configuration2.ex.ConfigurationException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.SortedMap;
/**
* 读取支付相关配置(从配置文件中读取)
*/
public class WxPayConfigUtil {
/** 微信支付统一下单接口(POST) */
public final static String UNIFIED_ORDER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder";
private static PropertiesConfiguration configs;
/** 应用ID*/
public static String APP_ID;
/** 商户号*/
public static String MCH_ID;
/** API密钥*/
public static String API_KEY;
/**商户API私钥路径*/
public static String MCH_API_KEY_PATH;
/**商户证书序列号*/
public static String MCH_SERIAL_NUMBER;
/**商户APIv3密钥*/
public static String MCH_API_V3_KEY;
// 项目启动时,需调用此静态方法加载配置
public static synchronized void init(String filePath) {
if (configs != null) {
return;
}
try {
FileBasedConfigurationBuilder<PropertiesConfiguration> builder =
new FileBasedConfigurationBuilder<PropertiesConfiguration>(PropertiesConfiguration.class)
.configure(new Parameters().properties()
.setFileName(filePath)
.setThrowExceptionOnMissing(true)
.setListDelimiterHandler(new DefaultListDelimiterHandler(';'))
.setIncludesAllowed(false));
configs = builder.getConfiguration();
} catch (ConfigurationException e) {
e.printStackTrace();
}
if (configs == null) {
throw new IllegalStateException("can`t find file by path:" + filePath);
}
APP_ID = configs.getString("APP_ID");
MCH_ID = configs.getString("MCH_ID");
API_KEY = configs.getString("API_KEY");
MCH_API_KEY_PATH = configs.getString("MCH_API_KEY_PATH");
MCH_SERIAL_NUMBER = configs.getString("MCH_SERIAL_NUMBER");
MCH_API_V3_KEY = configs.getString("MCH_API_V3_KEY");
}
}
下单
下单
/**
* 下单
* @param orderNo 订单编号
* @param amount 订单金额
*/
public WxApiUnifiedOrderResultVO pay(String orderNo, BigDecimal amount) {
// [请求参数]
SortedMap<String, String> params = new TreeMap<>();
// 1.构建请求参数
this.buildAppPayParams(params, orderNo, amount);
// 2.参数转xml字符串(微信v2版本api规范,需传xml)
String requestXml = WxPayCommonUtil.getRequestXml(params);
System.out.println("请求微信xml参数:");
System.out.println(requestXml);
try {
// 3.调用微信统一下单api
String resXml = HttpUtil.postData(WxPayConfigUtil.UNIFIED_ORDER_URL, requestXml);
// 4.解析响应结果(微信api响应的结果是xml格式,需转成自己需要的格式)
Map<String, String> payData = XmlUtil.doXMLParse(resXml);
// 5.保存微信响应内容(响应内容是前端拉起微信支付必备的参数)
return this.revertWxResult(payData, params.get("sign"));
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
回调通知
@ApiOperation("微信支付后回调")
@PostMapping("/order/wx/notify")
public void wxNotify(HttpServletRequest request, HttpServletResponse response) {
log.info("微信支付回调通知");
SortedMap<String, String> packageParams = parseWxParam(request);
log.info("收到的参数:" + JSON.toJSONString(packageParams));
// 账号信息 key
String key = WxPayConfigUtil.API_KEY;
// 判断签名是否正确
if (WxPayCommonUtil.isTenpaySign("UTF-8", packageParams, key)) {
log.info("微信支付成功回调");
if ("SUCCESS".equals(packageParams.get("result_code"))) {
// 支付成功
String orderNo = packageParams.get("out_trade_no");
log.info("微信订单号{}付款成功", orderNo);
// 这里根据实际业务场景 做相应的操作
orderService.updateResponse(PayTypeEnum.TYPE_2.getCode(), packageParams);
outPutStreamNotify(response, "SUCCESS", "OK");
} else {
log.error("支付失败,错误信息:{}", packageParams.get("err_code"));
outPutStreamNotify(response, "FAIL", packageParams.get("err_code"));
}
} else {
log.error("通知签名验证失败");
}
}
退款
退款
/**
* 退款申请
* @param orderNo 订单号
* @param mchOrderNo 微信订单号
* @param refundOrderNo 退款订单号
* @param orderTotalAmount 订单总金额
* @param refundAmount 本次退款金额
*/
public void refundApply(String orderNo, String mchOrderNo, String refundOrderNo, BigDecimal orderTotalAmount, BigDecimal refundAmount) {
Config config = new RSAAutoCertificateConfig.Builder()
.merchantId(WxPayConfigUtil.MCH_ID)
.privateKeyFromPath(WxPayConfigUtil.MCH_API_KEY_PATH)
.merchantSerialNumber(WxPayConfigUtil.MCH_SERIAL_NUMBER)
.apiV3Key(WxPayConfigUtil.MCH_API_V3_KEY)
.build();
// 构建service
RefundService service = new RefundService.Builder().config(config).build();
// 请求参数
CreateRequest request = new CreateRequest();
request.setTransactionId(mchOrderNo);
request.setOutTradeNo(orderNo);
request.setOutRefundNo(refundOrderNo);
request.setReason("退款原因");
request.setNotifyUrl("回调通知地址");
AmountReq amountReq = new AmountReq();
amountReq.setTotal(Long.valueOf(MoneyUtil.YuanToFen(orderTotalAmount.toString()))); // 元转分
amountReq.setRefund(Long.valueOf(MoneyUtil.YuanToFen(refundAmount.toString()))); // 元转分
amountReq.setCurrency("CNY");
request.setAmount(amountReq);
try {
Refund refund = service.create(request);
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
回调通知
@ApiOperation("微信退款回调")
@PostMapping("/order/wx/refund/notify")
public void wxRefund(HttpServletRequest request, HttpServletResponse response) {
log.info("微信退款回调通知");
String requestStr = readRequestStr(request);
log.info("收到请求参数:" + requestStr);
Map<String, String> map = JSON.parseObject(requestStr, new TypeReference<Map<String, String>>(){});
log.info("[map][{}]", JSON.toJSONString(map));
AesUtil aesUtil = new AesUtil(WxPayConfigUtil.MCH_API_V3_KEY.getBytes());
try {
Map<String, String> resource = JSON.parseObject(map.get("resource"), new TypeReference<Map<String, String>>(){});
String decrypt = aesUtil.decryptToString(resource.get("associated_data").getBytes(), resource.get("nonce").getBytes(), resource.get("ciphertext"));
log.info("[解密结果][{}]", decrypt);
Map<String, String> dataMap = JSON.parseObject(decrypt, new TypeReference<Map<String, String>>(){});
String transactionId = dataMap.get("transaction_id"); // 微信支付订单号
String out_trade_no = dataMap.get("out_trade_no"); // 订单号
String out_refund_no = dataMap.get("out_refund_no"); // 退款单号
// 业务处理
orderRefundService.updateIsRefundOk(out_refund_no);
} catch (Exception e) {
e.printStackTrace();
log.info("[解密异常][{}]", e.getMessage());
throw new CustomException(e.getMessage());
}
}
评论区