# 微信小程序之V3微信支付 **Repository Path**: samsom/wx_pay_study ## Basic Information - **Project Name**: 微信小程序之V3微信支付 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2022-07-21 - **Last Updated**: 2022-07-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 微信支付Native-V3系列 # 01、准备工作 ## 01、适合人群 1、只要服务于企业开发中需要使用微信支付的小伙伴 2、必须要有一定的java基础 ## 02、微信支付官网 [https://pay.weixin.qq.com](https://pay.weixin.qq.com/) ## 03、商家注册 https://pay.weixin.qq.com/index.php/apply/applyment_home/guide_normal ## 04、小程序官网 https://mp.weixin.qq.com/wxamp/ ## 05、小程序注册 https://mp.weixin.qq.com/cgi-bin/registermidpage?action=index&lang=zh_CN&token= ## 06、相关接口和文档 接口文档:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml Native接口规则:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml ## 07、相关工具下载 微信小程序:https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html hbuilderx工具:https://www.dcloud.io/hbuilderx.html finalshell工具:http://www.hostbuf.com/t/988.html # 02、商户注册&证书安装 官网说明:https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_1.shtml ## 01、接入前准备 > 商户/服务商在接入前首先要判断自己公司注册区域适用的接入模式,微信支付目前提供两种接入方式:直连模式和服务商模式。 ### 直连模式: 信息、资金流:微信支付—>直连商户 直连模式,商户自行申请入驻微信支付,无需服务商协助。(商户平台申请)成为普通商户 ### 服务商模式: ![img](README.assets/kuangstudy6049bbb5-0e66-4c3e-aeac-b5cb1d566fff.png) 服务商模式,商户申请成为微信支付服务商,服务商自身无法作为一个普通商户直接发起交易,其发起交易必须传入相关特约商户商户号的参数信息。(服务商平台申请)成为普通服务商 请结合自身实际情况来选择接入模式。 ## 02、接入步骤和流程 ### 02-1、申请APPID 也就申请微信公众号或者微信小程序 > 注册地址:https://mp.weixin.qq.com/cgi-bin/registermidpage?action=index&lang=zh_CN&token= ### 02-2、微信支付注册商家-申请mchid > 商家号注册:https://pay.weixin.qq.com/index.php/apply/applyment_home/guide_normal 注册成功后登录扫码登录即可、如下获取商户号: ![img](README.assets/kuangstudy3581f31e-d77e-419b-8108-114dd6fdf25d.png) 或者 ![img](README.assets/kuangstudy6fadaaf7-c5aa-4556-ad34-fbc978663bda.png) ### 02-3、绑定APPID及mchid ![img](README.assets/kuangstudy0053c746-c7ca-4a6c-9355-63ba4cd043fc.png) ### 02-4、配置API key API v3密钥主要用于平台证书解密、回调信息解密。配置如下: #### 登录微信商户平台,进入【账户中心 > API安全 > API安全】目录,点击【设置密钥】。 ![img](README.assets/kuangstudyb810da0d-dbaf-453d-aa7f-1801b68511c3.png) #### 在弹出窗口中点击“已沟通”。 ![img](README.assets/kuangstudyd4f526ed-bf9f-4060-96a9-3162b2d58769.png) #### 输入API密钥,内容为32位字符,包括数字及大小写字母。点击获取短信验证码。 ![img](README.assets/kuangstudy4818c78d-8f39-41c0-9054-ba0242f3958a.png) ![img](README.assets/kuangstudy4594e371-b013-4b2d-87a3-ab6e3fb4437c.png) #### 输入短信验证码,点击“确认”即设置成功。 它是一串32位的随机数,生成代码的随机数如下: ```java /** * 生成随机数 * @return */ public static String getNonceStr(){ return UUID.randomUUID().toString() .replaceAll("-", "") .substring(0, 32); } public static void main(String[] args) { System.out.println(getNonceStr()); } ``` ### 02-5、下载并配置商户证书 商户可登录微信商户平台,在【账户中心】->【API安全】->【API证书】目录下载证书 #### 从2018年底开始,微信支付新入驻机构及商户都将使用CA签发证书,在证书申请页面上点击“下载证书”。 ![img](README.assets/kuangstudye4a97fdb-8bf0-4ec9-891c-76c8afa8d2ac.png) #### 在弹出窗口内点击“下载证书工具”按钮下载证书工具。 ![img](README.assets/kuangstudy7dd86137-6f47-4e60-aa1a-1d0b911e0eff.png) #### 安装证书工具并打开,选择证书需要存储的路径后点击“申请证书”。 ![img](README.assets/kuangstudyc06a8e95-232c-40a4-9cbe-b7a88111c96e.png) #### 在证书工具中,将复制的商户信息粘贴并点击“下一步” ![img](README.assets/kuangstudy12783f5b-e470-4d79-b610-a02eeec0dabd.png) #### 获取请求串 ![img](README.assets/kuangstudye46ff47b-0633-421e-abf4-33755f2bb0f0.png) ![img](README.assets/kuangstudy351c75b2-939a-4a29-9bc2-acdb20f200b3.png) ![img](README.assets/kuangstudy912917d6-2647-4852-b2cf-78b63f51e98c.png) #### 生成证书串 步骤1 在【商户平台】-“复制证书串”环节,点击“复制证书串”按钮后; 步骤2 在【证书工具】-“复制请求串”环节,点击“下一步”按钮进入“粘贴证书串”环节; 步骤3 在【证书工具】-“粘贴证书串”环节,点击“粘贴”按钮后; 步骤4 点击“下一步”按钮,进入【证书工具】-“生成证书”环节 ![img](README.assets/kuangstudyb3ba1c68-87b5-4a2b-ac1e-745b6cc7bc50.png) ![img](README.assets/kuangstudyb99948d1-fd01-46b7-a719-c8665beff836.png) ![img](README.assets/kuangstudy590021b3-fa1b-4fd4-a701-dd82cf31d8e5.png) #### 在【证书工具】-“生成证书”环节,已完成申请证书流程,点击“查看证书文件夹”,查看已生成的证书文件。 ![img](README.assets/kuangstudy64f53a08-f677-46c9-bd1f-da578bff6aad.png) # 03、产品签约 ## 01、目标 完成JSPAPI产品的签约 ## 02、具体步骤 ![img](README.assets/kuangstudyaf12a7be-17c3-4dd9-9901-877915984817.png) # 04、开发步骤及参数准备 官网:https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_2.shtml ## 01、目标 01、完成项目的搭建 02、在项目的pom.xml中完成相关微信支付的依赖 03、定义支付相关的请求HttpUtils.java 04、定义支付WeixinNavtiveController.java完成支付逻辑 05、完成二维码和微信navtive支付地址的融合用流输入 07、定义页面支付页面和页面控制的Controller 08、使用标签完成二维码的展示,并完成扫码支付 09、测试支付回调地址 ## 02、微信支付依赖所需参数 ```yaml ########################微信支付参数####################################### #微信商户号 wechat: mchId: 1370687602 #商家API证书序列号 mchSerialNo: 160C30064405BDB2F12FFD30EF759ABC56D5E48C #商户在微信公众平台申请服务号对应的APPID appId: wx2f823cdc8dfba815 #回调报文解密V3密钥key v3Key: 0139a6d9e93fb88d3d68e0a8a1b06bd6 #微信获取平台证书列表地址 certificates: url: https://api.mch.weixin.qq.com/v3/certificates #微信统一下单Navtive的API地址,用于二维码支付 unifiedOrder: url: https://api.mch.weixin.qq.com/v3/pay/transactions/native #异步接收微信支付结果通知的回调地址 callback: http://api.kuangstudy.com/api/pay/callback #商户证书私钥路径 key: path: C:/tools/1370687602_20210504_cert/apiclient_key.pem ########################微信支付参数####################################### ``` ## 02、整体流程图和架构如下 ![img](README.assets/kuangstudy1656861e-3acc-4c7d-a83e-7a57e0130fa7.png) ## 03、参数获取的来源 ### 03-1、微信商户号 > wechat.mchId = xxxx ![img](README.assets/kuangstudy6fadaaf7-c5aa-4556-ad34-fbc978663bda.png) ### 03-2、商家API证书序列号 > wechat.mchSerialNo = xxxx ![img](README.assets/kuangstudy1023357c-586f-41fa-b790-f8a436c06340.png) ### 03-3、商户在微信公众平台申请服务号对应的APPID > wechat.appId = xxx 1、微信小程序地址:https://mp.weixin.qq.com/ 2、扫码登录以后如下 ![img](README.assets/kuangstudy7be0c2cd-3eb0-49df-be24-831048886266.png) ### 03-4、回调报文解密V3密钥key > wechat.v3Key = xxxx ![img](README.assets/kuangstudy9d75472f-b05b-40bb-8fcb-498eafea5f7c.png) ### 03-5、微信获取平台证书列表地址用于签名验证 ``` #这个值是固定的wechat.certificates.url = https://api.mch.weixin.qq.com/v3/certificates ``` 参考:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay5_1.shtml ![img](README.assets/kuangstudy74b4a4f7-4d51-4be2-9375-096f7b0aa958.png) > 关于为什么要签名验证如下有说明: > 官网:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml > `微信支付API v3 key要求商户对请求进行签名。微信支付会在收到请求后进行签名的验证。如果签名验证不通过,微信支付API v3将会拒绝处理请求,并返回401 Unauthorized。` #### 签名代码 ```java import okhttp3.HttpUrl; import java.security.Signature; import java.util.Base64; // Authorization: // GET - getToken("GET", httpurl, "") // POST - getToken("POST", httpurl, json) String schema = "WECHATPAY2-SHA256-RSA2048"; HttpUrl httpurl = HttpUrl.parse(url); String getToken(String method, HttpUrl url, String body) { String nonceStr = "your nonce string"; long timestamp = System.currentTimeMillis() / 1000; String message = buildMessage(method, url, timestamp, nonceStr, body); String signature = sign(message.getBytes("utf-8")); return "mchid=\"" + yourMerchantId + "\"," + "nonce_str=\"" + nonceStr + "\"," + "timestamp=\"" + timestamp + "\"," + "serial_no=\"" + yourCertificateSerialNo + "\"," + "signature=\"" + signature + "\""; } String sign(byte[] message) { Signature sign = Signature.getInstance("SHA256withRSA"); sign.initSign(yourPrivateKey); sign.update(message); return Base64.getEncoder().encodeToString(sign.sign()); } String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) { String canonicalUrl = url.encodedPath(); if (url.encodedQuery() != null) { canonicalUrl += "?" + url.encodedQuery(); } return method + "\n" + canonicalUrl + "\n" + timestamp + "\n" + nonceStr + "\n" + body + "\n"; } ``` ### 03-6、微信统一下单Navtive的API地址,用于二维码支付 > wechat.unifiedOrder.url = https://api.mch.weixin.qq.com/v3/pay/transactions/native 来源地址:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml ![img](README.assets/kuangstudyafaf19c4-83ea-479d-9f38-0373cac34174.png) #### navtive支付请求官方代码,仅做参考 ```java public void CreateOrder() throws Exception{ HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/native"); // 请求body参数 String reqdata = "{" + "\"time_expire\":\"2018-06-08T10:34:56+08:00\"," + "\"amount\": {" + "\"total\":100," + "\"currency\":\"CNY\"" + "}," + "\"mchid\":\"1230000109\"," + "\"description\":\"Image形象店-深圳腾大-QQ公仔\"," + "\"notify_url\":\"https://www.weixin.qq.com/wxpay/pay.php\"," + "\"out_trade_no\":\"1217752501201407033233368018\"," + "\"goods_tag\":\"WXG\"," + "\"appid\":\"wxd678efh567hg6787\"," + "\"attach\":\"自定义数据说明\"," + "\"detail\": {" + "\"invoice_id\":\"wx123\"," + "\"goods_detail\": [" + "{" + "\"goods_name\":\"iPhoneX 256G\"," + "\"wechatpay_goods_id\":\"1001\"," + "\"quantity\":1," + "\"merchant_goods_id\":\"商品编码\"," + "\"unit_price\":828800" + "}," + "{" + "\"goods_name\":\"iPhoneX 256G\"," + "\"wechatpay_goods_id\":\"1001\"," + "\"quantity\":1," + "\"merchant_goods_id\":\"商品编码\"," + "\"unit_price\":828800" + "}" + "]," + "\"cost_price\":608800" + "}," + "\"scene_info\": {" + "\"store_info\": {" + "\"address\":\"广东省深圳市南山区科技中一道10000号\"," + "\"area_code\":\"440305\"," + "\"name\":\"腾讯大厦分店\"," + "\"id\":\"0001\"" + "}," + "\"device_id\":\"013467007045764\"," + "\"payer_client_ip\":\"14.23.150.211\"" + "}" + "}"; StringEntity entity = new StringEntity(reqdata); entity.setContentType("application/json"); httpPost.setEntity(entity); httpPost.setHeader("Accept", "application/json"); //完成签名并执行请求 CloseableHttpResponse response = httpClient.execute(httpPost); try { int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200) { //处理成功 System.out.println("success,return body = " + EntityUtils.toString(response.getEntity())); } else if (statusCode == 204) { //处理成功,无返回Body System.out.println("success"); } else { System.out.println("failed,resp code = " + statusCode+ ",return body = " + EntityUtils.toString(response.getEntity())); throw new IOException("request failed"); } } finally { response.close(); } } ``` ### 03-7、异步接收微信支付结果通知的回调地址 > wechat.callback = xxxx ![img](README.assets/kuangstudy69d8e876-5005-4a7a-a5b7-7215065b266c.png) ### 03-8、商户证书私钥路径 > wechat.key.path = xxxxxxx > 在windows中存储在你指定的目录下如下: ![img](README.assets/kuangstudy64f53a08-f677-46c9-bd1f-da578bff6aad.png) #### 生成证书串 步骤1 在【商户平台】-“复制证书串”环节,点击“复制证书串”按钮后; 步骤2 在【证书工具】-“复制请求串”环节,点击“下一步”按钮进入“粘贴证书串”环节; 步骤3 在【证书工具】-“粘贴证书串”环节,点击“粘贴”按钮后; 步骤4 点击“下一步”按钮,进入【证书工具】-“生成证书”环节 ![img](README.assets/kuangstudyb3ba1c68-87b5-4a2b-ac1e-745b6cc7bc50.png) ![img](README.assets/kuangstudyb99948d1-fd01-46b7-a719-c8665beff836.png) #### 在【证书工具】-“生成证书”环节,已完成申请证书流程,点击“查看证书文件夹”,查看已生成的证书文件。 ![img](README.assets/kuangstudy590021b3-fa1b-4fd4-a701-dd82cf31d8e5.png) ![img](README.assets/kuangstudy64f53a08-f677-46c9-bd1f-da578bff6aad.png) # 05、参数接口API ## 01、目录 对统一下单API的理解 ## 02、概述 官网参考:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml 商户Native支付统一下单接口,微信后台系统返回链接参数code_url,商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付。 接口说明 > 适用对象: 直连商户 > 请求URL:https://api.mch.weixin.qq.com/v3/pay/transactions/native > 请求方式:POST ## 03、必填请求参数 参考官网:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml ## 04、返回值是: ``` { "code_url": "weixin://wxpay/bizpayurl?pr=p4lpSuKzz" } ``` ## 05、其实简单原理如下: ![img](README.assets/kuangstudye2630fe2-132d-4ede-af43-731fa528d1ab.png) ![img](README.assets/kuangstudy9c9bb4cb-4c51-4947-9e7d-39e0b3db8b5c.png) # 06、实战开发-框架的搭建 ## 01、目标 完成微信支付native的框架搭建 ## 02、实现步骤 1、新建一个springboot单体架构工程 2、在pom.xml中配置和整合ssm的依赖 3、配置环境隔离以及数据库连接配置 4、新建数据库weixindb和数据库表kss_courses 5、新建课程产品对应的业务entity、mapper、service和controller 6、静态资源导入(ksd.css,jquery,vue,axios) 7、新建index.html和coursedetail.html 8、在IndexController配置路由进行对应的index和coursedetail.html跳转 9、实现课程列表业务接口的对接工作 ## 03、具体实现 ### 03-1、新建一个springboot单体架构工程 ![img](README.assets/kuangstudyf40acdc3-41b9-4e4e-9858-9e802aa424fa.png) ### 03-2、在pom.xml中配置和整合ssm的依赖 ```xml org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-freemarker org.projectlombok lombok 1.18.12 org.springframework.boot spring-boot-starter-test test org.apache.httpcomponents httpclient 4.5.3 org.apache.httpcomponents httpmime 4.5.2 com.google.zxing core 3.3.0 commons-io commons-io 2.6 com.google.code.gson gson 2.8.6 mysql mysql-connector-java 5.1.10 com.baomidou mybatis-plus-boot-starter 3.4.0 com.fasterxml.jackson.dataformat jackson-dataformat-avro org.apache.commons commons-lang3 3.6 ``` ### 03-3、配置环境隔离以及数据库链接配置 #### application.yml ```yaml spring: profiles: active: dev freemarker: suffix: .html jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 locale: zh_CN # 解决json返回过程中long的精度丢失问题 generator: write-numbers-as-strings: true write-bigdecimal-as-plain: true ``` #### application-dev.yml ```yaml server: port: 8080 spring: datasource: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/weixindb?serverTimezone=GMT%2b8&useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: root hikari: connection-timeout: 60000 validation-timeout: 3000 idle-timeout: 60000 login-timeout: 5 max-lifetime: 60000 maximum-pool-size: 400 minimum-idle: 100 read-only: false logging: level: root: debug ``` #### application-prod.yml ```yaml server: port: 80 # 数据库连接 spring: datasource: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://8.210.51.92:3306/kuangstudydb?serverTimezone=GMT%2b8&useUnicode=true&characterEncoding=utf-8&useSSL=false username: kuangstudydb password: BNJptkrCRayf6NEw hikari: connection-timeout: 60000 validation-timeout: 3000 idle-timeout: 60000 login-timeout: 5 max-lifetime: 60000 maximum-pool-size: 400 minimum-idle: 100 read-only: false logging: level: root: info ``` ### 03-4、新建数据库weixindb和数据库表kss_courses ```sql /* Navicat MySQL Data Transfer Source Server : localhost Source Server Version : 50733 Source Host : localhost:3306 Source Database : weixindb Target Server Type : MYSQL Target Server Version : 50733 File Encoding : 65001 Date: 2021-05-10 20:13:39 */ SET FOREIGN_KEY_CHECKS=0; -- ---------------------------- -- Table structure for kss_courses -- ---------------------------- DROP TABLE IF EXISTS `kss_courses`; CREATE TABLE `kss_courses` ( `courseid` varchar(32) NOT NULL COMMENT '课程唯一id', `title` varchar(100) DEFAULT NULL COMMENT '课程标题', `intro` varchar(500) DEFAULT NULL COMMENT '课程简短介绍', `img` varchar(300) DEFAULT NULL COMMENT '课程封面地址', `price` decimal(10,2) DEFAULT NULL COMMENT '课程的活动价', `status` int(1) DEFAULT NULL COMMENT '状态:已发布/未发布', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `update_time` datetime DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`courseid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of kss_courses -- ---------------------------- INSERT INTO `kss_courses` VALUES ('1317503462556848129', '预科阶段', '学习编程之前你要了解的知识!', '/assert/course/c1/02.jpg', '0.01', '1', '2020-10-18 00:31:18', '2021-04-01 10:56:38'); INSERT INTO `kss_courses` VALUES ('1317503769349214209', '入门环境搭建', '工欲善其事,必先利其器!', '/assert/course/c1/03.jpg', '0.01', '1', '2020-10-18 00:32:31', '2021-04-01 10:53:10'); INSERT INTO `kss_courses` VALUES ('1317504142650658818', '基础语法学习', '基础决定你未来的高度!', '/assert/course/c1/04.jpg', '0.01', '1', '2020-10-18 00:34:00', '2021-04-01 10:54:18'); INSERT INTO `kss_courses` VALUES ('1317504447027105793', '流程控制学习', '程序的本质就是这些!', '/assert/course/c1/05.jpg', '0.01', '1', '2020-10-18 00:35:13', '2021-04-01 10:56:03'); INSERT INTO `kss_courses` VALUES ('1317504610634321921', '方法详解', '封装的思想!', '/assert/course/c1/06.jpg', '0.01', '1', '2020-10-18 00:35:52', '2021-04-01 10:55:04'); INSERT INTO `kss_courses` VALUES ('1317504817342205954', '数组详解', '最简单的数据结构!', '/assert/course/c1/07.jpg', '0.01', '1', '2020-10-18 00:35:52', '2020-10-18 00:35:52'); INSERT INTO `kss_courses` VALUES ('1317504988834713602', '面向对象编程', 'Java的精髓OOP!', '/assert/course/c1/08.jpg', '0.01', '1', '2020-10-18 00:35:52', '2020-10-18 00:35:52'); INSERT INTO `kss_courses` VALUES ('1377518279077142529', '第三方支付课程-支付宝', '第三方支付课程-支付宝', '/assert/course/c10/07.jpg', '0.01', '1', '2020-10-18 00:18:08', '2021-04-01 10:54:25'); ``` ### 03-5、新建课程产品对应的业务entity、mapper、service和controller #### entity ```java package com.kuangstudy.wxpay.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.ToString; import lombok.experimental.Accessors; import java.util.Date; /** * @author 徐柯 * @Title: * @Package * @Description: * @date 2021/5/1012:39 */ @Data @Accessors(chain = true) @ToString @AllArgsConstructor @NoArgsConstructor @TableName("kss_courses") public class Course { // 课程id @TableId(type = IdType.ID_WORKER_STR) private String courseid; // 课程标题 private String title; // 课程介绍 private String intro; // 课程封面 private String img; // 课程价格 private String price; // 课程状态 0未发布1发布 private Integer status; // 创建时间 private Date createTime; // 更新时间 private Date updateTime; } ``` #### mapper ```java package com.kuangstudy.wxpay.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.kuangstudy.wxpay.entity.Course; /** * @author 徐柯 * @Title: * @Package * @Description: * @date 2021/5/1012:44 */ public interface CourseMapper extends BaseMapper { } ``` #### service和serviceimpl ```java package com.kuangstudy.wxpay.service; import com.baomidou.mybatisplus.extension.service.IService; import com.kuangstudy.wxpay.entity.Course; /** * @author 徐柯 * @Title: * @Package * @Description: * @date 2021/5/1012:46 */ public interface CourseService extends IService { } ``` ### 03-6、静态资源导入(ksd.css,jquery,vue,axios) > 代码篇幅过长,详看视频,或者相关文件中查找对应的资源文件放入到static目录下,如下: > ![img](README.assets/kuangstudy85d5929f-3e65-4542-9e2e-0a0d73eba274.png) ### 03-7、新建index.html和coursedetail.html ![img](README.assets/kuangstudyd297d91f-c380-4b07-9a9e-6c37b04be4a7.png) #### index.html ```html Document

