文章目录

  • 前提
    • 整体介绍
    • 我的maven依赖
    • 1、整体流程
    • 2、openid 的获取
    • 3、统一下单Controller(预支付订单)
    • 4、配置类和配置文件
    • 5、工具类
    • 6、前端接收到必要的参数,进行调起支付页面
    • 7、微信支付通知,notify_url的回调Controller
    • 8、前端小程序端,定时器调用查询订单状态
    • 9、后端提供给小程序查询订单状态的接口
    • 10、用户取消订单
    • 11、商户端迟迟未收到异步通知结果
    • 12、申请退款
    • 13、退款回调通知
    • 欢迎+Q群讨论:821596752

前提

在进行JSAPI微信支付之前,需要准备好一下配置

  • 申请小程序的appid:wxaxxxxxxxxxxbxx8a (类似这样的)

  • 申请商户号:1xxxxxxxxx6

  • 小程序开通微信支付,绑定已经申请好的商户号。登录小程序后台(mp.weixin.qq.com)。点击左侧导航栏的微信支付,在页面中进行开通。(

    注意:以上信息的申请都需要使用企业账户,个人账户不行

    ​ 商户号官网地址:pay.weixin.qq.com

    ​ 小程序官网地址: mp.weixin.qq.com

    • 需要在商户端(pay.weixin.qq.com),api安全配置好apiv3的密钥

整体介绍

博主这篇博客,主要是小程序对接微信支付(JSAPI)

后端:spring boot

前端:微信小程序,uinapp

适用人群:已经申请好所有的资料,小程序平台,微信商户平台等等,本文不提供任何资料。并且需要有自己的业务场景,部分代码无法直接运行,需要加入自己的订单结构

我的maven依赖

<dependencies><dependency><groupId>org.jdom</groupId><artifactId>jdom2</artifactId><version>2.0.6.1</version></dependency>
<!--        json处理--><dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId></dependency>
<!--        微信支付SDk--><dependency><groupId>com.github.wechatpay-apiv3</groupId><artifactId>wechatpay-apache-httpclient</artifactId><version>0.4.7</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId></dependency>
<!--        mysql驱动--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.25</version></dependency>
<!--        mybatis-plus--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.3.1</version></dependency>
<!--        lombok--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency>
<!--        web--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
<!--        swagger--><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.7.0</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>2.7.0</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies>

1、整体流程

支付的流程图:

1-1、如上图,第2点请求下单,访问我们自己的后端接口所需的参数

字段名 变量名 类型 必填 示例值 描述
应用ID appid string[1,32] wxd678efh567hg6787 由微信生成的应用ID,全局唯一。请求基础下单接口时请注意APPID的应用属性,例如公众号场景下,需使用应用属性为公众号的服务号APPID
直连商户号 mchid string[1,32] 1230000109 直连商户的商户号,由微信支付生成并下发。
商品描述 description string[1,127] Image形象店-深圳腾大-QQ公仔 商品描述
商户订单号 out_trade_no string[6,32] 1217752501201407033233368018 商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一
通知地址 notify_url string[1,256] 可以先随便写一个不存的地址都行,不影响正常支付,但是获取不到支付结果信息,无法进行修改订单状态 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 公网域名必须为https,如果是走专线接入,使用专线NAT IP或者私有回调域名可使用http
订单金额 amount object HashMap<Object, Object> amountMap = new HashMap<>(); amountMap.put(“total”,fee);//金额 amountMap.put(“currency”,“CNY”);//货币类型 订单金额信息,他需要一个map,需要进行一层嵌套,可以去参考官网https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml
支付者 payer object //支付者 HashMap<Object, Object> playerMap = new HashMap<>(); playerMap.put(“openid”,openid); 支付者信息,用户在直连商户appid下的唯一标识。 下单前需获取到用户的Openid

以上参数需要注意,openid的获取

2、openid 的获取

需要在小程序端调用wx.login获取临时登陆凭证在获取openid

wx.login({success (res) {if (res.code) {//发起网络请求wx.request({url: 'https://example.com/onLogin',//这个接口是需要自己去编写的。也就是下方的那个controller   @GetMapping("/onLogin") public string onLogin(HttpServletRequest request)方法data: {code: res.code}})} else {console.log('登录失败!' + res.errMsg)}}
})

通过上面这个方法获取到res.code然后我们自己在编写一个后端接口,去获取openid

下面这个是我自己写的controller

     @Resourceprivate WxPayConfig wxPayConfig;//这个是一个wx的配置类@Resourceprivate CloseableHttpClient wxPayClient;//配置类中的一个bean@GetMapping("/onLogin")public string onLogin(HttpServletRequest request){String js_code = request.getParameter("code");//前端发起请求携带上面获取到的code,后端接收//app Secret是小程序密钥(在mp.weixin.qq.com中的开发管理-》开发设置-》AppSecret(小程序密钥)中设置)String baseUrl="https://api.weixin.qq.com/sns/jscode2session?appid="+wxPayConfig.getAppid()+"&secret="+wxPayConfig.getAppSecret()+"&js_code="+js_code+"&grant_type=authorization_code";String res=null;try {//这里发起请求获取到session-key,和openidres = requestByGetMethod(baseUrl).split("/n")[0];System.out.println(res);} catch (Exception e) {e.printStackTrace();}log.info("res:"+res);return res;//返回给前端}//这个方法就是用于发起get请求的
/*** 模拟发送url Get 请求* @param url* @return*/public String requestByGetMethod(String url) {log.info("发起get请求");CloseableHttpClient httpClient = HttpClients.createDefault();StringBuilder entityStringBuilder = null;try {HttpGet get = new HttpGet(url);CloseableHttpResponse httpResponse = null;httpResponse = httpClient.execute(get);try {HttpEntity entity = httpResponse.getEntity();entityStringBuilder = new StringBuilder();if (null != entity) {BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(httpResponse.getEntity().getContent(), "UTF-8"), 8 * 1024);String line = null;while ((line = bufferedReader.readLine()) != null) {entityStringBuilder.append(line + "/n");}}} finally {httpResponse.close();}} catch (Exception e) {e.printStackTrace();} finally {try {if (httpClient != null) {httpClient.close();}} catch (IOException e) {e.printStackTrace();}}return entityStringBuilder.toString();}

3、统一下单Controller(预支付订单)

调用下单接口,返回prepay_id等信息提供给前端,供前端调起支付页面,这里也对应官方图的第二点下单请求

注意:在请求中你需要携带一下参数,具体需要的参数可以看1-1的表格

下面这个controller是在前端

     @Resourceprivate WxPayConfig wxPayConfig;@Resourceprivate CloseableHttpClient wxPayClient;@Resourceprivate Verifier verifier;@ResponseBody@RequestMapping("returnparam")public HashMap<String, String> doOrder(HttpServletRequest request, HttpServletResponse response) throws Exception{request.setCharacterEncoding("UTF-8");response.setCharacterEncoding("UTF-8");//得到openid(微信用户唯一的openid)String openid = request.getParameter("openid");//得到价钱(自定义)int fee = 0;//单位是分if (null != request.getParameter("price")) {fee = Integer.parseInt(request.getParameter("price").toString());}//得到商品的ID(自定义)String goodsid=request.getParameter("goodsid");//订单标题(自定义)String title = request.getParameter("title");//时间戳,String times = System.currentTimeMillis() + "";//订单编号(自定义 这里以时间戳+随机数)Random random = new Random();String did = times+random.nextInt(1000);log.info("生成订单");//调用统一下单APIHttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi");// 请求body参数Gson gson = new Gson();HashMap<Object, Object> paramsMap = new HashMap<>();paramsMap.put("appid",wxPayConfig.getAppid());//appidparamsMap.put("mchid",wxPayConfig.getMchId());//商户号paramsMap.put("description",title);//商品描述paramsMap.put("out_trade_no",did);//商户订单号paramsMap.put("notify_url","http://d4a93w.natappfree.cc/wxBuy");//通知地址,可随便写,如果不需要通知的话,不影响支付,但是影响后续修改订单状态//订单金额HashMap<Object, Object> amountMap = new HashMap<>();amountMap.put("total",fee);//金额amountMap.put("currency","CNY");//货币类型paramsMap.put("amount",amountMap);//支付者HashMap<Object, Object> playerMap = new HashMap<>();playerMap.put("openid",openid);paramsMap.put("payer",playerMap);//将参数转化未json字符串String jsonParamsMap = gson.toJson(paramsMap);log.info("请求参数:"+jsonParamsMap);StringEntity entity = new StringEntity(jsonParamsMap,"utf-8");entity.setContentType("application/json");httpPost.setEntity(entity);httpPost.setHeader("Accept", "application/json");//完成签名并执行请求CloseableHttpResponse resp = wxPayClient.execute(httpPost);try {int statusCode = resp.getStatusLine().getStatusCode();String bodyAsString = EntityUtils.toString(resp.getEntity());if (statusCode == 200) { //处理成功log.info("成功,返回结果 = " + bodyAsString);} else if (statusCode == 204) { //处理成功,无返回Bodylog.info("成功");} else {System.out.println("小程序下单失败,响应码 = " + statusCode + ",返回结果 = " + bodyAsString);throw new IOException("request failed");}//相应结果HashMap<String,String> resultMap = gson.fromJson(bodyAsString, HashMap.class);//获取prepay—idString prepayId = resultMap.get("prepay_id");//获取到perpayid之后需要对数据进行二次封装,前端调起支付必须存在的参数HashMap<String, String> payMap = new HashMap<>();payMap.put("appid",wxPayConfig.getAppid());//appidlong currentTimestamp = System.currentTimeMillis();//时间戳,别管那么多,他就是需要payMap.put("timeStamp",currentTimestamp+"");String nonceStr = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);;//随机字符串,别管那么多他就是需要,要咱就给payMap.put("nonceStr",nonceStr);//apiv3只支持这种加密方式payMap.put("signType","RSA");payMap.put("package","prepay_id="+prepayId);//通过appid,timeStamp,nonceStr,signType,package以及商户密钥进行key=value形式进行拼接加密//加密方法我会放在这个代码段段下面String aPackage = buildMessageTwo("传入你的appid", currentTimestamp, nonceStr, payMap.get("package"));//获取对应的签名//加密方法我会放在这个代码段段下面String paySign = sign(wxPayConfig.getPrivateKeyPath(),aPackage.getBytes("utf-8"));payMap.put("paySign",paySign);/*** 在这里你可以加入自己的数据库操作,存储一条订单信息,状态为未支付就行了*  在这里你可以加入自己的数据库操作,存储一条订单信息,状态为未支付就行了*  在这里你可以加入自己的数据库操作,存储一条订单信息,状态为未支付就行了*/log.info("给前端的玩意:"+payMap);//前端会根据这些参数调起支付页面//到这里,就已经完成了官网图中的第8步了return payMap;}finally {resp.close();}}

4、配置类和配置文件

注意:下面这个配置文件需要读取resources中的wxpay.properties配置文件,等下我也会把我的wxpay.properties贴到下方,还有一个证书文件,需要放置在与src同级的目录中《apiclient_key.pem》,这个文件的获取在https://pay.weixin.qq.com/index.php/core/cert/api_cert#/中申请API证书,也就是申请APIV3的那个页面

一定要记得把证书放入到项目目录中!!!
aoiclient_key.pem就是证书,放置在与src同级目录中

下方这个是一个properties文件,跟yml配置同级

package com.wanliu.paymentdemo.config;//import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
//import com.wechat.pay.contrib.apache.httpclient.auth.*;
//import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
//import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.util.Base64;
import java.util.Map;
import java.util.UUID;@Configuration
@PropertySource("classpath:wxpay.properties") //读取配置文件
@ConfigurationProperties(prefix="wxpay") //读取wxpay节点
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
@Slf4j
public class WxPayConfig {// 商户号private String mchId;// 商户API证书序列号private String mchSerialNo;// 商户私钥文件private String privateKeyPath;// APIv3密钥private String apiV3Key;// APPIDprivate String appid;// 微信服务器地址,这个字段没有在,本文中使用到可以不用管private String domain;// APIv2密钥private String partnerKey;//小程序密匙private String appSecret;/*** 获取商户的私钥文件* @param filename* @return*/private PrivateKey getPrivateKey(String filename){try {return PemUtil.loadPrivateKey(new FileInputStream(filename));} catch (FileNotFoundException e) {throw new RuntimeException("私钥文件不存在", e);}}/*** 获取签名验证器* @return*/@Beanpublic Verifier getVerifier() throws Exception {log.info("获取签名验证器");//获取商户私钥PrivateKey privateKey = getPrivateKey(privateKeyPath);// 私钥签名对象PrivateKeySigner keySigner = new PrivateKeySigner(mchSerialNo, privateKey);// 身份认证对象WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, keySigner);// 获取证书管理器实例CertificatesManager certificatesManager = CertificatesManager.getInstance();// 向证书管理器增加需要自动更新平台证书的商户信息certificatesManager.putMerchant(mchId, wechatPay2Credentials,apiV3Key.getBytes(StandardCharsets.UTF_8));// ... 若有多个商户号,可继续调用putMerchant添加商户信息Verifier verifier = certificatesManager.getVerifier(mchId);return verifier;}/*** 获取http请求对象* @param verifier* @return*/@Bean("wxPayClient")public CloseableHttpClient getWxPayClient(Verifier verifier){log.info("获取httpclient");//获取商户私钥PrivateKey privateKey = getPrivateKey(privateKeyPath);// 从证书管理器中获取verifierWechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create().withMerchant(mchId, mchSerialNo, privateKey).withValidator(new WechatPay2Validator(verifier));// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新CloseableHttpClient httpClient = builder.build();return httpClient;}/*** 获取HttpClient,无需进行应答签名验证,跳过验签的流程*/@Bean(name = "wxPayNoSignClient")public CloseableHttpClient getWxPayNoSignClient(){//获取商户私钥PrivateKey privateKey = getPrivateKey(privateKeyPath);//用于构造HttpClientWechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()//设置商户信息.withMerchant(mchId, mchSerialNo, privateKey)//无需进行签名验证、通过withValidator((response) -> true)实现.withValidator((response) -> true);// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新CloseableHttpClient httpClient = builder.build();log.info("== getWxPayNoSignClient END ==");return httpClient;}public String decryptFromResource(Map<String,Object> bodyMap) throws GeneralSecurityException {log.info("秘文解密");//通知数据Map<String,String > resourceMap =(Map<String, String>) bodyMap.get("resource");//数据秘文String ciphertext = resourceMap.get("ciphertext");//获取随机串String nonce = resourceMap.get("nonce");String associated_data = resourceMap.get("associated_data");log.info("秘文===》{}",ciphertext);AesUtil aesUtil = new AesUtil(getApiV3Key().getBytes(StandardCharsets.UTF_8));//获取明文(解密后的数据)String plainText = aesUtil.decryptToString(associated_data.getBytes(StandardCharsets.UTF_8),nonce.getBytes(StandardCharsets.UTF_8),ciphertext);log.info("明文====》{}",plainText);return plainText;}/*** 拼接五个参数* @param appId* @param timestamp* @param nonceStr* @param packag* @return*/public  String buildMessageTwo(String appId, long timestamp, String nonceStr, String packag) {return appId + "\n"+ timestamp + "\n"+ nonceStr + "\n"+ packag + "\n";}/*** 进行二次封装* @param wxCertPath* @param message* @return* @throws NoSuchAlgorithmException* @throws SignatureException* @throws IOException* @throws InvalidKeyException* @throws java.security.InvalidKeyException*/public  String sign(String wxCertPath,byte[] message) throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException, java.security.InvalidKeyException {Signature sign = Signature.getInstance("SHA256withRSA"); //SHA256withRSAsign.initSign(PemUtil.loadPrivateKey(new FileInputStream(wxCertPath))); // 微信证书私钥sign.update(message);return Base64.getEncoder().encodeToString(sign.sign());}/*** 获取32位随机字符串* @return*/public  String getNonceStr(){return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);}/*** 获取当前时间戳,单位秒* @return*/public long getCurrentTimestamp() {return System.currentTimeMillis()/1000;}
}

我的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 # 你申请的APIv3密钥
# APPID
wxpay.appid=wx74862e0dfcf69954
# 微信服务器地址
wxpay.domain=https://api.mch.weixin.qq.com
# 接收结果通知地址
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
wxpay.notify-domain=https://500c-219-143-130-12.ngrok.io# APIv2密钥
wxpay.partnerKey=T6m9iK73b0kn9g5v426MKfHQH7X8rKwb#小程序密匙
wxpay.appSecret=你的小程序密钥,在mp.weixin.qq.com中的开发设置里面,appid的下面可以去配置,或重置

5、工具类

httpUtils解析通知信息

这个HttpUtils主要是用于解析notify_url回调地址,传回来的数据,解析请求头中的数据,微信官方会告诉你支付结果,并由你根据这个结果的状态做一些处理,比如修改数据库订单状态或者其他的

httpUtils主要是解析微信给我们的通知信息,有什么(强调,解析出来的东西也有加密的信息,需要再次解密,也就是验签的过程了)

注意:你接收到通知后,需要应答微信官方,否则微信官方会认为通知失败,然后在24h4m内,反复通知你

package com.wanliu.paymentdemo.util;import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;public class HttpUtils {/*** 将通知参数转化为字符串* @param request* @return*/public static String readData(HttpServletRequest request) {BufferedReader br = null;try {StringBuilder result = new StringBuilder();br = request.getReader();for (String line; (line = br.readLine()) != null; ) {if (result.length() > 0) {result.append("\n");}result.append(line);}return result.toString();} catch (IOException e) {throw new RuntimeException(e);} finally {if (br != null) {try {br.close();} catch (IOException e) {e.printStackTrace();}}}}
}

WechatPay2ValidatorForRequest验签

WechatPay2ValidatorForRequest就是从微信官方发给我的加密信息,进行验签解密。大家主要看WechatPay2ValidatorForRequest中的validate方法即可

package com.wanliu.paymentdemo.util;import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;/*** @author xy-peng*/
public class WechatPay2ValidatorForRequest {protected static final Logger log = LoggerFactory.getLogger(WechatPay2Validator.class);/*** 应答超时时间,单位为分钟*/protected static final long RESPONSE_EXPIRED_MINUTES = 5;protected final Verifier verifier;protected final String  requestId;protected final String  body;public WechatPay2ValidatorForRequest(Verifier verifier, String requestId,String body) {this.verifier = verifier;this.requestId=requestId;this.body=body;}protected static IllegalArgumentException parameterError(String message, Object... args) {message = String.format(message, args);return new IllegalArgumentException("parameter error: " + message);}protected static IllegalArgumentException verifyFail(String message, Object... args) {message = String.format(message, args);return new IllegalArgumentException("signature verify fail: " + message);}public final boolean validate(HttpServletRequest request) throws IOException {try {//处理请求参数validateParameters(request);//构造验签名串String message = buildMessage(request);//从请求头中拿到验签名序列号String serial = request.getHeader(WECHAT_PAY_SERIAL);//从请求头中拿到携带的签名String signature = request.getHeader(WECHAT_PAY_SIGNATURE);//验签处理if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",serial, message, signature, requestId);}} catch (IllegalArgumentException e) {log.warn(e.getMessage());return false;}return true;}protected final void validateParameters(HttpServletRequest request) {// NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at lastString[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};String header = null;for (String headerName : headers) {header = request.getHeader(headerName);if (header == null) {throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);}}//获取时间戳,判断请求是否过期String timestampStr = header;try {//通过时间戳,创建一个基于时间戳的时间对象Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));// 拒绝过期的请求if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);}} catch (DateTimeException | NumberFormatException e) {throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);}}protected final String buildMessage(HttpServletRequest request) throws IOException {String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);String nonce = request.getHeader(WECHAT_PAY_NONCE);return timestamp + "\n"+ nonce + "\n"+ body + "\n";}protected final String getResponseBody(CloseableHttpResponse response) throws IOException {HttpEntity entity = response.getEntity();return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";}}

6、前端接收到必要的参数,进行调起支付页面

在这里调起支付页面之后,用户进行输入密码,支付等等,如果支付那么微信商户平台会收到你的汇款,然后微信官方会发起请求通知你支付状态,也就是刚刚你在统一下单的时候设置的notify_url参数的接口地址

这里的必要参数也就是第三步统一下单那个步骤中的,@RequestMapping(“returnparam”)接口返回的参数

uni.request({//这个是uniapp版本,下方我会贴微信小程序版本,微信小程序的往下看url: that.$api.baseURL+"/returnparam",//你的后端接口method:"GET",data:{//要传入的参数//openidopenid: openId,//订单总金额price: order.amount,//订单号goodsid: order.number,//titletitle: ''},//上面这里是去调用下单success: (res2) => {//这里是访问returnparam接口成功后就会调起支付console.log(res2);uni.requestPayment({//这里是官方图中的第9步,这里是uniapp官方的调起支付方法//后端返回的参数,这里下面就是returnparam接口返回的参数timeStamp: res2.data.data.timeStamp,nonceStr: res2.data.data.nonceStr,package: res2.data.data.package,signType: res2.data.data.signType,paySign: res2.data.data.paySign,appId: res2.data.data.appid,success (res3) { console.log(res3);},fail (res3) { console.log(res3);}})}})

这里就是微信小程序的一个请求过程

   let result = await request({url: '/returnparam',method: 'get',data: {//openidopenid:wx.getStorageSync("openid"),//订单总金额price:this.data.price,//订单号goodsid:wx.getStorageSync("orderCode"),//titletitle: '订单描述',},});console.log(result);if(result.appid!=null){wx.requestPayment({timeStamp:result.timeStamp,nonceStr:result.nonceStr,package: result.package,signType: result.signType,paySign: result.paySign,appId: request.appid,success:function(res){ wx.showToast({title: '支付成功!',}),setTimeout(()=>{wx.redirectTo({url: '/pages/index/index'})}, 3000)},fail :function(res) {console.log("调用失败");console.log(res);},complete:function(res){console.log("成功与否");}})

7、微信支付通知,notify_url的回调Controller

这个一定要是外网可以访问的地址,建议使用内网穿透

给大家推荐一篇内网穿透的博客:https://blog.csdn.net/Lfl202116888/article/details/124932062?spm=1001.2014.3001.5502

     @Resourceprivate WxPayConfig wxPayConfig;@Resourceprivate CloseableHttpClient wxPayClient;@Resourceprivate Verifier verifier;/*** 微信通知回调地址* @param request* @param response* @return*/@PostMapping("/wxBuy")public String wxBuy(HttpServletRequest request,HttpServletResponse response){Gson gson = new Gson();//创建一个应答对象HashMap<String, String> map = new HashMap<>();try {//处理通知参数String body = HttpUtils.readData(request);HashMap<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 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("通知验签成功");//TODO : 处理订单//这里可以调用你要处理业务逻辑的service,我这里就不写了,//然后解密数据在业务service中调用就行,我在这里就直接调用了//解密密文,获取明文String plaintText = wxPayConfig.decryptFromResource(bodyMap);//将明文转为mapHashMap plaintTextMap = gson.fromJson(plaintText, HashMap.class);//获取支付下单的时候,传入的商户订单号,可以根据这个订单号去获取我们的一个订单记录,从而更新订单状态String orderNo = (String) plaintTextMap.get("out_trade_no");//业务编号String transactionId = (String) plaintTextMap.get("transaction_id");//trade_type,支付类型,如果有需要的话, 你可以存储在数据库中,这里我们的数据,基本上都是JSapi支付类型String tradeType = (String) plaintTextMap.get("trade_type");//交易状态String tradeState = (String) plaintTextMap.get("trade_state");//还有很多,为这里就不一一去写了/*** 在更新你订单状态之前,可以先根据orderNo,查询数据库中是否有这个订单* 然后查询这个订单是否已经被处理过,也就是状态是否是已经支付的状态* 如果这个订单已经被处理了,那么我们可以直接return,没有被处理过我们在处理* 这样可以避免数据库被反复的操作** 微信官方的解释是:*  同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。*  推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,*  并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,*  则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,*  以避免函数重入造成的数据混乱。*///更新订单状态/** 你的数据库操作* 一定要存储解密出来的transaction_id字段在数据库中* 如果需要跟微信官方对账的话,是需要提供这个字段进行一个查账的* *///成功应答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);}}

在上面这个controller中,处理订单那个部分,其实还有很多参数可以去获取,根据业务需要求,我这里只是举例,获取了几个比较重要的,具体详细可以去官网中查看:微信支付-开发者文档 (qq.com)

还有具体的业务,我也没有去写了,大家根据你们自己的业务去完成即可

8、前端小程序端,定时器调用查询订单状态

支付成功跳转支付成功的页面,然后清除定时器停止循环查单,如果用户一直没有支付的话,定时器反复查单,接口只会返回一个101的状态码,和一个支付中的状态信息。如果用户退出支付页面,也要记得关闭定时器.。这里的查单是查询我们自己数据库中的订单状态不是,微信官方的查单

9、后端提供给小程序查询订单状态的接口

因为我这里没有具体的业务操作,所以我吧大体思路给大家写了一下,大家可以参考一下

/*** 提供给前端查询支付订单状态的接口* @param orderNo* @return*/@GetMapping("/query-order-status/{orderNo}")public R queryOderStatus(@PathVariable("orderNo") String orderNo){//调用你的service去根据orderNo*(订单id)去获取当前订单//if判断,你根据订单id查询到到订单对象中到订单状态是否为成功状态(已经支付)//成功返回,状态码200,已经消息,支付成功//如果状态不是已经支付,则返回状态码101(这个状态码自己自己设定,只是提供给前端做判断的),消息,支付中}

10、用户取消订单

用户如果取消订单的话,那么要干两件事情

  • 调用微信支付官方的,关单接口
  • 更改为我们自己数据库中,这条订单的订单状态为,取消订单
    @PostMapping("/cancel/{orderNo}")public R cancel(@PathVariable String orderNo) throws Exception {log.info("取消订单");//调用service业务,我这里为了给大家方便掩饰,就直接写controller中了//调用微信支付的关单接口closeOrder(orderNo);//修改我们自己数据库中的订单状态为订单已取消//这里我没有具体业务,我就不写了,大家根据自己的情况去写return R.ok().setMessage("订单已取消");}public void closeOrder(String orderNo) throws Exception {log.info("关单接口的调用,订单号===》{}",orderNo);//微信官方提供的关单url,我们进行一个拼串String url="https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/"+orderNo+"/close";HttpPost httpPost = new HttpPost(url);//在请求体中还需要携带商户号Gson gson = new Gson();HashMap<String, String> paramsMap = new HashMap<>();paramsMap.put("mchid",wxPayConfig.getMchId());//参数都组装完成之后,需要转为jsonString jsonParamsMap = gson.toJson(paramsMap);//将请求参数设置到请求对象中StringEntity entity = new StringEntity(jsonParamsMap,"utf-8");entity.setContentType("application/json");httpPost.setEntity(entity);httpPost.setHeader("Accept", "application/json");//完成签名并执行请求CloseableHttpResponse resp = wxPayClient.execute(httpPost);try {int statusCode = resp.getStatusLine().getStatusCode();if (statusCode == 200) { //处理成功log.info("成功200," );} else if (statusCode == 204) { //处理成功,无返回Bodylog.info("成功204");} else {System.out.println("小程序下单失败,响应码 = " + statusCode );throw new IOException("request failed");}}finally {resp.close();}}

11、商户端迟迟未收到异步通知结果

如果我们商户后台,迟迟没有收到异步通知的结果,那么我们应该主动的去调用微信官方的查单接口,查询这个订单到底有没有支付成功,如果成功了我们就要去修改我们的数据库中这条订单的,订单状态

为什么要这么做呢?

因为有的时候,用户支付成功之后,可能因为网络的一个延迟啊,或者有些其他的原因导致,消息没有及时的更新,微信官方,也没有及时的给我们发送一个通知,没有发送通知的话,我们商户端,是不可能去修改用户的这个订单为,已支付的一个状态。

所以我们如果一直没有收到这个订单的消息,我们需要主动的查询这个订单的一个情况

所以说,这个查单的这个流程,肯定是需要我们在后端写一个定时任务去调用的

这里只是一个查询订单的一个接口,如果有需要的话大家可以看看

 @GetMapping("/query/{orderNo}")public R queryOrder(@PathVariable String orderNo) throws Exception {String result = findOrder(orderNo);return R.ok().setMessage("查询成功").data("result",result);}public String findOrder(String orderNo) throws Exception {log.info("查询订单");
//        同理,这里应该是调用业务方法。为了方便演示,我直接写在controller中String url="https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/"+orderNo+"?mchid="+wxPayConfig.getMchId();HttpGet httpGet = new HttpGet(url);httpGet.setHeader("Accept","application/json");//完成签名并执行请求CloseableHttpResponse resp = wxPayClient.execute(httpGet);try {int statusCode = resp.getStatusLine().getStatusCode();String bodyAsString = EntityUtils.toString(resp.getEntity());if (statusCode == 200) { //处理成功log.info("成功,返回结果 = " + bodyAsString);} else if (statusCode == 204) { //处理成功,无返回Bodylog.info("成功");} else {System.out.println("小程序下单失败,响应码 = " + statusCode + ",返回结果 = " + bodyAsString);throw new IOException("request failed");}log.info("bodyAsString:"+bodyAsString);return bodyAsString;}finally {resp.close();}}

这个才是定时任务,开始轮询查单

注意:这里开启定时任务的这个注解,一定要先在spring boot的启动类上加一个注解:@EnableScheduling

 /*** 从第0秒开始每隔30秒执行一次,查询创建超过5分钟,并且没有支付第订单*/@Scheduled(cron = "0/30 * * * * ?")public void orderConfirm() throws Exception {//这里还是一样,应该是去调用业务方法,为了演示我直接写出来//TODO :查询创建超过5分钟并且没有支付的订单Instant instant=Instant.now().minus(Duration.ofMinutes(5));//得到五分钟之前的时间//创建查询对象QueryWrapper<OrderInfo> queryWrapper=new QueryWrapper<>();//拼接查询调教queryWrapper.eq("order_status","未支付");//这里的未支付,应该是写枚举,或者0,1,2这些数据的,这里为了一眼能让大家看出来我就直接写了,状态=未支付queryWrapper.le("create_time",instant);//这里调用查询所以订单的方法,吧queryWrapper传入进去,得到一个list,便是所有超过五分钟,并且未支付的订单了//这里我就模拟一个结果出来,当然他没有数据List<OrderInfo> orderInfoList=new ArrayList<>();//这里查询出所有的超时订单之后,我们应该去核实一下,这些超时的订到单,是不是真的都是未支付的//所以我们需要去调用,微信官方的,查单接口//因为这里需要去判断,是用户真的没有支付,还是用户支付了,但是微信官方给我们发起的通知,我们没有接收到,导致订单状态没有被更改//如果是用户没有支付,那我们需要对这个订单进行一个关单,并修改状态为超时//如果用户支付了,但是是因为我们没有收到通知,那么我们则主动查单,修改订单状态为支付成功for (OrderInfo orderInfo : orderInfoList) {//直接调用写好对查单方法即可String result = findOrder(orderInfo.getOrderNo());Gson gson = new Gson();HashMap hashMap = gson.fromJson(result, HashMap.class);//获取微信官方的订单状态,获取到到数据应该是"trade_state": "SUCCESS",这样的Object trade_state = hashMap.get("trade_state");//判断订单状态//这里所有的状态还是建议写枚举,然后根据枚举去做判断//如果查询出来的这个订单是已经支付的话,那么我们吧我们自己的数据库中的订单状态也改成已经支付//如果判断是未支付的话,那么就要调用微信的关单接口,也就是我的博客中的第9点用户取消订单中的closeOrder方法去进行关单//并且修改我们自己数据库中的订单状态为超时关闭}}

12、申请退款

申请退款的流程,跟支付是差不多的

先申请退款,然后微信收到退款申请后,退款

退款成功后,给我们商户端发起一个通知

我们接收通知,修改状态为退款成功,并应答微信官方,说我们已经收到你的通知了

如因为其他原因商户端没有收到异步通知,如博客中第10步,也是一样的,进行一个查单

//《orderNo》是我们自己生成的订单号
//《reason》是退款理由
//这里退款有两种模式,
//第一种是使用我们生成的订单号进行查询并退款
//第二种就是,在第三步的时候,微信会返回给我们一个微信官方的订单号进行退款
//这里我选择的是第一种方式
@PostMapping("/refunds/{orderNo}/{reason}")public R refunds(@PathVariable String orderNo,@PathVariable String reason) throws IOException {log.info("申请退款");//先根据订单号去查询到,当前要退款到这个订单//我这里就创建一个空对象来代替OrderInfo orderInfo = new OrderInfo();//这里还要创建一个退款单对象RefundInfo refundInfo = new RefundInfo();//设置退款单中有那些信息refundInfo.setOrderNo(orderNo);//订单编号refundInfo.setRefundNo(wxPayConfig.getNonceStr());//退款单号,这里的退款单号,也是我们自己生成的订单,供保存退款信息的,微信那边也需要我们提供一个这个单号,一般的业务都会需要保存一个退款的信息,所以这里需要这个退款单号refundInfo.setTotalFee(orderInfo.getTotalFee());//原订单金额refundInfo.setRefund(orderInfo.getTotalFee());//要退款的金额refundInfo.setReason(reason);//退款原因//大家的退款单对象,可以根据自己的业务需求去设计。退款单也是需要一张表去存储的//这里去调用数据库操作,去保存refundInfo这样的一条退款单信息//保存好退款到记录之后//调用退款Api进行退款操作log.info("调用退款APi");String url="https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";HttpPost httpPost = new HttpPost(url);//组装请求参数Gson gson = new Gson();Map paramsMap = new HashMap<>();paramsMap.put("out_trade_no",orderNo);//订单编号paramsMap.put("out_refund_no",refundInfo.getRefundNo());//退款单号paramsMap.put("reason",reason);//退款原因paramsMap.put("notify_url","http://d4a93w.natappfree.cc/refunds/notify");//退款成功异步回调地址Map amountMap = new HashMap<>();//正确对写法应该是这样,但是我上吗创建的是空对象,所里这里我需要吧退款金额之类的写死
//        amountMap.put("refund",refundInfo.getRefund());//退款金额
//        amountMap.put("total",refundInfo.getTotalFee());//原订单金额、amountMap.put("refund",1);//退款金额amountMap.put("total",1);//原订单金额amountMap.put("currency","CNY");//退款币种paramsMap.put("amount",amountMap);//将参数转为jsonString 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 resp = wxPayClient.execute(httpPost);try {//解析响应结果String bodyAsString = EntityUtils.toString(resp.getEntity());int statusCode = resp.getStatusLine().getStatusCode();if (statusCode == 200) {log.info("退款成功,退款返回结果 = " + bodyAsString);} else if (statusCode == 204) {log.info("成功");} else {throw new RuntimeException("退款异常,响应码 = " + statusCode + ",退款返回结果 = " + bodyAsString);}//更新订单状态为退款中//这里大家自己去写数据库操作更改状态//更新退款单,根据退款的bodyAsString的数据,获取你想要保留的参数,保存到退款单中,以便后续的退款//比如退款单号(微信官方的),可以供我们后续如果对这笔退款有疑问,可以进行一个退款查询return R.ok();}finally {resp.close();}}

这里给大家看看我的这个退款过程

这里的退款参数具体的大家可以去看看官网上,有哪些是你们需要存入退款单中的:微信支付-开发者文档 (qq.com)

13、退款回调通知

这个是在申请退款中notify_url填写的这个参数的地址,会被微信回调的接口。微信回告知我们退款的成功的信息,需要我们进行接收,解密信息,然后应答微信官方我们收到了信息,在接收信息解密的过程中,我们可以判断是否退款成功,我们就可以进行修改退款单、订单的一些状态,把他们修改成退款成功

这里的回调逻辑,与博客中的第6点基本一致,只是进行的操作不同,一个是退款,一个是支付

         @PostMapping("/refunds/notify")public String refundsNotify(HttpServletRequest request,HttpServletResponse response) {log.info("退款结果通知");Gson gson = new Gson();HashMap<String, String> map = new HashMap<>();//应答对象try {//处理通知参数String body = HttpUtils.readData(request);HashMap<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("通知验签成功");//处理退款单//解密密文,获取明文String plaintText = wxPayConfig.decryptFromResource(bodyMap);//将明文转为mapHashMap plaintTextMap = gson.fromJson(plaintText, HashMap.class);//获取支付下单的时候,传入的商户订单号,可以根据这个订单号去获取我们的一个订单记录,从而更新订单状态String orderNo = (String) plaintTextMap.get("out_trade_no");/*** 这里获取了单号可以去查询一个,这个订单的状态是不是退款中* 如果不是退款中,那直接return返回就行了,不用在执行下面的更新状态* 如果是退款中的话,我们需要更新状态为退款成功。这里写你自己的数据库操作更新即可* 如果你有退款单的话,同理,更新退款单状态即可**///成功应答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);}}

以上基本上就是所有的微信小程序JSAPI支付的完整流程,

  • 统一下单
  • 支付通知
  • 查询支付结果
  • 申请退款
  • 退款通知
  • 查询退款结果
  • 关闭订单

~~~

欢迎+Q群讨论:821596752

微信小程序微信支付《JSAPI支付》APIV3详细教程相关推荐

  1. 微信小程序中使用JSAPI支付

    微信小程序中使用JSAPI支付 在微信小程序中使用微信支付api[wx.requestPayment]需要传递以下字段 如何获取支付所需要的值 在微信小程序中使用微信支付api[wx.requestP ...

  2. 别再花冤枉钱,微信小程序免300元认证费用详细教程,助力企业/个体低成本发展

    受疫情影响,很多商家都认识到了发展建立私域流量的重要性,各企业争先恐后开始投入私域运营的行列,小程序成为了大家的主战场,无论品牌宣传展示,或者作为商城购物平台,都非常方便. 微信小程序 如果个人账号那 ...

  3. 2023年微信小程序获取手机号授权登录注册详细教程,包含服务端教程

    前言 小程序中有很多地方都会用到用户的手机号,比如登陆注册,填写收货地址等等.有了这个组件可以快速获取微信绑定手机号码,无须用户填写. 网上大多数教程还是往年的,而微信官方的api已做了修改.本篇文章 ...

  4. 微信小程序 - 最新获取用户昵称 / 头像(wx.getUserProfile 接口被废弃后的代替方案)详细教程,2022 年之后的所有微信小程序,获取用户信息最新详细教程,附带示例源代码

    前言 由于官方修改了 "用户头像昵称获取规则" ,导致网上几乎所有教程全部失效,本文来做最新详细教程. 2022 年往后(官方废弃了 wx.getUserProfile 接口),本 ...

  5. 微信小程序服务商下子商户支付下单接口

    微信小程序服务商下子商户支付下单流程 调用方法 <?php namespace app\index\controller; class WeixinPay extends Base { prot ...

  6. 微信小程序支付返回签名错误_java 微信小程序微信支付统一下订单及数字签名错误问题(后端)...

    今天来分享一下之前做微信小程序微信支付遇到的一些坑,博主这里是微信小程序支付功能,因此选择的微信支付方式是JSAPI支付方式(温馨提示左下角有音乐哦). 首先我们肯定是要在小程序后台绑定一个商户号的, ...

  7. 微信小程序 微信支付代码实现流程

    微信小程序 微信支付是一个很简单的流程  微信开发文档 地址:wx.requestPayment(Object object) | 微信开放文档 微信公众平台申请支付功能 百度一大堆例举代码 官方文档 ...

  8. 微信小程序—微信小程序端支付代码

    只有微信小程序端的代码,如下 Page({data: {},onLoad: function (options) {// 页面初始化 options为页面跳转所带来的参数var that = this ...

  9. 微信小程序---微信支付流程实现

    一.微信小程序又两种支付流程,一个是开发者通过云支付,另一个是非云支付手段.下面来介绍一下实现的流程和方法 二.微信小程序提供的支付流程图: 三.非云支付: 1.通过后端获取oder参数,后端返回一个 ...

  10. 微信小程序-微信支付退款

    微信小程序-微信支付退款 官方接口文档及相关附件 申请退款 SDK 错误集锦 调用该https://api.mch.weixin.qq.com/secapi/pay/refund接口需要应程序安装AP ...

最新文章

  1. Qt应用程序主窗口之一:主窗口框架
  2. 用JAVA操作ClearCase
  3. 你会么?图形不正,角度是随机的
  4. 华为端口聚合命令_华为交换机链路聚合配置命令
  5. appnode php,环境软件路径参考
  6. 2、Task 使用 ContinueWith 而不要使用 Wait
  7. Oracle从入门到精通
  8. 数学建模常用的四大模型
  9. STC 18B20温度传感器读写程序
  10. 微信小程序引入iconfont阿里字体
  11. 一文彻底学会Redis主从复制(高可用)
  12. 【Python爬虫】第一课 Python爬虫环境与爬虫简介
  13. Python基于YOLOv5的交通标志识别系统[源码&技术文档&部署视频&数据集]
  14. recovery/removal time
  15. windows命令行关闭已占用的端口
  16. 大数据之路、阿里巴巴大数据实践读书笔记 --- 第二章、日志采集
  17. 个人总结之MSP430F5510串口通讯(485)
  18. Android 开源UI框架汇总
  19. STM32替换Arduino直通车
  20. 5G智慧园区整体架构方案(ppt)

热门文章

  1. 采集数据发布到易优CMS的指定栏目
  2. css设置背景图片样式
  3. 计算机网络知识初步教案,计算机网络基础教案
  4. Android Camera 测试环境搭建:编译Android模拟器
  5. 解决EPLAN卡顿的方法,亲测有效
  6. APK 签名轮替方案 v3
  7. 周岭《认知觉醒》读书笔记
  8. 孔令德的计算机图形学课程实验
  9. 光伏出力预测的神经网络matlab编程,神经网络预测光伏出力|MATLAB 神经网络|MATLAB技术论坛 - Powered by Discuz!...
  10. 租用和购买服务器的区别