最近做个对接微信服务商平台的小程序项目,大概要实现的流程是:a)特约商户进件 > b)生成带参数的小程序码 > c)小程序支付 > d)分账,记录一下,希望能对需要的朋友有所帮助

开始

在开始之前建议仔细读微信官方文档,接口规则及api文档

https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay-1.shtml

https://pay.weixin.qq.com/wiki/doc/apiv3_partner/index.shtml

目录

整个流程开发步骤如下:

一、(签名)

二、(获取证书、敏感信息加密)

三、(上传图片)

四、(特约商户进件)

五、(生成小程序码)

六、(微信小程序支付)

七、(分账)

正文

在开始之前请确保你已经获取商户号、证书、秘钥、小程序appid、appsecret

一、签名

文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml

1、生成签名

/// <param name="url">微信的接口地址</param>
/// <param name="method">请求的方式GET,POST,PUT</param>
/// <param name="jsonParame">post请求的数据,json格式 ,get时传空</param>
/// <param name="privateKey">apiclient_key.pem中的内容,不要-----BEGIN PRIVATE KEY----- -----END PRIVATE KEY-----</param>
/// <param name="merchantId">发起请求的商户(包括直连商户、服务商或渠道商)的商户号 mchid</param>
/// <param name="serialNo">商户证书号</param>
/// <returns></returns>
protected string GetAuthorization(string url, string method, string jsonParame, string privateKey, string merchantId, string serialNo)
{
var uri = new Uri(url);
string urlPath = uri.PathAndQuery;
string nonce = Guid.NewGuid().ToString();
var timestamp = DateTimeOffset.Now.ToUnixTimeSeconds();
//数据签名 HTTP请求方法\n接口地址的url\n请求时间戳\n请求随机串\n请求报文主体\n
method = string.IsNullOrEmpty(method) ? "" : method;
string message = string.Format("{0}\n{1}\n{2}\n{3}\n{4}\n", method, urlPath, timestamp, nonce, jsonParame);
string signTxt = Sign(message, privateKey); //Authorization和格式
string authorzationTxt = string.Format("WECHATPAY2-SHA256-RSA2048 mchid=\"{0}\",nonce_str=\"{1}\",timestamp=\"{2}\",serial_no=\"{3}\",signature=\"{4}\"",
merchantId,
nonce,
timestamp,
serialNo,
signTxt
);
return authorzationTxt;
} protected string Sign(string message, string privateKey)
{
byte[] keyData = Convert.FromBase64String(privateKey);
using (CngKey cngKey = CngKey.Import(keyData, CngKeyBlobFormat.Pkcs8PrivateBlob))
using (RSACng rsa = new RSACng(cngKey))
{
byte[] data = System.Text.Encoding.UTF8.GetBytes(message);
return Convert.ToBase64String(rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
}
}

2、放到请求头

string Authorization = GetAuthorization(url, method, postData, privateKey, merchantId, serialNo);
request.Headers.Add("Authorization", Authorization);

3、完整的请求方法

/// <param name="url">微信的接口地址</param>
/// <param name="postData">post请求的数据,json格式 </param>
/// <param name="privateKey">apiclient_key.pem中的内容,不要-----BEGIN PRIVATE KEY----- -----END PRIVATE KEY-----</param>
/// <param name="merchantId">发起请求的商户(包括直连商户、服务商或渠道商)的商户号 mchid</param>
/// <param name="serialNo">商户证书号</param>
/// <returns></returns>
public string postJson(string url, string postData, string privateKey, string merchantId, string serialNo, string method = "POST")
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Method = method;
request.ContentType = "application/json;charset=UTF-8";
request.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3100.0 Safari/537.36";
request.Accept = "application/json"; string Authorization = GetAuthorization(url, method, postData, privateKey, merchantId, serialNo);
request.Headers.Add("Authorization", Authorization);
if (!string.IsNullOrEmpty(postData))
{
byte[] paramJsonBytes;
paramJsonBytes = System.Text.Encoding.UTF8.GetBytes(postData);
request.ContentLength = paramJsonBytes.Length;
Stream writer;
try
{
writer = request.GetRequestStream();
}
catch (Exception)
{
writer = null;
Console.Write("连接服务器失败!");
}
writer.Write(paramJsonBytes, 0, paramJsonBytes.Length);
writer.Close();
} HttpWebResponse response;
try
{
response = (HttpWebResponse)request.GetResponse();
}
catch (WebException ex)
{
response = ex.Response as HttpWebResponse;
}
Stream resStream = response.GetResponseStream();
StreamReader reader = new StreamReader(resStream);
string text = reader.ReadToEnd();
return text;
}

