日期
版本
作者
说明
2023-10-17
V-1.0
孔留锋
文档新建。
微信小程序支付开发 简介 背景 现有微信小程序,添加会员卡付费模块(延长使用时间),需要对接微信支付。
官网文档 微信支付官网 https://pay.weixin.qq.com/
微信小程序官网 https://mp.weixin.qq.com/
微信小程序支付文档
https://pay.weixin.qq.com/docs/merchant/products/mini-program-payment/introduction.html
数据准备 商户号 商户号:1654589714
微信商户平台-》账户中心-》商户信息
小程序ID(AppID) 小程序ID:wxe**
小程序后台-》设置-》基本设置-》账号信息
API证书申请 微信商户平台-》API安全-》下载证书工具-》安装-》解压
生产后 本地D:\Mac\2_CodeCompiller\WXCertUtil\cert 下有对应密钥文件
1654589714_20231017_cert.zip
解压后文件如下
apiclient_cert.p12
apiclient_cert.pem
apiclient_key.pem
证书使用说明.txt
APIv3密钥设置 随机密码生成器
FzgK
以上所有API密钥和证书需要妥善保管防止泄漏
商户证书序列号 微信商户平台-》API安全-》证书号
开发流程 流程图
重要步骤说明
小程序支付相关接口 商户平台小程序支付 官方文档
JSAPI下单 :通过本接口提交微信支付小程序支付订单。
查询订单 :通过此接口查询订单状态。
关闭订单 :通过此接口关闭待支付订单。
小程序调起支付 :通过小程序下单接口获取到发起支付的必要参数prepay_id,可以按照接口定义中的规则,调起小程序支付。
支付通知 :微信支付通过支付通知接口将用户支付成功消息通知给商户。
申请退款 :商户可以通过该接口将支付金额退还给买家。
查询单笔退款 :提交退款申请后,通过调用该接口查询退款状态 。
退款结果通知 :微信支付通过退款通知接口将用户退款成功消息通知给商户。
申请交易账单 :商户可以通过该接口获取交易账单文件的下载地址。
申请资金账单 :商户可以通过该接口获取资金账单文件的下载地址。
下载账单 :通过申请交易/资金账单获取到download_url在该接口获取到对应的账单。
JSAPI下单API 商户系统先调用该接口在微信支付服务后台生成预支付交易单 ,返回正确的预支付交易会话标识后再按Native、JSAPI、APP等不同场景生成交易串调起支付。
地址:JSAPI下单
注意:
应用ID(微信生成的应用ID,全局唯一)
直连商户号(直连商户的商户号,由微信支付生成并下发) 参考 接入准备中
商户订单号(商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一),需要结合系统自己定义
通知地址(异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 公网域名必须为https,如果是走专线接入,使用专线NAT IP或者私有回调域名可使用http)
查询订单API 商户可以通过查询订单接口主动查询订单状态,完成下一步的业务逻辑。查询订单状态可通过微信支付订单号 或商户订单号两种方式 查询,两种查询方式返回结果相同。
需要调用查询接口的情况 :
当商户后台、网络、服务器等出现异常,商户系统最终未接收到支付通知。
调用支付接口后,返回系统错误或未知交易状态情况。
调用付款码支付API,返回USERPAYING的状态。
调用关单或撤销接口API之前,需确认支付状态。
地址:查询订单
注意:
关闭订单API 以下情况需要调用关单接口:
注意:
关单没有时间限制,建议在订单生成后**间隔几分钟(最短5分钟)**再调用关单接口,避免出现订单状态同步不及时导致关单失败。
小程序调起支付API 通过JSAPI下单接口获取到发起支付的必要参数prepay_id,然后使用微信支付提供的小程序方法调起小程序支付。
地址:小程序调起支付
注意:
支付通知API 微信支付通过支付通知接口将用户支付成功消息通知给商户。
注意:
同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
如果在所有通知频率后没有收到微信侧回调,商户应调用查询订单接口确认订单状态。
特别提醒:商户系统对于开启结果通知的内容 一定要做签名验证 ,并校验通知的信息是否与商户侧的信息一致,防止数据泄露导致出现“假通知”,造成资金损失。
申请退款API 当交易发生之后一年内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付金额退还给买家,微信支付将在收到退款请求并且验证成功之后,将支付款按原路退还至买家账号上。
地址:申请退款
注意:
交易时间超过一年的订单无法提交退款
微信支付退款支持单笔交易分多次退款(不超50次),多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。申请退款总金额不能超过订单金额。 一笔退款失败后重新提交,请不要更换退款单号,请使用原商户退款单号
错误或无效请求频率限制:6qps,即每秒钟异常或错误的退款申请请求不超过6次
每个支付订单的部分退款次数不能超过50次
如果同一个用户有多笔退款,建议分不同批次进行退款,避免并发退款导致退款失败
申请退款接口的返回仅代表业务的受理情况,具体退款是否成功,需要通过退款查询接口获取结果
一个月之前的订单申请退款频率限制为:5000/min
同一笔订单多次退款的请求需相隔1分钟
查询单笔退款API 提交退款申请后,通过调用该接口查询退款状态。退款有一定延时,建议在提交退款申请后1分钟发起查询退款状态,一般来说零钱支付的退款5分钟内到账,银行卡支付的退款1-3个工作日到账。
地址:查询单笔退款
退款结果通知API 退款状态改变后,微信会把相关退款结果发送给商户。 地址:退款结果通知
注意:
对后台通知交互时,如果微信收到应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功
同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
如果在所有通知频率后没有收到微信侧回调。商户应调用查询订单接口确认订单状态。
申请交易账单API 微信支付按天提供交易账单文件,商户可以通过该接口获取账单文件的下载地址。文件内包含交易相关的金额、时间、营销等信息,供商户核对订单、退款、银行到账等情况。
地址:申请交易账单
注意:
申请资金账单API 微信支付按天提供微信支付账户的资金流水账单文件,商户可以通过该接口获取账单文件的下载地址。文件内包含该账户资金操作相关的业务单号、收支金额、记账时间等信息,供商户进行核对。
地址:申请资金账单
注意:
资金账单中的数据反映的是商户微信支付账户资金变动情况;
对账单中涉及金额的字段单位为“元”。
下载账单API 下载账单API为通用接口,交易/资金账单都可以通过该接口获取到对应的账单。
地址:下载账单
注意:
账单文件的下载地址的有效时间为30s。
强烈建议商户将实际账单文件的哈希值和之前从接口获取到的哈希值进行比对,以确认数据的完整性。
该接口响应的信息请求头中不包含微信接口响应的签名值,因此需要跳过验签的流程。
微信在次日9点启动生成前一天的对账单,建议商户10点后再获取。
代码开发 官网sdk引用 wechatpay-sdk
1 2 3 4 5 6 <dependency > <groupId > com.github.wechatpay-apiv3</groupId > <artifactId > wechatpay-java</artifactId > <version > 0.2.12</version > </dependency >
配置文件 1 2 3 4 5 6 7 wxpay: merchantId: 16 ******** merchantSerialNumber: 204AC*********************************** privateKeyPath: D:/Mac/2_CodeCompiller/WXCertUtil/cert/apiclient_key.pem apiV3Key: Fzg***************************** appid: wxe*************** notifyUrl: https://********.*****.cn/api/pay/notify
WxPayConfig 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 import com.wechat.pay.java.core.RSAAutoCertificateConfig;import com.wechat.pay.java.core.notification.NotificationConfig;import com.wechat.pay.java.core.notification.NotificationParser;import com.wechat.pay.java.service.payments.jsapi.JsapiService;import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;import lombok.Data;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import com.wechat.pay.java.core.Config;@Configuration @Data @ConfigurationProperties("wxpay") public class WxPayConfig { private String merchantId; private String merchantSerialNumber ; private String privateKeyPath; private String apiV3Key; private String appid; private String notifyUrl; @Bean("WechatConfig") public Config getWechatConfig () { return new RSAAutoCertificateConfig .Builder() .merchantId(merchantId) .privateKeyFromPath(privateKeyPath) .merchantSerialNumber(merchantSerialNumber) .apiV3Key(apiV3Key) .build(); } @Bean public JsapiServiceExtension getJsapiServiceExtension (Config wechatConfig) { return new JsapiServiceExtension .Builder() .config(wechatConfig).build(); } @Bean public NotificationParser getNotificationParser (Config wechatConfig) { return new NotificationParser ((NotificationConfig)wechatConfig); } }
核心接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 @RequestMapping("/api/applets/") @RestController @Slf4j public class PayApi { @Autowired private WxPayConfig wxPayConfig; @Autowired private JsapiServiceExtension jsapiServiceExtension; @Autowired private IVipProductService iVipProductService; @Autowired private IVipOrderInfoService iVipOrderInfoService; @Autowired private IVipUserInfoService iVipUserInfoService; @Autowired private AuthRequestEnhanceFactory factory; @Autowired private IVipPaymentInfoService iVipPaymentInfoService; @RequestMapping("/produce/list") public Result getProduceList () { List<ProduceVO> result = iVipProductService.getAppletsShowList(); return Result.OK(result); } @GetMapping({"/openId/{code}"}) @Log(title = "小程序-权限判断", businessType = BusinessType.APPLETS) @Transactional public @ResponseBody Result openId (@PathVariable String code) { VipUserInfo byIdEx = iVipUserInfoService.getByIdEx(); if (StringUtils.isEmpty(byIdEx.getOpenId())){ log.info("openId 不存在获取新的{}" ); AuthWechatAppletsOauthRequest authRequest = (AuthWechatAppletsOauthRequest)factory.get(AppletConstant.APPLETS_OAUTH_KEY); AuthCallback authCallback = new AuthCallback (); authCallback.setCode(code); authCallback.setAuthorization_code(AppletConstant.AGENT_UUID); AuthUser login = authRequest.login(authRequest.getAccessToken(authCallback)); VipUserInfo update = new VipUserInfo (); update.setUserId(byIdEx.getUserId()); update.setOpenId(login.getUuid()); iVipUserInfoService.updateByIdEx(update); log.info("openId 不存在获取新的{}" ,login.getUuid()); return Result.OK(login.getUuid()); } return Result.OK(byIdEx.getOpenId()); } @RequestMapping("/order/create") @Transactional public Result createOrder (String code,String openId) { log.info("小程序发起下单请求code:[{}],openId:[{}]" ,code,openId); VipProduct vo = iVipProductService.getByCode(code); if (vo==null ){ return Result.error500("商品编码不存在" ); } VipOrderInfo orderInfo = new VipOrderInfo (); orderInfo.setId(IdWorker.getId()); orderInfo.setTitle(vo.getTitle()); orderInfo.setUserId(SecurityUtils.getUserId()); orderInfo.setProductCode(code); orderInfo.setRealPrice(vo.getRealPrice()); orderInfo.setCreateTime(DateUtil.now()); iVipOrderInfoService.saveEx(orderInfo); PrepayRequest request = new PrepayRequest (); request.setAppid(wxPayConfig.getAppid()); request.setMchid(wxPayConfig.getMerchantId()); request.setDescription(orderInfo.getTitle()); request.setOutTradeNo(orderInfo.getId().toString()); request.setNotifyUrl(wxPayConfig.getNotifyUrl()); Amount amount = new Amount (); amount.setTotal(vo.getRealPrice()); request.setAmount(amount); Payer payer = new Payer (); payer.setOpenid(openId); request.setPayer(payer); PrepayWithRequestPaymentResponse prepayWithRequestPaymentResponse = null ; log.info("小程序发起下单-微信调用请求信息:[{}]" ,request.toString()); try { prepayWithRequestPaymentResponse = jsapiServiceExtension.prepayWithRequestPayment(request); }catch (HttpException e){ e.printStackTrace(); log.info("发送HTTP请求失败:error [{}]" ,e.getHttpRequest()); }catch (ServiceException e) { log.info("服务返回状态小于200或大于等于300:error [{}]" ,e.getResponseBody()); } catch (MalformedMessageException e) { log.info("务返回成功,返回体类型不合法,或者解析返回体失败:error [{}]" ,e.getMessage()); } String result = JSON.toJSONString(prepayWithRequestPaymentResponse); log.info("微信调用返回信息:[{}]" ,JSON.toJSONString(result)); VipPaymentInfo paymentInfo = new VipPaymentInfo (); paymentInfo.setOrderId(orderInfo.getId().toString()); paymentInfo.setTradeState(Transaction.TradeStateEnum.NOTPAY.name()); paymentInfo.setCreateBy(SecurityUtils.getUsername()); paymentInfo.setPrepayResponseContent(result); paymentInfo.setCreateTime(DateUtil.now()); iVipPaymentInfoService.saveEx(paymentInfo); prepayWithRequestPaymentResponse.setAppId(paymentInfo.getOrderId()); return Result.OK(prepayWithRequestPaymentResponse); } @RequestMapping("/order/status") @Transactional public Result queryOrder (String orderId) { log.info("订单常态查询更新->orderId:[{}]" ,orderId); QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest (); request.setOutTradeNo(orderId); request.setMchid(wxPayConfig.getMerchantId()); Transaction parse = jsapiServiceExtension.queryOrderByOutTradeNo(request); if (parse.getTradeState().name().equals(Transaction.TradeStateEnum.SUCCESS.name())){ String payTime = DateUtil.parseUTC(parse.getSuccessTime()).toString(); VipPaymentInfo paymentInfo = iVipPaymentInfoService.getByOrderId(parse.getOutTradeNo()); paymentInfo.setTransactionId(parse.getTransactionId()); paymentInfo.setTradeType(parse.getTradeType().name()); paymentInfo.setTradeState(parse.getTradeState().name()); paymentInfo.setPayerTime(payTime); paymentInfo.setPayerTotal(parse.getAmount().getPayerTotal()); paymentInfo.setContent(JSON.toJSONString(parse)); paymentInfo.setUpdateBy("微信支付回调" ); paymentInfo.setUpdateTime(DateUtil.now()); iVipPaymentInfoService.updateByIdEx(paymentInfo); VipOrderInfo vipOrderInfo = new VipOrderInfo (); vipOrderInfo.setId(Long.valueOf(parse.getOutTradeNo())); vipOrderInfo.setOrderStatus(1 ); vipOrderInfo.setPayTime(payTime); vipOrderInfo.setUpdateBy("微信支付回调" ); vipOrderInfo.setUpdateTime(DateUtil.now()); iVipOrderInfoService.updateByIdEx(vipOrderInfo); } return Result.OK("修改成功" ); } @RequestMapping("/order/list") @Transactional public Result orderList (@RequestParam(required = false,defaultValue = "1") Integer status) { LambdaQueryWrapper<VipOrderInfo> eq = Wrappers.lambdaQuery(VipOrderInfo.class).eq(VipOrderInfo::getOrderStatus, status) .eq(VipOrderInfo::getUserId, SecurityUtils.getUserId()); List<VipOrderInfo> list = iVipOrderInfoService.listEx(eq); List<VipOrderInfoVo> rows = new ArrayList <>(); for (VipOrderInfo vipOrderInfo : list) { VipOrderInfoVo vo = new VipOrderInfoVo (); BeanUtils.copyProperties(vipOrderInfo,vo); vo.setId(vipOrderInfo.getId().toString()); vo.setStatus(vipOrderInfo.getOrderStatus()); rows.add(vo); } return Result.OK(rows); } }
微信回调接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 @RequestMapping("/api/") @Controller @Slf4j public class PayNotificationApi { @Autowired private NotificationParser notificationParser; @Autowired private IVipPaymentInfoService iVipPaymentInfoService; @Autowired private IVipOrderInfoService iVipOrderInfoService; @RequestMapping("/pay/notify") public Object payNotify (HttpServletRequest request) { log.info("微信回调开始" ); try { String requestBody = IOUtil.toString(request.getInputStream()); log.info("微信回调开始-->requestBody :{}" ,requestBody); RequestParam requestParam = new RequestParam .Builder() .serialNumber(request.getHeader(WechatPayNotificationKey.Serial.getKey())) .nonce(request.getHeader(WechatPayNotificationKey.Nonce.getKey())) .signature(request.getHeader(WechatPayNotificationKey.Signature.getKey())) .timestamp(request.getHeader(WechatPayNotificationKey.Timestamp.getKey())) .body(requestBody) .build(); Transaction parse = notificationParser.parse(requestParam, Transaction.class); String content = JSON.toJSONString(parse); log.info("微信回调解析结果-->parse data :{}" , content); if (parse.getTradeState().name().equals(Transaction.TradeStateEnum.SUCCESS.name())){ String payTime = DateUtil.parse(parse.getSuccessTime()).toString(); VipPaymentInfo paymentInfo = iVipPaymentInfoService.getByOrderId(parse.getOutTradeNo()); if (paymentInfo!=null ){ paymentInfo.setTransactionId(parse.getTransactionId()); paymentInfo.setTradeType(parse.getTradeType().name()); paymentInfo.setTradeState(parse.getTradeState().name()); paymentInfo.setPayerTime(payTime); paymentInfo.setPayerTotal(parse.getAmount().getPayerTotal()); paymentInfo.setContent(content); paymentInfo.setUpdateBy("微信支付回调" ); paymentInfo.setUpdateTime(DateUtil.now()); iVipPaymentInfoService.updateByIdEx(paymentInfo); VipOrderInfo vipOrderInfo = new VipOrderInfo (); vipOrderInfo.setId(Long.valueOf(parse.getOutTradeNo())); vipOrderInfo.setOrderStatus(1 ); vipOrderInfo.setPayTime(payTime); vipOrderInfo.setUpdateBy("微信支付回调" ); vipOrderInfo.setUpdateTime(DateUtil.now()); iVipOrderInfoService.updateByIdEx(vipOrderInfo); } } }catch (ValidationException e){ log.error("签名验证失败" , e); return ResponseEntity.status(HttpStatus.UNAUTHORIZED); }catch (Exception e){ log.error("业务处理失败" , e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR); } return ResponseEntity.status(HttpStatus.OK); } }
异常记录 Illegal key size 1 Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.wechat.pay.java.core.Config]: nested exception is java.lang.IllegalArgumentException: java.security.InvalidKeyException: Illegal key size
查阅资料发现密钥长度受限制,原因JDK版本小于1.8_150的有影响。
1 2 3 4 C:\Users\Iduohua>java -version java version "1.8.0_131" Java(TM) SE Runtime Environment (build 1.8.0_131-b11) Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)
安装更新高版本JDK 1.8.0_361
1 2 3 4 C:\Users\Iduohua>java -version java version "1.8.0_361" Java(TM) SE Runtime Environment (build 1.8.0_361-b09) Java HotSpot(TM) 64-Bit Server VM (build 25.361-b09, mixed mode)