
SpringBoot(JAVA)整合小程序支付和退款功能详解
SpringBoot(java)整合微信小程序SDK支付和退款,小程序调用后端预支付接口,接口调用成功返回给小程序支付凭证id,小程序拿到支付凭证调用微信后台支付接口,小程序支付成功后,微信后台执行支付回调将支付订单信息返回(预支付调用微信后台是需要传入支付成功后自己本地的回调接口地址)
个人建议支付和退款不要自己去封装请求,因为有很多坑!而且还不一定能弄成功!代码复制直接可用。
需要注意有可能在执行编译证书的时候可能会报一个长度异常,是因为jdk1.8对密匙长度有限制,用jdk大于1.8的版本就能正常编译。
官方文档不是很清晰,都是零零散散的,但有总比没有好。
微信小程序支付文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml
微信小程序退款文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_9.shtml
微信小程序支付
在进行对接微信支付之前,我们首先需要将以下几点准备好:
- 申请APPID
- 申请商户号
- 小程序开通微信支付,绑定已经申请好的商户号。登录小程序后台(mp.weixin.qq.com)。点击左侧导航栏的微信支付,在页面中进行开通。(开通申请要求小程序已发布上线)
支付流程图:
微信小程序退款
当交易发生之后一年内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付金额退还给买家,微信支付将在收到退款请求并且验证成功之后,将支付款按原路退还至买家账号上。
(1)微信退款所需要的配置!,退款只需要证书即可。微信退款需要证书:资金发生变化需要证书。支付接口不需要。点击证书使用。按照步骤:下载证书。
(2)使用API证书
◆ apiclient_cert.p12是商户证书文件,除PHP外的开发均使用此证书文件。
◆ 商户如果使用.NET环境开发,请确认Framework版本大于2.0,必须在操作系统上双击安装证书apiclient_cert.p12后才能被正常调用。
◆ API证书调用或安装需要使用到密码,该密码的值为微信商户号(mch_id)
(3)API证书安全
1.证书文件不能放在web服务器虚拟目录,应放在有访问权限控制的目录中,防止被他人下载;
2.建议将证书文件名改为复杂且不容易猜测的文件名;
3.商户服务器要做好病毒和木马防护工作,不被非法侵入者窃取证书文件。
证书相关API:
退款注意事项:
1、交易时间超过一年的订单无法提交退款。
2、微信支付退款支持单笔交易分多次退款,多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。申请退款总金额不能超过订单金额。 一笔退款失败后重新提交,请不要更换退款单号,请使用原商户退款单号。
3、请求频率限制:150qps,即每秒钟正常的申请退款请求次数不超过150次。
4、每个支付订单的部分退款次数不能超过50次。
5、如果同一个用户有多笔退款,建议分不同批次进行退款,避免并发退款导致退款失败。
6、申请退款接口的返回仅代表业务的受理情况,具体退款是否成功,需要通过退款查询接口获取结果。
7、一个月之前的订单申请退款频率限制为:5000/min。
8、同一笔订单多次退款的请求需相隔1分钟。
导入依赖
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.2.11</version>
</dependency>
在application.yml添加对应配置
# 微信小程序支付配置信息
wx:
# 微信小程序appid
app-id:
# 小程序密钥
app-secret:
# 商户号
mch-id:
# 证书序列号
mch-serial-no:
# api密钥
api-key:
# 回调接口地址
notify-url:
# 证书地址
key-path: D:\pay\apiclient_key.pem
这里要说下回调地址和证书地址,回调地址是当创建订单后前端调用回调地址查看支付状态修改订单状态,但是要注意回调地址要放开token校验,证书地址是需要在微信API安全去下载一个证书,这个位置就是你证书所放的位置。如果需要退款回调地址就在添加一个参数即可
yml配置对应实体
@Component
@ConfigurationProperties(prefix = "wx")
@Data
@ToString
public class WxPayV3Bean {
//小程序appid
private String appId;
//商户号
private String mchId;
//证书序列号
private String mchSerialNo;
//小程序秘钥
private String appSecret;
//api秘钥
private String apiKey;
//回调接口地址
private String notifyUrl;
//证书地址
private String keyPath;
}
Util工具类
public class WXPayUtil {
private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final Random RANDOM = new SecureRandom();
public static String getSign(String signatureStr, String privateKey) throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException, URISyntaxException {
//replace 根据实际情况,不一定都需要
String replace = privateKey.replace("\\n", "\n");
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKeyFromPath(replace);
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(merchantPrivateKey);
sign.update(signatureStr.getBytes(StandardCharsets.UTF_8));
return Base64Utils.encodeToString(sign.sign());
}
/**
* 获取随机字符串 Nonce Str
*
* @return String 随机字符串
*/
public static String generateNonceStr() {
char[] nonceChars = new char[32];
for (int index = 0; index < nonceChars.length; ++index) {
nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
}
return new String(nonceChars);
}
}
预支付实体类
字段根据实际需求而变化
@Data
@Accessors(chain = true)
public class WXPayOrderReqVO {
@ApiModelProperty(value = "订单支付类型(商品订单;预约订单)",required = true)
@NotBlank(message = "订单支付类型不能为空!")
private String orderType;//附加数据,回调时可根据这个数据辨别订单类型或其他
@ApiModelProperty(value = "订单定金金额(单位:分)",required = true)
@NotNull(message = "订单定金金额不能为空!")
// @DecimalFormat(precision = 2)
private Integer amount;
@ApiModelProperty(value = "商品描述",required = true)
@NotBlank(message = "商品描述不能为空!")
private String description;
@ApiModelProperty(value = "openId",required = true)
@NotBlank(message = "openId不能为空!")
private String openId;
}
controller
Result 是自定义返回类,修改成自己的。
@Api
@RestController
@Slf4j
@RequestMapping("/order")
public class OrderController {
@Autowired
private WxPayService wxPayService;
/**
* 微信预支付
*
* @param req
* @param request
* @return
* @throws Exception
*/
@ApiOperation(value = "微信预支付", notes = "微信预支付")
@PostMapping("/createOrder")
public Result createOrder(@RequestBody @Validated WXPayOrderReqVO req, HttpServletRequest request) throws Exception {
log.info("传入参数:" + JSONObject.toJSONString(req));
return wxPayService.createOrder(req, request);
}
/**
* 微信支付回调
*
* @param request
* @return
* @throws Exception
*/
@ApiOperation(value = "微信支付回调", notes = "微信支付回调")
@PostMapping("/payNotify")
public Result payNotify(HttpServletRequest request) throws Exception {
log.info("-----------------------微信支付回调通知-----------------------");
//注意:回调接口需要暴露到公网上,且要放开token验证
return wxPayService.payNotify(request);
}
/**
* 退款
*
* @param
* @return
* @throws Exception
*/
@ApiOperation(value = "退款", notes = "退款")
@PostMapping("/refund")
public Result refund(@RequestParam("orderId") String orderId) {
if (orderId == null || "".equals(orderId)) {
return Result.error("参数为空");
}
return wxPayService.refund(orderId, "退款");
}
/**
* 微信退款回调
*
* @return
*/
@ApiOperation(value = "微信退款回调", notes = "微信退款回调")
@PostMapping("/refund/payNotify")
public Result refundPayNotify(HttpServletRequest request) throws Exception {
//注意:回调接口需要暴露到公网上,且要放开token验证
return wxPayService.refundNotify(request);
}
}
service
public interface WxPayService {
Result createOrder(WXPayOrderReqVO req, HttpServletRequest request) throws Exception;
Result payNotify(HttpServletRequest request) throws Exception;
Result refund(String orderId, String refundReason);
Result refundNotify(HttpServletRequest request);
}
serviceImpl
@Slf4j
@Service
public class WxPayServiceImpl implements WxPayService {
//注入支付对应配置
@Autowired
private WxPayV3Bean wxPayV3Bean;
@Autowired
private SysUserMapper userMapper;
@Autowired
private MachineOrderMapper orderMapper;
private static byte[] certData;
/**
* 预支付接口
* @param req
* @param request
* @return
* @throws Exception
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Result createOrder(WXPayOrderReqVO req, HttpServletRequest request) throws Exception {
Map<String, Object> params = new HashMap<>();
synchronized (this) {
SysUser user = userMapper.getUserByOpenId(req.getOpenId());
if (user == null || StringUtil.isEmpty(user.getOpenid())) {
return Result.error("无效用户");
}
//生成商户订单号
String tradeNo = getTradeNo();
// 使用自动更新平台证书的RSA配置,配置微信支付的自动证书管理功能
Config config =
new RSAAutoCertificateConfig.Builder()
.merchantId(wxPayV3Bean.getMchId())
.privateKeyFromPath(wxPayV3Bean.getKeyPath())
.merchantSerialNumber(wxPayV3Bean.getMchSerialNo())
.apiV3Key(wxPayV3Bean.getApiKey())
.build();
// 构建service,用于处理JSAPI支付相关的操作
JsapiService service = new JsapiService.Builder().config(config).build();
// 创建预支付订单的请求对象
PrepayRequest prepayRequest = new PrepayRequest();
Amount amount = new Amount();
amount.setTotal(req.getAmount());
prepayRequest.setAmount(amount);
prepayRequest.setAppid(wxPayV3Bean.getAppId());
prepayRequest.setMchid(wxPayV3Bean.getMchId());
prepayRequest.setNotifyUrl(wxPayV3Bean.getNotifyUrl());
prepayRequest.setDescription(req.getDescription());
prepayRequest.setOutTradeNo(tradeNo);
prepayRequest.setAttach(req.getOrderType());
Payer payer = new Payer();
payer.setOpenid(user.getOpenid());
prepayRequest.setPayer(payer);
// 调用下单方法,得到应答
PrepayResponse response = service.prepay(prepayRequest);
log.info("调用下单方法请求返回:" + response);
Long timeStamp = System.currentTimeMillis() / 1000;
params.put("timeStamp", timeStamp);
String substring = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
params.put("nonceStr", substring);
String signatureStr = Stream.of(wxPayV3Bean.getAppId(), String.valueOf(timeStamp), substring, "prepay_id=" + response.getPrepayId())
.collect(Collectors.joining("\n", "", "\n"));
String sign = WXPayUtil.getSign(signatureStr, wxPayV3Bean.getKeyPath());
params.put("paySign", sign);
params.put("package", "prepay_id=" + response.getPrepayId());
params.put("tradeNo", tradeNo);
//保存订单信息
try {
MachineOrder order = new MachineOrder();
//自己的业务代码
orderMapper.insert(order);
} catch (Exception e) {
e.printStackTrace();
}
}
return Result.OK(params);
}
/**
* 支付回调
*
* @param request
* @return
* @throws Exception
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Result payNotify(HttpServletRequest request) throws Exception {
//读取请求体的信息
ServletInputStream inputStream = request.getInputStream();
StringBuffer stringBuffer = new StringBuffer();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String s;
//读取回调请求体
while ((s = bufferedReader.readLine()) != null) {
stringBuffer.append(s);
}
String s1 = stringBuffer.toString();
String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
String nonce = request.getHeader(WECHAT_PAY_NONCE);
String signType = request.getHeader("Wechatpay-Signature-Type");
String serialNo = request.getHeader(WECHAT_PAY_SERIAL);
String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
// 构造一个RSAAutoCertificateConfig
NotificationConfig config = new RSAAutoCertificateConfig.Builder()
.merchantId(wxPayV3Bean.getMchId())
.privateKeyFromPath(wxPayV3Bean.getKeyPath())
.merchantSerialNumber(wxPayV3Bean.getMchSerialNo())
.apiV3Key(wxPayV3Bean.getApiKey())
.build();
// 初始化 NotificationParser
NotificationParser parser = new NotificationParser(config);
RequestParam requestParam = new RequestParam.Builder()
.serialNumber(serialNo)
.nonce(nonce)
.signature(signature)
.timestamp(timestamp)
// 若未设置signType,默认值为 WECHATPAY2-SHA256-RSA2048
.signType(signType)
.body(s1)
.build();
Transaction parse = parser.parse(requestParam, Transaction.class);
log.info("支付回调信息:" + JSONObject.toJSONString(parse));
try {
/**
* SUCCESS:支付成功
* REFUND:转入退款
* NOTPAY:未支付
* CLOSED:已关闭
* REVOKED:已撤销(付款码支付)
* USERPAYING:用户支付中(付款码支付)
* PAYERROR:支付失败(其他原因,如银行返回失败)
*/
//修改订单状态方法
updateAccountDetail(parse);
// 这里可以根据状态做不同的处理,我这里只处理支付成功
if ("SUCCESS".equals(parse.getTradeState())) {
//你的业务代码
log.info("该订单:" + parse.getOutTradeNo() + "已经进行回调,不可重复回调");
return Result.ok();
}
} catch (Exception e) {
e.printStackTrace();
}
return Result.ok();
}
/**
* 微信小程序退款
*
* @param orderId 订单id
* @param refundReason 退款原因
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Result refund(String orderId, String refundReason) {
synchronized (this) {
MachineOrder order = orderMapper.selectById(orderId);
if (order == null) {
return Result.error("无订单信息");
}
try {
// 使用自动更新平台证书的RSA配置
// 一个商户号只能初始化一个配置,否则会因为重复的下载任务报错
Config config =
new RSAAutoCertificateConfig.Builder()
.merchantId(wxPayV3Bean.getMchId())
//使用 SDK 不需要计算请求签名和验证应答签名
// 使用 com.wechat.pay.java.core.util 中的函数从本地文件中加载商户私钥,商户私钥会用来生成请求的签名
.privateKeyFromPath(wxPayV3Bean.getKeyPath())
.merchantSerialNumber(wxPayV3Bean.getMchSerialNo())
.apiV3Key(wxPayV3Bean.getApiKey())
.build();
// 构建退款service
RefundService service = new RefundService.Builder().config(config).build();
//构建退款请求
CreateRequest request = new CreateRequest();
// request.setXxx(val)设置所需参数,具体参数可见Request定义
//构建订单金额信息
AmountReq amountReq = new AmountReq();
//退款金额
amountReq.setRefund(Long.valueOf((int) (order.getPaymentMoney() * 100)));
//原订单金额
amountReq.setTotal(Long.valueOf((int) (order.getPaymentMoney() * 100)));
//货币类型(默认人民币)
amountReq.setCurrency("CNY");
request.setAmount(amountReq);
request.setOutTradeNo(order.getOrderNum());
request.setReason("退款");
//商户退款单号
request.setOutRefundNo(String.valueOf(order.getOrderNum()));
//退款通知回调地址,开设可不设,我是没有设置的
//如果要设置就在配置加上对应的参数和参数值即可
// request.setNotifyUrl(wxPayV3Bean.getRefundNotify());
// 调用微信sdk退款接口
Refund refund = service.create(request);
log.info("请求退款返回:" + refund);
//接收退款返回参数
// Status status = refund.getStatus();
if (Status.SUCCESS.equals(refund.getStatus().SUCCESS)) {
//说明退款成功,开始接下来的业务操作
//你的业务代码,根据请求返回状态修改对应订单状态
return Result.ok("退款成功");
}
if (Status.PROCESSING.equals(refund.getStatus().PROCESSING)) {
//你的业务代码,根据请求返回状态修改对应订单状态
return Result.OK("退款中");
}
if (Status.ABNORMAL.equals(refund.getStatus().ABNORMAL)) {
//你的业务代码,根据请求返回状态修改对应订单状态
return Result.error("退款异常");
}
if (Status.CLOSED.equals(refund.getStatus().CLOSED)) {
//你的业务代码,根据请求返回状态修改对应订单状态
return Result.error("退款关闭");
}
} catch (Exception e) {
e.printStackTrace();
}
}
return Result.error("退款失败");
}
/**
* 退款回调
* @param request
* @return
*/
@Override
public Result refundNotify(HttpServletRequest request) {
try {
//读取请求体的信息
ServletInputStream inputStream = request.getInputStream();
StringBuffer stringBuffer = new StringBuffer();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String s;
//读取回调请求体
while ((s = bufferedReader.readLine()) != null) {
stringBuffer.append(s);
}
String s1 = stringBuffer.toString();
String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
String nonce = request.getHeader(WECHAT_PAY_NONCE);
String signType = request.getHeader("Wechatpay-Signature-Type");
String serialNo = request.getHeader(WECHAT_PAY_SERIAL);
String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
// 如果已经初始化了 RSAAutoCertificateConfig,可直接使用
// 没有的话,则构造一个
NotificationConfig config = new RSAAutoCertificateConfig.Builder()
.merchantId(wxPayV3Bean.getMchId())
.privateKeyFromPath(wxPayV3Bean.getKeyPath())
.merchantSerialNumber(wxPayV3Bean.getMchSerialNo())
.apiV3Key(wxPayV3Bean.getApiKey())
.build();
// 初始化 NotificationParser
NotificationParser parser = new NotificationParser(config);
RequestParam requestParam = new RequestParam.Builder()
.serialNumber(serialNo)
.nonce(nonce)
.signature(signature)
.timestamp(timestamp)
// 若未设置signType,默认值为 WECHATPAY2-SHA256-RSA2048
.signType(signType)
.body(s1)
.build();
RefundNotification parse = parser.parse(requestParam, RefundNotification.class);
System.out.println("parse = " + parse);
//parse.getRefundStatus().equals("SUCCESS");说明退款成功
//这里和上面退款返回差不多的处理,可以抽成一个公共的方法
if (Status.SUCCESS.equals(parse.getRefundStatus().SUCCESS)) {
//你的业务代码,根据请求返回状态修改对应订单状态
return Result.ok("退款成功");
}
if (Status.PROCESSING.equals(parse.getRefundStatus().PROCESSING)) {
//你的业务代码,根据请求返回状态修改对应订单状态
return Result.OK("退款中");
}
if (Status.ABNORMAL.equals(parse.getRefundStatus().ABNORMAL)) {
//你的业务代码,根据请求返回状态修改对应订单状态
return Result.error("退款异常");
}
if (Status.CLOSED.equals(parse.getRefundStatus().CLOSED)) {
//你的业务代码,根据请求返回状态修改对应订单状态
return Result.error("退款关闭");
}
} catch (Exception e) {
e.printStackTrace();
}
return Result.error("回调失败");
}
/**
* 修改订单状态
*
*/
public void updateAccountDetail(Transaction parse) {
//根据自己业务修改
Map<String, Object> map = new HashMap<>();
map.put("order_num", parse.getOutTradeNo().toString());
map.put("transactionId", parse.getTransactionId().toString());
map.put("tradeState", parse.getTradeState().toString());
log.info("更新订单数据:" + JSONObject.toJSONString(map));
orderMapper.updateOrderStatus(map);
}
/**
* 时间+id为订单号
*
* @param
* @return
*/
public String getTradeNo() {
String idStr = WXPayUtil.generateNonceStr();
// long timestamp = DateUtils.getCurrentTimestamp();
// //序列号是为了保证生成的订单号的唯一性
AtomicInteger sequence = new AtomicInteger(0);
int nextSequence = sequence.getAndIncrement();
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] messageDigest = md.digest(String.valueOf(nextSequence).getBytes());
BigInteger no = new BigInteger(1, messageDigest);
String encrypted = no.toString(10); // 将十六进制转为十进制表示的字符串
// 如果加密结果长度超过20位,则截取前20位
if (encrypted.length() > 20) {
encrypted = encrypted.substring(0, 20);
}
// String tradeNo = timestamp + idStr;
return idStr;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
更多推荐
所有评论(0)