提交 c675b8b7 编写于 作者: K kuiyu

增加抖音担保支付业务SDK

上级 8936129c
......@@ -306,3 +306,6 @@ OpenCover/
upload
Plugins
/tests/RsCode.Storage.Tests/appsettings.json
/tests/RsCode.Douyin.Tests/douyin.json
/tests/RsCode.Douyin.Tests/appsettings.json
/tests/RsCode.Douyin.Tests/cert/douyin

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30523.141
# Visual Studio Version 17
VisualStudioVersion = 17.5.33424.131
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RsCode.AspNetCore.Tests", "tests\RsCode.AspNetCore.Tests\RsCode.AspNetCore.Tests.csproj", "{0EC076FE-1424-40D1-B27E-7DF86B12123E}"
EndProject
......@@ -19,6 +19,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RsCode.Storage.QiniuStorage
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RsCode.Storage.Tests", "tests\RsCode.Storage.Tests\RsCode.Storage.Tests.csproj", "{4018983D-89AB-4A7F-81DC-77631FC0D3D1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RsCode.Douyin", "src\RsCode.Douyin\RsCode.Douyin.csproj", "{A12AA294-E96D-4475-B0A6-B793D96E1EFF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RsCode.Douyin.Tests", "tests\RsCode.Douyin.Tests\RsCode.Douyin.Tests.csproj", "{64052172-FEA5-4CB5-90C7-F04743C05D36}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
......@@ -53,6 +57,14 @@ Global
{4018983D-89AB-4A7F-81DC-77631FC0D3D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4018983D-89AB-4A7F-81DC-77631FC0D3D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4018983D-89AB-4A7F-81DC-77631FC0D3D1}.Release|Any CPU.Build.0 = Release|Any CPU
{A12AA294-E96D-4475-B0A6-B793D96E1EFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A12AA294-E96D-4475-B0A6-B793D96E1EFF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A12AA294-E96D-4475-B0A6-B793D96E1EFF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A12AA294-E96D-4475-B0A6-B793D96E1EFF}.Release|Any CPU.Build.0 = Release|Any CPU
{64052172-FEA5-4CB5-90C7-F04743C05D36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{64052172-FEA5-4CB5-90C7-F04743C05D36}.Debug|Any CPU.Build.0 = Debug|Any CPU
{64052172-FEA5-4CB5-90C7-F04743C05D36}.Release|Any CPU.ActiveCfg = Release|Any CPU
{64052172-FEA5-4CB5-90C7-F04743C05D36}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
......@@ -60,6 +72,7 @@ Global
GlobalSection(NestedProjects) = preSolution
{0EC076FE-1424-40D1-B27E-7DF86B12123E} = {A1F1CC2D-DD98-4784-A2A7-36FF487F0476}
{4018983D-89AB-4A7F-81DC-77631FC0D3D1} = {A1F1CC2D-DD98-4784-A2A7-36FF487F0476}
{64052172-FEA5-4CB5-90C7-F04743C05D36} = {A1F1CC2D-DD98-4784-A2A7-36FF487F0476}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E888CD95-C5BE-484E-A8A1-356835B130AD}
......
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
namespace RsCode.Douyin.Core
{
public class DouyinClient : IDouyinClient
{
List<DouyinOptions> options;
DouyinHttpClient httpClient;
IHttpContextAccessor httpContextAccessor;
ILogger log;
public DouyinClient( IOptionsSnapshot<List<DouyinOptions>> options, DouyinHttpClient httpClient, IHttpContextAccessor httpContextAccessor,ILogger<DouyinClient> logger)
{
this.options = options.Value;
this.httpClient = httpClient;
this.httpContextAccessor = httpContextAccessor;
log= logger;
}
DouyinOptions clientOptions { get; set; }
public DouyinOptions UseAppId(string appId)
{
clientOptions = options.FirstOrDefault(o => o.AppId == appId);
if (clientOptions == null)
throw new Exception($"配置有误,未找到appid={appId}配置");
var httpHandler = new HttpHandler(clientOptions);
httpClient.LoadHandler(httpHandler);
return clientOptions;
}
public async Task<T> SendAsync<T>(DouyinRequest request) where T : DouyinResponse
{
try
{
if (clientOptions == null)
{
throw new Exception("未调用UseAppId()方法");
}
var url = request.GetApiUrl();
var method = request.RequestMethod().ToUpper();
if (method == "GET")
{
var res = await httpClient.GetAsync<T>(url);
return res;
}
if (method == "POST")
{
string json = JsonSerializer.Serialize(request, request.GetType(),new JsonSerializerOptions {Encoder= JavaScriptEncoder.Create(System.Text.Unicode.UnicodeRanges.All) });
HttpContent httpContent = new StringContent(json, Encoding.UTF8, "application/json");
var res = await httpClient.PostAsync<T>(url, httpContent);
return res;
}
throw new Exception("未定义的请求方式");
}
catch (Exception e)
{
throw e;
}
}
public string GetIp()
{
var httpContext = httpContextAccessor;
var ip = httpContext.HttpContext?.Request.Headers["X-Real-IP"].FirstOrDefault() ?? httpContext.HttpContext?.Request.HttpContext.Connection.RemoteIpAddress.ToString();
return string.IsNullOrWhiteSpace(ip) ? "0.0.0.0" : ip;
}
}
}
\ No newline at end of file
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System.Text.Json;
namespace RsCode.Douyin.Core
{
public class DouyinHttpClient
{
public HttpClient httpClient { get; private set; }
public DouyinHttpClient(HttpClient client)
{
httpClient = client;
}
public void LoadHandler(HttpHandler httpHandler)
{
httpClient = new HttpClient(httpHandler);
httpClient.BaseAddress = new Uri("https://open.douyin.com");
}
public async Task<T> GetAsync<T>(string url)
where T : DouyinResponse
{
using (var response = await httpClient.GetAsync(url))
{
int statusCode = Convert.ToInt32(response.StatusCode);
using (var responseStream = await response.Content.ReadAsStreamAsync())
{
var res = await response.Content.ReadAsStringAsync();
return await JsonSerializer.DeserializeAsync<T>(responseStream);
}
}
}
public async Task<T> PostAsync<T>(string url, HttpContent httpContent)
where T : DouyinResponse
{
using (httpContent)
using (var response = await httpClient.PostAsync(url, httpContent))
{
var res = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<T>(res);
}
}
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System.Runtime.InteropServices;
using System.Text.Json.Serialization;
namespace RsCode.Douyin
{
public class DouyinOptions
{
/// <summary>
/// 获取或设置抖音开放平台应用 Key。
/// </summary>
[Newtonsoft.Json.JsonProperty("appid")]
[JsonPropertyName("appid")] public string AppId { get; set; } = default!;
/// <summary>
/// 获取或设置抖音开放平台应用密钥。
/// </summary>
[Newtonsoft.Json.JsonProperty("secret")]
[JsonPropertyName("secret")]
public string Secret { get; set; } = default!;
/// <summary>
/// salt
/// </summary>
[Newtonsoft.Json.JsonProperty("salt")]
[JsonPropertyName("salt")]
public string Salt { get; set; } = default!;
/// <summary>
/// 【某小程序】-【功能】-【支付】-【支付产品】-【支付设置】中获取Token
/// </summary>
[Newtonsoft.Json.JsonProperty("token")]
[JsonPropertyName("token")]
public string Token { get; set; } = default!;
/// <summary>
/// 某应用的私钥路径
/// </summary>
[Newtonsoft.Json.JsonProperty("privateKeyPath")]
[JsonPropertyName("privateKeyPath")]
public string PrivateKeyPath { get; set; } = default!;
/// <summary>
/// 抖音平台的公钥路径
/// </summary>
[Newtonsoft.Json.JsonProperty("publicKeyPath")]
[JsonPropertyName("publicKeyPath")]
public string PublicKeyPath { get; set; } = default!;
public string GetPublicKey()
{
if (string.IsNullOrWhiteSpace(PublicKeyPath))
{
throw new Exception("未配置抖音平台公钥路径");
}
var path = Path.Combine(Environment.CurrentDirectory, PublicKeyPath);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
path = path.Replace("/", "\\");
}
else
{
path = path.Replace("\\", "/");
}
if (!File.Exists(path))
{
throw new Exception($"未找到抖音平台公钥文件{path}");
}
string s = File.ReadAllText(path);//.Replace("-----BEGIN PUBLIC KEY-----", "").Replace("-----END PUBLIC KEY-----", "").Replace("\n", "");
return s;
}
public string GetPrivateKey()
{
if (string.IsNullOrWhiteSpace(PrivateKeyPath))
{
throw new Exception("未配置抖音应用私钥文件");
}
var path = Path.Combine(Environment.CurrentDirectory, PrivateKeyPath);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
path = path.Replace("/", "\\");
}
else
{
path = path.Replace("\\", "/");
}
if (!File.Exists(path))
{
throw new Exception($"未找到抖音应用私钥文件 {path}");
}
string s = File.ReadAllText(path).Replace("-----BEGIN PRIVATE KEY-----", "").Replace("-----END PRIVATE KEY-----", "").Replace("-----BEGIN RSA PRIVATE KEY-----", "").Replace("-----END RSA PRIVATE KEY-----", "").Replace("\n","");
return s;
}
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
namespace RsCode.Douyin
{
public abstract class DouyinRequest
{
public virtual string RequestMethod()
{
return "POST";
}
public virtual string GetApiUrl()
{
throw new Exception("请配置接口地址");
}
/// <summary>
/// 担保支付请求签名算法
/// </summary>
/// <param name="options"></param>
/// <returns></returns>
public string CreateParamSign(DouyinOptions options)
{
List<string> paramList = new List<string>();
//对属性进行ascii码排序
var props = this.GetType().GetProperties();
foreach (var prop in props)
{
//排除不需要参数
if (prop.Name == "AppId" || prop.Name == "ThirdPartyId" || prop.Name == "Sign" || prop.Name == "OtherSettleParams")
{ continue;
}
var value = prop.GetValue(this);
if (value == null)
{
//paramList.Add("<nil>");
}
else
{
paramList.Add(value.ToString().Trim());
}
}
paramList.Add(options.Salt);
var list = paramList.ToArray();
Array.Sort(list, string.CompareOrdinal);
string s = "";
foreach (var obj in list)
{
if(obj==null||string.IsNullOrWhiteSpace(obj))
{
continue;
}
string value = obj.ToString();
value = value.Trim();
if(value.Equals("")||value.Equals("null"))
{
continue;
}
if (string.IsNullOrEmpty(s))
{
s=obj.Trim();
}else
{
s += $"&{obj}";
}
}
var result = DouyinTool.MD5Sign(s);
return result;
}
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System.Text.Json.Serialization;
namespace RsCode.Douyin.Core
{
public class DouyinResponse
{
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using RsCode.Douyin.Core;
using System.Text.Json.Serialization;
namespace RsCode.Douyin
{
public class DouyinResult<T>
where T:DouyinResponse
{
/// <summary>
/// 错误码
/// </summary>
[JsonPropertyName("err_no")]
public long ErrNo { get; set; }
/// <summary>
/// 错误信息
/// </summary>
[JsonPropertyName("err_tips")]
public string ErrTips { get; set; }
[JsonPropertyName("data")]
public T Data { get; set; }
[JsonPropertyName("error")]
public int Error { get; set; }
[JsonPropertyName("errcode")]
public long ErrCode { get; set; }
[JsonPropertyName("errmsg")]
public string ErrMsg { get; set; }
[JsonPropertyName("message")]
public string Message { get; set; }
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Http;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Modes;
using Org.BouncyCastle.Crypto.Parameters;
using Microsoft.Extensions.Primitives;
namespace RsCode.Douyin.Core
{
public class DouyinTool
{
/// <summary>
/// 生成随机数
/// </summary>
/// <returns></returns>
public static string GetNonceStr()
{
var randomGenerator = new RandomGenerator();
return randomGenerator.GetRandomUInt().ToString();
}
/// <summary>
/// 生成时间戳,标准北京时间,时区为东八区,自1970年1月1日 0点0分0秒以来的秒数
/// </summary>
/// <returns></returns>
public static string GetTimeStamp()
{
TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
return Convert.ToInt64(ts.TotalSeconds).ToString();
}
/// 计算文件的 SHA256 值
/// </summary>
/// <param name="fileName">要计算 SHA256 值的文件名和路径</param>
/// <returns>SHA256值16进制字符串</returns>
public static string SHA256File(string fileName)
{
return HashFile(fileName, "sha256");
}
public static string HashFile(FileStream fs, string algName = "sha256")
{
using (fs)
{
byte[] hashBytes = HashData(fs, algName);
return ByteArrayToHexString(hashBytes);
}
}
/// <summary>
/// 计算文件的哈希值
/// </summary>
/// <param name="fileName">要计算哈希值的文件名和路径</param>
/// <param name="algName">算法:sha1,md5</param>
/// <returns>哈希值16进制字符串</returns>
private static string HashFile(string fileName, string algName)
{
if (!System.IO.File.Exists(fileName))
return string.Empty;
FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read);
byte[] hashBytes = HashData(fs, algName);
fs.Close();
return ByteArrayToHexString(hashBytes);
}
/// <summary>
/// 字节数组转换为16进制表示的字符串
/// </summary>
private static string ByteArrayToHexString(byte[] buf)
{
string returnStr = "";
if (buf != null)
{
for (int i = 0; i < buf.Length; i++)
{
returnStr += buf[i].ToString("X2");
}
}
return returnStr;
}
/// <summary>
/// 计算哈希值
/// </summary>
/// <param name="stream">要计算哈希值的 Stream</param>
/// <param name="algName">算法:sha1,md5</param>
/// <returns>哈希值字节数组</returns>
private static byte[] HashData(Stream stream, string algName)
{
HashAlgorithm algorithm;
if (algName == null)
{
throw new ArgumentNullException("algName 不能为 null");
}
if (string.Compare(algName, "sha256", true) == 0)
{
algorithm = SHA256.Create();
}
else
{
if (string.Compare(algName, "md5", true) != 0)
{
throw new Exception("algName 只能使用 sha256 或 md5");
}
algorithm = System.Security.Cryptography.MD5.Create();
}
return algorithm.ComputeHash(stream);
}
public static bool IsBase64String(string str)
{
try
{
Convert.FromBase64String(str);
return true;
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// 使用平台公钥对提交的信息进行加密
/// 最终提交请求时,需对敏感信息加密,如身份证、银行卡号。
/// 加密算法是RSA,使用从接口下载到的公钥进行加密,非后台下载到的私钥。
///
/// </summary>
/// <param name="text">要加密的明文</param>
/// <param name="publicKey"> -----BEGIN CERTIFICATE----- 开头的string,转为bytes </param>
/// <returns></returns>
public static string RSAEncrypt(string text, byte[] publicKey)
{
using (var x509 = new X509Certificate2(publicKey))
{
var rsaParam = x509.GetRSAPublicKey().ExportParameters(false);
var rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(rsaParam);
var buff = rsa.Encrypt(Encoding.UTF8.GetBytes(text), true);
return Convert.ToBase64String(buff);
}
}
public static string AesGcmDecrypt(string AES_KEY, 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);
}
/// <summary>
/// 数据签名
/// </summary>
/// <param name="message"></param>
/// <param name="rsa">签名的证书</param>
/// <returns></returns>
public static string Sign(string message, DouyinOptions options)
{
string privateKey = options.GetPrivateKey();
byte[] keyData = Convert.FromBase64String(privateKey);
var rsa = RSA.Create();
//适用该方法的版本https://learn.microsoft.com/zh-cn/dotnet/api/system.security.cryptography.asymmetricalgorithm.importpkcs8privatekey?view=net-7.0
// rsa.ImportPkcs8PrivateKey(keyData, out _); java
rsa.ImportRSAPrivateKey(keyData, out _);
byte[] data = System.Text.Encoding.UTF8.GetBytes(message);
return Convert.ToBase64String(rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
}
public static int Price(decimal price)
{
return Convert.ToInt32(price * 100);
}
public static decimal Price(int price)
{
return decimal.Round(Convert.ToDecimal(price) / 100, 2);
}
/// <summary>
/// rfc3339标准格式时间
/// </summary>
/// <param name="dt"></param>
/// <returns></returns>
public static string ConvertDateTime(DateTime dt)
{
return dt.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK");
}
/// <summary>
/// 获取客户端IP
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public string GetIp(HttpContext context)
{
string ip = "0.0.0.0";
if (context != null)
{
ip = context.Request.Headers["X-Forwarded-For"].FirstOrDefault();
if (string.IsNullOrEmpty(ip))
{
ip = context.Connection.RemoteIpAddress.ToString();
}
}
return ip;
}
public static string MD5Sign(string inputStr)
{
byte[] hashBytes = MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(inputStr));
StringBuilder sb = new StringBuilder();
foreach (byte b in hashBytes)
{
sb.Append(b.ToString("x2"));
}
return sb.ToString();
}
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System.Net.Http.Headers;
using System.Security.Authentication;
namespace RsCode.Douyin.Core
{
public class HttpHandler:DelegatingHandler
{
DouyinOptions options;
public HttpHandler(DouyinOptions options)
{
this.options = options;
HttpClientHandler handler = new HttpClientHandler();
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
handler.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls;
handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true;
InnerHandler = handler;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var auth = await BuildAuthAsync(request);
request.Headers.Add("Byte-Authorization", $"SHA256-RSA2048 {auth}");
// request.Headers.Add("Content-Type", "application/json");
request.Headers.UserAgent.Add(new ProductInfoHeaderValue(new ProductHeaderValue("Unknown")));
request.Headers.AcceptCharset.Add(new StringWithQualityHeaderValue("utf-8"));
return await base.SendAsync(request, cancellationToken);
}
protected async Task<string> BuildAuthAsync(HttpRequestMessage request)
{
string method = request.Method.ToString();
string body = "";
if (method == "POST" || method == "PUT" || method == "PATCH")
{
var content = request.Content;
body = await content.ReadAsStringAsync();
}
string uri = request.RequestUri.PathAndQuery;
var timestamp = DateTimeOffset.Now.ToUnixTimeSeconds();
string nonce = DouyinTool.GetNonceStr();// Path.GetRandomFileName();
string message = $"{method}\n{uri}\n{timestamp}\n{nonce}\n{body}\n";
string signature =DouyinTool.Sign(message,options);
return $"appid=\"{options.AppId}\",nonce_str=\"{nonce}\",timestamp=\"{timestamp}\",signature=\"{signature}\"";
}
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using Microsoft.AspNetCore.Http;
using RsCode.Douyin.Core;
namespace RsCode.Douyin
{
public interface IDouyinClient
{
Task<T> SendAsync<T>(DouyinRequest request) where T : DouyinResponse;
DouyinOptions UseAppId(string appId);
string GetIp();
}
}
\ No newline at end of file
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System.Security.Cryptography;
namespace RsCode.Douyin.Core
{
public class RandomGenerator
{
readonly RNGCryptoServiceProvider csp;
public RandomGenerator()
{
csp = new RNGCryptoServiceProvider();
}
public int Next(int minValue, int maxExclusiveValue)
{
if (minValue >= maxExclusiveValue)
throw new ArgumentOutOfRangeException("minValue must be lower than maxExclusiveValue");
long diff = (long)maxExclusiveValue - minValue;
long upperBound = uint.MaxValue / diff * diff;
uint ui;
do
{
ui = GetRandomUInt();
} while (ui >= upperBound);
return (int)(minValue + (ui % diff));
}
public uint GetRandomUInt()
{
var randomBytes = GenerateRandomBytes(sizeof(uint));
return BitConverter.ToUInt32(randomBytes, 0);
}
private byte[] GenerateRandomBytes(int bytesNumber)
{
byte[] buffer = new byte[bytesNumber];
csp.GetBytes(buffer);
return buffer;
}
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using RsCode.Douyin.Core;
namespace RsCode.Douyin
{
public static class ServiceExtensions
{
public static void AddDouyin(this IServiceCollection services)
{
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddHttpClient<DouyinHttpClient>();
services.AddScoped<IDouyinClient, DouyinClient>();
var configBuilder = new ConfigurationBuilder()
.AddJsonFile("douyin.json", optional: true, reloadOnChange: true);
var config = configBuilder.Build();
var app = config.GetSection("app");
services.Configure<List<DouyinOptions>>(options => config.GetSection("app").Bind(options));
//增加内存缓存
services.AddMemoryCache();
}
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System.Text.Json.Serialization;
namespace RsCode.Douyin.OAuth
{
//https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/server/interface-request-credential/get-access-token
public class AccessTokenRequest: DouyinRequest
{
public AccessTokenRequest()
{
}
public AccessTokenRequest(string appid,string secret)
{
AppId = appid;
AppSecret = secret;
}
public AccessTokenRequest(DouyinOptions options)
{
AppId=options.AppId; AppSecret=options.Secret;
}
/// <summary>
/// 获取或设置应用 ID
/// </summary>
[JsonPropertyName("appid")]
public string AppId { get; set; }
/// <summary>
/// 获取或设置应用密钥。
/// </summary>
[JsonPropertyName("secret")]
public string AppSecret { get; set; }
/// <summary>
///获取 access_token 时值为 client_credential
/// </summary>
[JsonPropertyName("grant_type")]
public string GrantType { get; set; } = "client_credential";
public override string GetApiUrl()
{
//https://open-sandbox.douyin.com/api/apps/v2/token
return "https://developer.toutiao.com/api/apps/v2/token";
}
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using Newtonsoft.Json;
using RsCode.Douyin.Core;
using System.Text.Json.Serialization;
namespace RsCode.Douyin.OAuth
{
//https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/server/interface-request-credential/get-access-token
public class AccessTokenResponse:DouyinResponse
{
/// <summary>
/// 错误码
/// </summary>
[JsonPropertyName("err_no")]
public long ErrNo { get; set; }
/// <summary>
/// 错误信息
/// </summary>
[JsonPropertyName("err_tips")]
public string ErrTips { get; set; }
[JsonPropertyName("data")]
public AccessTokenResult Data { get; set; }
}
public class AccessTokenResult
{
/// <summary>
///获取的 access_token
/// </summary>
[JsonPropertyName("access_token")]
[JsonProperty("access_token")]
public string AccessToken { get; set; }
/// <summary>
/// access_token 有效时间,单位:秒
/// </summary>
[JsonPropertyName("expires_in")]
[JsonProperty("expires_in")]
public int ExpiresIn { get; set; }
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System.Text.Json.Serialization;
namespace RsCode.Douyin.OAuth
{
/// <summary>
/// <see cref="https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/server/log-in/code-2-session"/>
/// </summary>
public class Code2SessionRequest:DouyinRequest
{
public Code2SessionRequest(DouyinOptions options,string code,string anonymousCode)
{
AppId = options.AppId;
AppSecret = options.Secret;
if(!string.IsNullOrWhiteSpace(code))
{
Code= code;
}
if (!string.IsNullOrWhiteSpace(anonymousCode))
{
AnonymousCode= anonymousCode;
}
}
/// <summary>
/// 小程序 ID
/// </summary>
[JsonPropertyName("appid")]
public string AppId { get; set; }
/// <summary>
/// 小程序的 APP Secret,可以在开发者后台获取
/// </summary>
[JsonPropertyName("secret")]
public string AppSecret { get; set; }
/// <summary>
///获取 access_token 时值为 client_credential
/// </summary>
[JsonPropertyName("code")]
public string Code { get; set; }
/// <summary>
/// login 接口返回的匿名登录凭证
/// </summary>
[JsonPropertyName("anonymous_code")]
public string AnonymousCode { get; set; }
public override string GetApiUrl()
{
//沙盒地址 https://open-sandbox.douyin.com/api/apps/v2/jscode2session
return "https://developer.toutiao.com/api/apps/v2/jscode2session";
}
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using RsCode.Douyin.Core;
using System.Text.Json.Serialization;
namespace RsCode.Douyin.OAuth
{
public class Code2SessionResponse:DouyinResponse
{
/// <summary>
/// 错误码
/// </summary>
[JsonPropertyName("err_no")]
public long ErrNo { get; set; }
/// <summary>
/// 错误信息
/// </summary>
[JsonPropertyName("err_tips")]
public string ErrTips { get; set; }
[JsonPropertyName("data")]
public Code2SessionResult Data { get; set; }
}
public class Code2SessionResult
{
/// <summary>
/// 会话密钥,如果请求时有 code 参数才会返回
/// </summary>
[JsonPropertyName("session_key")]
public string SessionKey { get; set; }
/// <summary>
/// 用户在当前小程序的 ID,如果请求时有 code 参数才会返回
/// </summary>
[JsonPropertyName("openid")]
public string OpenId { get; set; }
/// <summary>
/// 匿名用户在当前小程序的 ID,如果请求时有 anonymous_code 参数才会返回
/// </summary>
[JsonPropertyName("anonymous_openid")]
public string AnonymousOpenId { get; set; }
/// <summary>
/// 用户在小程序平台的唯一标识符,请求时有 code 参数才会返回。如果开发者拥有多个小程序,可通过 unionid 来区分用户的唯一性。
/// </summary>
[JsonPropertyName("unionid")]
public string UnionId { get; set; }
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System.Text.Json.Serialization;
namespace RsCode.Douyin.Payment
{
public class NpoiOrder
{
/// <summary>
/// 开发者侧业务单号。用作幂等控制。该订单号是和担保支付的支付单号绑定的,也就是预下单时传入的 out_order_no 字段,长度 <= 64byte
/// </summary>
[JsonPropertyName("order_id")]
public string OrderId { get; set; }
/// <summary>
/// 订单创建的时间,13 位毫秒时间戳
/// </summary>
[JsonPropertyName("create_time")]
public long CreateTime { get; set; }
/// <summary>
///订单状态
/// </summary>
[JsonPropertyName("status")]
public string Status { get; set; }
/// <summary>
/// 订单商品总数
/// </summary>
[JsonPropertyName("amount")]
public int Amount { get; set; }
/// <summary>
/// 订单总价,单位为分
/// </summary>
[JsonPropertyName("total_price")]
public int TotalPrice { get; set; }
/// <summary>
/// 小程序订单详情页 path,长度<=1024 byte
/// </summary>
[JsonPropertyName("detail_url")]
public string DetailUrl { get; set; }
/// <summary>
/// 子订单商品列表,不可为空
/// </summary>
[JsonPropertyName("item_list")]
public NpoiItem[] ItemList { get; set; }
}
public class NpoiItem
{
/// <summary>
/// 开发者侧商品 ID,长度 <= 64 byte
/// </summary>
[JsonPropertyName("item_code")]
public string ItemCode { get; set; }
/// <summary>
/// 子订单商品图片 URL, 长度 <= 512 byte
/// </summary>
[JsonPropertyName("img")]
public string Img { get; set; }
/// <summary>
/// 子订单商品介绍标题,长度 <= 256 byte
/// </summary>
[JsonPropertyName("title")]
public string Title { get; set; }
/// <summary>
/// 子订单商品介绍副标题,长度 <= 256 byte
/// </summary>
[JsonPropertyName("sub_title")]
public string SubTitle { get; set; }
/// <summary>
/// 单类商品的数目
/// </summary>
[JsonPropertyName("amount")]
public int Amount { get; set; }
/// <summary>
/// 单类商品的总价,单位为分
/// </summary>
[JsonPropertyName("price")]
public int Price { get; set; }
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System.Text.Json.Serialization;
namespace RsCode.Douyin.Payment
{
public class OrderSyncRequest:DouyinRequest
{
public override string GetApiUrl()
{
return "https://developer.toutiao.com/api/apps/order/v2/push";
}
/// <summary>
/// 第三方在抖音开放平台申请的 ClientKey
///注意:POI 订单必传
/// </summary>
[JsonPropertyName("client_key")]
public string ClientKey { get; set; }
/// <summary>
/// 服务端 API 调用标识,通过 getAccessToken 获取
/// </summary>
[JsonPropertyName("access_token")]
public string AccessToken { get; set; }
/// <summary>
/// POI 店铺同步时使用的开发者侧店铺 ID,购买店铺 ID,长度 < 256 byte 注意:POI 订单必传
/// </summary>
[JsonPropertyName("ext_shop_id")]
public int ExtShopId { get; set; }
/// <summary>
/// 做订单展示的字节系 app 名称,目前为固定值“douyin”
/// </summary>
[JsonPropertyName("app_name")]
public string AppName { get; set; } = "douyin";
/// <summary>
/// 小程序用户的 open_id,通过 code2Session 获取
/// </summary>
[JsonPropertyName("open_id")]
public string OpenId { get; set; }
/// <summary>
/// json string,根据不同订单类型有不同的结构体,请参见 order_detail 字段说明(json string)
/// </summary>
[JsonPropertyName("order_detail")]
public string OrderDetail { get; set; }
/// <summary>
/// 普通小程序订单订单状态,POI 订单可以忽略
///0:待支付
///1:已支付
///2:已取消(用户主动取消或者超时未支付导致的关单)
///4:已核销(核销状态是整单核销,即一笔订单买了 3 个券,核销是指 3 个券核销的整单)
///5:退款中
///6:已退款
///8:退款失败
///注意:普通小程序订单必传,担保支付分账依赖该状态
/// </summary>
[JsonPropertyName("order_status")]
public int OrderStatus { get; set; }
/// <summary>
/// 订单类型,枚举值:
///0:普通小程序订单(非POI订单)
///9101:团购券订单(POI 订单)
///9001:景区门票订单(POI订单)
/// </summary>
[JsonPropertyName("order_type")]
public int OrderType { get; set; }
/// <summary>
/// 订单信息变更时间,13 位毫秒级时间戳
/// </summary>
[JsonPropertyName("update_time")]
public int UpdateTime { get; set; }
/// <summary>
/// 自定义字段,用于关联具体业务场景下的特殊参数,长度 < 2048byte
/// </summary>
[JsonPropertyName("extra")]
public string Extra { get; set; }
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System.Text.Json.Serialization;
namespace RsCode.Douyin.Payment
{
/// <summary>
/// https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/server/ecpay/order/order-sync
/// </summary>
public class OrderSyncResponse:DouyinResponse
{
/// <summary>
/// 返回码,详见错误码
/// </summary>
[JsonPropertyName("err_code")]
public int ErrCode { get; set; }
/// <summary>
/// 错误信息
/// </summary>
[JsonPropertyName("err_msg")]
public string ErrMsg{ get; set; }
/// <summary>
/// POI 等关联业务推送结果,非 POI 订单为空,JSON 字符串
/// </summary>
[JsonPropertyName("body")]
public string Body { get; set; }
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace RsCode.Douyin.Payment
{
public class PayNotifyData
{
/// <summary>
/// Unix 时间戳,字符串类型
/// </summary>
[JsonPropertyName("timestamp")]
public string Timestamp { get; set; }
/// <summary>
/// 随机数
/// </summary>
[JsonPropertyName("nonce")]
public string Nonce { get; set; }
/// <summary>
/// 订单信息的 json 字符串
/// </summary>
[JsonPropertyName("msg")]
public string Msg { get; set; }
/// <summary>
/// 签名
/// </summary>
[JsonPropertyName("msg_signature")]
public string MsgSignature { get; set; }
/// <summary>
/// 回调类型标记,支付成功回调为"payment"
/// </summary>
[JsonPropertyName("type")]
public string Type { get; set; }
/// <summary>
/// 担保支付回调签名算法
/// 所有字段(验证时注意不包含 sign 签名本身,不包含空字段与 type 常量字段)内容与平台上配置的 token
/// </summary>
/// <param name="options"></param>
/// <returns></returns>
public bool VerifySign(DouyinOptions options)
{
SortedDictionary<string, string> dics = new SortedDictionary<string, string>();
dics.Add(String2Unicode(Timestamp), Timestamp);
dics.Add(String2Unicode(Nonce), Nonce);
dics.Add(String2Unicode(Msg), Msg);
dics.Add(String2Unicode(options.Token), options.Token);
string signSource = "";
foreach (var item in dics)
{
signSource += item.Value;
}
byte[] StrRes = Encoding.Default.GetBytes(signSource);
HashAlgorithm iSHA = new SHA1CryptoServiceProvider();
StrRes = iSHA.ComputeHash(StrRes);
StringBuilder signBuilder = new StringBuilder();
foreach (byte iByte in StrRes)
{
signBuilder.AppendFormat("{0:x2}", iByte);
}
var sign= signBuilder.ToString();
return sign == MsgSignature;
}
/// <summary>
/// 字符串转Unicode
/// </summary>
/// <param name="source">源字符串</param>
/// <returns>Unicode编码后的字符串</returns>
private string String2Unicode(string source)
{
var bytes = Encoding.Unicode.GetBytes(source);
var stringBuilder = new StringBuilder();
for (var i = 0; i < bytes.Length; i += 2)
{
stringBuilder.AppendFormat("\\u{0}{1}", bytes[i + 1].ToString("x").PadLeft(2, '0'), bytes[i].ToString("x").PadLeft(2, '0'));
}
return stringBuilder.ToString();
}
/// <summary>
/// byte数组转换成十六进制字符串
/// </summary>
/// <param name="byteArray"></param>
/// <returns></returns>
private string bytesToHexStr(byte[] byteArray)
{
StringBuilder sb = new StringBuilder();
foreach (byte b in byteArray)
{
sb.Append(b.ToString("X2"));
}
return sb.ToString();
}
/// <summary>
/// 用MD5加密字符串
/// </summary>
/// <param name="jsonData">待加密的字符串</param>
/// <returns></returns>
public string MD5Encrypt(string jsonData)
{
MD5CryptoServiceProvider md5Hasher = new MD5CryptoServiceProvider();
byte[] hashedDataBytes;
hashedDataBytes = md5Hasher.ComputeHash(Encoding.GetEncoding("gb2312").GetBytes(jsonData));
StringBuilder tmp = new StringBuilder();
foreach (byte i in hashedDataBytes)
{
tmp.Append(i.ToString("x2"));
}
return tmp.ToString();
}
}
public class PayNotifyOrderInfo
{
[JsonPropertyName("appid")]
public string AppId { get; set; }
[JsonPropertyName("cp_orderno")]
public string CpOrderNo { get; set; }
[JsonPropertyName("cp_extra")]
public string CpExtra { get; set; }
[JsonPropertyName("way")]
public string Way { get; set; }
[JsonPropertyName("payment_order_no")]
public string PaymentOrderNo { get; set; }
[JsonPropertyName("total_amount")]
public int TotalAmount { get; set; }
[JsonPropertyName("status")]
public string Status { get; set; }
[JsonPropertyName("seller_uid")]
public string SellerUid { get; set; }
[JsonPropertyName("extra")]
public object Extra { get; set; }
[JsonPropertyName("item_id")]
public string ItemId { get; set; }
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System.Text.Json.Serialization;
namespace RsCode.Douyin.Payment
{
/// <summary>
/// 支付结果查询
/// <see cref="https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/server/ecpay/pay-list/query"/>
/// </summary>
public class PayQueryRequest:DouyinRequest
{
public override string GetApiUrl()
{
return "https://developer.toutiao.com/api/apps/ecpay/v1/query_order";
}
public PayQueryRequest(string appId,string orderNo,string thirdPartyId="")
{
AppId = appId;
OutOrderNo = orderNo;
ThirdPartyId = thirdPartyId;
}
/// <summary>
/// 小程序APPID
/// </summary>
[JsonPropertyName("app_id")]
public string AppId { get; set; }
/// <summary>
/// 开发者侧的订单号, 同一小程序下不可重复
/// </summary>
[JsonPropertyName("out_order_no")]
public string OutOrderNo { get; set; }
/// <summary>
///签名
/// </summary>
[JsonPropertyName("sign")]
public string Sign { get; set; }
/// <summary>
/// 第三方平台服务商 id,非服务商模式留空
/// </summary>
[JsonPropertyName("thirdparty_id")]
public string ThirdPartyId { get; set; }
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System.Text.Json.Serialization;
namespace RsCode.Douyin.Payment
{
public class PayQueryResponse:DouyinResponse
{
/// <summary>
/// 返回码,详见错误码
/// </summary>
[JsonPropertyName("err_no")]
public int ErrNo { get; set; }
/// <summary>
/// 返回码描述,详见错误码描述
/// </summary>
[JsonPropertyName("err_tips")]
public string ErrTips { get; set; }
/// <summary>
/// 开发者侧的订单号
/// </summary>
[JsonPropertyName("out_order_no")]
public string OutOrderNo { get; set; }
/// <summary>
/// 抖音侧的订单号
/// </summary>
[JsonPropertyName("order_id")]
public string OrderId { get; set; }
/// <summary>
/// 支付信息
/// </summary>
[JsonPropertyName("payment_info")]
public PaymentInfo PaymentInfo { get; set; }
[JsonPropertyName("cps_info")]
public CpsInfo CpsInfo { get; set; }
}
public class PaymentInfo
{
/// <summary>
/// 支付金额,单位为分
/// </summary>
[JsonPropertyName("total_fee")]
public int TotalFee { get; set; }
/// <summary>
/// 支付状态枚举值:
/// SUCCESS:成功 TIMEOUT:超时未支付 PROCESSING:处理中 FAIL:失败
/// </summary>
[JsonPropertyName("order_status")]
public string OrderStatus { get; set; }
/// <summary>
/// 支付时间, 格式为"yyyy-MM-dd hh:mm:ss"
/// </summary>
[JsonPropertyName("pay_time")]
public string PayTime { get; set; }
/// <summary>
/// 支付渠道, 1-微信支付,2-支付宝支付,10-抖音支付
/// </summary>
[JsonPropertyName("way")]
public int Way { get; set; }
/// <summary>
/// 支付渠道侧的支付单号
/// </summary>
[JsonPropertyName("channel_no")]
public string ChannelNo { get; set; }
/// <summary>
/// 该笔交易卖家商户号
/// </summary>
[JsonPropertyName("seller_uid")]
public string SellerUid { get; set; }
/// <summary>
/// 订单来源视频对应视频 id
/// </summary>
[JsonPropertyName("item_id")]
public string ItemId { get; set; }
/// <summary>
/// 开发者自定义字段
/// </summary>
[JsonPropertyName("cp_extra")]
public string CpExtra { get; set; }
}
public class CpsInfo
{
// <summary>
/// 达人分佣金额,单位为分。后续商户在进行分账时需要注意可分账金额应扣除达人分佣金额。
//注意:由于订单归因与佣金计算存在延迟,支付成功后立即查询可能未计算完成,建议开发者在支付成功后分账前再进行查询。
/// </summary>
[JsonPropertyName("share_amount")]
public string ShareAmount { get; set; }
// <summary>
/// 达人抖音号
/// </summary>
[JsonPropertyName("douyin_id")]
public string DouyinId { get; set; }
// <summary>
/// 达人昵称
/// </summary>
[JsonPropertyName("nickname")]
public string NickName { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace RsCode.Douyin.Payment
{
/// <summary>
/// https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/server/ecpay/order/order-sync#_接口说明
/// </summary>
public class PoiOrder9001
{
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System.Text.Json.Serialization;
namespace RsCode.Douyin.Payment
{
/// <summary>
/// 9101团购券类型
/// https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/server/ecpay/order/order-sync#_接口说明
/// </summary>
public class PoiOrder9101
{
/// <summary>
/// 开发者侧业务单号。用作幂等控制。该订单号是和担保支付的支付单号绑定的,也就是预下单时传入的 out_order_no 字段,长度 <= 64byte
/// </summary>
[JsonPropertyName("ext_order_id")]
public string ExtOrderId { get; set; }
/// <summary>
///状态
///枚举值:
///10:已取消(抖音订单中心可看到,状态为"已取消")
///110:待支付
///310:未使用
///340:已使用
///410:退款中
///420: 退款成功
///430: 退款失败
/// </summary>
[JsonPropertyName("status")]
public int Status { get; set; }
/// <summary>
/// 商铺名字,长度 <= 256 byte
/// </summary>
[JsonPropertyName("shop_name")]
public string ShopName { get; set; }
/// <summary>
///订单详情页的外链跳转类型,通过该接口上传的都为 2
///1:H5
///2:抖音小程序
/// </summary>
[JsonPropertyName("entry_type")]
public int EntryType { get; set; }
/// <summary>
/// 订单详情页的外链跳转 schema 参数,格式为 json 字符串。长度 <= 512byte,具体参数详见 entry_schema 说明
/// </summary>
[JsonPropertyName("entry_schema")]
public string EntrySchema { get; set; }
/// <summary>
/// 下单时间(13位毫秒时间戳)
/// </summary>
[JsonPropertyName("create_order_time")]
public int CreateOrderTime { get; set; }
/// <summary>
/// 订单描述,长度<=500 byte
/// </summary>
[JsonPropertyName("description")]
public string Description { get; set; }
/// <summary>
/// 订单总金额(单位:分)
/// </summary>
[JsonPropertyName("total_price")]
public int TotalPrice { get; set; }
/// <summary>
/// 支付时间(13位毫秒时间戳),未付款时不用传
/// </summary>
[JsonPropertyName("pay_time")]
public string PayTime { get; set; }
/// <summary>
/// 开发者侧卡劵核销门店ID,未核销时不用传,长度 <= 256 byte
/// </summary>
[JsonPropertyName("ext_valid_shop_id")]
public string ExtValidShopId { get; set; }
/// <summary>
///开发者侧卡劵核销门店对应的抖音poiId,ext_valid_shop_id未匹配抖音POI时不用传,长度<= 128 byte
/// </summary>
[JsonPropertyName("valid_poi_id_str")]
public string ValidPoiIdStr { get; set; }
/// <summary>
/// 订单总价,单位为分
/// </summary>
[JsonPropertyName("ext_goods_id")]
public string ExtGoodsId { get; set; }
/// <summary>
/// 商品名称,长度 <= 256 byte
/// </summary>
[JsonPropertyName("goods_name")]
public string GoodsName { get; set; }
/// <summary>
/// 商品描述信息。向用户介绍商品,长度 <= 120byte
/// </summary>
[JsonPropertyName("goods_info")]
public string GoodsInfo { get; set; }
/// <summary>
/// 商品图片,完整的url地址 长度 <= 512 byte
/// </summary>
[JsonPropertyName("goods_cover_image")]
public string GoodsCoverImage { get; set; }
/// <summary>
/// 商品详情页的外链跳转类型,通过该接口上传的都为2 1: H5 2: 抖音小程序
/// </summary>
[JsonPropertyName("goods_entry_type")]
public int GoodsEntryType { get; set; }
/// <summary>
/// 商品详情页的外链跳转schema参数,格式为 JSON 字符串,长度 <= 512 byte, 详见 entry_schema 说明
/// </summary>
[JsonPropertyName("goods_entry_schema")]
public string GoodsEntrySchema { get; set; }
/// <summary>
/// 生效时间,yyyy-MM-dd HH:mm:ss 格式字符串,24 小时制
/// </summary>
[JsonPropertyName("start_valid_time")]
public string StartValidTime { get; set; }
/// <summary>
/// 失效时间,yyyy-MM-dd HH:mm:ss格式字符串,24小时制
/// </summary>
[JsonPropertyName("end_valid_time")]
public string EndValidTime { get; set; }
/// <summary>
/// 用户购买团购券的数量
/// </summary>
[JsonPropertyName("ticket_num")]
public int TicketNum { get; set; }
/// <summary>
/// 开发者侧券 ID,该信息用于用户可以明确的感知是哪一张券。格式为 JSON 数组字符串,每个 ID 长度 <= 64byte
/// </summary>
[JsonPropertyName("ext_ticket_ids")]
public object ExtTicketIds { get; set; }
/// <summary>
/// 券的使用说明。JSON 数组字符串,最多可以有10条,每条长度 <= 50byte。必须写明券的使用条件、领取条件、退款规则,请参考示例。
/// </summary>
[JsonPropertyName("ticket_description")]
public object TicketDescription { get; set; }
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System.Text.Json.Serialization;
namespace RsCode.Douyin.Payment
{
/// <summary>
/// 预支付
/// <see cref="https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/server/ecpay/pay-list/pay"/>
/// </summary>
public class PrepayRequest:DouyinRequest
{
public PrepayRequest(string appId, string orderNo,decimal totalAmount,string notifyUrl,string subject="抖音商品")
{
AppId =appId;
OutOrderNo= orderNo;
TotalAmount = DouyinTool.Price(totalAmount);
NotifyUrl = notifyUrl;
Subject = subject;
Body = subject;
}
public override string GetApiUrl()
{
string url = "https://developer.toutiao.com/api/apps/ecpay/v1/create_order";
return url;
}
/// <summary>
/// 小程序APPID
/// </summary>
[JsonPropertyName("app_id")]
public string AppId { get; set; }
/// <summary>
/// 开发者侧的订单号。 只能是数字、大小写字母_-*且在同一个app_id下唯一
/// </summary>
[JsonPropertyName("out_order_no")]
public string OutOrderNo { get; set; }
/// <summary>
/// 支付价格。 单位为[分]
/// </summary>
[JsonPropertyName("total_amount")]
public int TotalAmount { get; set; }
/// <summary>
/// 商品描述。 长度限制不超过 128 字节且不超过 42 字符
/// </summary>
[JsonPropertyName("subject")]
public string Subject { get; set; }
/// <summary>
/// 商品详情 长度限制不超过 128 字节且不超过 42 字符
/// </summary>
[JsonPropertyName("body")]
public string Body { get; set; }
/// <summary>
/// 订单过期时间(秒)。最小5分钟,最大2天,小于5分钟会被置为5分钟,大于2天会被置为2天
/// </summary>
[JsonPropertyName("valid_time")]
public int ValidTime { get; set; } = 3600 * 48;
/// <summary>
/// 签名
/// </summary>
[JsonPropertyName("sign")]
public string Sign { get; set; }
/// <summary>
/// 开发者自定义字段,回调原样回传。 超过最大长度会被截断
/// </summary>
[JsonPropertyName("cp_extra")]
public string CpExtra { get; set; }
/// <summary>
/// 商户自定义回调地址,必须以 https 开头,支持 443 端口。 指定时,支付成功后抖音会请求该地址通知开发者
/// </summary>
[JsonPropertyName("notify_url")]
public string NotifyUrl { get; set; }
/// <summary>
/// 第三方平台服务商 id,非服务商模式留空
/// </summary>
[JsonPropertyName("thirdparty_id")]
public string ThirdPartyId { get; set; }
/// <summary>
/// 可用此字段指定本单使用的收款商户号(目前为灰度功能,需要联系平台运营添加白名单,白名单添加1小时后生效;未在白名单的小程序,该字段不生效)
/// </summary>
[JsonPropertyName("store_uid")]
public string StoreUid { get; set; }
/// <summary>
/// 是否屏蔽支付完成后推送用户抖音消息,1-屏蔽 0-非屏蔽,默认为0。 特别注意: 若接入POI, 请传1。因为POI订单体系会发消息,所以不用再接收一次担保支付推送消息,
/// </summary>
[JsonPropertyName("disable_msg")]
public int DisableMsg { get; set; }
/// <summary>
/// 支付完成后推送给用户的抖音消息跳转页面,开发者需要传入在app.json中定义的链接,如果不传则跳转首页。
/// </summary>
[JsonPropertyName("msg_page")]
public string MsgPage { get; set; }
/// <summary>
/// 订单拓展信息,详见下面
/// expand_order_info参数说明
/// </summary>
[JsonPropertyName("expand_order_info")]
public ExpandOrderInfo ExpandOrderInfo { get; set; }
/// <summary>
/// 屏蔽指定支付方式,屏蔽多个支付方式,请使用逗号","分割,枚举值:
/// 屏蔽微信支付:LIMIT_WX
/// 屏蔽支付宝支付:LIMIT_ALI
/// 屏蔽抖音支付:LIMIT_DYZF
/// 特殊说明:若之前开通了白名单,平台会保留之前屏蔽逻辑;若传入该参数,会优先以传入的为准,白名单则无效
/// </summary>
[JsonPropertyName("limit_pay_way")]
public string LimitPayWay { get; set; }
}
public class ExpandOrderInfo
{
/// <summary>
/// 配送费原价,单位为[分],仅外卖小程序需要传对应信息
/// </summary>
[JsonPropertyName("original_delivery_fee")]
public int OriginalDeliveryFee { get; set; }
/// <summary>
/// 实付配送费,单位为[分],仅外卖小程序需要传对应信息
/// </summary>
[JsonPropertyName("actual_delivery_fee")]
public int ActualDeliveryFee { get; set; }
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System.Text.Json.Serialization;
namespace RsCode.Douyin.Payment
{
public class PrepayResponse:DouyinResponse
{
/// <summary>
/// 返回码,详见错误码
/// </summary>
[JsonPropertyName("err_no")]
public int ErrNo { get; set; }
/// <summary>
/// 返回码描述,详见错误码描述
/// </summary>
[JsonPropertyName("err_tips")]
public string ErrTips { get; set; }
[JsonPropertyName("data")]
public PrepayResult Data { get; set; }
}
public class PrepayResult
{
/// <summary>
/// 抖音侧唯一订单号
/// </summary>
[JsonPropertyName("order_id")]
public string OrderId { get; set; }
/// <summary>
/// 签名后的订单信息
/// </summary>
[JsonPropertyName("order_token")]
public string OrderToken { get; set; }
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System.Text.Json.Serialization;
namespace RsCode.Douyin.Payment
{
public class RefundNotifyData
{
/// <summary>
/// Unix 时间戳,字符串类型
/// </summary>
[JsonPropertyName("timestamp")]
public string Timestamp { get; set; }
/// <summary>
/// 随机数
/// </summary>
[JsonPropertyName("nonce")]
public string Nonce { get; set; }
/// <summary>
/// 订单信息的 json 字符串
/// 对应RefundNotifyResult
/// </summary>
[JsonPropertyName("msg")]
public string Msg { get; set; }
/// <summary>
/// 签名
/// </summary>
[JsonPropertyName("msg_signature")]
public string MsgSignature { get; set; }
/// <summary>
/// 回调类型标记,支付成功回调为"payment"
/// </summary>
[JsonPropertyName("type")]
public string Type { get; set; }
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System.Text.Json.Serialization;
namespace RsCode.Douyin.Payment
{
public class RefundNotifyResult
{
/// <summary>
/// 当前交易发起的小程序id
/// </summary>
[JsonPropertyName("appid")]
public string AppId { get; set; }
/// <summary>
///开发者侧的退款订单号
/// </summary>
[JsonPropertyName("cp_refundno")]
public string CpRefundNo { get; set; }
/// <summary>
/// 预下单时开发者传入字段
/// </summary>
[JsonPropertyName("cp_extra")]
public string CpExtra { get; set; }
/// <summary>
/// 状态枚举值:
/// SUCCESS:成功 FAIL:失败
/// </summary>
[JsonPropertyName("refund_status")]
public string RefundStatus { get; set; }
/// <summary>
/// 退款金额,单位为分
/// </summary>
[JsonPropertyName("refund_amount")]
public int RefundAmount { get; set; }
/// <summary>
///退款时间, 格式为"yyyy-MM-dd hh:mm:ss"
/// </summary>
[JsonPropertyName("refunded_at")]
public string RefundAt { get; set; }
/// <summary>
/// 退款失败原因描述,详见发起退款错误码
/// </summary>
[JsonPropertyName("message")]
public string Message { get; set; }
/// <summary>
/// 抖音侧订单号
/// </summary>
[JsonPropertyName("order_id")]
public string OrderId { get; set; }
/// <summary>
/// 抖音退款单号
/// </summary>
[JsonPropertyName("refund_no")]
public string RefundNo { get; set; }
/// <summary>
/// 是否为分账后退款
/// </summary>
[JsonPropertyName("is_all_settled")]
public bool IsAllSettled { get; set; }
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace RsCode.Douyin.Payment
{
public class RefundQueryRequest:DouyinRequest
{
public override string GetApiUrl()
{
return "https://developer.toutiao.com/api/apps/ecpay/v1/query_refund";
}
/// <summary>
/// 小程序APPID
/// </summary>
[JsonPropertyName("app_id")]
public string AppId { get; set; }
/// <summary>
/// 商户退款单号
/// </summary>
[JsonPropertyName("out_refund_no")]
public string OutRefundNo { get; set; }
/// <summary>
/// 签名
/// </summary>
[JsonPropertyName("sign")]
public string Sign { get; set; }
/// <summary>
/// 第三方平台服务商 id,非服务商模式留空
/// </summary>
[JsonPropertyName("thirdparty_id")]
public string ThirdPartyId { get; set; }
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System.Text.Json.Serialization;
namespace RsCode.Douyin.Payment
{
public class RefundQueryResponse:DouyinResponse
{
/// <summary>
/// 错误码
/// </summary>
[JsonPropertyName("err_no")]
public long ErrNo { get; set; }
/// <summary>
/// 错误信息
/// </summary>
[JsonPropertyName("err_tips")]
public string ErrTips { get; set; }
/// <summary>
///
/// </summary>
[JsonPropertyName("refundInfo")]
public RefundResult RefundInfo { get; set; }
}
public class RefundResult
{
/// <summary>
/// 退款金额,单位为分
/// </summary>
[JsonPropertyName("refund_amount")]
public int RefundAmount { get; set; }
/// <summary>
/// 退款状态枚举值:
/// SUCCESS:成功 PROCESSING:处理中 FAIL:失败
/// </summary>
[JsonPropertyName("refund_status")]
public string RefundStatus { get; set; }
/// <summary>
///退款时间, 格式为"yyyy-MM-dd hh:mm:ss"
/// </summary>
[JsonPropertyName("refunded_at")]
public string RefundAt { get; set; }
/// <summary>
/// 退款渠道,TRUE:分账后退款,现金户出款
/// FALSE:分账前退款,在途户出款
/// </summary>
[JsonPropertyName("is_all_settled")]
public bool IsAllSettled { get; set; }
/// <summary>
/// 抖音退款单号
/// </summary>
[JsonPropertyName("refund_no")]
public string RefundNo { get; set; }
/// <summary>
/// 开发者自定义字段,回调原样回传
/// </summary>
[JsonPropertyName("cp_extra")]
public string CpExtra { get; set; }
/// <summary>
/// 退款错误描述
/// </summary>
[JsonPropertyName("msg")]
public string Msg { get; set; }
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System.Text.Json.Serialization;
namespace RsCode.Douyin.Payment
{
/// <summary>
/// <see cref="https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/server/ecpay/refund-list/refund"/>
/// </summary>
public class RefundRequest:DouyinRequest
{
public override string GetApiUrl()
{
return "https://developer.toutiao.com/api/apps/ecpay/v1/create_refund";
}
/// <summary>
/// 小程序APPID
/// </summary>
[JsonPropertyName("app_id")]
public string AppId { get; set; }
/// <summary>
/// 商户分配支付单号,标识进行退款的订单
/// </summary>
[JsonPropertyName("out_order_no")]
public string OutOrderNo { get; set; }
/// <summary>
/// 商户分配退款号,保证在商户中唯一
/// </summary>
[JsonPropertyName("out_refund_no")]
public string OutRefundNo { get; set; }
/// <summary>
///退款原因
/// </summary>
[JsonPropertyName("reason")]
public string Reason { get; set; }
/// <summary>
/// 退款金额,单位分
/// </summary>
[JsonPropertyName("refund_amount")]
public int RefundAmount { get; set; }
/// <summary>
/// 签名
/// </summary>
[JsonPropertyName("sign")]
public string Sign { get; set; }
/// <summary>
/// 开发者自定义字段,回调原样回传。 超过最大长度会被截断
/// </summary>
[JsonPropertyName("cp_extra")]
public string CpExtra { get; set; }
/// <summary>
/// 商户自定义回调地址,必须以 https 开头,支持 443 端口
/// </summary>
[JsonPropertyName("notify_url")]
public string NotifyUrl { get; set; }
/// <summary>
/// 第三方平台服务商id,服务商模式接入必传,非服务商模式留空
/// </summary>
[JsonPropertyName("thirdparty_id")]
public string ThirdPartyId { get; set; }
/// <summary>
/// 是否屏蔽支付完成后推送用户抖音消息,1-屏蔽 0-非屏蔽,默认为0。 特别注意: 若接入POI, 请传1。因为POI订单体系会发消息,所以不用再接收一次担保支付推送消息,请传1
/// </summary>
[JsonPropertyName("disable_msg")]
public int DisableMsg { get; set; }
/// <summary>
/// 退款完成后推送给用户的抖音消息跳转页面,开发者需要传入在app.json中定义的链接,如果不传则跳转首页。
/// </summary>
[JsonPropertyName("msg_page")]
public string MsgPage { get; set; }
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace RsCode.Douyin.Payment
{
public class RefundResponse:DouyinResponse
{
/// <summary>
/// 错误码
/// </summary>
[JsonPropertyName("err_no")]
public long ErrNo { get; set; }
/// <summary>
/// 错误信息
/// </summary>
[JsonPropertyName("err_tips")]
public string ErrTips { get; set; }
/// <summary>
/// 担保交易服务端退款单号
/// </summary>
[JsonPropertyName("refund_no")]
public string RefundNo { get; set; }
}
}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PackageProjectUrl>https://rscode.cn</PackageProjectUrl>
<PackageIcon>rscode64_64.png</PackageIcon>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\RsCode.AspNetCore\RsCode.AspNetCore.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="rscode64_64.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="Mount\" />
</ItemGroup>
</Project>
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System.Text.Json.Serialization;
namespace RsCode.Douyin.Storage
{
/// <summary>
/// 删除存储到字节跳动的云存储服务的 key-value 数据。当开发者不需要该用户信息时,需要删除,以免占用过大的存储空间。
/// <see cref="https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/server/data-caching/remove-user-storage"/>
/// </summary>
public class RemoveUserStorageRequest: DouyinRequest
{
/// <summary>
/// 服务端 API 调用标识
/// <see cref="https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/server/interface-request-credential/get-access-token/"/>
/// </summary>
[JsonPropertyName("access_token")]
public string AccessToken { get; set; }
/// <summary>
/// 登录用户唯一标识
/// </summary>
[JsonPropertyName("openid")]
public string OpenId { get; set; }
/// <summary>
/// 用户登录态签名,参考用户登录态签名算法
/// <see cref="https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/server/other/user-login-sign/"/>
/// </summary>
[JsonPropertyName("signature")]
public string Signature { get; set; }
/// <summary>
/// 用户登录态签名的编码方法
/// <see cref="https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/server/other/user-login-sign/"/>
/// </summary>
[JsonPropertyName("sig_method")]
public string SigMethod { get; set; }
/// <summary>
/// (body 中) 要删除的用户数据的 key list
/// </summary>
[JsonPropertyName("key")]
public string[] Key { get; set; }
public override string GetApiUrl()
{
return "https://developer.toutiao.com/api/apps/remove_user_storage";
}
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace RsCode.Douyin.Storage
{
public class RemoveUserStorageResponse: DouyinResponse
{
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace RsCode.Douyin.Storage
{
/// <summary>
/// 以 key-value 形式存储用户数据到小程序平台的云存储服务。若开发者无内部存储服务则可接入,免费且无需申请。一般情况下只存储用户的基本信息,禁止写入大量不相干信息。
/// <see cref="https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/server/data-caching/set-user-storage"/>
/// </summary>
public class SetUserStorageReqeust:DouyinRequest
{
/// <summary>
/// 服务端 API 调用标识
/// </summary>
[JsonPropertyName("access_token")]
public string SessionKey { get; set; }
/// <summary>
/// 登录用户唯一标识
/// <see cref="https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/server/log-in/code-2-session/"/>
/// </summary>
[JsonPropertyName("openid")]
public string OpenId { get; set; }
/// <summary>
/// 用户登录态签名
/// <see cref="https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/server/other/user-login-sign/"/>
/// </summary>
[JsonPropertyName("signature")]
public string Signature { get; set; }
/// <summary>
/// 用户登录态签名的编码方法,参考用户登录态签名算法
/// <see cref="https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/server/other/user-login-sign/"/>
/// </summary>
[JsonPropertyName("sig_method")]
public string SigMethod { get; set; }
/// <summary>
/// (body 中) 要设置的用户数据
/// </summary>
[JsonPropertyName("kv_list")]
public Object[] KvList { get; set; }
}
}
/*
* 项目:抖音开放平台SDK RsCode.Douyin
* 协议:MIT License 2.0
* 作者:河南软商网络科技有限公司
* 项目己托管于
* github
https://github.com/kuiyu/RsCode.git
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace RsCode.Douyin.Storage
{
public class SetUserStorageResponse: DouyinResponse
{
/// <summary>
/// 会话密钥,如果请求时有 code 参数才会返回
/// </summary>
[JsonPropertyName("session_key")]
public string SessionKey { get; set; }
/// <summary>
/// 用户在当前小程序的 ID,如果请求时有 code 参数才会返回
/// </summary>
[JsonPropertyName("openid")]
public string OpenId { get; set; }
/// <summary>
/// 匿名用户在当前小程序的 ID,如果请求时有 anonymous_code 参数才会返回
/// </summary>
[JsonPropertyName("anonymous_openid")]
public string AnonymousOpenId { get; set; }
/// <summary>
/// 用户在小程序平台的唯一标识符,请求时有 code 参数才会返回。如果开发者拥有多个小程序,可通过 unionid 来区分用户的唯一性。
/// </summary>
[JsonPropertyName("unionid")]
public string UnionId { get; set; }
}
}
global using RsCode.Douyin.Core;
global using RsCode.Douyin.OAuth;
\ No newline at end of file
......@@ -28,7 +28,7 @@ namespace RsCode.Storage.Aliyun
AccessKeySecret = optionsSnapshot.Value.AccessKeySecret;
}
public async Task<AlibabaCloud.SDK.Sts20150401.Models.AssumeRoleResponseBody> GetStsTokenAsync(string sessionName, string endpointName, string roleArn = "", int durationSeconds = 900)
public async Task<AlibabaCloud.SDK.Sts20150401.Models.AssumeRoleResponseBody> GetStsTokenAsync(string sessionName, string endpointName, string roleArn = "", int durationSeconds = 3600)
{
try
{
......@@ -176,13 +176,13 @@ namespace RsCode.Storage.Aliyun
return await Task.Run(()=> client.DeleteObject(bucketName, key));
}
public Uri GeneratePresignedUri(string bucketName,string key,string endpointName, AssumeRoleResponseBodyCredentials stsToken)
public Uri GeneratePresignedUri(string bucketName,string key,string endpointName, AssumeRoleResponseBodyCredentials stsToken, int minute = 60 )
{
var endpoint = Options.GetEndPoint(endpointName);
var client = GetOssClient(stsToken.AccessKeyId, stsToken.AccessKeySecret, endpoint.Oss, stsToken.SecurityToken);
//创建临时访问的url
return client.GeneratePresignedUri(bucketName,key,DateTime.Now.AddMinutes(5),SignHttpMethod.Get);
return client.GeneratePresignedUri(bucketName,key,DateTime.Now.AddMinutes(minute),SignHttpMethod.Get);
}
......
......@@ -9,7 +9,7 @@ namespace RsCode.Storage.Aliyun
{
public interface IAliyunOssService
{
Task<AlibabaCloud.SDK.Sts20150401.Models.AssumeRoleResponseBody> GetStsTokenAsync( string sessionName,string endpointName, string roleArn = "", int durationSeconds = 900);
Task<AlibabaCloud.SDK.Sts20150401.Models.AssumeRoleResponseBody> GetStsTokenAsync( string sessionName,string endpointName, string roleArn = "", int durationSeconds = 3600);
AlibabaCloud.SDK.Sts20150401.Client CreateStsClient(string accessKeyId, string accessKeySecret, string endpoint);
......@@ -24,7 +24,7 @@ namespace RsCode.Storage.Aliyun
/// <param name="endpointName"></param>
/// <param name="stsToken"></param>
/// <returns></returns>
Uri GeneratePresignedUri(string bucketName, string key, string endpointName, AssumeRoleResponseBodyCredentials stsToken);
Uri GeneratePresignedUri(string bucketName, string key, string endpointName, AssumeRoleResponseBodyCredentials stsToken,int minute=60);
Task<DeleteObjectResult> DeleteFileAsync(string bucketName, string key, string endpointName, AssumeRoleResponseBodyCredentials stsToken);
/// <summary>
/// 查询指定key对应UrL
......
......@@ -20,6 +20,7 @@ using log4net;
using System;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
namespace RsCode
{
......@@ -62,10 +63,10 @@ namespace RsCode
/// <param name="log4Level">记录日志等级,枚举</param>
public static void WriteLog(Type type, string logContent, Log4NetLevel log4Level,Exception ex=null)
{
//log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType)
var repositories = LogManager.GetAllRepositories();
var repository = repositories.FirstOrDefault();
if (repository == null) return;
ILog log = type == null ? LogManager.GetLogger(repository.Name, "") : LogManager.GetLogger(type);
......
namespace RsCode.Douyin.Tests
{
public class AccessTokenTest
{
IDouyinClient douyinClient;
public AccessTokenTest(IDouyinClient douyinClient)
{
this.douyinClient = douyinClient;
}
[Fact]
public async Task GetAccessTokenTest()
{
var options = douyinClient.UseAppId("tt6be31d5ef4dfda0b01");
var ret = await douyinClient.SendAsync<AccessTokenResponse>(new AccessTokenRequest { AppId = options.AppId, AppSecret = options.Secret });
Assert.Equal(0, ret.ErrNo);
}
}
}
\ No newline at end of file
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AspectCore.Extensions.Hosting" Version="2.3.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="Xunit.DependencyInjection" Version="8.7.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\RsCode.Douyin\RsCode.Douyin.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="cert\douyin\private.key">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="cert\douyin\public.key">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="douyin.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
using RsCode.Douyin.Payment;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace RsCode.Douyin.Tests
{
public class SignTest
{
IDouyinClient douyinClient;
public SignTest(IDouyinClient douyinClient)
{
this.douyinClient = douyinClient;
}
[Fact]
public void CheckSignTest()
{
string s = "POST\n/abc\n1680835692\ngjjRNfQlzoDIJtVDOfUe\n{\"eventTime\":1677653869000,\"status\":102}\n";
string exp = "RFQ65hHlo4xyZ6EC31LZC0SzsyN0nd2Fb2wAiISvY1mkiC6G8gn2QZwLGq7qgjenRGl/Z8OrTtkBHWb9GOazkJFkHrPeRqogqnwZ+kSOxGvtou8FPN669E1wwb+BShN4pIUgPFzaukR9/rCRBsbLoq9RPVA2sbf3iKoHGa81zhXjQSuFbF1CyiWkL5qqniNTM/BSfwfLZfPW8nBanRl3U+mQaymbj0DCF0ZdWhFz1FnZPAfEpx8YEwFNZWtxzz4p3WJ1swnUocJC4LXoDazo6DhEPDuoZXOXrB1SqzL1wRqA4p8uj3Z8Seki/PMGWiGpGWPMv3tJyvWmzMOuVJtEjg==";
var options = douyinClient.UseAppId("tt6be31d5ef4dfda0b01");
string ret = DouyinTool.Sign(s, options);
Assert.Equal(exp, ret);
}
[Fact]
public void ParamSign()
{
var options = douyinClient.UseAppId("tt6be31d5ef4dfda0b01");
PrepayRequest request = new PrepayRequest(options.AppId, "abdeiekd", 18, "http://rscode.cn");
var json = JsonSerializer.Serialize(request);
var sign=request.CreateSign(options);
Assert.Equal("61a3191ee426dc64f6f409052a0653b7", sign);
}
}
}
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using RsCode.AspNetCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace RsCode.Douyin.Tests
{
public class Startup
{
// public IHostBuilder CreateHostBuilder([AssemblyName assemblyName]) { }
// 自定义 host 构建
public void ConfigureHost(IHostBuilder hostBuilder)
{
hostBuilder
.ConfigureAppConfiguration(builder =>
{
// 注册配置
builder.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", true, true);
})
.ConfigureServices((context, services) =>
{
// 注册自定义服务
services.AddRsCode();
services.AddDouyin();
})
// .UseServiceContext(o=>
//{
// var p = Predicates.ForNameSpace("RsCode.Storage.QiniuStorage.Core");
// o.AddDataAnnotations(p);
//})
;
}
// 支持的形式:
// ConfigureServices(IServiceCollection services)
// ConfigureServices(IServiceCollection services, HostBuilderContext hostBuilderContext)
// ConfigureServices(HostBuilderContext hostBuilderContext, IServiceCollection services)
public void ConfigureServices(IServiceCollection services, HostBuilderContext hostBuilderContext)
{
//services.TryAddSingleton<CustomService>();
}
// 可以添加要用到的方法参数,会自动从注册的服务中获取服务实例,类似于 asp.net core 里 Configure 方法
public void Configure(IServiceProvider applicationServices)
{
// 有一些测试数据要初始化可以放在这里
// InitData();
}
}
}
global using Xunit;
global using RsCode.Douyin.Core;
global using RsCode.Douyin.OAuth;
\ No newline at end of file
<?xml version="1.0" encoding="utf-8" ?>
<log4net>
<!--数据日志-->
<appender name="InfoRollingFileAppender" type="log4net.Appender.RollingFileAppender">
<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
<param name="File" value="Log\Info\" />
<param name="AppendToFile" value="true" />
<param name="rollingStyle" value="Date" />
<param name="datePattern" value="yyyy-MM-dd.'Info.log'" />
<param name="staticLogFileName" value="false" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%n%n%n 记录时间:%date %n日志级别: %-5level %n类型:%logger %n日志描述:%message %newline%newline" />
</layout>
<filter type="log4net.Filter.LevelRangeFilter">
<param name="LevelMin" value="INFO" />
<param name="LevelMax" value="INFO" />
</filter>
</appender>
<!--错误日志-->
<appender name="ErrorRollingFileAppender" type="log4net.Appender.RollingFileAppender">
<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
<param name="File" value="Log\Error\" />
<param name="AppendToFile" value="true" />
<param name="rollingStyle" value="Date" />
<param name="datePattern" value="yyyy-MM-dd.'error.log'" />
<param name="staticLogFileName" value="false" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%n%n%n 记录时间:%date %n日志级别: %-5level %n出错类:%logger %n错误信息:%message %newline%newline" />
</layout>
<filter type="log4net.Filter.LevelRangeFilter">
<param name="LevelMin" value="ERROR" />
<param name="LevelMax" value="ERROR" />
</filter>
</appender>
<!--调试日志-->
<appender name="DebugRollingFileAppender" type="log4net.Appender.RollingFileAppender">
<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
<param name="File" value="Log\Debug\" />
<param name="AppendToFile" value="true" />
<param name="rollingStyle" value="Date" />
<param name="datePattern" value="yyyy-MM-dd.'debug.log'" />
<param name="staticLogFileName" value="false" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%n%n%n 记录时间:%date %n日志级别: %-5level %n类型:%logger %n调试信息:%message %newline%newline" />
</layout>
<filter type="log4net.Filter.LevelRangeFilter">
<param name="LevelMin" value="DEBUG" />
<param name="LevelMax" value="DEBUG" />
</filter>
</appender>
<root>
<level value="DEBUG" />
<!--文件形式记录日志-->
<appender-ref ref="ErrorRollingFileAppender" />
<appender-ref ref="DebugRollingFileAppender" />
<appender-ref ref="InfoRollingFileAppender" />
</root>
</log4net>
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册