二、获取证书、敏感信息加密

调用特约商户进件接口之前需要做三个工作:获取证书、敏感信息加密、上传图片

获取证书的目的是敏感信息加密需要用证书里解密得到的pubkey,然后用pubkey去对敏感信息进行加密

1、获取证书

获取证书接口比较简单,直接调用上边的请求方法

public static certModel GetCert()
{
string url = "https://api.mch.weixin.qq.com/v3/certificates";
string merchantId = WxPayConfig.MCHID; //商户号
string serialNo = WxPayConfig.SERIAL_NO; //证书编号
string privateKey = WxPayConfig.PRIVATEKEY; // NOTE: 私钥不包括私钥文件起始的-----BEGIN PRIVATE KEY----- 亦不包括结尾的-----END PRIVATE KEY----- string transactionsResponse = postJson(url, string.Empty, privateKey, merchantId, serialNo,"GET");
var result = JsonConvert.DeserializeObject<certModel>(transactionsResponse);
return result;
}

GetCert()

用到的model

public class certModel
{
public List<Data> data { get; set; }
} public class Data
{
public string serial_no { get; set; }
public string effective_time { get; set; }
public string expire_time { get; set; }
public Encrypt_certificate encrypt_certificate { get; set; } }
public class Encrypt_certificate
{
public string algorithm { get; set; }
public string nonce { get; set; }
public string associated_data { get; set; }
public string ciphertext { get; set; }
}

certModel

调用成功直接返回证书list,我们需要用v3秘钥解密得到公钥

var cmodel = GetCert().data.OrderByDescending(t => t.expire_time).FirstOrDefault();
string pubkey = AesGcmHelper.AesGcmDecrypt(cmodel.encrypt_certificate.associated_data, cmodel.encrypt_certificate.nonce, cmodel.encrypt_certificate.ciphertext);
pubkey = pubkey.Replace("-----BEGIN CERTIFICATE-----", "").Replace("-----END CERTIFICATE-----", ""); //解密方法
public class AesGcmHelper
{
private static string ALGORITHM = "AES/GCM/NoPadding";
private static int TAG_LENGTH_BIT = 128;
private static int NONCE_LENGTH_BYTE = 12;
private static string AES_KEY = WxPayConfig.V3KEY;//你的v3秘钥 public static string AesGcmDecrypt(string associatedData, string nonce, string ciphertext)
{
GcmBlockCipher gcmBlockCipher = new GcmBlockCipher(new AesEngine());
AeadParameters aeadParameters = new AeadParameters(
new KeyParameter(Encoding.UTF8.GetBytes(AES_KEY)),
128,
Encoding.UTF8.GetBytes(nonce),
Encoding.UTF8.GetBytes(associatedData));
gcmBlockCipher.Init(false, aeadParameters); byte[] data = Convert.FromBase64String(ciphertext);
byte[] plaintext = new byte[gcmBlockCipher.GetOutputSize(data.Length)];
int length = gcmBlockCipher.ProcessBytes(data, 0, data.Length, plaintext, 0);
gcmBlockCipher.DoFinal(plaintext, length);
return Encoding.UTF8.GetString(plaintext);
}
}

pubkey

2、敏感信息加密

我们上一步得到了pubkey,然后对一些敏感信息字段(如用户的住址、银行卡号、手机号码等)进行加密

//text 为要加密的字段值
RSAEncrypt(text, UTF8Encoding.UTF8.GetBytes(pubkey)); public static string RSAEncrypt(string text, byte[] publicKey)
{
using (var x509 = new X509Certificate2(publicKey))
{
using (var rsa = (RSACryptoServiceProvider)x509.PublicKey.Key)
{
var buff = rsa.Encrypt(Encoding.UTF8.GetBytes(text), RSAEncryptionPadding.OaepSHA1); return Convert.ToBase64String(buff);
}
}
}