学相伴微信支付V3-Native系列

你购买的课程是:{ {course.title} },价格是:¥{ {course.price} }

1 第一阶段:JavaSE

``` #### coursedetail.html ```html Document

学相伴微信支付V3-Native系列

你购买的课程是:${course.title},价格是:¥${course.price}

``` ### 03-8、在IndexController配置路由进行对应的index和coursedetail.html跳转 ```html package com.kuangstudy.wxpay.controller; import com.kuangstudy.wxpay.service.CourseService; import com.kuangstudy.wxpay.vo.R; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.*; @Controller public class IndexController { @Autowired CourseService courseService; /** * 跳转课程首页 * @return */ @GetMapping("index") public String index() { return "index"; } /** * 查询所有课程 * @return */ @ResponseBody @GetMapping("/loadcourse") public R loadCourse() { return R.ok().data("courses", courseService.list()); } /** * 跳转课程明细 * @param id * @param modelMap * @return */ @GetMapping("coursedetail/{id}") public String coursedetail(@PathVariable("id")String id, ModelMap modelMap){ modelMap.put("course",courseService.getById(id)); return "coursedetail"; } } ``` ### 03-9、实现课程列表业务接口的对接工作并访问 > 在浏览器访问http://localhost:8080/index ![img](README.assets/kuangstudya69ed328-12c7-4a9e-9f22-b991b18d675a.png) # 07、实战开发-微信支付接口定义 官网:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml ## 01、目标 完成微信支付native的接口定义和二维码生成 > 商户Native支付统一下单接口,微信后台系统返回链接参数code_url,商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付。 ## 02、接口说明 - 适用对象: 直连商户 - 请求URL:https://api.mch.weixin.qq.com/v3/pay/transactions/native - 请求方式:POST ## 03、官方图解 ![img](README.assets/kuangstudy758b7c21-84e3-4694-bded-0ed399687e4c.png) ## 04、飞哥图解 ![img](README.assets/kuangstudy9c9bb4cb-4c51-4947-9e7d-39e0b3db8b5c.png) ## 05、实现步骤 01、在开发环境的配置和在生成环境的配置隔离 02、定义WechatPayUtils.java获取商家私钥和平台证书 03、在配置类中实现CommandLineRunner接口完成参数的注入和初始化 04、定义微信支付接口请求类HttpUtils.java 05、定义微信支付接口WeixinNavtiveController.java类 06、完成支付接口的与zxing生成二维码 07、进行测试生成微信支付二维码 ## 06、实现步骤 ### 06-01、在开发环境的配置和在生成环境的配置隔离 #### 在application-dev.yml新增 ```yaml ########################微信支付参数####################################### #微信商户号 wechat: mchId: 1370687602 #商家API证书序列号 mchSerialNo: 160C30064405BDB2F12FFD30EF759ABC56D5E48C #商户在微信公众平台申请服务号对应的APPID appId: wx2f823cdc8dfba815 #回调报文解密V3密钥key v3Key: 0139a6d9e93fb88d3d68e0a8a1b06bd6 #微信获取平台证书列表地址 certificates: url: https://api.mch.weixin.qq.com/v3/certificates #微信统一下单Navtive的API地址,用于二维码支付 unifiedOrder: url: https://api.mch.weixin.qq.com/v3/pay/transactions/native jsurl: https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi #异步接收微信支付结果通知的回调地址 callback: https://api.kuangstudy.com/api/pay/callback #商户证书私钥路径 key: path: D:/1370687602_20210504_cert/apiclient_key.pem ########################################################################### ``` #### 在application-prod.yml新增 ```yaml ########################微信支付参数####################################### #微信商户号 wechat: mchId: 1370687602 #商家API证书序列号 mchSerialNo: 160C30064405BDB2F12FFD30EF759ABC56D5E48C #商户在微信公众平台申请服务号对应的APPID appId: wx2f823cdc8dfba815 #回调报文解密V3密钥key v3Key: 0139a6d9e93fb88d3d68e0a8a1b06bd6 #微信获取平台证书列表地址 certificates: url: https://api.mch.weixin.qq.com/v3/certificates #微信统一下单Navtive的API地址,用于二维码支付 unifiedOrder: url: https://api.mch.weixin.qq.com/v3/pay/transactions/native jsurl: https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi #异步接收微信支付结果通知的回调地址 callback: https://api.kuangstudy.com/api/pay/callback #商户证书私钥路径 key: path: /www/web/weixincert/apiclient_key.pem ########################################################################### ``` #### 注意事项1 > 区别点在于:商家证书的私钥路径不同,因为一个是linux环境一个是windows环境。 > 在部署linux环境的时候,可能会引发加密的的java异常。这个时候需要手动配置jdk和jdk-security安全相关的环境才可以校验通过。 > > ```java > #1:先解压jdk-8u291-linux-i586.tar.gz > tar -zxvf jdk-8u291-linux-i586.tar.gz > #2:然后把jce_policy-8.zip中所有local_policy.jar和US_export_policy.jar放入到 /www/jdk1.8.0_291/jre/lib > #3:编辑/etc/profile文件如下图vim /etc/profile > ``` > > ![img](README.assets/kuangstudy5d8bc3a0-03c0-4e4f-a113-8d6d6299c91d.png) ``` # 4: 然后重启即可> source /etc/profile# 5、检验jdk是否生效>java -version ``` ![img](README.assets/kuangstudydcd7c535-c1ef-471b-9a94-da6dccda00dd.png) #### 注意事项2 > 合成二维码的时候需要用的一个logo。在本机放在工程的resources即可。如果是在linux系统下,这个时候就需要放在指定的目录如下: > ![img](README.assets/kuangstudy4da84a88-e599-4566-a390-c5e50ac60b2f.png) ### 06-02、定义WechatPayUtils.java获取商家私钥和平台证书 > 参考官网:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml ```java package com.kuangstudy.wxpay.utils; import com.fasterxml.jackson.databind.JsonNode; import com.kuangstudy.wxpay.common.KsdStaticParameter; import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Paths; import java.security.*; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Base64; import java.util.HashMap; import java.util.Map; import java.util.UUID; /** * 参考官网 https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml */ public class WechatPayUtils { /** * 获取私钥。 * * @param filename 私钥文件路径 (required) * @return 私钥对象 */ public static PrivateKey getPrivateKey(String filename) throws IOException { System.out.println("filename:"+filename); String content = new String(Files.readAllBytes(Paths.get(filename)), "utf-8"); try { String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "") .replace("-----END PRIVATE KEY-----", "") .replaceAll("\\s+", ""); KeyFactory kf = KeyFactory.getInstance("RSA"); return kf.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey))); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("当前Java环境不支持RSA", e); } catch (InvalidKeySpecException e) { throw new RuntimeException("无效的密钥格式"); } } /** * 生成token * @param method * @param url * @param body * @return * @throws Exception */ public static String getToken(String method, URL url, String body) throws Exception { String nonceStr = getNonceStr(); long timestamp = System.currentTimeMillis() / 1000; String message = buildMessage(method, url, timestamp, nonceStr, body); String signature = sign(message.getBytes("utf-8")); return "WECHATPAY2-SHA256-RSA2048 "+"mchid=\"" + KsdStaticParameter.mchId + "\"," + "nonce_str=\"" + nonceStr + "\"," + "timestamp=\"" + timestamp + "\"," + "serial_no=\"" + KsdStaticParameter.mchSerialNo + "\"," + "signature=\"" + signature + "\""; } /** * 生成token * @param method * @param url * @param body * @return * @throws Exception */ public static Map getTokenWeixin(String method, URL url, String body,String prepay_id) throws Exception { String nonceStr = getNonceStr(); long timestamp = System.currentTimeMillis() / 1000; String message = buildMessage(method, url, timestamp, nonceStr, body); String signature = sign(message.getBytes("utf-8")); Map map = new HashMap<>(); map.put("timeStamp",String.valueOf(timestamp)); map.put("nonceStr",nonceStr); map.put("package", "prepay_id=" + prepay_id); map.put("signType","RSA"); map.put("paySign",signature); return map; } /** * 生成签名 * @param message * @return * @throws Exception */ public static String sign(byte[] message) throws Exception { Signature sign = Signature.getInstance("SHA256withRSA"); sign.initSign(KsdStaticParameter.privateKey); sign.update(message); return Base64.getEncoder().encodeToString(sign.sign()); } /** * 生成签名串 * @param method * @param url * @param timestamp * @param nonceStr * @param body * @return */ public static String buildMessage(String method, URL url, long timestamp, String nonceStr, String body) { String canonicalUrl = url.getPath(); if (url.getQuery() != null) { canonicalUrl += "?" + url.getQuery(); } return method + "\n" + canonicalUrl + "\n" + timestamp + "\n" + nonceStr + "\n" + body + "\n"; } /** * 生成随机数 * @return */ public static String getNonceStr(){ return UUID.randomUUID().toString() .replaceAll("-", "") .substring(0, 32); } /** * 获取平台证书 * @return */ public static Map refreshCertificate() throws Exception { Map certificateMap = new HashMap(); // 1: 执行get请求 JsonNode jsonNode = HttpUtils.doGet(KsdStaticParameter.certificatesUrl); // 2: 获取平台验证的相关参数信息 JsonNode data = jsonNode.get("data"); if(data!=null){ for(int i=0;i certificateMap = new ConcurrentHashMap<>(); public static void certificateMap(String serialNo) { } } ``` ### 06-04、定义微信支付接口请求类HttpUtils.java ```java package com.kuangstudy.wxpay.utils; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.http.HttpEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import java.net.URL; import java.util.HashMap; import java.util.Map; public class HttpUtils { private static final ObjectMapper JSON=new ObjectMapper(); /** * get方法 * @param url * @return */ public static JsonNode doGet(String url){ CloseableHttpClient httpClient = HttpClientBuilder.create().build(); HttpGet httpget = new HttpGet(url); httpget.addHeader("Content-Type", "application/json;charset=UTF-8"); httpget.addHeader("Accept", "application/json"); try{ String token = WechatPayUtils.getToken("GET", new URL(url), ""); httpget.addHeader("Authorization", token); CloseableHttpResponse httpResponse = httpClient.execute(httpget); if(httpResponse.getStatusLine().getStatusCode() == 200){ String jsonResult = EntityUtils.toString( httpResponse.getEntity()); return JSON.readTree(jsonResult); }else{ System.err.println(EntityUtils.toString( httpResponse.getEntity())); } }catch (Exception e){ e.printStackTrace(); }finally { try { httpClient.close(); }catch (Exception e){ e.printStackTrace(); } } return null; } /** * 封装post * @return */ public static Map doPost(String url, String body){ CloseableHttpClient httpClient = HttpClients.createDefault(); HttpPost httpPost = new HttpPost(url); httpPost.addHeader("Content-Type","application/json;chartset=utf-8"); httpPost.addHeader("Accept", "application/json"); try{ String token = WechatPayUtils.getToken("POST", new URL(url), body); httpPost.addHeader("Authorization", token); if(body==null){ throw new IllegalArgumentException("data参数不能为空"); } StringEntity stringEntity = new StringEntity(body,"utf-8"); httpPost.setEntity(stringEntity); CloseableHttpResponse httpResponse = httpClient.execute(httpPost); HttpEntity httpEntity = httpResponse.getEntity(); if(httpResponse.getStatusLine().getStatusCode() == 200){ String jsonResult = EntityUtils.toString(httpEntity); return JSON.readValue(jsonResult,HashMap.class); }else{ System.err.println("微信支付错误信息"+EntityUtils.toString(httpEntity)); } }catch (Exception e){ e.printStackTrace(); }finally { try{ httpClient.close(); }catch (Exception e){ e.printStackTrace(); } } return null; } } ``` ### 06-05、定义微信支付接口WeixinNavtiveController.java类 ```java package com.kuangstudy.wxpay.controller; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.zxing.EncodeHintType; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import com.kuangstudy.wxpay.common.KsdStaticParameter; import com.kuangstudy.wxpay.utils.*; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.FileCopyUtils; import org.springframework.util.ResourceUtils; import org.springframework.web.bind.annotation.*; import javax.imageio.ImageIO; import javax.imageio.stream.ImageOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.security.cert.X509Certificate; import java.util.HashMap; import java.util.Map; @RestController @Log4j2 public class WeixinNavtiveController { @Value("${spring.profiles.active}") private String profiles; /** * 付款订单Api,根据传入的订单号 生成付款二维码 * * @param courseid * @param response */ @RequestMapping("/weixinpay") @ResponseBody public byte[] weixinpay(HttpServletResponse response) throws JsonProcessingException { //封装请求参数 Map map = new HashMap(); map.put("appid", KsdStaticParameter.appId); map.put("mchid", KsdStaticParameter.mchId); //临时写死配置 map.put("description", "测试数据"); map.put("out_trade_no", new SnowflakeIdWorker(1, 1).nextId() + ""); map.put("notify_url", KsdStaticParameter.notifyUrl); Map amount = new HashMap(); //订单金额 单位分 amount.put("total", Integer.parseInt(getMoney("0.01"))); amount.put("currency", "CNY"); map.put("amount", amount); ObjectMapper objectMapper = new ObjectMapper(); String body = objectMapper.writeValueAsString(map); Map stringObjectMap = HttpUtils.doPost(KsdStaticParameter.unifiedOrderUrl, body); String codeUrl = stringObjectMap.get("code_url").toString(); //生成付款二维码 //生成二维码配置 Map hints = new HashMap<>(); //设置纠错等级 hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L); //编码类型 hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); try { // 7: 生成微信支付二维码 ByteArrayOutputStream output = new ByteArrayOutputStream(); String logopath = null; if(profiles.equals("dev")) { logopath = ResourceUtils.getFile("classpath:favicon.png").getAbsolutePath(); }else{ logopath = ResourceUtils.getFile("/www/web/favicon.png").getAbsolutePath(); } BufferedImage buff = QRCodeUtil.encode(codeUrl, logopath, false); ImageOutputStream imageOut = ImageIO.createImageOutputStream(output); ImageIO.write(buff, "JPEG", imageOut); imageOut.close(); ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray()); return FileCopyUtils.copyToByteArray(input); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 元转换成分 * * @param money * @return */ private String getMoney(String money) { if (money == null || money.equalsIgnoreCase("0")) { return ""; } // 金额转化为分为单位 // 处理包含, ¥ 或者$的金额 String currency = money.replaceAll("\\$|\\¥|\\,", ""); int index = currency.indexOf("."); int length = currency.length(); Long amLong = 0l; if (index == -1) { amLong = Long.valueOf(currency + "00"); } else if (length - index >= 3) { amLong = Long.valueOf((currency.substring(0, index + 3)).replace(".", "")); } else if (length - index == 2) { amLong = Long.valueOf((currency.substring(0, index + 2)).replace(".", "") + 0); } else { amLong = Long.valueOf((currency.substring(0, index + 1)).replace(".", "") + "00"); } return amLong.toString(); } } ``` ### 06-06、完成支付接口的与zxing生成二维码 ```java // 7: 生成微信支付二维码 ByteArrayOutputStream output = new ByteArrayOutputStream(); String logopath = null; if(profiles.equals("dev")) { logopath = ResourceUtils.getFile("classpath:favicon.png").getAbsolutePath(); }else{ logopath = ResourceUtils.getFile("/www/web/favicon.png").getAbsolutePath(); } BufferedImage buff = QRCodeUtil.encode(codeUrl, logopath, false); ImageOutputStream imageOut = ImageIO.createImageOutputStream(output); ImageIO.write(buff, "JPEG", imageOut); imageOut.close(); ``` ### 06-07、进行测试生成微信支付二维码 ```java

