SpringBoot+Vue 微信支付API V3
SpringBoot+Vue 微信支付
gitee仓库链接,前后端和辅助资料都上传了
wxpaydemo
1.微信支付产品介绍
微信支付主要包括付款码支付
,JSAPI支付
,小程序支付
,Native支付
,APP支付
,刷脸支付
等场景,本次主要学习Native
支付,适用于pc网站,常见的微信扫一扫支付方式.
2.前置工作
(1)获取微信商户号
微信商户平台:https://pay.weixin.qq.com/
场景:Native支付 步骤:提交资料 => 签署协议 => 获取商户号
(2)在微信开放平台获取APPID
微信公众平台:https://mp.weixin.qq.com/
步骤:注册服务号 => 服务号认证 => 获取APPID => 绑定商户号
(3)获取API密钥
APIv2版本的接口需要此秘钥 步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 设置API密钥
(4)获取APIV3密钥
APIv3版本的接口需要此秘钥
步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 设置APIv3密钥 随机密码生成工具:https://suijimimashengcheng.bmcx.com/
(5)申请商户API证书
APIv3版本的所有接口都需要
APIv2版本的高级接口需要(如:退款、企业红包、企业付款等)
步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 申请API证书
(6)获取微信怕平台证书
可以预先下载,也可以通过编程的方式获取。后面的课程中,我们会通过编程的方式来获取。
3.创建Springboot微信支付案例
(1)创建sprigboot项目
略
(2)添加依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
(3)创建application.yml配置文件
server:port: 8090
spring:application:name: payment-demo
(4)创建controller包
创建controller包,创建ProductController类
@Api(tags = "微信支付")
@RestController
@RequestMapping("/api/product")
@CrossOrigin //跨域
public class ProductController {@ApiOperation(value = "test")@GetMapping("/test")public String test() {return "hello";}
}
(5)引入swagger3依赖
<!--swagger3依赖--><dependency><groupId>io.springfox</groupId><artifactId>springfox-boot-starter</artifactId><version>3.0.0</version></dependency><!--美化ui--><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-ui</artifactId><version>3.0.3</version></dependency>
(6)swagger3配置类
@Configuration
@EnableOpenApi
//@EnableSwaggerBootstrapUI
public class SwaggerConfig {@Value("${swagger.enabled}")private boolean enable;/*** 创建API* http:IP:端口号/swagger-ui/index.html 原生地址* http:IP:端口号/doc.html bootStrap-UI地址*/@Beanpublic Docket createRestApi() {return new Docket(DocumentationType.OAS_30).pathMapping("/")// 用来创建该API的基本信息,展示在文档的页面中(自定义展示的信息)/*.enable(enable)*/.apiInfo(apiInfo())// 设置哪些接口暴露给Swagger展示.select()// 扫描所有有注解的api,用这种方式更灵活.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))// 扫描指定包中的swagger注解.apis(RequestHandlerSelectors.basePackage("com.acerola.paymentdemo.controller"))// 扫描所有 .apis(RequestHandlerSelectors.any()).paths(PathSelectors.regex("(?!/ApiError.*).*")).paths(PathSelectors.any()).build()// 支持的通讯协议集合.protocols(newHashSet("https", "http")).securitySchemes(securitySchemes()).securityContexts(securityContexts());}/*** 支持的通讯协议集合* @param type1* @param type2* @return*/private Set<String> newHashSet(String type1, String type2){Set<String> set = new HashSet<>();set.add(type1);set.add(type2);return set;}/*** 认证的安全上下文*/private List<SecurityScheme> securitySchemes() {List<SecurityScheme> securitySchemes = new ArrayList<>();securitySchemes.add((SecurityScheme) new ApiKey("token", "token", "header"));return securitySchemes;}/*** 授权信息全局应用*/private List<SecurityContext> securityContexts() {List<SecurityContext> securityContexts = new ArrayList<>();securityContexts.add(SecurityContext.builder().securityReferences(defaultAuth()).forPaths(PathSelectors.any()).build());return securityContexts;}private List<SecurityReference> defaultAuth() {AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];authorizationScopes[0] = authorizationScope;List<SecurityReference> securityReferences = new ArrayList<>();securityReferences.add(new SecurityReference("Authorization", authorizationScopes));return securityReferences;}/*** 添加摘要信息*/private ApiInfo apiInfo() {// 用ApiInfoBuilder进行定制return new ApiInfoBuilder()// 设置标题.title("微信支付")// 描述.description("微信支付")// 作者信息.contact(new Contact("doctorCloud", null, null))// 版本.version("版本号:V.1")//协议.license("The Apache License")//协议url.licenseUrl("http://www.baidu.com").build();}
}
(7)测试swagger文档
http://localhost:8090/doc.html#/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-noQdPbG7-1648467247291)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220326104137315.png)]
(8)引入lombok,简化实体开发
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency>
(9)创建R类,统一返回结果
@Data //生成set、get等方法
public class R {private Integer code;private String message;private Map<String, Object> data = new HashMap<>();public static R ok() {R r = new R();r.setCode(0);r.setMessage("成功");return r;}public static R error() {R r = new R();r.setCode(-1);r.setMessage("失败");return r;}public R data(String key, Object value) {this.data.put(key, value);return this;}
}
(10)配置json时间格式
spring:jackson: #json时间格式date-format: yyyy-MM-dd HH:mm:sstime-zone: GMT+8
(11)创建mysql数据库
创建payment_demo数据库,并执行以下sql
USE `payment_demo`;/*Table structure for table `t_order_info` */CREATE TABLE `t_order_info` (`id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '订单id',`title` varchar(256) DEFAULT NULL COMMENT '订单标题',`order_no` varchar(50) DEFAULT NULL COMMENT '商户订单编号',`user_id` bigint(20) DEFAULT NULL COMMENT '用户id',`product_id` bigint(20) DEFAULT NULL COMMENT '支付产品id',`total_fee` int(11) DEFAULT NULL COMMENT '订单金额(分)',`code_url` varchar(50) DEFAULT NULL COMMENT '订单二维码连接',`order_status` varchar(10) DEFAULT NULL COMMENT '订单状态',`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;/*Table structure for table `t_payment_info` */CREATE TABLE `t_payment_info` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '支付记录id',`order_no` varchar(50) DEFAULT NULL COMMENT '商户订单编号',`transaction_id` varchar(50) DEFAULT NULL COMMENT '支付系统交易编号',`payment_type` varchar(20) DEFAULT NULL COMMENT '支付类型',`trade_type` varchar(20) DEFAULT NULL COMMENT '交易类型',`trade_state` varchar(50) DEFAULT NULL COMMENT '交易状态',`payer_total` int(11) DEFAULT NULL COMMENT '支付金额(分)',`content` text COMMENT '通知参数',`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;/*Table structure for table `t_product` */CREATE TABLE `t_product` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品id',`title` varchar(20) DEFAULT NULL COMMENT '商品名称',`price` int(11) DEFAULT NULL COMMENT '价格(分)',`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;/*Data for the table `t_product` */insert into `t_product`(`title`,`price`) values ('Java课程',1);
insert into `t_product`(`title`,`price`) values ('大数据课程',1);
insert into `t_product`(`title`,`price`) values ('前端课程',1);
insert into `t_product`(`title`,`price`) values ('UI课程',1);/*Table structure for table `t_refund_info` */CREATE TABLE `t_refund_info` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '退款单id',`order_no` varchar(50) DEFAULT NULL COMMENT '商户订单编号',`refund_no` varchar(50) DEFAULT NULL COMMENT '商户退款单编号',`refund_id` varchar(50) DEFAULT NULL COMMENT '支付系统退款单号',`total_fee` int(11) DEFAULT NULL COMMENT '原订单金额(分)',`refund` int(11) DEFAULT NULL COMMENT '退款金额(分)',`reason` varchar(50) DEFAULT NULL COMMENT '退款原因',`refund_status` varchar(10) DEFAULT NULL COMMENT '退款状态',`content_return` text COMMENT '申请退款返回参数',`content_notify` text COMMENT '退款结果通知参数',`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
(12) 集成mybatis-plus
引入依赖
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--持久层-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
(13) 配置数据库连接
datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/payment_demo?serverTimezone=GMT%2B8&characterEncoding=utf-8username: rootpassword: 123456
(14)定义实体类
BaseEntity:
@Data
public class BaseEntity {//定义主键策略:跟随数据库的主键自增@TableId(value = "id", type = IdType.AUTO)private String id; //主键private Date createTime;//创建时间private Date updateTime;//更新时间
}
OrderInfo:
@Data
@TableName("t_order_info")
public class OrderInfo extends BaseEntity{private String title;//订单标题private String orderNo;//商户订单编号private Long userId;//用户idprivate Long productId;//支付产品idprivate Integer totalFee;//订单金额(分)private String codeUrl;//订单二维码连接private String orderStatus;//订单状态
}
PaymentInfo:
@Data
@TableName("t_payment_info")
public class PaymentInfo extends BaseEntity{private String orderNo;//商品订单编号private String transactionId;//支付系统交易编号private String paymentType;//支付类型private String tradeType;//交易类型private String tradeState;//交易状态private Integer payerTotal;//支付金额(分)private String content;//通知参数
}
Product:
@Data
@TableName("t_product")
public class Product extends BaseEntity{private String title; //商品名称private Integer price; //价格(分)
}
RefundInfo:
@Data
@TableName("t_refund_info")
public class RefundInfo extends BaseEntity{private String orderNo;//商品订单编号private String refundNo;//退款单编号private String refundId;//支付系统退款单号private Integer totalFee;//原订单金额(分)private Integer refund;//退款金额(分)private String reason;//退款原因private String refundStatus;//退款单状态private String contentReturn;//申请退款返回参数private String contentNotify;//退款结果通知参数
}
(15)定义持久层
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iISHg9Ga-1648467247295)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220326150017853.png)]
(16)定义mybatis-plus的yml文件配置
mybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImplmapper-locations:- classpath: src/main/resources/mapper/*.xml
(17) 定义业务层
定义业务层接口继承 IService<>
定义业务层接口的实现类,并继承 ServiceImpl<,>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O9NP08o5-1648467247297)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220326150316641.png)]
(18) 查询所有商品测试
@ApiOperation("商品列表")@GetMapping("/list")public R list() {List<Product> list = productService.list();return R.ok().data("productList", list);}
swagger测试:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b6b9wTzl-1648467247298)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220326151342914.png)]
4. 搭建前端环境
此处为vue基础,略过
5. 基础支付API V3
(1) 引入支付参数
1.1定义微信支付相关参数
将资料文件夹中的 wxpay.properties 复制到resources目录中 这个文件定义了之前我们准备的微信支付相关的参数,例如商户号、APPID、API秘钥等等
wxpay.properties:
# 微信支付相关参数
# 商户号
wxpay.mch-id=1558950191
# 商户API证书序列号
wxpay.mch-serial-no=34345964330B66427E0D3D28826C4993C77E631F
# 商户私钥文件
wxpay.private-key-path=apiclient_key.pem
# APIv3密钥
wxpay.api-v3-key=UDuLFDcmy5Eb6o0nTNZdu6ek4DDh4K8B
# APPID
wxpay.appid=wx74862e0dfcf69954
# 微信服务器地址
wxpay.domain=https://api.mch.weixin.qq.com
# 接收结果通知地址
wxpay.notify-domain=https://7d92-115-171-63-135.ngrok.io
1.2 读取支付参数
WxPayConfig.java:
@Configuration
@PropertySource("classpath:wxpay.properties") //读取配置文件
@ConfigurationProperties(prefix="wxpay") //读取wxpay节点
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
public class WxPayConfig {// 商户号private String mchId;// 商户API证书序列号private String mchSerialNo;// 商户私钥文件private String privateKeyPath;// APIv3密钥private String apiV3Key;// APPIDprivate String appid;// 微信服务器地址private String domain;// 接收结果通知地址private String notifyDomain;}
1.3 测试支付参数的获取
在controller包建立TestController类
@Api(tags = "测试控制器")
@RestController
@RequestMapping("/api/test")
public class TestController {@Autowiredprivate WxPayConfig wxPayConfig;@ApiOperation(value = "测试支付参数的获取")@GetMapping("/get-wx-pay-config")public R getWxPayConfig() {String mchId = wxPayConfig.getMchId();return R.ok().data("mchId", mchId);}
}
1.4 配置 Annotation Processor
可以帮助我们生成自定义配置的元数据信息,让配置文件和Java代码之间的对应参数可以自动定位,方便开发。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency>
1.5 设置wxpay.properties为springboot配置文件
让IDEA可以识别配置文件,将配置文件的图标展示成SpringBoot的图标,同时配置文件的内容可以高亮显示
File -> Project Structure -> Modules -> 选择小叶子
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q2UbX2qu-1648467247299)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220326163120508.png)]
(2) 加载商户私钥
2.1复制商户私钥
2.2 引入SDK
<dependency><groupId>com.github.wechatpay-apiv3</groupId><artifactId>wechatpay-apache-httpclient</artifactId><version>0.3.0</version></dependency>
2.3测试商户私钥的获取
在WxPayConfig.java中添加获取商户私钥文件的方法
/*** 获取商户私钥文件** @param filename* @return* @throws FileNotFoundException*/public PrivateKey getPrivateKey(String filename){try {return PemUtil.loadPrivateKey(new FileInputStream(filename));} catch (FileNotFoundException e) {throw new RuntimeException("私钥不存在",e);}}
在 PaymentDemoApplicationTests 测试类中添加如下方法,测试私钥对象是否能够获取出来。
@Testpublic void testGetPrivateKey() {//获取私钥路径String privateKeyPath = wxPayConfig.getPrivateKeyPath();//获取商户私钥PrivateKey privateKey = wxPayConfig.getPrivateKey(privateKeyPath);System.out.println(privateKey);}
(3)获取签名验证器和HttpClient
3.1 证书密钥使用说明
3.2 获取签名验证器
https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (定时更新平台证书功能) 平台证书:平台证书封装了微信的公钥,商户可以使用平台证书中的公钥进行验签。 签名验证器:帮助我们进行验签工作,我们单独将它定义出来,方便后面的开发。
@Beanpublic ScheduledUpdateCertificatesVerifier getVerifier() {//获取商户私钥PrivateKey privateKey = getPrivateKey(privateKeyPath);
//私钥签名对象(签名)PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo,privateKey);
//身份认证对象(验签)WechatPay2Credentials wechatPay2Credentials = newWechatPay2Credentials(mchId, privateKeySigner);
// 使用定时更新的签名验证器,不需要传入证书ScheduledUpdateCertificatesVerifier verifier = newScheduledUpdateCertificatesVerifier(wechatPay2Credentials,apiV3Key.getBytes(StandardCharsets.UTF_8));return verifier;}
3.3 获取 HttpClient 对象
https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (定时更新平台证书功能) HttpClient 对象:是建立远程连接的基础,我们通过SDK创建这个对象。
/*** 获取HttpClient对象** @param verifier* @return*/@Beanpublic CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifierverifier) {//获取商户私钥PrivateKey privateKey = getPrivateKey(privateKeyPath);//用于构造HttpClientWechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create().withMerchant(mchId, mchSerialNo, privateKey).withValidator(new WechatPay2Validator(verifier));// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新CloseableHttpClient httpClient = builder.build();return httpClient;}
(4)API字典和相关工具
4.1 API列表
模块名称 | 功能列表 | 描述 |
---|---|---|
Native支付 | Native下单 | 通过本接口提交微信支付Native支付订单 |
查询订单 | 通过此接口查询订单状态 | |
关闭订单 | 通过此接口关闭待支付订单 | |
Native调起支付 | 商户后台系统先调用微信支付的Native支付接口,微信后台系统返回链接参数code_url,商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付。 | |
支付结果通知 | 微信支付通过支付通知接口将用户支付成功消息通知给商户 | |
申请退款 | 商户可以通过该接口将支付金额退还给买家 | |
查询单笔退款 | 提交退款申请后,通过调用该接口查询退款状态 | |
退款结果通知 | 微信支付通过退款通知接口将用户退款成功消息通知给商户 | |
申请交易账单 | 商户可以通过该接口获取交易账单文件的下载地址 | |
申请资金账单 | 商户可以通过该接口获取资金账单文件的下载地址 | |
下载账单 | 通过申请交易/资金账单获取到download_url在该接口获取到对应的账单。 |
4.2 接口规则
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay2_0.shtml 微信支付 APIv3 使用 JSON 作为消息体的数据交换格式。
我们使用谷歌的json处理
<!--json处理-->
<dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId>
</dependency>
4.3定义枚举
为了开发方便,我们预先在项目中定义一些枚举。枚举中定义的内容包括接口地址,支付状态等信息。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ybh6aBlk-1648467247302)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220326174737976.png)]
4.4添加工具类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H0L2h6Zk-1648467247303)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220326174822867.png)]
(5)Native下单API
5.1Native下单流程
微信支付-开发者文档 (qq.com)
5.2 Native下单API
微信支付-开发者文档 (qq.com)
5.2.1 创建WxPayController
package com.acerola.paymentdemo.controller;import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/*** @program: payment-demo* @description:* @author: Acerola* @create: 2022-03-26 17:53**/
@CrossOrigin
@RestController
@RequestMapping("/api/wx-pay")
@Api(tags = "网站微信支付")
@Slf4j
public class WxPayController {}
5.2.2 创建service层
接口:
package com.acerola.paymentdemo.service;public interface WxPayService {}
实现:
package com.acerola.paymentdemo.service.impl;import com.acerola.paymentdemo.service.WxPayService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;/*** @program: payment-demo* @description:* @author: Acerola* @create: 2022-03-26 17:55**/
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {}
5.2.3 定义WxPayController方法
R对象中添加 @Accessors(chain = true),使其可以链式操作
/*** Native下单** @param productId* @return* @throws Exception*/@ApiOperation("调用统一下单API,生成支付二维码")@PostMapping("/native/{productId}")public R nativePay(@PathVariable Long productId) throws Exception {log.info("发起支付请求");//返回支付二维码连接和订单号Map<String, Object> map = wxPayService.nativePay(productId);return R.ok().setData(map);}
5.2.4 定义WxPayService方法
实现
@Autowiredprivate WxPayConfig wxPayConfig;@Autowiredprivate CloseableHttpClient wxPayClient;/*** 创建订单,调用Native支付接口** @param productId* @return code_url 和 订单号*/@SneakyThrows@Overridepublic Map<String, Object> nativePay(Long productId) {log.info("生成订单");//生成订单OrderInfo orderInfo = new OrderInfo();orderInfo.setTitle("test");orderInfo.setOrderNo(OrderNoUtils.getOrderNo()); //订单号orderInfo.setProductId(productId);orderInfo.setTotalFee(1); //分orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());//TODO:存入数据库log.info("调用统一下单API");//调用统一下单APIHttpPost httpPost = newHttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));// 请求body参数Gson gson = new Gson();Map paramsMap = new HashMap();paramsMap.put("appid", wxPayConfig.getAppid());paramsMap.put("mchid", wxPayConfig.getMchId());paramsMap.put("description", orderInfo.getTitle());paramsMap.put("out_trade_no", orderInfo.getOrderNo());paramsMap.put("notify_url",wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));Map amountMap = new HashMap();amountMap.put("total", orderInfo.getTotalFee());amountMap.put("currency", "CNY");paramsMap.put("amount", amountMap);//将参数转换成json字符串String jsonParams = gson.toJson(paramsMap);log.info("请求参数:" + jsonParams);StringEntity entity = new StringEntity(jsonParams, "utf-8");entity.setContentType("application/json");httpPost.setEntity(entity);httpPost.setHeader("Accept", "application/json");//完成签名并执行请求CloseableHttpResponse response = wxPayClient.execute(httpPost);try {String bodyAsString = EntityUtils.toString(response.getEntity());//响应体int statusCode = response.getStatusLine().getStatusCode();//响应状态码if (statusCode == 200) { //处理成功log.info("成功, 返回结果 = " + bodyAsString);} else if (statusCode == 204) { //处理成功,无返回Bodylog.info("成功");} else {log.info("Native下单失败,响应码 = " + statusCode + ",返回结果 = " +bodyAsString);throw new IOException("request failed");}//响应结果Map<String, String> resultMap = gson.fromJson(bodyAsString,HashMap.class);//二维码String codeUrl = resultMap.get("code_url");Map<String, Object> map = new HashMap<>();map.put("codeUrl", codeUrl);map.put("orderNo", orderInfo.getOrderNo());return map;} finally {response.close();}}
5.2.5 测试调用统一下单API,生成支付二维码
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BDPFpSkB-1648467247306)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220326181237729.png)]
5.3 创建课程订单
5.3.1 保存订单
OrderInfoService
接口:
OrderInfo createOrderByProductId(Long productId);
实现:
@Resourceprivate ProductMapper productMapper;@Overridepublic OrderInfo createOrderByProductId(Long productId) {//查找已存在但未支付的订单OrderInfo orderInfo = this.getNoPayOrderByProductId(productId);if( orderInfo != null){return orderInfo;}//获取商品信息Product product = productMapper.selectById(productId);//生成订单orderInfo = new OrderInfo();orderInfo.setTitle(product.getTitle());orderInfo.setOrderNo(OrderNoUtils.getOrderNo()); //订单号orderInfo.setProductId(productId);orderInfo.setTotalFee(product.getPrice()); //分orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());baseMapper.insert(orderInfo);return orderInfo;}
查找未支付订单:OrderInfoService中添加辅助方法
/*** 根据商品id查询未支付订单* 防止重复创建订单对象** @param productId* @return*/private OrderInfo getNoPayOrderByProductId(Long productId) {QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();queryWrapper.eq("product_id", productId);queryWrapper.eq("order_status", OrderStatus.NOTPAY.getType());// queryWrapper.eq("user_id", userId);OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);return orderInfo;}
5.3.2 缓存二维码
OrderInfoService
接口:
OrderInfo createOrderByProductId(Long productId);
实现:
/*** 存储订单二维码* @param orderNo* @param codeUrl*/@Overridepublic void saveCodeUrl(String orderNo, String codeUrl) {QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();queryWrapper.eq("order_no", orderNo);OrderInfo orderInfo = new OrderInfo();orderInfo.setCodeUrl(codeUrl);baseMapper.update(orderInfo, queryWrapper);}
5.3.3 修改WxPayServiceImpl 的 nativePay 方法
@Autowiredprivate OrderInfoService orderInfoService;/*** 创建订单,调用Native支付接口** @param productId* @return code_url 和 订单号*/@SneakyThrows@Overridepublic Map<String, Object> nativePay(Long productId) {log.info("生成订单");//生成订单OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId);String codeUrl = orderInfo.getCodeUrl();if(orderInfo != null && !StringUtils.isEmpty(codeUrl)){log.info("订单已存在,二维码已保存");//返回二维码Map<String, Object> map = new HashMap<>();map.put("codeUrl", codeUrl);map.put("orderNo", orderInfo.getOrderNo());return map;}log.info("调用统一下单API");//调用统一下单APIHttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));// 请求body参数Gson gson = new Gson();Map paramsMap = new HashMap();paramsMap.put("appid", wxPayConfig.getAppid());paramsMap.put("mchid", wxPayConfig.getMchId());paramsMap.put("description", orderInfo.getTitle());paramsMap.put("out_trade_no", orderInfo.getOrderNo());paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));Map amountMap = new HashMap();amountMap.put("total", orderInfo.getTotalFee());amountMap.put("currency", "CNY");paramsMap.put("amount", amountMap);//将参数转换成json字符串String jsonParams = gson.toJson(paramsMap);log.info("请求参数 ===> {}" + jsonParams);StringEntity entity = new StringEntity(jsonParams,"utf-8");entity.setContentType("application/json");httpPost.setEntity(entity);httpPost.setHeader("Accept", "application/json");//完成签名并执行请求CloseableHttpResponse response = wxPayClient.execute(httpPost);try {String bodyAsString = EntityUtils.toString(response.getEntity());//响应体int statusCode = response.getStatusLine().getStatusCode();//响应状态码if (statusCode == 200) { //处理成功log.info("成功, 返回结果 = " + bodyAsString);} else if (statusCode == 204) { //处理成功,无返回Bodylog.info("成功");} else {log.info("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString);throw new IOException("request failed");}//响应结果Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);//二维码codeUrl = resultMap.get("code_url");//保存二维码String orderNo = orderInfo.getOrderNo();orderInfoService.saveCodeUrl(orderNo, codeUrl);//返回二维码Map<String, Object> map = new HashMap<>();map.put("codeUrl", codeUrl);map.put("orderNo", orderInfo.getOrderNo());return map;} finally {response.close();}}
5.4 显示订单列表
在我的订单页面按时间倒序显示订单列表
5.4.1 创建OrderInfoController
package com.acerola.paymentdemo.controller;import com.acerola.paymentdemo.common.R;
import com.acerola.paymentdemo.entity.OrderInfo;
import com.acerola.paymentdemo.service.OrderInfoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.List;/*** @program: payment-demo* @description:* @author: Acerola* @create: 2022-03-27 17:57**/@CrossOrigin //开放前端的跨域访问
@Api(tags = "商品订单管理")
@RestController
@RequestMapping("/api/order-info")
public class OrderInfoController {@Autowiredprivate OrderInfoService orderInfoService;@ApiOperation("订单列表")@GetMapping("/list")public R list() {List<OrderInfo> list = orderInfoService.listOrderByCreateTimeDesc();return R.ok().data("list", list);}
}
5.4.2 定义 OrderInfoService 方法
接口:
List<OrderInfo> listOrderByCreateTimeDesc();
实现:
/*** 查询订单列表,并倒序查询* @return*/@Overridepublic List<OrderInfo> listOrderByCreateTimeDesc() {QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<OrderInfo>().orderByDesc("create_time");return baseMapper.selectList(queryWrapper);}
(6)支付通知API
6.1 内网穿透
6.1.1下载内网穿透工具
下载ngrok
6.1.2 设置authtoken
ngrok authtoken 26y23wgoy8AEsyn4CC9qfLbEvK0_2A4okRhSWNU6GprLQjyHZ
6.1.3 启动内网穿透
ngrok http 8090
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mHnBKosO-1648467247307)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220327201945354.png)]
6.1.4 测试外网访问
http://922c-117-158-127-32.ngrok.io/api/order-info/list
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3aEg2FgA-1648467247308)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220327232105872.png)]
6.2 接收通知和返回应答
支付通知API:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml
6.2.1启动ngrok
ngrok http 8090
6.2.2设置通知地址
wxpay.properties
注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
wxpay.notify-domain=https://7d92-115-171-63-135.ngrok.io
6.2.3创建通知接口
通知规则:用户支付完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理 该消息,并返回应答。对后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信认 为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保 证通知最终能成功。(通知频率为 15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m)
/*** 支付通知* 微信支付通过支付通知接口将用户支付成功消息通知给商户*/@ApiOperation("支付通知")@PostMapping("/native/notify")public String nativeNotify(HttpServletRequest request, HttpServletResponseresponse) {Gson gson = new Gson();Map<String, String> map = new HashMap<>();//应答对象//处理通知参数String body = HttpUtils.readData(request);Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);log.info("支付通知的id ===> {}", bodyMap.get("id"));log.info("支付通知的完整数据 ===> {}", body);//TODO : 签名的验证//TODO : 处理订单//成功应答:成功应答必须为200或204,否则就是失败应答response.setStatus(200);map.put("code", "SUCCESS");map.put("message", "成功");return gson.toJson(map);}
6.2.4测试失败应答
用失败应答替换成功应答
@PostMapping("/native/notify")public String nativeNotify(HttpServletRequest request, HttpServletResponseresponse) throws Exception {Gson gson = new Gson();Map<String, String> map = new HashMap<>();try {} catch (Exception e) {e.printStackTrace();// 测试错误应答response.setStatus(500);map.put("code", "ERROR");map.put("message", "系统错误");return gson.toJson(map);}}
6.3验签
6.3.1 工具类
参考SDK源码中的 WechatPay2Validator 创建通知验签工具类 WechatPay2ValidatorForRequest
6.3.2 验签
@Resource
private Verifier verifier;
更新WxPayController中nativeNotify方法
/*** 支付通知* 微信支付通过支付通知接口将用户支付成功消息通知给商户*/@ApiOperation("支付通知")@PostMapping("/native/notify")public String nativeNotify(HttpServletRequest request, HttpServletResponseresponse) throws IOException {Gson gson = new Gson();Map<String, String> map = new HashMap<>();//应答对象//处理通知参数String body = HttpUtils.readData(request);Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);String requestId = (String) bodyMap.get("id");log.info("支付通知的id ===> {}", bodyMap.get("id"));log.info("支付通知的完整数据 ===> {}", body);//TODO : 签名的验证//签名的验证WechatPay2ValidatorForRequest validator= new WechatPay2ValidatorForRequest(verifier, body, requestId);if (!validator.validate(request)) {log.error("通知验签失败");//失败应答response.setStatus(500);map.put("code", "ERROR");map.put("message", "通知验签失败");return gson.toJson(map);}log.info("通知验签成功");//TODO : 处理订单//成功应答:成功应答必须为200或204,否则就是失败应答response.setStatus(200);map.put("code", "SUCCESS");map.put("message", "成功");return gson.toJson(map);}
6.4解密
6.4.1 WxPayController
nativeNotify 方法中添加处理订单的代码
//处理订单
wxPayService.processOrder(bodyMap);
6.4.2 WxPayService
接口:
void processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException;
实现:
/*** 对称解密** @param bodyMap* @return*/private String decryptFromResource(Map<String, Object> bodyMap) throwsGeneralSecurityException {log.info("密文解密");//通知数据Map<String, String> resourceMap = (Map) bodyMap.get("resource");//数据密文String ciphertext = resourceMap.get("ciphertext");//随机串String nonce = resourceMap.get("nonce");//附加数据String associatedData = resourceMap.get("associated_data");log.info("密文 ===> {}", ciphertext);AesUtil aesUtil = newAesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));String plainText =aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),nonce.getBytes(StandardCharsets.UTF_8),ciphertext);log.info("明文 ===> {}", plainText);return plainText;}
6.5处理订单
6.5.1 完善processOrder方法
@Autowiredprivate PaymentInfoService paymentInfoService;@Overridepublic void processOrder(Map<String, Object> bodyMap) throwsGeneralSecurityException {log.info("处理订单");String plainText = decryptFromResource(bodyMap);//转换明文Gson gson = new Gson();Map<String, Object> plainTextMap = gson.fromJson(plainText, HashMap.class);String orderNo = (String)plainTextMap.get("out_trade_no");//更新订单状态orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);//记录支付日志paymentInfoService.createPaymentInfo(plainText);}
6.5.2更新订单状态
OrderInfoService
接口:
void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus);
实现:
/*** 根据订单编号更新订单状态** @param orderNo* @param orderStatus*/@Overridepublic void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus) {log.info("更新订单状态 ===> {}", orderStatus.getType());QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();queryWrapper.eq("order_no", orderNo);OrderInfo orderInfo = new OrderInfo();orderInfo.setOrderStatus(orderStatus.getType());baseMapper.update(orderInfo, queryWrapper);}
6.5.3处理支付日志
PaymentInfoService
接口:
void createPaymentInfo(String plainText);
实现:
/*** 记录支付日志** @param plainText*/@Overridepublic void createPaymentInfo(String plainText) {log.info("记录支付日志");Gson gson = new Gson();Map<String, Object> plainTextMap = gson.fromJson(plainText, HashMap.class);String orderNo = (String) plainTextMap.get("out_trade_no");String transactionId = (String) plainTextMap.get("transaction_id");String tradeType = (String) plainTextMap.get("trade_type");String tradeState = (String) plainTextMap.get("trade_state");Map<String, Object> amount = (Map) plainTextMap.get("amount");Integer payerTotal = ((Double) amount.get("payer_total")).intValue();PaymentInfo paymentInfo = new PaymentInfo();paymentInfo.setOrderNo(orderNo);paymentInfo.setPaymentType(PayType.WXPAY.getType());paymentInfo.setTransactionId(transactionId);paymentInfo.setTradeType(tradeType);paymentInfo.setTradeState(tradeState);paymentInfo.setPayerTotal(payerTotal);paymentInfo.setContent(plainText);baseMapper.insert(paymentInfo);}
6.6处理重复通知
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JIRw5qzC-1648467247310)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220328104343532.png)]
在 processOrder 方法中,更新订单状态之前,添加如下代码
OrderInfoService
接口:
String getOrderStatus(String orderNo);
实现:
/*** 根据订单号获取订单状态** @param orderNo* @return*/@Overridepublic String getOrderStatus(String orderNo) {QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();queryWrapper.eq("order_no", orderNo);OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);//防止被删除的订单的回调通知的调用if (orderInfo == null) {return null;}return orderInfo.getOrderStatus();}
在 processOrder 方法中,更新订单状态之前,添加如下代码
//处理重复通知//保证接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的String orderStatus = orderInfoService.getOrderStatus(orderNo);if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {return;}
6.7数据锁
6.7.1定义ReentrantLock
定义 ReentrantLock 进行并发控制。注意,必须手动释放锁。
private final ReentrantLock lock = new ReentrantLock();
@Overridepublic void processOrder(Map<String, Object> bodyMap) throwsGeneralSecurityException {log.info("处理订单");//解密报文String plainText = decryptFromResource(bodyMap);//将明文转换成mapGson gson = new Gson();HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);String orderNo = (String) plainTextMap.get("out_trade_no");/*在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱*///尝试获取锁:// 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放if (lock.tryLock()) {try {//处理重复的通知//接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。String orderStatus = orderInfoService.getOrderStatus(orderNo);if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {return;}//模拟通知并发try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}//更新订单状态orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.SUCCESS);//记录支付日志paymentInfoService.createPaymentInfo(plainText);} finally {//要主动释放锁lock.unlock();}}}
(7)商户定时查询本地订单
7.1 后端定义商户查单接口
支付成功后,商户侧查询本地数据库,订单是否支付成功
/*** 查询本地订单状态*/@ApiOperation("查询本地订单状态")@GetMapping("/query-order-status/{orderNo}")public R queryOrderStatus(@PathVariable String orderNo) {String orderStatus = orderInfoService.getOrderStatus(orderNo);if (OrderStatus.SUCCESS.getType().equals(orderStatus)) {//支付成功return R.ok();}return R.ok().setCode(101).setMessage("支付中...");}
7.2 前端定时轮询查单
在二维码展示页面,前端定时轮询查询订单是否已支付,如果支付成功则跳转到订单页面
7.2.1定义定时器
//启动定时器
this.timer = setInterval(() => {
//查询订单是否支付成功
this.queryOrderStatus()
}, 3000)
7.2.2 查询订单
// 查询订单状态
queryOrderStatus() {
orderInfoApi.queryOrderStatus(this.orderNo).then(response => {
console.log('查询订单状态:' + response.code)
// 支付成功后的页面跳转
if (response.code === 0) {
console.log('清除定时器')
clearInterval(this.timer)
// 三秒后跳转到订单列表
setTimeout(() => {
this.$router.push({ path: '/success' })
}, 3000)
}
})
}
(8)用户取消订单API
实现用户主动取消订单的功能
8.1定义取消订单接口
WxPayController中添加接口方法
/*** 用户取消订单** @param orderNo* @return* @throws Exception*/@ApiOperation("用户取消订单")@PostMapping("/cancel/{orderNo}")public R cancel(@PathVariable String orderNo) throws Exception {log.info("取消订单");wxPayService.cancelOrder(orderNo);return R.ok().setMessage("订单已取消");}
8.2WxPayService
接口:
void cancelOrder(String orderNo) throws Exception;
实现:
/*** 用户取消订单** @param orderNo*/@Overridepublic void cancelOrder(String orderNo) throws Exception {//调用微信支付的关单接口this.closeOrder(orderNo);//更新商户端的订单状态orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL);}
关单方法
/*** 关单接口的调用** @param orderNo*/private void closeOrder(String orderNo) throws Exception {log.info("关单接口的调用,订单号 ===> {}", orderNo);//创建远程请求对象String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(), orderNo);url = wxPayConfig.getDomain().concat(url);HttpPost httpPost = new HttpPost(url);//组装json请求体Gson gson = new Gson();Map<String, String> paramsMap = new HashMap<>();paramsMap.put("mchid", wxPayConfig.getMchId());String jsonParams = gson.toJson(paramsMap);log.info("请求参数 ===> {}", jsonParams);//将请求参数设置到请求对象中StringEntity entity = new StringEntity(jsonParams, "utf-8");entity.setContentType("application/json");httpPost.setEntity(entity);httpPost.setHeader("Accept", "application/json");//完成签名并执行请求CloseableHttpResponse response = wxPayClient.execute(httpPost);try {int statusCode = response.getStatusLine().getStatusCode();//响应状态码if (statusCode == 200) { //处理成功log.info("成功200");} else if (statusCode == 204) { //处理成功,无返回Bodylog.info("成功204");} else {log.info("Native下单失败,响应码 = " + statusCode);throw new IOException("request failed");}} finally {response.close();}}
(9)微信支付查单API
9.1 查单接口的调用
商户后台未收到异步支付结果通知时,商户应该主动调用《微信支付查单接口》,同步订单状态
9.1.1 WxPayController
/*** 查询订单** @param orderNo* @return* @throws URISyntaxException* @throws IOException*/@ApiOperation("查询订单:测试订单状态用")@GetMapping("query/{orderNo}")public R queryOrder(@PathVariable String orderNo) throws Exception {log.info("查询订单");String bodyAsString = wxPayService.queryOrder(orderNo);return R.ok().setMessage("查询成功").data("bodyAsString", bodyAsString);}
9.1.2 WxPayService
接口:
String queryOrder(String orderNo) throws Exception;
实现:
/*** 查单接口调用*/@SneakyThrows@Overridepublic String queryOrder(String orderNo) {log.info("查单接口调用 ===> {}", orderNo);String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo);url = wxPayConfig.getDomain().concat(url).concat("? mchid=").concat(wxPayConfig.getMchId());HttpGet httpGet = new HttpGet(url);httpGet.setHeader("Accept", "application/json");//完成签名并执行请求CloseableHttpResponse response = wxPayClient.execute(httpGet);try {String bodyAsString = EntityUtils.toString(response.getEntity());//响应体int statusCode = response.getStatusLine().getStatusCode();//响应状态码if (statusCode == 200) { //处理成功log.info("成功, 返回结果 = " + bodyAsString);} else if (statusCode == 204) { //处理成功,无返回Bodylog.info("成功");} else {log.info("Native下单失败,响应码 = " + statusCode + ",返回结果 = " +bodyAsString);throw new IOException("request failed");}return bodyAsString;} finally {response.close();}}
9.2 集成Spring Task
Spring 3.0后提供Spring Task实现任务调度
9.2.1 启动类添加注解
@EnableScheduling
9.2.2 测试定时任务
创建 task 包,创建 WxPayTask.java
package com.acerola.paymentdemo.task;import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;/*** @program: payment-demo* @description:* @author: Acerola* @create: 2022-03-28 17:12**/@Slf4j
@Component
public class WxPayTask {/*** 测试* (cron="秒 分 时 日 月 周")* *:每隔一秒执行* 0/3:从第0秒开始,每隔3秒执行一次* 1-3: 从第1秒开始执行,到第3秒结束执行* 1,2,3:第1、2、3秒执行* ?:不指定,若指定日期,则不指定周,反之同理*/@Scheduled(cron = "0/3 * * * * ?")public void task1() {log.info("task1 执行");}
}
9.3定时查找超时订单
9.3.1WxPayTask
@Autowiredprivate OrderInfoService orderInfoService;@Autowiredprivate WxPayService wxPayService;/*** 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单*/@Scheduled(cron = "0/30 * * * * ?")public void orderConfirm() throws Exception {log.info("orderConfirm 被执行......");List<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDuration(5);for (OrderInfo orderInfo : orderInfoList) {String orderNo = orderInfo.getOrderNo();log.warn("超时订单 ===> {}", orderNo);//核实订单状态:调用微信支付查单接口wxPayService.checkOrderStatus(orderNo);}}
9.3.2 OrderInfoService
接口:
List<OrderInfo> getNoPayOrderByDuration(int i);
实现:
/*** 找出创建超过minutes分钟并且未支付的订单** @param minutes* @return*/@Overridepublic List<OrderInfo> getNoPayOrderByDuration(int minutes) {//minutes分钟之前的时间Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();queryWrapper.eq("order_status", OrderStatus.NOTPAY.getType());queryWrapper.le("create_time", instant);List<OrderInfo> orderInfoList = baseMapper.selectList(queryWrapper);return orderInfoList;}
9.4 处理超时订单
WxPayService
接口:
void checkOrderStatus(String orderNo);
实现:
/*** 根据订单号查询微信支付查单接口,核实订单状态* 如果订单已支付,则更新商户端订单状态,并记录支付日志* 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态** @param orderNo*/@SneakyThrows@Overridepublic void checkOrderStatus(String orderNo) {log.warn("根据订单号核实订单状态 ===> {}", orderNo);//调用微信支付查单接口String result = this.queryOrder(orderNo);Gson gson = new Gson();Map resultMap = gson.fromJson(result, HashMap.class);//获取微信支付端的订单状态Object tradeState = resultMap.get("trade_state");//判断订单状态if (WxTradeState.SUCCESS.getType().equals(tradeState)) {log.warn("核实订单已支付 ===> {}", orderNo);//如果确认订单已支付则更新本地订单状态orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);//记录支付日志paymentInfoService.createPaymentInfo(result);}if (WxTradeState.NOTPAY.getType().equals(tradeState)) {log.warn("核实订单未支付 ===> {}", orderNo);//如果订单未支付,则调用关单接口this.closeOrder(orderNo);//更新本地订单状态orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);}}
9.4.1 根据订单号获取订单
接口:
OrderInfo getOrderByOrderNo(String orderNo);
实现:
/*** 根据订单号获取订单* @param orderNo* @return*/@Overridepublic OrderInfo getOrderByOrderNo(String orderNo) {QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();queryWrapper.eq("order_no", orderNo);OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);return orderInfo;}
(10) 申请退款API
文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_9.shtml
10.1 创建退款单
RefundsInfoService
接口:
RefundInfo createRefundByOrderNo(String orderNo, String reason);
实现:
@Autowiredprivate OrderInfoService orderInfoService;/*** 根据订单号创建退款订单** @param orderNo* @return*/@Overridepublic RefundInfo createRefundByOrderNo(String orderNo, String reason) {//根据订单号获取订单信息OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);//根据订单号生成退款订单RefundInfo refundInfo = new RefundInfo();refundInfo.setOrderNo(orderNo);//订单编号refundInfo.setRefundNo(OrderNoUtils.getRefundNo());//退款单编号refundInfo.setTotalFee(orderInfo.getTotalFee());//原订单金额(分)refundInfo.setRefund(orderInfo.getTotalFee());//退款金额(分)refundInfo.setReason(reason);//退款原因//保存退款订单baseMapper.insert(refundInfo);return refundInfo;}
10.2 更新退款单
RefundInfoService
接口:
void updateRefund(String content);
实现:
/*** 记录退款记录** @param content*/@Overridepublic void updateRefund(String content) {//将json字符串转换成MapGson gson = new Gson();Map<String, String> resultMap = gson.fromJson(content, HashMap.class);//根据退款单编号修改退款单QueryWrapper<RefundInfo> queryWrapper = new QueryWrapper<>();queryWrapper.eq("refund_no", resultMap.get("out_refund_no"));//设置要修改的字段RefundInfo refundInfo = new RefundInfo();refundInfo.setRefundId(resultMap.get("refund_id"));//微信支付退款单号//查询退款和申请退款中的返回参数if (resultMap.get("status") != null) {refundInfo.setRefundStatus(resultMap.get("status"));//退款状态refundInfo.setContentReturn(content);//将全部响应结果存入数据库的content字段}//退款回调中的回调参数if (resultMap.get("refund_status") != null) {refundInfo.setRefundStatus(resultMap.get("refund_status"));//退款状态refundInfo.setContentNotify(content);//将全部响应结果存入数据库的content字段}//更新退款单baseMapper.update(refundInfo, queryWrapper);}
10.3 申请退款
10.3.1 WxPayController
@ApiOperation("申请退款")@PostMapping("/refunds/{orderNo}/{reason}")public R refunds(@PathVariable String orderNo, @PathVariable String reason)throws Exception {log.info("申请退款");wxPayService.refund(orderNo, reason);return R.ok();}
10.3.2 WxPayService
接口:
void refund(String orderNo, String reason);
实现:
@Autowiredprivate RefundInfoService refundsInfoService;
/*** 退款** @param orderNo* @param reason* @throws IOException*/@SneakyThrows@Transactional(rollbackFor = Exception.class)@Overridepublic void refund(String orderNo, String reason) {log.info("创建退款单记录");//根据订单编号创建退款单RefundInfo refundsInfo = refundsInfoService.createRefundByOrderNo(orderNo,reason);log.info("调用退款API");//调用统一下单APIString url =wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());HttpPost httpPost = new HttpPost(url);// 请求body参数Gson gson = new Gson();Map paramsMap = new HashMap();paramsMap.put("out_trade_no", orderNo);//订单编号paramsMap.put("out_refund_no", refundsInfo.getRefundNo());//退款单编号paramsMap.put("reason", reason);//退款原因paramsMap.put("notify_url",wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));//退款通知地址Map amountMap = new HashMap();amountMap.put("refund", refundsInfo.getRefund());//退款金额amountMap.put("total", refundsInfo.getTotalFee());//原订单金额amountMap.put("currency", "CNY");//退款币种paramsMap.put("amount", amountMap);//将参数转换成json字符串String jsonParams = gson.toJson(paramsMap);log.info("请求参数 ===> {}" + jsonParams);StringEntity entity = new StringEntity(jsonParams, "utf-8");entity.setContentType("application/json");//设置请求报文格式httpPost.setEntity(entity);//将请求报文放入请求对象httpPost.setHeader("Accept", "application/json");//设置响应报文格式//完成签名并执行请求,并完成验签CloseableHttpResponse response = wxPayClient.execute(httpPost);try {//解析响应结果String bodyAsString = EntityUtils.toString(response.getEntity());int statusCode = response.getStatusLine().getStatusCode();if (statusCode == 200) {log.info("成功, 退款返回结果 = " + bodyAsString);} else if (statusCode == 204) {log.info("成功");} else {throw new RuntimeException("退款异常, 响应码 = " + statusCode + ", 退款返回结果 = " + bodyAsString);}//更新订单状态orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.REFUND_PROCESSING);//更新退款单refundsInfoService.updateRefund(bodyAsString);} finally {response.close();}}
(11)查询退款API
文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_10.shtml
11.1 查单接口的调用
11.1.1 WxPayController
/*** 查询退款** @param refundNo* @return* @throws Exception*/@ApiOperation("查询退款:测试用")@GetMapping("/query-refund/{refundNo}")public R queryRefund(@PathVariable String refundNo) throws Exception {log.info("查询退款");String result = wxPayService.queryRefund(refundNo);return R.ok().setMessage("查询成功").data("result", result);}
11.1.2 WxPayService
接口:
String queryRefund(String refundNo);
实现:
@SneakyThrows@Overridepublic String queryRefund(String refundNo) {log.info("查询退款接口调用 ===> {}", refundNo);String url = String.format(WxApiType.DOMESTIC_REFUNDS_QUERY.getType(),refundNo);url = wxPayConfig.getDomain().concat(url);//创建远程Get 请求对象HttpGet httpGet = new HttpGet(url);httpGet.setHeader("Accept", "application/json");//完成签名并执行请求CloseableHttpResponse response = wxPayClient.execute(httpGet);try {String bodyAsString = EntityUtils.toString(response.getEntity());int statusCode = response.getStatusLine().getStatusCode();if (statusCode == 200) {log.info("成功, 查询退款返回结果 = " + bodyAsString);} else if (statusCode == 204) {log.info("成功");} else {throw new RuntimeException("查询退款异常, 响应码 = " + statusCode + ", 查询退款返回结果 = " + bodyAsString);}return bodyAsString;} finally {response.close();}}
11.2 定时查找退款中的订单
11.2.1 WxPayTask
/*** 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未成功的退款单*/@Scheduled(cron = "0/30 * * * * ?")public void refundConfirm() throws Exception {log.info("refundConfirm 被执行......");//找出申请退款超过5分钟并且未成功的退款单List<RefundInfo> refundInfoList =refundInfoService.getNoRefundOrderByDuration(5);for (RefundInfo refundInfo : refundInfoList) {String refundNo = refundInfo.getRefundNo();log.warn("超时未退款的退款单号 ===> {}", refundNo);//核实订单状态:调用微信支付查询退款接口wxPayService.checkRefundStatus(refundNo);}}
11.2.2 RefundInfoService
接口:
List<RefundInfo> getNoRefundOrderByDuration(int minutes);
实现:
/*** 找出申请退款超过minutes分钟并且未成功的退款单** @param minutes* @return*/@Overridepublic List<RefundInfo> getNoRefundOrderByDuration(int minutes) {//minutes分钟之前的时间Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));QueryWrapper<RefundInfo> queryWrapper = new QueryWrapper<>();queryWrapper.eq("refund_status", WxRefundStatus.PROCESSING.getType());queryWrapper.le("create_time", instant);List<RefundInfo> refundInfoList = baseMapper.selectList(queryWrapper);return refundInfoList;}
11.3 处理超时未退款订单
WxPayService
核实订单状态
接口:
void checkRefundStatus(String refundNo);
实现:
/*** 根据退款单号核实退款单状态** @param refundNo* @return*/@Transactional(rollbackFor = Exception.class)@Overridepublic void checkRefundStatus(String refundNo) {log.warn("根据退款单号核实退款单状态 ===> {}", refundNo);//调用查询退款单接口String result = this.queryRefund(refundNo);//组装json请求体字符串Gson gson = new Gson();Map<String, String> resultMap = gson.fromJson(result, HashMap.class);//获取微信支付端退款状态String status = resultMap.get("status");String orderNo = resultMap.get("out_trade_no");if (WxRefundStatus.SUCCESS.getType().equals(status)) {log.warn("核实订单已退款成功 ===> {}", refundNo);//如果确认退款成功,则更新订单状态orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.REFUND_SUCCESS);//更新退款单refundsInfoService.updateRefund(result);}if (WxRefundStatus.ABNORMAL.getType().equals(status)) {log.warn("核实订单退款异常 ===> {}", refundNo);//如果确认退款成功,则更新订单状态orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.REFUND_ABNORMAL);//更新退款单refundsInfoService.updateRefund(result);}}
(12) 退款结果通知API
文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_11.shtml
12.1 接收退款通知
WxPayController
/*** 退款结果通知* 退款状态改变后,微信会把相关退款结果发送给商户。*/@PostMapping("/refunds/notify")public String refundsNotify(HttpServletRequest request, HttpServletResponseresponse) {log.info("退款通知执行");Gson gson = new Gson();Map<String, String> map = new HashMap<>();//应答对象try {//处理通知参数String body = HttpUtils.readData(request);Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);String requestId = (String) bodyMap.get("id");log.info("支付通知的id ===> {}", requestId);//签名的验证WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest= new WechatPay2ValidatorForRequest(verifier, requestId, body);if (!wechatPay2ValidatorForRequest.validate(request)) {log.error("通知验签失败");//失败应答response.setStatus(500);map.put("code", "ERROR");map.put("message", "通知验签失败");return gson.toJson(map);}log.info("通知验签成功");//处理退款单wxPayService.processRefund(bodyMap);//成功应答response.setStatus(200);map.put("code", "SUCCESS");map.put("message", "成功");return gson.toJson(map);} catch (Exception e) {e.printStackTrace();//失败应答response.setStatus(500);map.put("code", "ERROR");map.put("message", "失败");return gson.toJson(map);}}
12.2 处理订单和退款单
WxPayService
接口:
void processRefund(Map<String, Object> bodyMap);
实现:
/*** 处理退款单*/@SneakyThrows@Transactional(rollbackFor = Exception.class)@Overridepublic void processRefund(Map<String, Object> bodyMap) {log.info("退款单");//解密报文String plainText = decryptFromResource(bodyMap);//将明文转换成mapGson gson = new Gson();HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);String orderNo = (String) plainTextMap.get("out_trade_no");if (lock.tryLock()) {try {String orderStatus = orderInfoService.getOrderStatus(orderNo);if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)) {return;}//更新订单状态orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.REFUND_SUCCESS);//更新退款单refundsInfoService.updateRefund(plainText);} finally {//要主动释放锁lock.unlock();}}}
(13) 账单
13.1 申请交易账单和资金账单
13.1.1 WxPayController
@ApiOperation("获取账单url:测试用")@GetMapping("/querybill/{billDate}/{type}")public R queryTradeBill(@PathVariable String billDate,@PathVariable String type) throws Exception {log.info("获取账单url");String downloadUrl = wxPayService.queryBill(billDate, type);return R.ok().setMessage("获取账单url成功").data("downloadUrl", downloadUrl);}
13.1.2 WxPayService
接口:
String queryBill(String billDate, String type);
实现:
/*** 申请账单** @param billDate* @param type* @return* @throws Exception*/@SneakyThrows@Overridepublic String queryBill(String billDate, String type) {log.warn("申请账单接口调用 {}", billDate);String url = "";if ("tradebill".equals(type)) {url = WxApiType.TRADE_BILLS.getType();} else if ("fundflowbill".equals(type)) {url = WxApiType.FUND_FLOW_BILLS.getType();} else {throw new RuntimeException("不支持的账单类型");}url = wxPayConfig.getDomain().concat(url).concat("?bill_date=").concat(billDate);//创建远程Get 请求对象HttpGet httpGet = new HttpGet(url);httpGet.addHeader("Accept", "application/json");//使用wxPayClient发送请求得到响应CloseableHttpResponse response = wxPayClient.execute(httpGet);try {String bodyAsString = EntityUtils.toString(response.getEntity());int statusCode = response.getStatusLine().getStatusCode();if (statusCode == 200) {log.info("成功, 申请账单返回结果 = " + bodyAsString);} else if (statusCode == 204) {log.info("成功");} else {throw new RuntimeException("申请账单异常, 响应码 = " + statusCode + ", 申请账单返回结果 = " + bodyAsString);}//获取账单下载地址Gson gson = new Gson();Map<String, String> resultMap = gson.fromJson(bodyAsString,HashMap.class);return resultMap.get("download_url");} finally {response.close();}}
13.2 下载账单
13.2.1 WxPayController
@ApiOperation("下载账单")@GetMapping("/downloadbill/{billDate}/{type}")public R downloadBill(@PathVariable String billDate,@PathVariable String type) throws Exception {log.info("下载账单");String result = wxPayService.downloadBill(billDate, type);return R.ok().data("result", result);}
13.2.2 WxPayService
接口:
String downloadBill(String billDate, String type);
实现:
@Autowiredprivate CloseableHttpClient wxPayNoSignClient;/*** 下载账单** @param billDate* @param type* @return* @throws Exception*/@SneakyThrows@Overridepublic String downloadBill(String billDate, String type) {log.warn("下载账单接口调用 {}, {}", billDate, type);//获取账单url地址String downloadUrl = this.queryBill(billDate, type);//创建远程Get 请求对象HttpGet httpGet = new HttpGet(downloadUrl);httpGet.addHeader("Accept", "application/json");//使用wxPayClient发送请求得到响应CloseableHttpResponse response = wxPayNoSignClient.execute(httpGet);try {String bodyAsString = EntityUtils.toString(response.getEntity());int statusCode = response.getStatusLine().getStatusCode();if (statusCode == 200) {log.info("成功, 下载账单返回结果 = " + bodyAsString);} else if (statusCode == 204) {log.info("成功");} else {throw new RuntimeException("下载账单异常, 响应码 = " + statusCode + ", 下载账单返回结果 = " + bodyAsString);}return bodyAsString;} finally {response.close();}}
(httpGet);
try {
String bodyAsString = EntityUtils.toString(response.getEntity());
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
log.info("成功, 申请账单返回结果 = " + bodyAsString);
} else if (statusCode == 204) {
log.info(“成功”);
} else {
throw new RuntimeException("申请账单异常, 响应码 = " + statusCode + ", 申请账单返回结果 = " + bodyAsString);
}
//获取账单下载地址
Gson gson = new Gson();
Map<String, String> resultMap = gson.fromJson(bodyAsString,
HashMap.class);
return resultMap.get(“download_url”);
} finally {
response.close();
}
}
#### 13.2 下载账单##### 13.2.1 WxPayController```java@ApiOperation("下载账单")@GetMapping("/downloadbill/{billDate}/{type}")public R downloadBill(@PathVariable String billDate,@PathVariable String type) throws Exception {log.info("下载账单");String result = wxPayService.downloadBill(billDate, type);return R.ok().data("result", result);}
13.2.2 WxPayService
接口:
String downloadBill(String billDate, String type);
实现:
@Autowiredprivate CloseableHttpClient wxPayNoSignClient;/*** 下载账单** @param billDate* @param type* @return* @throws Exception*/@SneakyThrows@Overridepublic String downloadBill(String billDate, String type) {log.warn("下载账单接口调用 {}, {}", billDate, type);//获取账单url地址String downloadUrl = this.queryBill(billDate, type);//创建远程Get 请求对象HttpGet httpGet = new HttpGet(downloadUrl);httpGet.addHeader("Accept", "application/json");//使用wxPayClient发送请求得到响应CloseableHttpResponse response = wxPayNoSignClient.execute(httpGet);try {String bodyAsString = EntityUtils.toString(response.getEntity());int statusCode = response.getStatusLine().getStatusCode();if (statusCode == 200) {log.info("成功, 下载账单返回结果 = " + bodyAsString);} else if (statusCode == 204) {log.info("成功");} else {throw new RuntimeException("下载账单异常, 响应码 = " + statusCode + ", 下载账单返回结果 = " + bodyAsString);}return bodyAsString;} finally {response.close();}}
SpringBoot+Vue 微信支付API V3相关推荐
- 微信支付API v3接口使用应用篇
目录 前言 版本 应用 基础配置 1.申请商户API证书 2.设置接口密钥 3.下载平台证书 接口实测 微信支付API官方客户端 1.客户端 2.支付调起参数签名 3.回调通知 参考资料 前言 最近新 ...
- 微信支付API V3版本JAVA开发指南
微信支付版本V3的Demo,在官方上下载下来,压根就是不能直接用的东西,你要想学会用,你就得一层一层的看源码,看文档,要求你事无巨细的做一个接入者. 如果接入API需要让人看源码来理解,我觉得是一件让 ...
- 微信支付API v3签名与验签-APP支付问题
目录 使用API v3微信支付遇到的问题: 1.微信请求客户端配置 2.生成预付款订单 3.拼接字符串使用API v3签名 4.微信支付成功后通知 使用API v3微信支付遇到的问题: 1.jdk版本 ...
- 微信支付API v3 Native支付
废话不多说直接上代码 不熟悉的直接私信我 依赖 <dependency><groupId>com.github.wechatpay-apiv3</groupId> ...
- 微信支付 API V3 JSAPI支付 JAVA下载账单
下载账单 写这个主要是太气人了,开发文档未写具体的代码示例.网上各种搜索了一天都是V2接口的示例V3的标题党,感觉被欺骗了,太气人了(V2接口有个参数APPID,具体业务使用了多个APPID所以不合适 ...
- SpringBoot对接微信支付之JSAPI
分享SpringBoot整合微信公众号支付项目,对接微信JSAPI支付类型遇到的问题和过程封装的工具类,目前已正常使用,有问题大家评论区互动哈,有需要源码的可以私信我. 1.创建SpringBoot项 ...
- SpringBoot集成微信支付V3
河南循中网络科技有限公司 - 精心创作,详细分解,按照步骤,均可成功! 文章目录 吐槽! 此文章包含实现了哪些接口? 学习资料 集成微信支付V3 什么是"商户证书"?什么是&quo ...
- SpringBoot整合微信支付开发在线教育视频网站(完整版)
目录 ├─code.zip ├─第 1 章项目介绍和前期准备 │ ├─1-1 SpringBoot整合微信支付开发在线教育视频站点介绍.TS │ ├─1-2 中大型公司里面项目开发流程讲解.TS ...
- 前端 VUE 微信支付 JSAPI
在威信公众号之中的产品H5页面,在购买时需要直接唤起微信支付,完成投保.核保流程.今天分享自己在唤起微信支付中的一些体会,希望可以帮助到大家. 先给大家将官方的说明文档附上,感兴趣的可以直接看看 ...
- 【毕业设计】基于springboot + vue微信小程序商城
目录 前言 创新点/亮点✨ 毕设目录 一.视频展示 二.系统介绍 三.项目地址 四.运行环境 五.设计模块 ①前台 ②后台 六.系统功能模块结构图 七. 准备阶段 ①使用真实支付 ②使用模拟支付 八. ...
最新文章
- 万字长文带你看尽深度学习中的各种卷积网络
- 【 FPGA 】FIR滤波器的采样速率与系统时钟速率不同时的资源消耗分析
- ACM网络赛金华赛区的一道关于树的题:Family Name List
- Gimmie — 一个创新的 GNOME 面板按次
- 讲解Linux服务器被黑解决方法
- 元宇宙iwemeta: 2021年云计算行业发展研究报告
- java的圆周率_java学习日记,圆周率的打印
- OTT交付如何超越传统广电交付,为用户带来高质量视频网络——对话Synamedia流媒体技术发展经理卢彦林...
- .NET中的正则表达式 (三)RegexCompilationInfo 类
- 大学生学编程系列」第五篇:自学编程需要多久才能找到工作?
- mysql修改表的备注信息_修改mysql 数据库的 表的列的备注信息
- 使用Fluent NHibernate和AngularJS的Master Chef(第1部分)ASP.NET Core MVC
- 12大深度学习开源框架(caffe,tensorflow,pytorch,mxnet等)汇总详解
- 无线短距通信技术标准:WIFI,蓝牙,ZigBee
- 这项技术曾应用于无人驾驶,荣耀10将其移植到手机上这样操作!
- 六石管理学:公司要有应付没钱的预案,包括裁员
- 数学建模方法 — 【01】模糊数学
- 进击的海姆达尔Heimdallr,2021年链游最后一趟财富专列
- 2021.06.29【R语言】丨png转pdf批量生成
- Android客户端与PC服务器如何实现Socket通信
热门文章
- linux7.5有哪些版本,CentOS Linux 7.5正式发布,基于Red Hat Enterprise Linux 7.5
- Internet Explorer无法打开Internet站点
- PowerBuilder -- 条码打印
- HTML编辑器UEditor的简单使用
- 生活随记 - 尝试与师傅沟通争取自己的权益
- html打印 去除页眉页脚,js客户端打印html并且去掉页眉、页脚
- 小型微利企业税收筹划策略探析
- 中国数据中心最新规划图,中国数据中心建设情况
- B站傅希鸣-ElasticSearch学习笔记(ES 入门)
- ubuntu18.04智能拼音候选字体调节方法