RSAEncrypt

这一步需要注意

//使用OaepSHA1
var buff = rsa.Encrypt(Encoding.UTF8.GetBytes(text), RSAEncryptionPadding.OaepSHA1);

三、上传图片

特约商户进件需要上传身份证、营业执照、银行卡等,这就需要通过图片上传API预先生成MediaID

先看接口文档https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter2_1_1.shtml

我封装了一个方法,直接上代码

public string UploadImg(string imgPath)
{
string filePath = HttpContext.Current.Server.MapPath(imgPath);
var filename = Path.GetFileName(filePath);
FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
Byte[] imgBytesIn = new Byte[fs.Length];
fs.Read(imgBytesIn, 0, imgBytesIn.Length);
fs.Close(); byte[] hash = SHA256Managed.Create().ComputeHash(imgBytesIn); StringBuilder builder = new StringBuilder();
for (int i = 0; i < hash.Length; i++)
{
builder.Append(hash[i].ToString("x2"));
}
var sha256 = builder.ToString();
string metaStr = "{\"filename\":\""+ filename + "\",\"sha256\":\"" + sha256 + "\"}";
string media_id = UploadImgApi(metaStr, imgBytesIn, filename);
return media_id;
} public static string UploadImgApi(string metaStr, Byte[] imgBytesIn,string filename)
{
string url = "https://api.mch.weixin.qq.com/v3/merchant/media/upload"; string merchantId = WxPayConfig.MCHID; //商户号
string serialNo = WxPayConfig.SERIAL_NO; //证书编号
string privateKey = WxPayConfig.PRIVATEKEY;
#region 定义请求体中的内容 并转成二进制 string boundary = "lc199aecd61b4653ef";
string Enter = "\r\n";
string campaignIDStr1
= "--" + boundary
+ Enter
+ "Content-Disposition: form-data; name=\"meta\";"
+ Enter
+ "Content-Type:application/json;"
+ Enter
+ Enter
+ metaStr
+ Enter
+ "--" + boundary
+ Enter
+ "Content-Disposition:form-data;name=\"file\";filename=\""+ filename + "\";"
+ Enter
+ "Content-Type:image/jpeg"
+ Enter
+ Enter;
byte[] byteData2
= imgBytesIn;
string campaignIDStr3
= Enter
+ "--" + boundary
+ Enter;
var byteData1 = System.Text.Encoding.UTF8.GetBytes(campaignIDStr1); var byteData3 = System.Text.Encoding.UTF8.GetBytes(campaignIDStr3);
#endregion string transactionsResponse = UploadImg_postJson(url, byteData1, byteData2, byteData3, metaStr, privateKey, merchantId, serialNo, boundary, "POST");
var result=JsonConvert.DeserializeObject<uploadModel>(transactionsResponse);
Thread.Sleep(500);
return result.media_id;
} public class uploadModel
{
public string media_id { get; set; }
}

UploadImg

上传图片api需要注意请求主体类型、参与签名的字符串及body格式

我又单独写了个请求方法

public string UploadImg_postJson(string url, byte[] b1, byte[] b2, byte[] b3, string metaStr, string privateKey, string merchantId, string serialNo, string boundary, string method = "POST")
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Method = method;
//request.ContentType = "application/json;charset=UTF-8";
request.ContentType = "multipart/form-data;boundary=" + boundary;
request.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3100.0 Safari/537.36";
request.Accept = "application/json";
string Authorization = GetAuthorization(url, method, metaStr, privateKey, merchantId, serialNo);
request.Headers.Add("Authorization", Authorization); Stream writer;
try
{
writer = request.GetRequestStream();
}
catch (Exception)
{
writer = null;
}
writer.Write(b1, 0, b1.Length);
writer.Write(b2, 0, b2.Length);
writer.Write(b3, 0, b3.Length);
writer.Close(); HttpWebResponse response;
try
{
response = (HttpWebResponse)request.GetResponse();
}
catch (WebException ex)
{
response = ex.Response as HttpWebResponse;
}
Stream resStream = response.GetResponseStream();
StreamReader reader = new StreamReader(resStream);
string text = reader.ReadToEnd();
return text;
}