学相伴微信支付V3-Native系列

你购买的课程是:预科阶段,价格是:¥0.01

``` # 08、实战开发-业务对接微信支付 ## 01、目标 完成课程产品和微信支付的对接工作 ## 02、实现步骤 1、点击课程完成每个产品和微信支付的生成和绑定 2、将微信支付中静态的数据修改成课程数据即可 ## 03、具体操作 ### 03-1、点击课程完成每个产品和微信支付的生成和绑定 ```java Document

学相伴微信支付V3-Native系列

你购买的课程是:{ {course.title} },价格是:¥{ {course.price} }

1 第一阶段:JavaSE

``` ### 03-2、将微信支付中静态的数据修改成课程数据即可 ```java package com.kuangstudy.wxpay.controller; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.zxing.EncodeHintType; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import com.kuangstudy.wxpay.common.KsdStaticParameter; import com.kuangstudy.wxpay.entity.Course; import com.kuangstudy.wxpay.service.CourseService; import com.kuangstudy.wxpay.utils.*; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.FileCopyUtils; import org.springframework.util.ResourceUtils; import org.springframework.web.bind.annotation.*; import javax.imageio.ImageIO; import javax.imageio.stream.ImageOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.security.cert.X509Certificate; import java.util.HashMap; import java.util.Map; @RestController @Log4j2 public class WeixinNavtiveController { @Autowired private CourseService courseService; @Value("${spring.profiles.active}") private String profiles; /** * 付款订单Api,根据传入的订单号 生成付款二维码 * * @param courseid * @param response */ @RequestMapping("/weixinpay") @ResponseBody public byte[] unifiedOrder(String courseid, HttpServletResponse response) throws JsonProcessingException { Course course = courseService.getById(courseid); if (course == null) return null; //封装请求参数 Map map = new HashMap(); map.put("appid", KsdStaticParameter.appId); map.put("mchid", KsdStaticParameter.mchId); //临时写死配置 map.put("description", course.getTitle()); map.put("out_trade_no", new SnowflakeIdWorker(1, 1).nextId() + ""); map.put("notify_url", KsdStaticParameter.notifyUrl); Map amount = new HashMap(); //订单金额 单位分 amount.put("total", Integer.parseInt(getMoney(course.getPrice()))); amount.put("currency", "CNY"); map.put("amount", amount); ObjectMapper objectMapper = new ObjectMapper(); String body = objectMapper.writeValueAsString(map); Map stringObjectMap = HttpUtils.doPost(KsdStaticParameter.unifiedOrderUrl, body); String codeUrl = stringObjectMap.get("code_url").toString(); //生成付款二维码 //生成二维码配置 Map hints = new HashMap<>(); //设置纠错等级 hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L); //编码类型 hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); try { // 7: 生成微信支付二维码 ByteArrayOutputStream output = new ByteArrayOutputStream(); String logopath = null; if(profiles.equals("dev")) { logopath = ResourceUtils.getFile("classpath:favicon.png").getAbsolutePath(); }else{ logopath = ResourceUtils.getFile("/www/web/favicon.png").getAbsolutePath(); } BufferedImage buff = QRCodeUtil.encode(codeUrl, logopath, false); ImageOutputStream imageOut = ImageIO.createImageOutputStream(output); ImageIO.write(buff, "JPEG", imageOut); imageOut.close(); ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray()); return FileCopyUtils.copyToByteArray(input); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 元转换成分 * * @param money * @return */ private String getMoney(String money) { if (money == null || money.equalsIgnoreCase("0")) { return ""; } // 金额转化为分为单位 // 处理包含, ¥ 或者$的金额 String currency = money.replaceAll("\\$|\\¥|\\,", ""); int index = currency.indexOf("."); int length = currency.length(); Long amLong = 0l; if (index == -1) { amLong = Long.valueOf(currency + "00"); } else if (length - index >= 3) { amLong = Long.valueOf((currency.substring(0, index + 3)).replace(".", "")); } else if (length - index == 2) { amLong = Long.valueOf((currency.substring(0, index + 2)).replace(".", "") + 0); } else { amLong = Long.valueOf((currency.substring(0, index + 1)).replace(".", "") + "00"); } return amLong.toString(); } } ``` # 09、实战开发-回调地址notify_url&解密 ## 01、目标 1、配置微信支付回调地址 2、回调报文解密 3、定义回调支付逻辑,完成回调地址获取微信支付响应的参数 ## 02、具体实现 ### 02-1、配置支付回调地址 参考官网:[https://pay.weixin.qq.com](https://pay.weixin.qq.com/) ![img](README.assets/kuangstudy3b79bd5d-8029-4b39-ab41-2d5135e520a8.png) ### 02-2、回调报文解密 参考官网:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_2.shtml 为了保证安全性,微信支付在回调通知和平台证书下载接口中,对关键信息进行了AES-256-GCM加密。本章节详细介绍了加密报文的格式,以及如何进行解密。 ```java package com.kuangstudy.wxpay.utils; import java.io.IOException; import java.security.GeneralSecurityException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Base64; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; public class AesUtil { static final int KEY_LENGTH_BYTE = 32; static final int TAG_LENGTH_BIT = 128; private final byte[] aesKey; public AesUtil(byte[] key) { if (key.length != KEY_LENGTH_BYTE) { throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节"); } this.aesKey = key; } public String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext) throws GeneralSecurityException, IOException { try { Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); SecretKeySpec key = new SecretKeySpec(aesKey, "AES"); GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce); cipher.init(Cipher.DECRYPT_MODE, key, spec); cipher.updateAAD(associatedData); return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), "utf-8"); } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { throw new IllegalStateException(e); } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { throw new IllegalArgumentException(e); } } } ``` ### 02-3、定义回调支付逻辑,完成回调地址获取微信支付响应的参数 参考官网:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml ```java package com.kuangstudy.wxpay.controller; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.zxing.EncodeHintType; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import com.kuangstudy.wxpay.common.KsdStaticParameter; import com.kuangstudy.wxpay.entity.Course; import com.kuangstudy.wxpay.service.CourseService; import com.kuangstudy.wxpay.utils.*; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.util.FileCopyUtils; import org.springframework.util.ResourceUtils; import org.springframework.web.bind.annotation.*; import javax.imageio.ImageIO; import javax.imageio.stream.ImageOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.security.cert.X509Certificate; import java.util.Date; import java.util.HashMap; import java.util.Map; @RestController @Log4j2 @RequestMapping("/api") public class WeixinNavtiveController { @Autowired private CourseService courseService; @Value("${spring.profiles.active}") private String profiles; /** * 付款订单Api,根据传入的订单号 生成付款二维码 * * @param courseid * @param response */ @RequestMapping("/weixinpay") @ResponseBody public byte[] weixinpay(String courseid, HttpServletResponse response) throws JsonProcessingException { Course course = courseService.getById(courseid); if (course == null) return null; //封装请求参数 Map map = new HashMap(); map.put("appid", KsdStaticParameter.appId); map.put("mchid", KsdStaticParameter.mchId); //临时写死配置 map.put("description", course.getTitle()); map.put("out_trade_no", new SnowflakeIdWorker(1, 1).nextId() + ""); map.put("notify_url", KsdStaticParameter.notifyUrl); Map amount = new HashMap(); // 附属参数 Map attachMap = new HashMap<>(); attachMap.put("courseid",courseid); attachMap.put("userid",1); map.put("attach",JsonUtil.obj2String(attachMap)); //订单金额 单位分 amount.put("total", Integer.parseInt(getMoney(course.getPrice()))); amount.put("currency", "CNY"); map.put("amount", amount); ObjectMapper objectMapper = new ObjectMapper(); String body = objectMapper.writeValueAsString(map); Map stringObjectMap = HttpUtils.doPost(KsdStaticParameter.unifiedOrderUrl, body); String codeUrl = stringObjectMap.get("code_url").toString(); //生成付款二维码 //生成二维码配置 Map hints = new HashMap<>(); //设置纠错等级 hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L); //编码类型 hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); try { // 7: 生成微信支付二维码 ByteArrayOutputStream output = new ByteArrayOutputStream(); String logopath = null; if(profiles.equals("dev")) { logopath = ResourceUtils.getFile("classpath:favicon.png").getAbsolutePath(); }else{ logopath = ResourceUtils.getFile("/www/web/favicon.png").getAbsolutePath(); } BufferedImage buff = QRCodeUtil.encode(codeUrl, logopath, false); ImageOutputStream imageOut = ImageIO.createImageOutputStream(output); ImageIO.write(buff, "JPEG", imageOut); imageOut.close(); ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray()); return FileCopyUtils.copyToByteArray(input); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 支付回调通知 * * @param body * @param request * @return */ @PostMapping("pay/callback") public Map orderPayCallback(@RequestBody Map body, HttpServletRequest request) { log.info("1----------->微信支付回调开始"); Map result = new HashMap(); //1:获取微信支付回调的获取签名信息 String timestamp = request.getHeader("Wechatpay-Timestamp"); String nonce = request.getHeader("Wechatpay-Nonce"); ObjectMapper objectMapper = new ObjectMapper(); try { // 2: 开始解析报文体 String data = objectMapper.writeValueAsString(body); String message = timestamp + "\n" + nonce + "\n" + data + "\n"; //3:获取应答签名 String sign = request.getHeader("Wechatpay-Signature"); //4:获取平台对应的证书 String serialNo = request.getHeader("Wechatpay-Serial"); if (!KsdStaticParameter.certificateMap.containsKey(serialNo)) { KsdStaticParameter.certificateMap = WechatPayUtils.refreshCertificate(); } X509Certificate x509Certificate = KsdStaticParameter.certificateMap.get(serialNo); if (!WechatPayUtils.verify(x509Certificate, message.getBytes(), sign)) { throw new IllegalArgumentException("微信支付签名验证失败:" + message); } log.info("签名验证成功"); Map resource = (Map) body.get("resource"); // 5:回调报文解密 AesUtil aesUtil = new AesUtil(KsdStaticParameter.v3Key.getBytes()); //解密后json字符串 String decryptToString = aesUtil.decryptToString( resource.get("associated_data").getBytes(), resource.get("nonce").getBytes(), resource.get("ciphertext")); log.info("2------------->decryptToString====>{}",decryptToString); //6:获取微信支付返回的信息 Map jsonData = objectMapper.readValue(decryptToString, Map.class); //7: 支付状态的判断 如果是success就代表支付成功 if ("SUCCESS".equals(jsonData.get("trade_state"))) { // 8:获取支付的交易单号,流水号,和附属参数 String out_trade_no = jsonData.get("out_trade_no").toString(); String transaction_id = jsonData.get("transaction_id").toString(); String attach = jsonData.get("attach").toString(); //TODO 根据订单号查询支付状态,如果未支付,更新支付状态 为已支付 log.info("3----------->微信支付成功,支付流水号是:{},附属参数是:{}", out_trade_no,attach); log.info("4----------->微信支付成功,支付流水号是:{}", transaction_id); // 转换附属参数 HashMap map = JsonUtil.string2Obj(attach,HashMap.class); // 9:保存用户支付信息 UserPay userPay = new UserPay(); userPay.setUserid(String.valueOf(map.get("userid"))); userPay.setNickname("飞哥"); userPay.setPrice("1"); userPay.setCourseid(String.valueOf(map.get("courseid"))); userPay.setTradeno(out_trade_no); userPay.setCreateTime(new Date()); userPayService.saveOrUpdate(userPay); } result.put("code", "SUCCESS"); result.put("message", "成功"); } catch (Exception e) { result.put("code", "fail"); result.put("message", "系统错误"); e.printStackTrace(); } return result; } /** * 元转换成分 * * @param money * @return */ private String getMoney(String money) { if (money == null || money.equalsIgnoreCase("0")) { return ""; } // 金额转化为分为单位 // 处理包含, ¥ 或者$的金额 String currency = money.replaceAll("\\$|\\¥|\\,", ""); int index = currency.indexOf("."); int length = currency.length(); Long amLong = 0l; if (index == -1) { amLong = Long.valueOf(currency + "00"); } else if (length - index >= 3) { amLong = Long.valueOf((currency.substring(0, index + 3)).replace(".", "")); } else if (length - index == 2) { amLong = Long.valueOf((currency.substring(0, index + 2)).replace(".", "") + 0); } else { amLong = Long.valueOf((currency.substring(0, index + 1)).replace(".", "") + "00"); } return amLong.toString(); } } ``` ### 02-3、测试结果如下 ![img](README.assets/kuangstudyf108ef52-9843-4819-9efc-ce68c85a3df5.png) # 10、实战开发-保存用户支支付明细 ## 01、目标 1、定义用户支付订单明细表 2、定义用户支付订单entity、service、mapper、controller 3、微信支付回调中添加用户支付成功业务 4、完成支付回调处理 ## 02、具体实现 ### 02-1、定义用户支付订单明细表 ```sql /* Navicat MySQL Data Transfer Source Server : localhost Source Server Version : 50733 Source Host : localhost:3306 Source Database : weixindb Target Server Type : MYSQL Target Server Version : 50733 File Encoding : 65001 Date: 2021-05-10 23:27:10 */ SET FOREIGN_KEY_CHECKS=0; -- ---------------------------- -- Table structure for kss_user_pay -- ---------------------------- DROP TABLE IF EXISTS `kss_user_pay`; CREATE TABLE `kss_user_pay` ( `id` int(11) NOT NULL AUTO_INCREMENT, `courseid` varchar(32) NOT NULL COMMENT '课程唯一id', `userid` varchar(32) DEFAULT NULL COMMENT '课程标题', `nickname` varchar(500) DEFAULT NULL COMMENT '课程简短介绍', `tradeno` varchar(300) DEFAULT NULL COMMENT '课程封面地址', `price` decimal(10,2) DEFAULT NULL COMMENT '课程的活动价', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `update_time` datetime DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4; -- ---------------------------- -- Records of kss_user_pay -- ---------------------------- INSERT INTO `kss_user_pay` VALUES ('1', '1317503462556848129', '1', '飞哥', '11', '1.00', '2021-05-10 23:06:58', null); ``` ### 02-2、定义用户支付订单entity,service,mapper,controller #### entity ```java package com.kuangstudy.wxpay.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.ToString; import lombok.experimental.Accessors; import java.util.Date; /** * @author 徐柯 * @Title: * @Package * @Description: * @date 2021/5/1022:43 */ @Data @Accessors(chain = true) @ToString @AllArgsConstructor @NoArgsConstructor @TableName("kss_user_pay") public class UserPay { // 主键 @TableId(type = IdType.AUTO) private Integer id; // 购买课程 private String courseid; // 支付用户 private String userid; // 支付用户名称 private String nickname; // 课程价格 private String price; // 交易流水号 private String tradeno; // 创建时间 private Date createTime; // 更新时间 private Date updateTime; } ``` ### mapper ```java package com.kuangstudy.wxpay.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.kuangstudy.wxpay.entity.UserPay; /** * @author 徐柯 * @Title: * @Package * @Description: * @date 2021/5/1022:43 */ public interface UserPayMapper extends BaseMapper { } ``` ### service ```java package com.kuangstudy.wxpay.service; import com.baomidou.mybatisplus.extension.service.IService; import com.kuangstudy.wxpay.entity.UserPay; /** * @author 徐柯 * @Title: * @Package * @Description: * @date 2021/5/1022:43 */ public interface UserPayService extends IService { } ``` ### serviceimpl ```java package com.kuangstudy.wxpay.service; import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.kuangstudy.wxpay.entity.UserPay; import com.kuangstudy.wxpay.mapper.UserPayMapper; import org.springframework.stereotype.Service; /** * @author 徐柯 * @Title: * @Package * @Description: * @date 2021/5/1022:43 */ @Service public class UserPayServiceImpl extends ServiceImpl implements UserPayService { } ``` ### 02-3、微信支付回调中添加用户支付成功业务 ```java package com.kuangstudy.wxpay.controller; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.zxing.EncodeHintType; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import com.kuangstudy.wxpay.common.KsdStaticParameter; import com.kuangstudy.wxpay.entity.Course; import com.kuangstudy.wxpay.entity.UserPay; import com.kuangstudy.wxpay.service.CourseService; import com.kuangstudy.wxpay.service.UserPayService; import com.kuangstudy.wxpay.utils.*; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.util.FileCopyUtils; import org.springframework.util.ResourceUtils; import org.springframework.web.bind.annotation.*; import javax.imageio.ImageIO; import javax.imageio.stream.ImageOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.security.cert.X509Certificate; import java.util.Date; import java.util.HashMap; import java.util.Map; @RestController @Log4j2 @RequestMapping("/api") public class WeixinNavtiveController { @Autowired private CourseService courseService; @Autowired private UserPayService userPayService; @Value("${spring.profiles.active}") private String profiles; /** * 付款订单Api,根据传入的订单号 生成付款二维码 * * @param courseid * @param response */ @RequestMapping("/weixinpay") @ResponseBody public byte[] weixinpay(String courseid, HttpServletResponse response) throws JsonProcessingException { Course course = courseService.getById(courseid); if (course == null) return null; //封装请求参数 Map map = new HashMap(); map.put("appid", KsdStaticParameter.appId); map.put("mchid", KsdStaticParameter.mchId); //临时写死配置 map.put("description", course.getTitle()); map.put("out_trade_no", new SnowflakeIdWorker(1, 1).nextId() + ""); map.put("notify_url", KsdStaticParameter.notifyUrl); Map amount = new HashMap(); // 附属参数 Map attachMap = new HashMap<>(); attachMap.put("courseid",courseid); attachMap.put("userid",1); map.put("attach",JsonUtil.obj2String(attachMap)); //订单金额 单位分 amount.put("total", Integer.parseInt(getMoney(course.getPrice()))); amount.put("currency", "CNY"); map.put("amount", amount); ObjectMapper objectMapper = new ObjectMapper(); String body = objectMapper.writeValueAsString(map); Map stringObjectMap = HttpUtils.doPost(KsdStaticParameter.unifiedOrderUrl, body); String codeUrl = stringObjectMap.get("code_url").toString(); //生成付款二维码 //生成二维码配置 Map hints = new HashMap<>(); //设置纠错等级 hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L); //编码类型 hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); try { // 7: 生成微信支付二维码 ByteArrayOutputStream output = new ByteArrayOutputStream(); String logopath = null; if(profiles.equals("dev")) { logopath = ResourceUtils.getFile("classpath:favicon.png").getAbsolutePath(); }else{ logopath = ResourceUtils.getFile("/www/web/favicon.png").getAbsolutePath(); } BufferedImage buff = QRCodeUtil.encode(codeUrl, logopath, false); ImageOutputStream imageOut = ImageIO.createImageOutputStream(output); ImageIO.write(buff, "JPEG", imageOut); imageOut.close(); ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray()); return FileCopyUtils.copyToByteArray(input); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 支付回调通知 * * @param body * @param request * @return */ @RequestMapping("pay/callback") public Map orderPayCallback(@RequestBody Map body, HttpServletRequest request) { log.info("1----------->微信支付回调开始"); Map result = new HashMap(); //获取签名信息 String timestamp = request.getHeader("Wechatpay-Timestamp"); String nonce = request.getHeader("Wechatpay-Nonce"); ObjectMapper objectMapper = new ObjectMapper(); try { String data = objectMapper.writeValueAsString(body); String message = timestamp + "\n" + nonce + "\n" + data + "\n"; //获取应答签名 String sign = request.getHeader("Wechatpay-Signature"); //获取平台对应的证书 String serialNo = request.getHeader("Wechatpay-Serial"); if (!KsdStaticParameter.certificateMap.containsKey(serialNo)) { KsdStaticParameter.certificateMap = WechatPayUtils.refreshCertificate(); } X509Certificate x509Certificate = KsdStaticParameter.certificateMap.get(serialNo); if (!WechatPayUtils.verify(x509Certificate, message.getBytes(), sign)) { throw new IllegalArgumentException("微信支付签名验证失败:" + message); } log.info("签名验证成功"); Map resource = (Map) body.get("resource"); AesUtil aesUtil = new AesUtil(KsdStaticParameter.v3Key.getBytes()); //解密后json字符串 String decryptToString = aesUtil.decryptToString( resource.get("associated_data").getBytes(), resource.get("nonce").getBytes(), resource.get("ciphertext")); log.info("2------------->decryptToString====>{}",decryptToString); Map jsonData = objectMapper.readValue(decryptToString, Map.class); //支付状态的判断 if ("SUCCESS".equals(jsonData.get("trade_state"))) { String out_trade_no = jsonData.get("out_trade_no").toString(); String transaction_id = jsonData.get("transaction_id").toString(); String attach = jsonData.get("attach").toString(); //TODO 根据订单号查询支付状态,如果未支付,更新支付状态 为已支付 log.info("3----------->微信支付成功,支付流水号是:{},附属参数是:{}", out_trade_no,attach); log.info("4----------->微信支付成功,支付流水号是:{}", transaction_id); // 保存用户订单明细 UserPay userPay = new UserPay(); userPay.setUserid("1"); userPay.setNickname("飞哥"); userPay.setPrice("1"); userPay.setTradeno(out_trade_no); userPay.setCreateTime(new Date()); userPayService.saveOrUpdate(userPay); } result.put("code", "SUCCESS"); result.put("message", "成功"); } catch (Exception e) { result.put("code", "fail"); result.put("message", "系统错误"); e.printStackTrace(); } return result; } /** * 元转换成分 * * @param money * @return */ private String getMoney(String money) { if (money == null || money.equalsIgnoreCase("0")) { return ""; } // 金额转化为分为单位 // 处理包含, ¥ 或者$的金额 String currency = money.replaceAll("\\$|\\¥|\\,", ""); int index = currency.indexOf("."); int length = currency.length(); Long amLong = 0l; if (index == -1) { amLong = Long.valueOf(currency + "00"); } else if (length - index >= 3) { amLong = Long.valueOf((currency.substring(0, index + 3)).replace(".", "")); } else if (length - index == 2) { amLong = Long.valueOf((currency.substring(0, index + 2)).replace(".", "") + 0); } else { amLong = Long.valueOf((currency.substring(0, index + 1)).replace(".", "") + "00"); } return amLong.toString(); } } ``` ### 02-4、完成支付回调处理 ![img](README.assets/kuangstudy4142b9fe-a906-4732-8767-5c4a7d953747.png) # 11、实战开发-回调监听和支付超时 ## 01、目标 1、定义监听支付成功的MonitorController 2、利用轮询机制监听微信支付成功的回调处理 3、优化轮询机制的支付超时问题 ## 02、具体实现 ### 02-1、定义监听支付成功的MonitorController ```java package com.kuangstudy.wxpay.controller; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.kuangstudy.wxpay.entity.UserPay; import com.kuangstudy.wxpay.service.UserPayService; import com.kuangstudy.wxpay.vo.R; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; /** * @author 徐柯 * @Title: * @Package * @Description: * @date 2021/5/1022:59 */ @Controller @RequestMapping("/api") public class MonitorController { @Autowired private UserPayService userPayService; /** * 定义监听类 * @param courseid * @return */ @ResponseBody @GetMapping("/paySuccess") public R paySuccess(String courseid) { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("courseid", courseid); queryWrapper.eq("userid", 1); int count = userPayService.count(queryWrapper); return count > 0 ? R.ok() : R.error(); } } ``` ### 02-2、利用轮询机制监听微信支付成功的回调处理 ```html Document

学相伴微信支付V3-Native系列

你购买的课程是:{ {course.title} },价格是:¥{ {course.price} }

1 第一阶段:JavaSE

``` ### 02-3、优化轮询机制的支付超时问题 ```html Document

学相伴微信支付V3-Native系列

你购买的课程是:{ {course.title} },价格是:¥{ {course.price} }

1 第一阶段:JavaSE

``` # 12、项目的发布和部署 ## 01、目标 完成项目的发布和部署 ## 02、步骤 1、准备一个阿里云服务器 2、将项目打成jar包 3、部署到云服务器接口 ## 03、项目打包命令 ```shell mvn clean package -Dmaven.skip.test=true ``` ## 04、启动项目 ```shell # 启动项目带端口和日志 nohup java -jar xxxx.jar --server.port=8080 >>1.txt & # 启动项目 nohup java -jar xxxx.jar >>1.txt & ```