• java接入apiv3微信小程序支付(以java的eladmin框架为例)


    一、需要准备的资料

            1.小程序AppID

                    如:wx2e56f5******

            2.商户号

                    如:1641******

            3.商户API私钥路径什么是商户API证书?如何获取商户API证书? 获取文件如下图:

                    如:

                    本地路径:E:\Env\eladmin\cert\c2\apiclient_key.pem(本地部署写入绝对路径)

                    服务器路径:/data/cert/eladmin/c2/apiclient_key.pem(服务器写入相对路径)

            4.商户证书序列号:登录商户平台【API安全】->【API证书】->【查看证书】,可查看商户API证书序列号。

                    如:4F24D009CDBC89A**********************

            5.商户APIV3密钥:API v3密钥 - 通用规则 | 微信支付商户文档中心

                    如:tYsmXJrIr*****************

                    【商户平台】->【账户中心】->【API安全】的页面设置该密钥,请求才能通过微信支付的签名校验。密钥的长度为32个字节。

            6.商户平台证书路径

                    如:

                    本地路径:E:\Env\eladmin\cert\c2\apiclient_cert.pem(本地部署写入绝对路径)

                    服务器路径:/data/cert/eladmin/c2/apiclient_cert.pem(服务器写入相对路径)

            7.商户平台证书路径p12

                    如:

                    本地路径:E:\Env\eladmin\cert\c2\apiclient_cert.p12(本地部署写入绝对路径)

                    服务器路径:/data/cert/eladmin/c2/apiclient_cert.p12(服务器写入相对路径)

    二、代码部分

    1.pom.xml内加入微信支付扩展
        
    1. com.github.wechatpay-apiv3
    2. wechatpay-java
    3. 0.2.7
    2.在resources的dev和prod内加入微信支付必填字段
        #微信支付字段
        
          
    
    1. wx:
    2. merchantId: 164*******
    3. privateKeyPath: E:\Env\eladmin\cert\c2\apiclient_key.pem
    4. certPath: E:\Env\eladmin\cert\c2\apiclient_cert.pem
    5. certP12Path: E:\Env\eladmin\cert\c2\apiclient_cert.p12
    6. merchantSerialNumber: 4F24D009CDBC**********************
    7. apiV3key: tYsmXJr******************
    8. appId: wx2e56f5******
    3.微信支付测试接口
       3-1:测试下单接口
           /api/weChatPay/ceshiJSAPI
       3-2:测试微信支付结果回调地址,当微信支付后,微信方返回支付信息,在回调接口内触发业务逻辑
           注:测试回调接口必须为线上接口,线下无法实现
              可先在线上将微信返回数据打印到txt文件内,然后在本地测试回调接口,触发业务逻辑
           /api/weChatPay/ceshiPayNotify(放入线上的测试回调接口)
           /api/weChatPay/payNotify(本地测试回调接口)--可在postman内测试
           测试时,在Headers内Wechatpay-Serial、Wechatpay-Nonce、Wechatpay-Timestamp、Wechatpay-Signature必填上
           微信返回的json,直接放入postman软件的body内raw的json内进行测试
           
    4.退款测试接口
       /api/weChatPay/ceshiRefund(回调接口不是必填项,可用可不用)
    1. WeChatPayService.java:可放在Service内
    2. ----------------------------------------------------
    3. package me.zhengjie.modules.api.service;
    4. import lombok.RequiredArgsConstructor;
    5. import org.springframework.beans.factory.annotation.Value;
    6. import org.springframework.stereotype.Service;
    7. import javax.servlet.ServletInputStream;
    8. import javax.servlet.http.HttpServletRequest;
    9. import java.io.BufferedReader;
    10. import java.io.IOException;
    11. import java.io.InputStreamReader;
    12. import java.nio.file.Files;
    13. import java.nio.file.Paths;
    14. import java.security.KeyFactory;
    15. import java.security.NoSuchAlgorithmException;
    16. import java.security.PrivateKey;
    17. import java.security.Signature;
    18. import java.security.spec.InvalidKeySpecException;
    19. import java.security.spec.PKCS8EncodedKeySpec;
    20. import java.util.Base64;
    21. import java.util.Random;
    22. @Service
    23. @RequiredArgsConstructor
    24. public class WeChatPayService {
    25. /** 商户API私钥路径 */
    26. @Value("${wx.privateKeyPath}") //可自己写死路径
    27. public String privateKeyPath;
    28. //=============生成签名=====开始===================================================
    29. /**
    30. * 作用:使用字段appId、timeStamp、nonceStr、package计算得出的签名值
    31. * 场景:根据微信统一下单接口返回的 prepay_id 生成调启支付所需的签名值
    32. * @param appId
    33. * @param timestamp
    34. * @param nonceStr
    35. * @param pack package
    36. * @return
    37. * @throws Exception
    38. */
    39. public String getSign(String appId, long timestamp, String nonceStr, String pack) throws Exception{
    40. String message = buildMessage(appId, timestamp, nonceStr, pack);
    41. String paySign= sign(message.getBytes("utf-8"));
    42. return paySign;
    43. }
    44. private String buildMessage(String appId, long timestamp, String nonceStr, String pack) {
    45. return appId + "\n"
    46. + timestamp + "\n"
    47. + nonceStr + "\n"
    48. + pack + "\n";
    49. }
    50. private String sign(byte[] message) throws Exception{
    51. Signature sign = Signature.getInstance("SHA256withRSA");
    52. //这里需要一个PrivateKey类型的参数,就是商户的私钥。
    53. sign.initSign(getPrivateKey(privateKeyPath));
    54. sign.update(message);
    55. return Base64.getEncoder().encodeToString(sign.sign());
    56. }
    57. /**
    58. * 获取私钥。
    59. *
    60. * @param filename 私钥文件路径 (required)
    61. * @return 私钥对象
    62. */
    63. public static PrivateKey getPrivateKey(String filename) throws IOException {
    64. String content = new String(Files.readAllBytes(Paths.get(filename)), "utf-8");
    65. try {
    66. String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
    67. .replace("-----END PRIVATE KEY-----", "")
    68. .replaceAll("\\s+", "");
    69. KeyFactory kf = KeyFactory.getInstance("RSA");
    70. return kf.generatePrivate(
    71. new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
    72. } catch (NoSuchAlgorithmException e) {
    73. throw new RuntimeException("当前Java环境不支持RSA", e);
    74. } catch (InvalidKeySpecException e) {
    75. throw new RuntimeException("无效的密钥格式");
    76. }
    77. }
    78. /**
    79. * 获取随机位数的字符串
    80. * @param length
    81. * @return
    82. */
    83. public static String getRandomString(int length) {
    84. String base = "abcdefghijklmnopqrstuvwxyz0123456789";
    85. Random random = new Random();
    86. StringBuffer sb = new StringBuffer();
    87. for (int i = 0; i < length; i++) {
    88. int number = random.nextInt(base.length());
    89. sb.append(base.charAt(number));
    90. }
    91. return sb.toString();
    92. }
    93. //=============生成签名=====结束===================================================
    94. /**
    95. * 获取请求文体
    96. * @param request
    97. * @return
    98. * @throws IOException
    99. */
    100. public static String getRequestBody(HttpServletRequest request) throws IOException {
    101. ServletInputStream stream = null;
    102. BufferedReader reader = null;
    103. StringBuffer sb = new StringBuffer();
    104. try {
    105. stream = request.getInputStream();
    106. // 获取响应
    107. reader = new BufferedReader(new InputStreamReader(stream));
    108. String line;
    109. while ((line = reader.readLine()) != null) {
    110. sb.append(line);
    111. }
    112. } catch (IOException e) {
    113. throw new IOException("读取返回支付接口数据流出现异常!");
    114. } finally {
    115. reader.close();
    116. }
    117. return sb.toString();
    118. }
    119. }
    1. Controller代码如下:
    2. package me.zhengjie.modules.api.rest;
    3. import cn.hutool.core.io.FileUtil;
    4. import com.alibaba.fastjson.JSONObject;
    5. import com.wechat.pay.java.core.Config;
    6. import com.wechat.pay.java.core.RSAAutoCertificateConfig;
    7. import com.wechat.pay.java.core.exception.ServiceException;
    8. import com.wechat.pay.java.core.notification.NotificationConfig;
    9. import com.wechat.pay.java.core.notification.NotificationParser;
    10. import com.wechat.pay.java.core.notification.RequestParam;
    11. import com.wechat.pay.java.service.payments.jsapi.JsapiService;
    12. import com.wechat.pay.java.service.payments.jsapi.model.Amount;
    13. import com.wechat.pay.java.service.payments.jsapi.model.Payer;
    14. import com.wechat.pay.java.service.payments.jsapi.model.PrepayRequest;
    15. import com.wechat.pay.java.service.payments.jsapi.model.PrepayResponse;
    16. import com.wechat.pay.java.service.payments.model.Transaction;
    17. import com.wechat.pay.java.service.refund.RefundService;
    18. import com.wechat.pay.java.service.refund.model.AmountReq;
    19. import com.wechat.pay.java.service.refund.model.CreateRequest;
    20. import com.wechat.pay.java.service.refund.model.Refund;
    21. import io.swagger.annotations.Api;
    22. import io.swagger.annotations.ApiOperation;
    23. import lombok.RequiredArgsConstructor;
    24. import lombok.extern.slf4j.Slf4j;
    25. import me.zhengjie.annotation.AnonymousAccess;
    26. import me.zhengjie.annotation.Log;
    27. import me.zhengjie.config.FileProperties;
    28. import me.zhengjie.exception.BadRequestException;
    29. import me.zhengjie.modules.api.service.WeChatPayService;
    30. import org.springframework.beans.factory.annotation.Value;
    31. import org.springframework.http.HttpStatus;
    32. import org.springframework.http.ResponseEntity;
    33. import org.springframework.transaction.annotation.Transactional;
    34. import org.springframework.web.bind.annotation.PostMapping;
    35. import org.springframework.web.bind.annotation.RequestMapping;
    36. import org.springframework.web.bind.annotation.RestController;
    37. import javax.servlet.http.HttpServletRequest;
    38. import java.io.IOException;
    39. import java.util.Date;
    40. import java.util.HashMap;
    41. import java.util.Map;
    42. import java.util.stream.Collectors;
    43. @Slf4j
    44. @RestController
    45. @RequiredArgsConstructor
    46. @Api(tags = "1.微信小程序")
    47. @RequestMapping("/api/weChatPay")
    48. public class WeChatPayContoller {
    49. private final WeChatPayService weChatPayService;
    50. private final FileProperties fileProperties;
    51. /**
    52. * appID
    53. */
    54. @Value("${wx.appId}") // 可自己写死--我引用的是自己写的配置内(写死的方式如:public String appId=wx12345678910)
    55. public String appId;
    56. /**
    57. * 商户号
    58. */
    59. @Value("${wx.merchantId}") // 可自己写死
    60. public String merchantId;
    61. /**
    62. * 商户API私钥路径
    63. */
    64. @Value("${wx.privateKeyPath}") //可自己写死
    65. public String privateKeyPath;
    66. /**
    67. * 商户证书序列号
    68. */
    69. @Value("${wx.merchantSerialNumber}") // 可自己写死
    70. public String merchantSerialNumber;
    71. /**
    72. * 商户APIV3密钥
    73. */
    74. @Value("${wx.apiV3key}") //可自己写死
    75. public String apiV3key;
    76. /**
    77. * 商户平台证书路径
    78. */
    79. @Value("${wx.certPath}") // 可自己写死
    80. public String certPath;
    81. /**
    82. * 商户平台证书路径p12
    83. */
    84. @Value("${wx.certP12Path}") //可自己写死
    85. public String certP12Path;
    86. private static Map configMap = new HashMap<>();
    87. private static Config config = null;
    88. private static JsapiService service = null;
    89. private static RefundService refundService = null;
    90. /**
    91. * JSAPI 测试下单接口
    92. */
    93. @PostMapping(value = "/ceshiJSAPI")
    94. @Log("JSAPI 测试下单接口")
    95. @ApiOperation("JSAPI 测试下单接口")
    96. @AnonymousAccess
    97. @Transactional(rollbackFor = Exception.class)
    98. public JSONObject ceshiJSAPI() throws Exception {
    99. if (config == null) {
    100. config =
    101. new RSAAutoCertificateConfig.Builder()
    102. .merchantId(merchantId)
    103. .privateKeyFromPath(privateKeyPath)
    104. .merchantSerialNumber(merchantSerialNumber)
    105. .apiV3Key(apiV3key)
    106. .build();
    107. }
    108. if (service == null) {
    109. service = new JsapiService.Builder().config(config).build();
    110. }
    111. // request.setXxx(val)设置所需参数,具体参数可见Request定义
    112. PrepayRequest request = new PrepayRequest();
    113. Amount amount = new Amount();
    114. amount.setTotal(1);//金额,1为0.01元
    115. request.setAmount(amount);
    116. request.setAppid(appId);
    117. request.setMchid(merchantId);
    118. request.setDescription("测试商品标题");//下单标题
    119. request.setNotifyUrl("https://www.ceshi123.com/api/weChatPay/ceshiPayNotify");//要求必须为线上接口
    120. String ddh = weChatPayService.getRandomString(20);
    121. request.setOutTradeNo(ddh);//订单号
    122. Payer payer = new Payer();
    123. payer.setOpenid("oMr**********");//openid
    124. request.setPayer(payer);
    125. PrepayResponse response = service.prepay(request);
    126. JSONObject result = new JSONObject();
    127. Long date = new Date().getTime();//获取当前时间戳
    128. String nonceStr = weChatPayService.getRandomString(28);//随机字符串,长度为32个字符以下
    129. result.put("timeStamp", date.toString());//时间戳
    130. result.put("nonceStr", nonceStr);//随机字符串,长度为32个字符以下
    131. result.put("package", "prepay_id=" + response.getPrepayId());//PrepayId
    132. result.put("signType", "RSA");//签名类型
    133. result.put("paySign", weChatPayService.getSign(appId, date, nonceStr, "prepay_id=" + response.getPrepayId()));//签名
    134. return result;
    135. }
    136. /**
    137. * 测试微信支付结果回调地址(放入线上测试)
    138. *
    139. * @param
    140. */
    141. @PostMapping(value = "/ceshiPayNotify")
    142. @Log("ceshiPayNotify方法")
    143. @ApiOperation("ceshiPayNotify方法")
    144. @AnonymousAccess
    145. public void ceshi_payNotify(HttpServletRequest request) throws IOException {
    146. //微信返回的证书序列号
    147. String serialNo = request.getHeader("Wechatpay-Serial");
    148. //微信返回的随机字符串
    149. String nonceStr = request.getHeader("Wechatpay-Nonce");
    150. //微信返回的时间戳
    151. String timestamp = request.getHeader("Wechatpay-Timestamp");
    152. //微信返回的签名
    153. String wechatSign = request.getHeader("Wechatpay-Signature");
    154. String singType = request.getHeader("Wechatpay-Signature-Type");
    155. String collect = request.getReader().lines().collect(Collectors.joining());
    156. try {
    157. FileUtil.writeUtf8String(collect, fileProperties.getPath().getPath() + "/huidiao-collect.txt");//打印看效果
    158. FileUtil.writeUtf8String(serialNo, fileProperties.getPath().getPath() + "/huidiao-serialNo.txt");//打印看效果
    159. FileUtil.writeUtf8String(singType, fileProperties.getPath().getPath() + "/huidiao-Signature-Type.txt");//打印看效果
    160. FileUtil.writeUtf8String(nonceStr, fileProperties.getPath().getPath() + "/huidiao-nonceStr.txt");//打印看效果
    161. FileUtil.writeUtf8String(timestamp, fileProperties.getPath().getPath() + "/huidiao-timestamp.txt");//打印看效果
    162. FileUtil.writeUtf8String(wechatSign, fileProperties.getPath().getPath() + "/huidiao-wechatSign.txt");//打印看效果
    163. //TODO 业务校验---可以写入自己的业务逻辑
    164. } catch (Exception e) {
    165. System.out.println("e = " + e);
    166. FileUtil.writeUtf8String("22222222222222222222", fileProperties.getPath().getPath() + "/huidiao22.txt");//打印看效果
    167. }
    168. }
    169. /**
    170. * 微信支付结果回调地址(本地)
    171. *
    172. * @param request
    173. * @return
    174. */
    175. @PostMapping(value = "/payNotify")
    176. @AnonymousAccess
    177. @Transactional(rollbackFor = Exception.class)
    178. public ResponseEntity wxCallback(HttpServletRequest request) throws Exception {
    179. //获取报文
    180. String body = weChatPayService.getRequestBody(request);
    181. //随机串
    182. String nonce = request.getHeader("Wechatpay-Nonce");
    183. //微信传递过来的签名
    184. String signature = request.getHeader("Wechatpay-Signature");
    185. //证书序列号(微信平台)
    186. String wechatPaySerial = request.getHeader("Wechatpay-Serial");
    187. String singType = request.getHeader("Wechatpay-Signature-Type");
    188. //时间戳
    189. String timestamp = request.getHeader("Wechatpay-Timestamp");
    190. if (configMap.get("config") == null) {
    191. config = new RSAAutoCertificateConfig.Builder()
    192. .merchantId(merchantId)
    193. .privateKeyFromPath(privateKeyPath)
    194. .merchantSerialNumber(merchantSerialNumber)
    195. .apiV3Key(apiV3key)
    196. .build();
    197. configMap.put("config", config);
    198. }
    199. else {
    200. config = configMap.get("config");
    201. }
    202. NotificationParser parser = new NotificationParser((NotificationConfig) config);
    203. RequestParam.Builder builder = new RequestParam.Builder();
    204. builder.body(body);
    205. builder.signature(signature);
    206. builder.nonce(nonce);
    207. builder.timestamp(timestamp);
    208. builder.serialNumber(wechatPaySerial);
    209. builder.signType(singType);
    210. RequestParam requestParam = builder.build();
    211. Transaction parse = parser.parse(requestParam, Transaction.class);
    212. if ("SUCCESS".equals(parse.getTradeState().toString())) {
    213. //支付成功,你的业务逻辑
    214. return new ResponseEntity<>(HttpStatus.OK);
    215. } else {
    216. throw new BadRequestException( "支付失败");
    217. }
    218. }
    219. /** 测试退款申请
    220. * @return*/
    221. @PostMapping(value = "/ceshiRefund")
    222. @Log("JSAPI 测试退款接口")
    223. @ApiOperation("JSAPI 测试退款接口")
    224. @AnonymousAccess
    225. @Transactional(rollbackFor = Exception.class)
    226. public JSONObject ceshiRefund(String No) throws Exception {
    227. if (config == null) {
    228. config = new RSAAutoCertificateConfig.Builder()
    229. .merchantId(merchantId)
    230. .privateKeyFromPath(privateKeyPath)
    231. .merchantSerialNumber(merchantSerialNumber)
    232. .apiV3Key(apiV3key)
    233. .build();
    234. }
    235. if (refundService == null) {
    236. // 初始化服务
    237. refundService = new RefundService.Builder().config(config).build();
    238. }
    239. CreateRequest request = new CreateRequest();
    240. request.setOutTradeNo(No);//商户订单号
    241. request.setOutRefundNo(weChatPayService.getRandomString(16));//商户退款单号,随机生成
    242. request.setReason("取消订单的退款");//退款原因
    243. AmountReq amount = new AmountReq();//金额信息
    244. amount.setRefund(Long.parseLong("1"));//退款金额(0.01元)
    245. amount.setTotal(Long.parseLong("1"));//原订单退款金额(0.01元)
    246. amount.setCurrency("CNY");//人民币
    247. request.setAmount(amount);
    248. // request.setNotifyUrl("https://https://www.ceshi123.com/api/weChatPay/ceshiRefundNotify");//回调接口,退款时非必填--这里我就没写接口
    249. JSONObject result = new JSONObject();
    250. try {
    251. Refund response = refundService.create(request);
    252. result.put("code", "SUCCESS");
    253. result.put("message", "退款成功");
    254. return result;
    255. } catch (ServiceException e) { // 服务返回状态小于200或大于等于300,例如500
    256. // 调用e.getResponseBody()获取返回体打印日志或上报监控,更多方法见ServiceException定义
    257. return JSONObject.parseObject(e.getResponseBody());
    258. }
    259. }
    260. @PostMapping(value = "/cancelWorder")
    261. @Log("取消订单)")
    262. @ApiOperation("取消订单")
    263. @Transactional(rollbackFor = Exception.class)
    264. public ResponseEntity cancelWorder(@RequestBody OwWorder resources) throws Exception {
    265. Long worderId=resources.getWorderId();//订单id
    266. OwWorderDto owWorderDto = owWorderService.findById(worderId);//根据订单id,获取订单信息
    267. //微信退款==开始
    268. WeChatRefundVo weChatRefundVo=new WeChatRefundVo();
    269. weChatRefundVo.setOutTradeNo(owWorderDto.getWorderNo().toString());//商户订单号
    270. weChatRefundVo.setRefund(owWorderDto.getMoney());//退款金额
    271. weChatRefundVo.setTotal(owWorderDto.getMoney());//原订单退款金额
    272. JSONObject json=weChatRefund(weChatRefundVo);
    273. System.out.println(json);
    274. System.out.println("=======================code========"+json.getString("code"));
    275. System.out.println("=======================message========"+json.getString("message"));
    276. if(json.getString("message").equals("订单已全额退款") || json.getString("code").equals("SUCCESS"))
    277. {
    278. //业务逻辑部分,改变订单状态等操作
    279. return new ResponseEntity<>(HttpStatus.OK);
    280. }
    281. else
    282. {
    283. throw new BadRequestException(json.getString("message"));
    284. }
    285. return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    286. }
    287. }
      1. WeChatRefundVo取消订单时,用到的对象:
      2. package me.zhengjie.modules.api.service.dto;
      3. import lombok.Data;
      4. import java.math.BigDecimal;
      5. @Data
      6. public class WeChatRefundVo {
      7. // 商户订单号
      8. private String outTradeNo;
      9. // 退款金额
      10. private BigDecimal refund;
      11. //原订单金额
      12. private BigDecimal total;
      13. }

       

    288. 相关阅读:
      ReentrantLock与synchronized区别之比较(面试)
      java: 程序包com.alibaba.excel.annotation不存在
      JVM中的 -Xms参数 设置 JVM 的初始堆大小
      OpenP2P实现内网穿透远程办公
      实施 ECM 系统的 6 大挑战——以及如何避免它们
      我所遇到的web前端最常见的面试 - 后续不断更新
      ECharts数据可视化项目【6】
      简单入门linux【三】linux 组和权限
      决策树与随机森林
      《canvas》之第6章 图片操作
    289. 原文地址:https://blog.csdn.net/hdxyzlh_0225/article/details/132803585