UploadImg_postJson

最终返回media_id,存入合适的位置,方便使用

{
"media_id": "H1ihR9JUtVj-J7CJqBUY5ZOrG_Je75H-rKhTG7FUmg9sxNTbRN54dFiUHnhg
rBQ6EKeHoGcHTJMHn5TAuLVjHUQDBInSWXcIHYXOeRa2OHA"
}

四、特约商户进件

上边步骤通了之后,到这就很简单了,这一步的主要难点是请求参数太多了,很容易出错

注意:

• 商户上送敏感信息时使用微信支付平台公钥加密,证书序列号包含在请求HTTP头部的Wechatpay-Seria

需要在请求接口helder里加入Wechatpay-Seria

request.Headers.Add("Wechatpay-Serial", serial_no);

另外一点注意的这里的serial_no是GetCert()接口里获取到的serial_no

调用成功返回微信支付申请单号

{
"applyment_id": 2000002124775691
}

这一步完成之后可以拿到applyment_id请求查询申请单接口获取结果

成功返回sign_url签约连接发给特约商户相应负责人完成签约,即结束商户进件流程

五、生成带参数的小程序码

特约商户签约完成之后,服务商平台便可以代商户发起收款,此时我们需要分别给不同的商户生成不同的收款码,其实只需要传入商家的id即可区别处理

//storeid是商家唯一Id
public static string CreateQR(string storeid)
{
{
var page = "pages/custom/index";//扫码打开页面
var scene = storeid;//参数
//获取小程序的appid和secret
var appId = WxPayConfig.XCXAPPID;
var secret = WxPayConfig.XCXKEY;
string result = HttpGet($"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appId}&secret={secret}"); tokenModel rb = JsonConvert.DeserializeObject<tokenModel>(result); var url = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + rb.access_token; var strUrl = url;
var request = (HttpWebRequest)WebRequest.Create(strUrl);
request.Method = "POST";
request.ContentType = "application/json;charset=UTF-8";
var data = new JsonData { ["page"] = page, ["scene"] = scene }; string jso = data.ToJson();
var payload = Encoding.UTF8.GetBytes(jso);
request.ContentLength = payload.Length;
var writer = request.GetRequestStream();
writer.Write(payload, 0, payload.Length);
writer.Close();
var response = (HttpWebResponse)request.GetResponse();
var s = response.GetResponseStream(); var qrCodeImgBase64 = StreamToBytes(s); //将流保存
string filename= storeid + ".png";
string returnpath= "/UpLoadFiles/StoreQR/" + filename;
string filepath = HttpContext.Current.Server.MapPath("/UpLoadFiles/StoreQR/") + filename;
System.IO.File.WriteAllBytes(filepath, qrCodeImgBase64);
return returnpath;
}
}

CreateQR

六、小程序支付JSAPI下单接口

有了商家码就能分别为商家发起支付申请,签约商户都有一个分配的商户号sub_mchid,注意请求参数里的商户号即这个,需要根据二维码的参数去获取

public static string V3Pay(string sub_mchid,string openid,int amount,string ordernumber,string description)
{
string url = "https://api.mch.weixin.qq.com/v3/pay/partner/transactions/jsapi"; string appid = WxPayConfig.XCXAPPID;
string merchantId = WxPayConfig.MCHID; //商户号
string serialNo = WxPayConfig.SERIAL_NO; //证书编号
string privateKey = WxPayConfig.PRIVATEKEY; // NOTE: 私钥不包括私钥文件起始的-----BEGIN PRIVATE KEY----- 亦不包括结尾的-----END PRIVATE KEY----- WxPayData postData = new WxPayData();
postData.SetValue("sp_appid", appid);
postData.SetValue("sp_mchid", merchantId);
postData.SetValue("sub_mchid", sub_mchid);
postData.SetValue("description", description);
postData.SetValue("out_trade_no", ordernumber);
postData.SetValue("notify_url", WxPayConfig.NOTIFY_URL);
WxPayData settle_info = new WxPayData();
settle_info.SetValue("profit_sharing",true);
postData.SetValue("settle_info", settle_info);
WxPayData _amount = new WxPayData();
_amount.SetValue("total", amount);
_amount.SetValue("currency", "CNY");
postData.SetValue("amount", _amount);
WxPayData payer = new WxPayData();
payer.SetValue("sp_openid", openid);
postData.SetValue("payer", payer); var postJson = postData.ToJsonFor();
string result = postJson(url, postJson, privateKey, merchantId, serialNo, "POST");
var result = JsonConvert.DeserializeObject<payModel>(result);
return result.prepay_id;
}

V3Pay

请求参数按照自己的方法去构建,json格式

通过JSAPI下单接口获取到发起支付的必要参数prepay_id,然后使用微信支付提供的小程序方法调起小程序支付

public static string GetJsApiParameters(string prepay_id)
{
string appid = WxPayConfig.XCXAPPID;
string privateKey = WxPayConfig.PRIVATEKEY;
string timestamp = WxPayApi.GenerateTimeStamp();
string nonceStr = WxPayApi.GenerateNonceStr();
string package = "prepay_id=" + prepay_id;
WxPayData jsApiParam = new WxPayData();
jsApiParam.SetValue("appId", appid);
jsApiParam.SetValue("timeStamp", timestamp);
jsApiParam.SetValue("nonceStr", nonceStr);
jsApiParam.SetValue("package", package);
jsApiParam.SetValue("signType", "RSA");
string message = string.Format("{0}\n{1}\n{2}\n{3}\n", appid, timestamp, nonceStr, package);
string signTxt = Sign(message, privateKey);
jsApiParam.SetValue("paySign", signTxt); string parameters = jsApiParam.ToJson();
return parameters;
}

GetJsApiParameters

返回给小程序调用wx.requestPayment(OBJECT)发起微信支付

注意回调URL:该链接是通过基础下单接口中的请求参数“notify_url”来设置的,要求必须为https地址。请确保回调URL是外部可正常访问的,且不能携带后缀参数,否则可能导致商户无法接收到微信的回调通知信息。回调URL示例: “https://pay.weixin.qq.com/wxpay/pay.action”

public class V3Notify
{ public callbackViewModel GetNotifyData()
{
//接收从微信后台POST过来的数据
System.IO.Stream s = System.Web.HttpContext.Current.Request.InputStream;
int count = 0;
byte[] buffer = new byte[1024];
StringBuilder builder = new StringBuilder();
while ((count = s.Read(buffer, 0, 1024)) > 0)
{
builder.Append(Encoding.UTF8.GetString(buffer, 0, count));
}
s.Flush();
s.Close();
s.Dispose(); var ReadStr = builder.ToString(); notifyModel wxPayNotify = Newtonsoft.Json.JsonConvert.DeserializeObject<notifyModel>(ReadStr);
//开始解密
string WxPayResourceDecryptModel = AesGcmHelper.AesGcmDecrypt(wxPayNotify.resource.associated_data, wxPayNotify.resource.nonce, wxPayNotify.resource.ciphertext);
var decryptModel= Newtonsoft.Json.JsonConvert.DeserializeObject<WxPayResourceDecryptModel>(WxPayResourceDecryptModel); var viewModel = new callbackViewModel();
if (decryptModel != null)
{
//查询
var model = queryOrder(decryptModel.out_trade_no, decryptModel.sp_mchid, decryptModel.sub_mchid);//订单查询接口
viewModel.code = model.trade_state;
viewModel.message = model.trade_state_desc;
}
else
{
viewModel.code = "FAIL";
viewModel.message = "数据解密失败";
}
return viewModel;
}
}

V3Notify

整个下单没有牵涉到业务方面的代码,你可以把你的业务代码写在合适的位置处理

七、分账

在请求分账之前先在微信服务商后台邀约商户授权分账并指定分账比例(最大30%),并且发起支付申请时请求参数指定profit_sharing为true

1、首先添加分账接收方

接收方必须先调用接口添加才能在下一步请求分账时使用,注意不允许重复的openid

public static receiverModel AddReceivers(string sub_mchid,string type,string account,string relation_type)
{
string url = "https://api.mch.weixin.qq.com/v3/profitsharing/receivers/add"; string appid = WxPayConfig.XCXAPPID;
string merchantId = WxPayConfig.MCHID; //商户号
string serialNo = WxPayConfig.SERIAL_NO; //证书编号
string privateKey = WxPayConfig.PRIVATEKEY; // NOTE: 私钥不包括私钥文件起始的-----BEGIN PRIVATE KEY----- 亦不包括结尾的-----END PRIVATE KEY----- WxPayData postData = new WxPayData();
postData.SetValue("sub_mchid", sub_mchid);
postData.SetValue("appid", appid);
postData.SetValue("type", type);
postData.SetValue("account", account);
postData.SetValue("relation_type", relation_type);
string postJson = postData.ToJson(); string transactionsResponse = postJson(url, postJson, privateKey, merchantId, serialNo, "POST");
var result = JsonConvert.DeserializeObject<receiverModel>(transactionsResponse);
return result;
}

AddReceivers

2、请求分账

public static receiverModel PayReceivers(string sub_mchid, string transaction_id, string out_order_no,List<WxPayData> receivers)
{
string url = "https://api.mch.weixin.qq.com/v3/profitsharing/orders"; string appid = WxPayConfig.XCXAPPID;
string merchantId = WxPayConfig.MCHID; //商户号
string serialNo = WxPayConfig.SERIAL_NO; //证书编号
string privateKey = WxPayConfig.PRIVATEKEY; // NOTE: 私钥不包括私钥文件起始的-----BEGIN PRIVATE KEY----- 亦不包括结尾的-----END PRIVATE KEY----- WxPayData postData = new WxPayData();
postData.SetValue("sub_mchid", sub_mchid);
postData.SetValue("appid", appid);
postData.SetValue("transaction_id", transaction_id);
postData.SetValue("out_order_no", out_order_no);
postData.SetValue("unfreeze_unsplit", true);
postData.SetValue("receivers", receivers);
string postJson = postData.ToJsonFor();
LogHelper.WriteLogToFile("PayReceivers-postJson:" + postJson); string transactionsResponse = postJson(url, postJson, privateKey, merchantId, serialNo, "POST");
var result = JsonConvert.DeserializeObject<receiverModel>(transactionsResponse);
return result;
}

PayReceivers

用到的model

public class receiverModel
{
public string sub_mchid { get; set; }
public string type { get; set; }
public string account { get; set; }
public string name { get; set; }
public string relation_type { get; set; }
public string custom_relation { get; set; }
}

receiverModel

至此完整流程全部走完,总结下来技术难度并不大,就是步骤多,并且要把握好调用的时机,再一个就是请求参数多,容易出错,要耐心。

如有问题请在评论区留言或私信我

最新文章

  1. [转]c++ vector 遍历方式
  2. JavaBean-DAO模式
  3. MATLAB将矩阵使用.txt文件格式保存
  4. [企业级linux安全管理]- 安全管理基础(1)
  5. JQuery的父、子、兄弟节点查找,节点的子节点循环
  6. IL来理解属性
  7. python——面向对象进阶
  8. 深入理解javascript函数进阶系列第四篇——惰性函数
  9. multiset和set
  10. 深刻理解Python中的元类(metaclass)【转】
  11. java中如何认定一个变量和方法
  12. vue面试
  13. hdu 5039 线段树+dfs序
  14. Linux服务器安装redis数据库教程
  15. Java中执行.exe文件
  16. nginx实战二
  17. Linq系列(5)——表达式树之案例应用
  18. flyweight模式
  19. VC中CRect类的简单介绍
  20. Oracle笔记-Multitable INSERT 的用法

热门文章

  1. android kotlin 子线程中调用界面UI组件崩溃
  2. .ssh/config 常用配置
  3. MySQL——备份与恢复
  4. 20210712考试-2021noip11
  5. Python - 虚拟环境 venv
  6. js不同地图坐标系经纬度转换(天地图,高德地图,百度地图,腾讯地图)
  7. 计算字符串的长度.len,RuneCountInString
  8. java的split方法中的regex参数
  9. Docker安装flink及避坑指南
  10. Jmeter导出测试报告