Merge pull request '🍒 修复token黑名单导致无法登陆的问题、优化Http日志记录功能、 优化自定义序列化' (#425) from jasondom/Admin.NET.Pro:v2-1 into v2
Reviewed-on: https://code.adminnet.top/Admin.NET/Admin.NET.Pro/pulls/425
This commit is contained in:
commit
7d77b148c8
@ -90,5 +90,4 @@
|
|||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="9.0.8" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="9.0.8" />
|
||||||
<PackageReference Include="My.Extensions.Localization.Json" Version="3.4.0" />
|
<PackageReference Include="My.Extensions.Localization.Json" Version="3.4.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -1,92 +0,0 @@
|
|||||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
|
||||||
//
|
|
||||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
|
||||||
//
|
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
|
||||||
|
|
||||||
namespace Admin.NET.Core;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 通用接口参数验证特性类
|
|
||||||
/// </summary>
|
|
||||||
[AttributeUsage(AttributeTargets.Property)]
|
|
||||||
public class CommonValidationAttribute : ValidationAttribute
|
|
||||||
{
|
|
||||||
private readonly Dictionary<string, string> _conditions;
|
|
||||||
private static readonly Dictionary<string, Delegate> CompiledConditions = [];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="conditionPairs">条件对参数,长度必须为偶数<br/>
|
|
||||||
/// 奇数字符串参数:动态条件<br/>
|
|
||||||
/// 偶数字符串参数:提示消息
|
|
||||||
/// </param>
|
|
||||||
/// <example>
|
|
||||||
/// <code lang="C">
|
|
||||||
/// public class ModelInput {
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// public string A { get; set; }
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// [CommonValidation(
|
|
||||||
/// "A == 1 <value>&&</value> B == null", "当 A == 1时,B不能为空",
|
|
||||||
/// "C == 2 <value>&&</value> B == null", "当 C == 2时,B不能为空"
|
|
||||||
/// )]
|
|
||||||
/// public string B { get; set; }
|
|
||||||
/// }
|
|
||||||
/// </code>
|
|
||||||
/// </example>
|
|
||||||
public CommonValidationAttribute(params string[] conditionPairs)
|
|
||||||
{
|
|
||||||
if (conditionPairs.Length % 2 != 0) throw new ArgumentException("条件对必须以偶数个字符串的形式提供。");
|
|
||||||
|
|
||||||
var conditions = new Dictionary<string, string>();
|
|
||||||
for (int i = 0; i < conditionPairs.Length; i += 2)
|
|
||||||
conditions.Add(conditionPairs[i], conditionPairs[i + 1]);
|
|
||||||
|
|
||||||
_conditions = conditions;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
|
|
||||||
{
|
|
||||||
foreach (var (expr, errorMessage) in _conditions)
|
|
||||||
{
|
|
||||||
var conditionKey = $"{validationContext.ObjectType.FullName}.{expr}";
|
|
||||||
if (!CompiledConditions.TryGetValue(conditionKey, out var condition))
|
|
||||||
{
|
|
||||||
condition = CreateCondition(validationContext.ObjectType, expr);
|
|
||||||
CompiledConditions[conditionKey] = condition;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((bool)condition.DynamicInvoke(validationContext.ObjectInstance)!)
|
|
||||||
{
|
|
||||||
return new ValidationResult(errorMessage ?? $"[{validationContext.MemberName}]校验失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ValidationResult.Success;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Delegate CreateCondition(Type modelType, string expression)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// 创建参数表达式
|
|
||||||
var parameter = Expression.Parameter(typeof(object), "x");
|
|
||||||
|
|
||||||
// 构建 Lambda 表达式
|
|
||||||
var lambda = DynamicExpressionParser.ParseLambda([Expression.Parameter(modelType, "x")], typeof(bool), expression);
|
|
||||||
|
|
||||||
// 创建新的 Lambda 表达式,接受 object 参数并调用编译后的表达式
|
|
||||||
var invokeExpression = Expression.Invoke(lambda, Expression.Convert(parameter, modelType));
|
|
||||||
var finalLambda = Expression.Lambda<Func<object, bool>>(invokeExpression, parameter);
|
|
||||||
|
|
||||||
return finalLambda.Compile();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"无法解析表达式 '{expression}': {ex.Message}", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
|
||||||
//
|
|
||||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
|
||||||
//
|
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
|
||||||
|
|
||||||
namespace Admin.NET.Core;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 自定义规范化结果特性
|
|
||||||
/// </summary>
|
|
||||||
[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = true)]
|
|
||||||
public class CustomUnifyResultAttribute : Attribute
|
|
||||||
{
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
public CustomUnifyResultAttribute(string name)
|
|
||||||
{
|
|
||||||
Name = name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -37,9 +37,4 @@ public class CommonConst
|
|||||||
/// 事件-发送异常邮件
|
/// 事件-发送异常邮件
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string SendErrorMail = "Send:ErrorMail";
|
public const string SendErrorMail = "Send:ErrorMail";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 远程请求请求头参数键值前缀
|
|
||||||
/// </summary>
|
|
||||||
public const string HttpRemoteHeaderKeyPrefix = "__HTTP_CLIENT_";
|
|
||||||
}
|
}
|
||||||
@ -9,8 +9,12 @@ namespace Admin.NET.Core;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Http远程服务扩展
|
/// Http远程服务扩展
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class HttpRemotesExtension
|
public static class HttpRemotesExtension {
|
||||||
{
|
private static readonly HttpRequestOptionsKey<string> HttpNameKey = new("__HTTP_CLIENT_NAME__");
|
||||||
|
private static readonly HttpRequestOptionsKey<HttpRemoteApiAttribute> AttrKey = new(nameof(HttpRemoteApiAttribute));
|
||||||
|
private static readonly HttpRequestOptionsKey<string> ReqPlaintextKey = new(nameof(SysLogHttp.RequestBodyPlaintext));
|
||||||
|
private static readonly HttpRequestOptionsKey<Func<HttpResponseMessage, Task<string>>> RespPlaintextFunc = new(nameof(SysLogHttp.ResponseBodyPlaintext) + "Func");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 添加Http远程服务
|
/// 添加Http远程服务
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -23,13 +27,12 @@ public static class HttpRemotesExtension
|
|||||||
{
|
{
|
||||||
if (prop.GetValue(options) is not HttpRemoteItem opt) continue;
|
if (prop.GetValue(options) is not HttpRemoteItem opt) continue;
|
||||||
services.AddHttpClient(opt.HttpName, client =>
|
services.AddHttpClient(opt.HttpName, client =>
|
||||||
{
|
|
||||||
client.BaseAddress = new Uri(opt.BaseAddress);
|
|
||||||
client.Timeout = TimeSpan.FromSeconds(opt.Timeout);
|
|
||||||
foreach (var kv in opt.Headers) client.DefaultRequestHeaders.Add(kv.Key, kv.Value);
|
|
||||||
})
|
|
||||||
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
|
|
||||||
{
|
{
|
||||||
|
client.BaseAddress = new Uri(opt.BaseAddress);
|
||||||
|
client.Timeout = TimeSpan.FromSeconds(opt.Timeout);
|
||||||
|
foreach (var kv in opt.Headers) client.DefaultRequestHeaders.Add(kv.Key, kv.Value);
|
||||||
|
})
|
||||||
|
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler {
|
||||||
UseCookies = opt.UseCookies
|
UseCookies = opt.UseCookies
|
||||||
})
|
})
|
||||||
.AddHttpMessageHandler<HttpLoggingHandler>();
|
.AddHttpMessageHandler<HttpLoggingHandler>();
|
||||||
@ -38,14 +41,73 @@ public static class HttpRemotesExtension
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 携带接口描述相关属性
|
/// 设置请求接口相关属性
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="builder"></param>
|
/// <param name="builder"></param>
|
||||||
/// <param name="attr"></param>
|
/// <param name="attr"></param>
|
||||||
/// <param name="reqPlaintext"></param>
|
/// <param name="plaintext">请求明文</param>
|
||||||
|
/// <param name="decryption">获取响应体明文的代理方法(解密)</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static HttpRequestBuilder SetRemoteApiAttr(this HttpRequestBuilder builder, HttpRemoteApiAttribute attr, string reqPlaintext = null)
|
public static HttpRequestBuilder SetReqRemoteApiAttr(this HttpRequestBuilder builder, HttpRemoteApiAttribute attr, string plaintext = null, Func<HttpResponseMessage, Task<string>> decryption = null)
|
||||||
{
|
{
|
||||||
return HttpLoggingHandler.SetRemoteApiAttr(builder, attr, reqPlaintext);
|
builder.SetOnPreSendRequest(conf =>
|
||||||
|
{
|
||||||
|
conf.Options.Set(AttrKey, attr);
|
||||||
|
conf.Options.Set(ReqPlaintextKey, plaintext);
|
||||||
|
conf.Options.Set(RespPlaintextFunc, decryption);
|
||||||
|
});
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取客户端名称
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string GetHttpClientName(this HttpRequestMessage request)
|
||||||
|
{
|
||||||
|
return request.Options.TryGetValue(HttpNameKey, out var name) ? name : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取Http远程接口属性
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static HttpRemoteApiAttribute GetHttpRemoteApiAttr(this HttpRequestMessage request)
|
||||||
|
{
|
||||||
|
return request.Options.TryGetValue(AttrKey, out var attr) ? attr : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取请求明文
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string GetRequestBodyPlaintext(this HttpRequestMessage request)
|
||||||
|
{
|
||||||
|
return request.Options.TryGetValue(ReqPlaintextKey, out var plaintext) ? plaintext : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取响应明文
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="response"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static async Task<string> GetResponseBodyPlaintext(this HttpResponseMessage response)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (response.RequestMessage != null && response.RequestMessage.Options.TryGetValue(RespPlaintextFunc, out var decryptionFunc))
|
||||||
|
{
|
||||||
|
return await decryptionFunc.Invoke(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Error("Http远程服务响应体解密失败", e);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4,6 +4,8 @@
|
|||||||
//
|
//
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||||
|
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Admin.NET.Core;
|
namespace Admin.NET.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -351,4 +353,16 @@ public static partial class ObjectExtension
|
|||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 对象转为自定义JSON字符串
|
||||||
|
/// </summary>
|
||||||
|
public static string ToCustomJson<T>(this T obj, JsonSerializerOptions options = null)
|
||||||
|
=> CustomJsonHelper.Serialize(obj, options);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自定义JSON字符串转为对象
|
||||||
|
/// </summary>
|
||||||
|
public static T FromCustomJson<T>(this string json, JsonSerializerOptions options = null)
|
||||||
|
=> CustomJsonHelper.Deserialize<T>(json, options);
|
||||||
}
|
}
|
||||||
47
Admin.NET/Admin.NET.Core/Job/SysDailyJob.cs
Normal file
47
Admin.NET/Admin.NET.Core/Job/SysDailyJob.cs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||||
|
//
|
||||||
|
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||||
|
//
|
||||||
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||||
|
|
||||||
|
namespace Admin.NET.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 系统每日作业任务(每天 00:00:00 执行)
|
||||||
|
/// </summary>
|
||||||
|
[JobDetail("job_daily", Description = "系统每日作业任务", GroupName = "default", Concurrent = false)]
|
||||||
|
[Daily(TriggerId = "trigger_daily", Description = "系统每日作业任务")]
|
||||||
|
public class SysDailyJob(IServiceScopeFactory serviceScopeFactory) : IJob
|
||||||
|
{
|
||||||
|
public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
using var serviceScope = serviceScopeFactory.CreateScope();
|
||||||
|
|
||||||
|
var db = serviceScope.ServiceProvider.GetRequiredService<ISqlSugarClient>().CopyNew();
|
||||||
|
var sysConfigService = serviceScope.ServiceProvider.GetRequiredService<SysConfigService>();
|
||||||
|
|
||||||
|
// 日志保留天数
|
||||||
|
var daysAgo = await sysConfigService.GetConfigValueByCode<int>(ConfigConst.SysLogRetentionDays);
|
||||||
|
// 删除访问日志
|
||||||
|
await db.Deleteable<SysLogVis>().Where(u => u.CreateTime < DateTime.Now.AddDays(-daysAgo)).ExecuteCommandAsync(stoppingToken);
|
||||||
|
// 删除操作日志
|
||||||
|
await db.Deleteable<SysLogOp>().Where(u => u.CreateTime < DateTime.Now.AddDays(-daysAgo)).ExecuteCommandAsync(stoppingToken);
|
||||||
|
// 删除差异日志
|
||||||
|
await db.Deleteable<SysLogDiff>().Where(u => u.CreateTime < DateTime.Now.AddDays(-daysAgo)).ExecuteCommandAsync(stoppingToken);
|
||||||
|
// 删除作业触发器运行记录
|
||||||
|
await db.Deleteable<SysJobTriggerRecord>().Where(u => u.CreatedTime < DateTime.Now.AddDays(-daysAgo)).ExecuteCommandAsync(stoppingToken);
|
||||||
|
|
||||||
|
var originColor = Console.ForegroundColor;
|
||||||
|
Console.ForegroundColor = ConsoleColor.Green;
|
||||||
|
var message = $"【定时任务】清理系统日志成功,清理 {daysAgo} 天前的日志数据 {DateTime.Now}";
|
||||||
|
Console.WriteLine(message);
|
||||||
|
Log.Information(message);
|
||||||
|
Console.ForegroundColor = originColor;
|
||||||
|
|
||||||
|
// 重置用户Token版本号
|
||||||
|
serviceScope.ServiceProvider.GetRequiredService<SysUserService>().ResetTokenVersion();
|
||||||
|
message = $"【启动任务】重置用户Token版本号 {DateTime.Now}";
|
||||||
|
Console.WriteLine(message);
|
||||||
|
Log.Information(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,12 +14,8 @@ namespace Admin.NET.Core;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class HttpLoggingHandler : DelegatingHandler, ITransient
|
public class HttpLoggingHandler : DelegatingHandler, ITransient
|
||||||
{
|
{
|
||||||
private static readonly string RespPlaintextKey = CommonConst.HttpRemoteHeaderKeyPrefix + "RESP_PLAINTEXT__";
|
private static readonly Lazy<UserManager> UserManager = new(() => App.GetService<UserManager>());
|
||||||
private static readonly string ReqPlaintextKey = CommonConst.HttpRemoteHeaderKeyPrefix + "REQ_PLAINTEXT__";
|
private static readonly string HttpNameKey = "__HTTP_CLIENT_NAME__";
|
||||||
private static readonly string IgnoreLogKey = CommonConst.HttpRemoteHeaderKeyPrefix + "IGNORE_LOG__";
|
|
||||||
private static readonly string ApiDescKey = CommonConst.HttpRemoteHeaderKeyPrefix + "API_DESC__";
|
|
||||||
private static readonly string HttpNameKey = CommonConst.HttpRemoteHeaderKeyPrefix + "NAME__";
|
|
||||||
private static readonly Lazy<UserManager> _userManager = new(() => App.GetService<UserManager>());
|
|
||||||
|
|
||||||
private readonly Dictionary<string, bool> _enabledLogMap;
|
private readonly Dictionary<string, bool> _enabledLogMap;
|
||||||
private readonly SysConfigService _sysConfigService;
|
private readonly SysConfigService _sysConfigService;
|
||||||
@ -45,27 +41,26 @@ public class HttpLoggingHandler : DelegatingHandler, ITransient
|
|||||||
if (!enabledLog) return await base.SendAsync(request, cancellationToken);
|
if (!enabledLog) return await base.SendAsync(request, cancellationToken);
|
||||||
|
|
||||||
// 判断当前配置日志开关
|
// 判断当前配置日志开关
|
||||||
(string apiDesc, bool ignoreLog, string reqPlaintext) = GetRemoteApiAttrAndRemove(request.Headers);
|
var httpClientName = request.GetHttpClientName();
|
||||||
_ = request.Options.TryGetValue<string>(HttpNameKey, out var httpName);
|
var attr = request.GetHttpRemoteApiAttr();
|
||||||
if (!string.IsNullOrWhiteSpace(httpName)) enabledLog = _enabledLogMap.GetOrDefault(httpName);
|
if (!string.IsNullOrWhiteSpace(httpClientName)) enabledLog = _enabledLogMap.GetOrDefault(httpClientName);
|
||||||
if (!enabledLog || ignoreLog) return await base.SendAsync(request, cancellationToken);
|
if (!enabledLog || attr?.IgnoreLog == true) return await base.SendAsync(request, cancellationToken);
|
||||||
|
|
||||||
var stopWatch = Stopwatch.StartNew();
|
var stopWatch = Stopwatch.StartNew();
|
||||||
var urlList = request.RequestUri?.LocalPath.Split("/") ?? [];
|
var urlList = request.RequestUri?.LocalPath.Split("/") ?? [];
|
||||||
var sysLogHttp = new SysLogHttp
|
var sysLogHttp = new SysLogHttp
|
||||||
{
|
{
|
||||||
HttpClientName = httpName,
|
HttpApiDesc = attr?.Desc,
|
||||||
HttpApiDesc = apiDesc,
|
HttpClientName = httpClientName,
|
||||||
HttpMethod = request.Method.Method,
|
HttpMethod = request.Method.Method,
|
||||||
ActionName = urlList.Length >= 2 ? $"{urlList[^2]}/{urlList[^1]}" : urlList.Length >= 1 ? urlList[^1] : null,
|
ActionName = urlList.Length >= 2 ? $"{urlList[^2]}/{urlList[^1]}" : urlList.Length >= 1 ? urlList[^1] : null,
|
||||||
RequestUrl = request.RequestUri?.ToString(),
|
RequestUrl = request.RequestUri?.ToString(),
|
||||||
RequestHeaders = request.Headers.ToDictionary(u => u.Key, u => u.Value.Join(";")).ToJson(),
|
RequestHeaders = request.Headers.ToDictionary(u => u.Key, u => u.Value.Join(";")).ToJson(),
|
||||||
RequestBodyPlaintext = reqPlaintext,
|
RequestBodyPlaintext = request.GetRequestBodyPlaintext(),
|
||||||
TenantId = _userManager.Value?.TenantId,
|
TenantId = UserManager.Value?.TenantId,
|
||||||
CreateUserId = _userManager.Value?.UserId,
|
CreateUserId = UserManager.Value?.UserId,
|
||||||
CreateUserName = _userManager.Value?.RealName,
|
CreateUserName = UserManager.Value?.RealName,
|
||||||
};
|
};
|
||||||
// _userManager
|
|
||||||
if (request.Content != null) sysLogHttp.RequestBody = await request.Content.ReadAsStringAsync(cancellationToken);
|
if (request.Content != null) sysLogHttp.RequestBody = await request.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
|
||||||
sysLogHttp.StartTime = DateTime.Now;
|
sysLogHttp.StartTime = DateTime.Now;
|
||||||
@ -75,6 +70,7 @@ public class HttpLoggingHandler : DelegatingHandler, ITransient
|
|||||||
stopWatch.Stop();
|
stopWatch.Stop();
|
||||||
sysLogHttp.EndTime = DateTime.Now;
|
sysLogHttp.EndTime = DateTime.Now;
|
||||||
sysLogHttp.StatusCode = response.StatusCode;
|
sysLogHttp.StatusCode = response.StatusCode;
|
||||||
|
sysLogHttp.ResponseBodyPlaintext = await response.GetResponseBodyPlaintext();
|
||||||
sysLogHttp.ResponseHeaders = response.Headers.ToDictionary(u => u.Key, u => u.Value.Join(";")).ToJson();
|
sysLogHttp.ResponseHeaders = response.Headers.ToDictionary(u => u.Key, u => u.Value.Join(";")).ToJson();
|
||||||
sysLogHttp.IsSuccessStatusCode = response.IsSuccessStatusCode ? YesNoEnum.Y : YesNoEnum.N;
|
sysLogHttp.IsSuccessStatusCode = response.IsSuccessStatusCode ? YesNoEnum.Y : YesNoEnum.N;
|
||||||
sysLogHttp.ResponseBody = await response.Content.ReadAsStringAsync(cancellationToken);
|
sysLogHttp.ResponseBody = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
@ -91,54 +87,7 @@ public class HttpLoggingHandler : DelegatingHandler, ITransient
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
sysLogHttp.Elapsed = stopWatch.ElapsedMilliseconds;
|
sysLogHttp.Elapsed = stopWatch.ElapsedMilliseconds;
|
||||||
await _eventPublisher.PublishAsync(nameof(AppEventSubscriber.CreateHttpLog), sysLogHttp, cancellationToken);
|
await _eventPublisher.PublishAsync(nameof(AppEventSubscriber.CreateHttpLog), sysLogHttp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取接口描述相关属性
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="headers"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
private static (string apiDesc, bool ignoreLog, string reqPlaintext) GetRemoteApiAttrAndRemove(HttpRequestHeaders headers)
|
|
||||||
{
|
|
||||||
var result = new
|
|
||||||
{
|
|
||||||
ReqPlaintext = headers?.FirstOrDefault(u => u.Key == ReqPlaintextKey).Value?.FirstOrDefault()?.ToString(),
|
|
||||||
ApiDesc = headers?.FirstOrDefault(u => u.Key == ApiDescKey).Value?.FirstOrDefault()?.ToString(),
|
|
||||||
IgnoreLog = headers?.FirstOrDefault(u => u.Key == IgnoreLogKey).Value?.ToBoolean() ?? false,
|
|
||||||
};
|
|
||||||
RemoveRemoteApiAttr(headers);
|
|
||||||
return (result.ApiDesc, result.IgnoreLog, result.ReqPlaintext);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 移除接口描述相关属性
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="headers"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
private static void RemoveRemoteApiAttr(HttpRequestHeaders headers)
|
|
||||||
{
|
|
||||||
if (headers == null) return;
|
|
||||||
var keys = headers
|
|
||||||
.Where(kv => kv.Key.StartsWith(CommonConst.HttpRemoteHeaderKeyPrefix))
|
|
||||||
.Select(u => u.Key)
|
|
||||||
.ToList();
|
|
||||||
foreach (var key in keys) headers.Remove(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 设置接口描述相关属性
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="builder"></param>
|
|
||||||
/// <param name="attr"></param>
|
|
||||||
/// <param name="reqPlaintext">请求文明</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static HttpRequestBuilder SetRemoteApiAttr(HttpRequestBuilder builder, HttpRemoteApiAttribute attr, string reqPlaintext = null)
|
|
||||||
{
|
|
||||||
builder.WithHeader(ReqPlaintextKey, reqPlaintext, replace:true);
|
|
||||||
builder.WithHeader(IgnoreLogKey, attr.IgnoreLog, replace:true);
|
|
||||||
builder.WithHeader(ApiDescKey, attr.Desc, replace:true);
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -12,7 +12,7 @@ namespace Admin.NET.Core;
|
|||||||
public sealed class HttpRemotesOptions : IConfigurableOptions
|
public sealed class HttpRemotesOptions : IConfigurableOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 企业微信接
|
/// 企业微信
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public HttpRemoteItem WorkWeixin { get; set; }
|
public HttpRemoteItem WorkWeixin { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,439 +1,441 @@
|
|||||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||||
//
|
//
|
||||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||||
//
|
//
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||||
|
|
||||||
using Furion.SpecificationDocument;
|
using Furion.SpecificationDocument;
|
||||||
using Lazy.Captcha.Core;
|
using Lazy.Captcha.Core;
|
||||||
|
|
||||||
namespace Admin.NET.Core.Service;
|
namespace Admin.NET.Core.Service;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 系统登录授权服务 🧩
|
/// 系统登录授权服务 🧩
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiDescriptionSettings(Order = 500, Description = "登录授权")]
|
[ApiDescriptionSettings(Order = 500, Description = "登录授权")]
|
||||||
[AppApiDescription("登录授权")]
|
[AppApiDescription("登录授权")]
|
||||||
public class SysAuthService : IDynamicApiController, ITransient
|
public class SysAuthService : IDynamicApiController, ITransient
|
||||||
{
|
{
|
||||||
private readonly UserManager _userManager;
|
private readonly UserManager _userManager;
|
||||||
private readonly SqlSugarRepository<SysUser> _sysUserRep;
|
private readonly SqlSugarRepository<SysUser> _sysUserRep;
|
||||||
private readonly SysConfigService _sysConfigService;
|
private readonly SysConfigService _sysConfigService;
|
||||||
private readonly SysCacheService _sysCacheService;
|
private readonly SysCacheService _sysCacheService;
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
private readonly ICaptcha _captcha;
|
private readonly ICaptcha _captcha;
|
||||||
private readonly IEventPublisher _eventPublisher;
|
private readonly IEventPublisher _eventPublisher;
|
||||||
|
|
||||||
public SysAuthService(UserManager userManager,
|
public SysAuthService(UserManager userManager,
|
||||||
SqlSugarRepository<SysUser> sysUserRep,
|
SqlSugarRepository<SysUser> sysUserRep,
|
||||||
SysConfigService sysConfigService,
|
SysConfigService sysConfigService,
|
||||||
SysCacheService sysCacheService,
|
SysCacheService sysCacheService,
|
||||||
IHttpContextAccessor httpContextAccessor,
|
IHttpContextAccessor httpContextAccessor,
|
||||||
ICaptcha captcha,
|
ICaptcha captcha,
|
||||||
IEventPublisher eventPublisher)
|
IEventPublisher eventPublisher)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_sysUserRep = sysUserRep;
|
_sysUserRep = sysUserRep;
|
||||||
_sysConfigService = sysConfigService;
|
_sysConfigService = sysConfigService;
|
||||||
_sysCacheService = sysCacheService;
|
_sysCacheService = sysCacheService;
|
||||||
_httpContextAccessor = httpContextAccessor;
|
_httpContextAccessor = httpContextAccessor;
|
||||||
_captcha = captcha;
|
_captcha = captcha;
|
||||||
_eventPublisher = eventPublisher;
|
_eventPublisher = eventPublisher;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 账号密码登录 🔖
|
/// 账号密码登录 🔖
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="input"></param>
|
/// <param name="input"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[DisplayName("账号密码登录")]
|
[DisplayName("账号密码登录")]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public virtual async Task<LoginOutput> Login([Required] LoginInput input)
|
public virtual async Task<LoginOutput> Login([Required] LoginInput input)
|
||||||
{
|
{
|
||||||
// 判断密码错误次数(缓存30分钟)
|
// 判断密码错误次数(缓存30分钟)
|
||||||
var keyPasswordErrorTimes = $"{CacheConst.KeyPasswordErrorTimes}{input.Account}";
|
var keyPasswordErrorTimes = $"{CacheConst.KeyPasswordErrorTimes}{input.Account}";
|
||||||
var passwordErrorTimes = _sysCacheService.Get<int>(keyPasswordErrorTimes);
|
var passwordErrorTimes = _sysCacheService.Get<int>(keyPasswordErrorTimes);
|
||||||
var passwordMaxErrorTimes = await _sysConfigService.GetConfigValueByCode<int>(ConfigConst.SysPasswordMaxErrorTimes);
|
var passwordMaxErrorTimes = await _sysConfigService.GetConfigValueByCode<int>(ConfigConst.SysPasswordMaxErrorTimes);
|
||||||
// 若未配置或误配置为0、负数, 则默认密码错误次数最大为5次
|
// 若未配置或误配置为0、负数, 则默认密码错误次数最大为5次
|
||||||
if (passwordMaxErrorTimes < 1) passwordMaxErrorTimes = 5;
|
if (passwordMaxErrorTimes < 1) passwordMaxErrorTimes = 5;
|
||||||
if (passwordErrorTimes > passwordMaxErrorTimes) throw Oops.Oh(ErrorCodeEnum.D1027);
|
if (passwordErrorTimes > passwordMaxErrorTimes) throw Oops.Oh(ErrorCodeEnum.D1027);
|
||||||
|
|
||||||
// 判断是否开启验证码并校验
|
// 判断是否开启验证码并校验
|
||||||
input.TenantId = input.TenantId <= 0 ? SqlSugarConst.DefaultTenantId : input.TenantId;
|
input.TenantId = input.TenantId <= 0 ? SqlSugarConst.DefaultTenantId : input.TenantId;
|
||||||
var tenant = _sysCacheService.Get<List<SysTenant>>(CacheConst.KeyTenant)?.FirstOrDefault(u => u.Id == input.TenantId);
|
var tenant = _sysCacheService.Get<List<SysTenant>>(CacheConst.KeyTenant)?.FirstOrDefault(u => u.Id == input.TenantId);
|
||||||
if (tenant == null)
|
if (tenant == null)
|
||||||
{
|
{
|
||||||
await App.GetRequiredService<SysTenantService>().CacheTenant(); // 重新生成租户列表缓存
|
await App.GetRequiredService<SysTenantService>().CacheTenant(); // 重新生成租户列表缓存
|
||||||
tenant = _sysCacheService.Get<List<SysTenant>>(CacheConst.KeyTenant)?.FirstOrDefault(u => u.Id == input.TenantId);
|
tenant = _sysCacheService.Get<List<SysTenant>>(CacheConst.KeyTenant)?.FirstOrDefault(u => u.Id == input.TenantId);
|
||||||
if (tenant == null) throw Oops.Oh(ErrorCodeEnum.D0007);
|
if (tenant == null) throw Oops.Oh(ErrorCodeEnum.D0007);
|
||||||
}
|
}
|
||||||
if (tenant.Captcha == true && !_captcha.Validate(input.CodeId.ToString(), input.Code))
|
if (tenant.Captcha == true && !_captcha.Validate(input.CodeId.ToString(), input.Code))
|
||||||
throw Oops.Oh(ErrorCodeEnum.D0008);
|
throw Oops.Oh(ErrorCodeEnum.D0008);
|
||||||
|
|
||||||
// 获取并验证账号
|
// 获取并验证账号
|
||||||
var user = await GetLoginUser(input.TenantId, account: input.Account);
|
var user = await GetLoginUser(input.TenantId, account: input.Account);
|
||||||
|
|
||||||
// 是否开启域登录验证
|
// 是否开启域登录验证
|
||||||
if (await _sysConfigService.GetConfigValueByCode<bool>(ConfigConst.SysDomainLogin))
|
if (await _sysConfigService.GetConfigValueByCode<bool>(ConfigConst.SysDomainLogin))
|
||||||
{
|
{
|
||||||
var userLdap = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysUserLdap>>().GetFirstAsync(u => u.UserId == user.Id && u.TenantId == user.TenantId);
|
var userLdap = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysUserLdap>>().GetFirstAsync(u => u.UserId == user.Id && u.TenantId == user.TenantId);
|
||||||
if (userLdap == null)
|
if (userLdap == null)
|
||||||
{
|
{
|
||||||
VerifyPassword(input.Password, keyPasswordErrorTimes, passwordErrorTimes, user);
|
VerifyPassword(input.Password, keyPasswordErrorTimes, passwordErrorTimes, user);
|
||||||
}
|
}
|
||||||
else if (!await App.GetRequiredService<SysLdapService>().AuthAccount(user.TenantId, userLdap.Account, CryptogramHelper.Decrypt(input.Password)))
|
else if (!await App.GetRequiredService<SysLdapService>().AuthAccount(user.TenantId, userLdap.Account, CryptogramHelper.Decrypt(input.Password)))
|
||||||
{
|
{
|
||||||
_sysCacheService.Set(keyPasswordErrorTimes, ++passwordErrorTimes, TimeSpan.FromMinutes(30));
|
_sysCacheService.Set(keyPasswordErrorTimes, ++passwordErrorTimes, TimeSpan.FromMinutes(30));
|
||||||
throw Oops.Oh(ErrorCodeEnum.D1000);
|
throw Oops.Oh(ErrorCodeEnum.D1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
VerifyPassword(input.Password, keyPasswordErrorTimes, passwordErrorTimes, user);
|
VerifyPassword(input.Password, keyPasswordErrorTimes, passwordErrorTimes, user);
|
||||||
|
|
||||||
// 登录成功则清空密码错误次数
|
// 登录成功则清空密码错误次数
|
||||||
_sysCacheService.Remove(keyPasswordErrorTimes);
|
_sysCacheService.Remove(keyPasswordErrorTimes);
|
||||||
|
|
||||||
return await CreateToken(user);
|
return await CreateToken(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取登录用户
|
/// 获取登录用户
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="tenantId"></param>
|
/// <param name="tenantId"></param>
|
||||||
/// <param name="account"></param>
|
/// <param name="account"></param>
|
||||||
/// <param name="phone"></param>
|
/// <param name="phone"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[NonAction]
|
[NonAction]
|
||||||
public async Task<SysUser> GetLoginUser(long tenantId, string account = null, string phone = null)
|
public async Task<SysUser> GetLoginUser(long tenantId, string account = null, string phone = null)
|
||||||
{
|
{
|
||||||
//// 若没有传值租户Id,则从请求页URL参数中获取租户Id(空则默认租户)
|
//// 若没有传值租户Id,则从请求页URL参数中获取租户Id(空则默认租户)
|
||||||
//if (tenantId < 1)
|
//if (tenantId < 1)
|
||||||
//{
|
//{
|
||||||
// var tenantidStr = _httpContextAccessor.HttpContext.Request.Query["tenantid"].ToString();
|
// var tenantidStr = _httpContextAccessor.HttpContext.Request.Query["tenantid"].ToString();
|
||||||
// tenantId = string.IsNullOrWhiteSpace(tenantidStr) ? SqlSugarConst.DefaultTenantId : long.Parse(tenantidStr);
|
// tenantId = string.IsNullOrWhiteSpace(tenantidStr) ? SqlSugarConst.DefaultTenantId : long.Parse(tenantidStr);
|
||||||
//}
|
//}
|
||||||
|
|
||||||
// 判断账号是否存在
|
// 判断账号是否存在
|
||||||
var user = await _sysUserRep.AsQueryable().Includes(t => t.SysOrg).IgnoreTenant()
|
var user = await _sysUserRep.AsQueryable().Includes(t => t.SysOrg).IgnoreTenant()
|
||||||
//.WhereIF(tenantId > 0, u => u.TenantId == tenantId)
|
//.WhereIF(tenantId > 0, u => u.TenantId == tenantId)
|
||||||
.WhereIF(!string.IsNullOrWhiteSpace(account), u => u.Account.Equals(account))
|
.WhereIF(!string.IsNullOrWhiteSpace(account), u => u.Account.Equals(account))
|
||||||
.WhereIF(!string.IsNullOrWhiteSpace(phone), u => u.Phone.Equals(phone))
|
.WhereIF(!string.IsNullOrWhiteSpace(phone), u => u.Phone.Equals(phone))
|
||||||
.FirstAsync();
|
.FirstAsync();
|
||||||
_ = user ?? throw Oops.Oh(ErrorCodeEnum.D0009);
|
_ = user ?? throw Oops.Oh(ErrorCodeEnum.D0009);
|
||||||
|
|
||||||
// 判断账号是否被冻结
|
// 判断账号是否被冻结
|
||||||
if (user.Status != StatusEnum.Enable) throw Oops.Oh(ErrorCodeEnum.D1017);
|
if (user.Status != StatusEnum.Enable) throw Oops.Oh(ErrorCodeEnum.D1017);
|
||||||
|
|
||||||
// 判断租户是否存在及状态
|
// 判断租户是否存在及状态
|
||||||
var tenant = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysTenant>>().GetFirstAsync(u => u.Id == user.TenantId);
|
var tenant = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysTenant>>().GetFirstAsync(u => u.Id == user.TenantId);
|
||||||
if (tenant?.Status != StatusEnum.Enable) throw Oops.Oh(ErrorCodeEnum.Z1003);
|
if (tenant?.Status != StatusEnum.Enable) throw Oops.Oh(ErrorCodeEnum.Z1003);
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证用户密码
|
/// 验证用户密码
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="password"></param>
|
/// <param name="password"></param>
|
||||||
/// <param name="keyPasswordErrorTimes"></param>
|
/// <param name="keyPasswordErrorTimes"></param>
|
||||||
/// <param name="passwordErrorTimes"></param>
|
/// <param name="passwordErrorTimes"></param>
|
||||||
/// <param name="user"></param>
|
/// <param name="user"></param>
|
||||||
private void VerifyPassword(string password, string keyPasswordErrorTimes, int passwordErrorTimes, SysUser user)
|
private void VerifyPassword(string password, string keyPasswordErrorTimes, int passwordErrorTimes, SysUser user)
|
||||||
{
|
{
|
||||||
// 国密SM2解密(前端密码传输SM2加密后的)
|
// 国密SM2解密(前端密码传输SM2加密后的)
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
password = CryptogramHelper.SM2Decrypt(password);
|
password = CryptogramHelper.SM2Decrypt(password);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
throw Oops.Oh(ErrorCodeEnum.D0010);
|
throw Oops.Oh(ErrorCodeEnum.D0010);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CryptogramHelper.CryptoType == CryptogramEnum.MD5.ToString())
|
if (CryptogramHelper.CryptoType == CryptogramEnum.MD5.ToString())
|
||||||
{
|
{
|
||||||
if (user.Password.Equals(MD5Encryption.Encrypt(password))) return;
|
if (user.Password.Equals(MD5Encryption.Encrypt(password))) return;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (CryptogramHelper.Decrypt(user.Password).Equals(password)) return;
|
if (CryptogramHelper.Decrypt(user.Password).Equals(password)) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_sysCacheService.Set(keyPasswordErrorTimes, ++passwordErrorTimes, TimeSpan.FromMinutes(30));
|
_sysCacheService.Set(keyPasswordErrorTimes, ++passwordErrorTimes, TimeSpan.FromMinutes(30));
|
||||||
throw Oops.Oh(ErrorCodeEnum.D1000);
|
throw Oops.Oh(ErrorCodeEnum.D1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证锁屏密码 🔖
|
/// 验证锁屏密码 🔖
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="password"></param>
|
/// <param name="password"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[DisplayName("验证锁屏密码")]
|
[DisplayName("验证锁屏密码")]
|
||||||
public virtual async Task<bool> UnLockScreen([Required, FromQuery] string password)
|
public virtual async Task<bool> UnLockScreen([Required, FromQuery] string password)
|
||||||
{
|
{
|
||||||
// 账号是否存在
|
// 账号是否存在
|
||||||
var user = await _sysUserRep.GetFirstAsync(u => u.Id == _userManager.UserId);
|
var user = await _sysUserRep.GetFirstAsync(u => u.Id == _userManager.UserId);
|
||||||
_ = user ?? throw Oops.Oh(ErrorCodeEnum.D0009);
|
_ = user ?? throw Oops.Oh(ErrorCodeEnum.D0009);
|
||||||
|
|
||||||
var keyPasswordErrorTimes = $"{CacheConst.KeyPasswordErrorTimes}{user.Account}";
|
var keyPasswordErrorTimes = $"{CacheConst.KeyPasswordErrorTimes}{user.Account}";
|
||||||
var passwordErrorTimes = _sysCacheService.Get<int>(keyPasswordErrorTimes);
|
var passwordErrorTimes = _sysCacheService.Get<int>(keyPasswordErrorTimes);
|
||||||
|
|
||||||
// 是否开启域登录验证
|
// 是否开启域登录验证
|
||||||
if (await _sysConfigService.GetConfigValueByCode<bool>(ConfigConst.SysDomainLogin))
|
if (await _sysConfigService.GetConfigValueByCode<bool>(ConfigConst.SysDomainLogin))
|
||||||
{
|
{
|
||||||
var userLdap = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysUserLdap>>().GetFirstAsync(u => u.UserId == user.Id && u.TenantId == user.TenantId);
|
var userLdap = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysUserLdap>>().GetFirstAsync(u => u.UserId == user.Id && u.TenantId == user.TenantId);
|
||||||
if (userLdap == null)
|
if (userLdap == null)
|
||||||
{
|
{
|
||||||
VerifyPassword(password, keyPasswordErrorTimes, passwordErrorTimes, user);
|
VerifyPassword(password, keyPasswordErrorTimes, passwordErrorTimes, user);
|
||||||
}
|
}
|
||||||
else if (!await App.GetRequiredService<SysLdapService>().AuthAccount(user.TenantId.Value, userLdap.Account, CryptogramHelper.Decrypt(password)))
|
else if (!await App.GetRequiredService<SysLdapService>().AuthAccount(user.TenantId.Value, userLdap.Account, CryptogramHelper.Decrypt(password)))
|
||||||
{
|
{
|
||||||
_sysCacheService.Set(keyPasswordErrorTimes, ++passwordErrorTimes, TimeSpan.FromMinutes(30));
|
_sysCacheService.Set(keyPasswordErrorTimes, ++passwordErrorTimes, TimeSpan.FromMinutes(30));
|
||||||
throw Oops.Oh(ErrorCodeEnum.D1000);
|
throw Oops.Oh(ErrorCodeEnum.D1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
VerifyPassword(password, keyPasswordErrorTimes, passwordErrorTimes, user);
|
VerifyPassword(password, keyPasswordErrorTimes, passwordErrorTimes, user);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 手机号登录 🔖
|
/// 手机号登录 🔖
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="input"></param>
|
/// <param name="input"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[DisplayName("手机号登录")]
|
[DisplayName("手机号登录")]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public virtual async Task<LoginOutput> LoginPhone([Required] LoginPhoneInput input)
|
public virtual async Task<LoginOutput> LoginPhone([Required] LoginPhoneInput input)
|
||||||
{
|
{
|
||||||
// 校验短信验证码
|
// 校验短信验证码
|
||||||
App.GetRequiredService<SysSmsService>().VerifyCode(new SmsVerifyCodeInput { Phone = input.Phone, Code = input.Code });
|
App.GetRequiredService<SysSmsService>().VerifyCode(new SmsVerifyCodeInput { Phone = input.Phone, Code = input.Code });
|
||||||
|
|
||||||
// 获取并验证账号
|
// 获取并验证账号
|
||||||
var user = await GetLoginUser(input.TenantId, phone: input.Phone);
|
var user = await GetLoginUser(input.TenantId, phone: input.Phone);
|
||||||
|
|
||||||
return await CreateToken(user);
|
return await CreateToken(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 生成Token令牌 🔖
|
/// 生成Token令牌 🔖
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="user"></param>
|
/// <param name="user"></param>
|
||||||
/// <param name="loginMode"></param>
|
/// <param name="loginMode"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[NonAction]
|
[NonAction]
|
||||||
public async Task<LoginOutput> CreateToken(SysUser user, LoginModeEnum loginMode = LoginModeEnum.PC)
|
public async Task<LoginOutput> CreateToken(SysUser user, LoginModeEnum loginMode = LoginModeEnum.PC)
|
||||||
{
|
{
|
||||||
// 单用户登录
|
// 单用户登录
|
||||||
await App.GetRequiredService<SysOnlineUserService>().SingleLogin(user.Id, loginMode);
|
await App.GetRequiredService<SysOnlineUserService>().SingleLogin(user.Id, loginMode);
|
||||||
|
|
||||||
// 生成Token令牌
|
// 生成Token令牌
|
||||||
var tokenExpire = await _sysConfigService.GetTokenExpire();
|
var tokenExpire = await _sysConfigService.GetTokenExpire();
|
||||||
var accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>
|
var accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
{ ClaimConst.UserId, user.Id },
|
{ ClaimConst.UserId, user.Id },
|
||||||
{ ClaimConst.TokenVersion, user.TokenVersion },
|
{ ClaimConst.TokenVersion, user.TokenVersion },
|
||||||
}, tokenExpire);
|
}, tokenExpire);
|
||||||
|
|
||||||
// 缓存用户Session
|
// 缓存用户Session
|
||||||
_userManager.SetSession(new()
|
_userManager.SetSession(new()
|
||||||
{
|
{
|
||||||
UserId = user.Id,
|
UserId = user.Id,
|
||||||
TenantId = user.TenantId,
|
TenantId = user.TenantId,
|
||||||
Account = user.Account,
|
Account = user.Account,
|
||||||
RealName = user.RealName,
|
RealName = user.RealName,
|
||||||
AccountType = user.AccountType,
|
AccountType = user.AccountType,
|
||||||
OrgId = user.OrgId,
|
OrgId = user.OrgId,
|
||||||
OrgName = user.SysOrg?.Name,
|
OrgName = user.SysOrg?.Name,
|
||||||
OrgType = user.SysOrg?.Type,
|
OrgType = user.SysOrg?.Type,
|
||||||
OrgLevel = user.SysOrg?.Level,
|
OrgLevel = user.SysOrg?.Level,
|
||||||
TokenVersion = user.TokenVersion,
|
TokenVersion = user.TokenVersion,
|
||||||
}, TimeSpan.FromMinutes(tokenExpire));
|
}, TimeSpan.FromMinutes(tokenExpire));
|
||||||
|
|
||||||
// 生成刷新Token令牌
|
// 生成刷新Token令牌
|
||||||
var refreshTokenExpire = await _sysConfigService.GetRefreshTokenExpire();
|
var refreshTokenExpire = await _sysConfigService.GetRefreshTokenExpire();
|
||||||
var refreshToken = JWTEncryption.GenerateRefreshToken(accessToken, refreshTokenExpire);
|
var refreshToken = JWTEncryption.GenerateRefreshToken(accessToken, refreshTokenExpire);
|
||||||
|
|
||||||
// 设置响应报文头
|
// 设置响应报文头
|
||||||
_httpContextAccessor.HttpContext.SetTokensOfResponseHeaders(accessToken, refreshToken);
|
_httpContextAccessor.HttpContext.SetTokensOfResponseHeaders(accessToken, refreshToken);
|
||||||
|
|
||||||
// Swagger Knife4UI-AfterScript登录脚本
|
// Swagger Knife4UI-AfterScript登录脚本
|
||||||
// ke.global.setAllHeader('Authorization', 'Bearer ' + ke.response.headers['access-token']);
|
// ke.global.setAllHeader('Authorization', 'Bearer ' + ke.response.headers['access-token']);
|
||||||
|
|
||||||
// 更新用户登录信息
|
// 更新用户登录信息
|
||||||
user.LastLoginIp = _httpContextAccessor.HttpContext.GetRemoteIpAddressToIPv4(true);
|
user.TokenVersion += 1;
|
||||||
(user.LastLoginAddress, double? longitude, double? latitude) = CommonHelper.GetIpAddress(user.LastLoginIp);
|
user.LastLoginIp = _httpContextAccessor.HttpContext.GetRemoteIpAddressToIPv4(true);
|
||||||
user.LastLoginTime = DateTime.Now;
|
(user.LastLoginAddress, double? longitude, double? latitude) = CommonHelper.GetIpAddress(user.LastLoginIp);
|
||||||
user.LastLoginDevice = CommonHelper.GetClientDeviceInfo(_httpContextAccessor.HttpContext?.Request?.Headers?.UserAgent);
|
user.LastLoginTime = DateTime.Now;
|
||||||
await _sysUserRep.AsUpdateable(user).UpdateColumns(u => new
|
user.LastLoginDevice = CommonHelper.GetClientDeviceInfo(_httpContextAccessor.HttpContext?.Request?.Headers?.UserAgent);
|
||||||
{
|
await _sysUserRep.AsUpdateable(user).UpdateColumns(u => new
|
||||||
u.LastLoginIp,
|
{
|
||||||
u.LastLoginAddress,
|
u.TokenVersion,
|
||||||
u.LastLoginTime,
|
u.LastLoginIp,
|
||||||
u.LastLoginDevice,
|
u.LastLoginAddress,
|
||||||
}).ExecuteCommandAsync();
|
u.LastLoginTime,
|
||||||
|
u.LastLoginDevice,
|
||||||
// 缓存用户Token版本
|
}).ExecuteCommandAsync();
|
||||||
_sysCacheService.Set($"{CacheConst.KeyUserToken}{user.Id}", $"{user.TokenVersion}");
|
|
||||||
|
// 缓存用户Token版本
|
||||||
// 发布系统登录事件
|
_sysCacheService.Set($"{CacheConst.KeyUserToken}{user.Id}", $"{user.TokenVersion}");
|
||||||
await _eventPublisher.PublishAsync(UserEventTypeEnum.Login, user);
|
|
||||||
|
// 发布系统登录事件
|
||||||
return new LoginOutput
|
await _eventPublisher.PublishAsync(UserEventTypeEnum.Login, user);
|
||||||
{
|
|
||||||
AccessToken = accessToken,
|
return new LoginOutput
|
||||||
RefreshToken = refreshToken
|
{
|
||||||
};
|
AccessToken = accessToken,
|
||||||
}
|
RefreshToken = refreshToken
|
||||||
|
};
|
||||||
/// <summary>
|
}
|
||||||
/// 获取当前登陆用户信息 🔖
|
|
||||||
/// </summary>
|
/// <summary>
|
||||||
/// <returns></returns>
|
/// 获取当前登陆用户信息 🔖
|
||||||
[DisplayName("获取当前登陆用户信息")]
|
/// </summary>
|
||||||
public virtual async Task<LoginUserOutput> GetUserInfo()
|
/// <returns></returns>
|
||||||
{
|
[DisplayName("获取当前登陆用户信息")]
|
||||||
var user = await _sysUserRep.GetByIdAsync(_userManager.UserId) ?? throw Oops.Oh(ErrorCodeEnum.D1011).StatusCode(401);
|
public virtual async Task<LoginUserOutput> GetUserInfo()
|
||||||
// 机构
|
{
|
||||||
var org = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysOrg>>().GetByIdAsync(user.OrgId);
|
var user = await _sysUserRep.GetByIdAsync(_userManager.UserId) ?? throw Oops.Oh(ErrorCodeEnum.D1011).StatusCode(401);
|
||||||
// 职位
|
// 机构
|
||||||
var pos = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysPos>>().GetByIdAsync(user.PosId);
|
var org = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysOrg>>().GetByIdAsync(user.OrgId);
|
||||||
// 角色集合
|
// 职位
|
||||||
var roles = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysUserRole>>().AsQueryable()
|
var pos = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysPos>>().GetByIdAsync(user.PosId);
|
||||||
.LeftJoin<SysRole>((u, a) => u.RoleId == a.Id)
|
// 角色集合
|
||||||
.Where(u => u.UserId == user.Id)
|
var roles = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysUserRole>>().AsQueryable()
|
||||||
.Select((u, a) => new RoleDto { Id = a.Id, Name = a.Name, Code = a.Code }).ToListAsync();
|
.LeftJoin<SysRole>((u, a) => u.RoleId == a.Id)
|
||||||
// 接口集合
|
.Where(u => u.UserId == user.Id)
|
||||||
var apis = (await App.GetRequiredService<SysRoleService>().GetUserApiList())[0];
|
.Select((u, a) => new RoleDto { Id = a.Id, Name = a.Name, Code = a.Code }).ToListAsync();
|
||||||
// 个性化水印文字(若系统水印为空则不显示)
|
// 接口集合
|
||||||
var watermarkText = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysTenant>>().AsQueryable().Where(u => u.Id == user.TenantId).Select(u => u.Watermark).FirstAsync();
|
var apis = (await App.GetRequiredService<SysRoleService>().GetUserApiList())[0];
|
||||||
if (!string.IsNullOrWhiteSpace(watermarkText))
|
// 个性化水印文字(若系统水印为空则不显示)
|
||||||
watermarkText += $"-{user.RealName}"; // $"-{user.RealName}-{_httpContextAccessor.HttpContext.GetRemoteIpAddressToIPv4(true)}-{DateTime.Now}";
|
var watermarkText = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysTenant>>().AsQueryable().Where(u => u.Id == user.TenantId).Select(u => u.Watermark).FirstAsync();
|
||||||
|
if (!string.IsNullOrWhiteSpace(watermarkText))
|
||||||
return new LoginUserOutput
|
watermarkText += $"-{user.RealName}"; // $"-{user.RealName}-{_httpContextAccessor.HttpContext.GetRemoteIpAddressToIPv4(true)}-{DateTime.Now}";
|
||||||
{
|
|
||||||
Id = user.Id,
|
return new LoginUserOutput
|
||||||
TenantId = user.TenantId,
|
{
|
||||||
Account = user.Account,
|
Id = user.Id,
|
||||||
RealName = user.RealName,
|
TenantId = user.TenantId,
|
||||||
Phone = user.Phone,
|
Account = user.Account,
|
||||||
IdCardNum = user.IdCardNum,
|
RealName = user.RealName,
|
||||||
Email = user.Email,
|
Phone = user.Phone,
|
||||||
AccountType = user.AccountType,
|
IdCardNum = user.IdCardNum,
|
||||||
Avatar = user.Avatar,
|
Email = user.Email,
|
||||||
Address = user.Address,
|
AccountType = user.AccountType,
|
||||||
Signature = user.Signature,
|
Avatar = user.Avatar,
|
||||||
OrgId = user.OrgId,
|
Address = user.Address,
|
||||||
OrgName = org?.Name,
|
Signature = user.Signature,
|
||||||
OrgType = org?.Type,
|
OrgId = user.OrgId,
|
||||||
PosName = pos?.Name,
|
OrgName = org?.Name,
|
||||||
Apis = apis,
|
OrgType = org?.Type,
|
||||||
Roles = roles,
|
PosName = pos?.Name,
|
||||||
WatermarkText = watermarkText,
|
Apis = apis,
|
||||||
LastChangePasswordTime = user.LastChangePasswordTime
|
Roles = roles,
|
||||||
};
|
WatermarkText = watermarkText,
|
||||||
}
|
LastChangePasswordTime = user.LastChangePasswordTime
|
||||||
|
};
|
||||||
///// <summary>
|
}
|
||||||
///// 获取刷新Token 🔖
|
|
||||||
///// </summary>
|
///// <summary>
|
||||||
///// <param name="accessToken"></param>
|
///// 获取刷新Token 🔖
|
||||||
///// <returns></returns>
|
///// </summary>
|
||||||
//[DisplayName("获取刷新Token")]
|
///// <param name="accessToken"></param>
|
||||||
//public string GetRefreshToken([FromQuery] string accessToken)
|
///// <returns></returns>
|
||||||
//{
|
//[DisplayName("获取刷新Token")]
|
||||||
// var refreshTokenExpire = _sysConfigService.GetRefreshTokenExpire().GetAwaiter().GetResult();
|
//public string GetRefreshToken([FromQuery] string accessToken)
|
||||||
// return JWTEncryption.GenerateRefreshToken(accessToken, refreshTokenExpire);
|
//{
|
||||||
//}
|
// var refreshTokenExpire = _sysConfigService.GetRefreshTokenExpire().GetAwaiter().GetResult();
|
||||||
|
// return JWTEncryption.GenerateRefreshToken(accessToken, refreshTokenExpire);
|
||||||
/// <summary>
|
//}
|
||||||
/// 退出系统 🔖
|
|
||||||
/// </summary>
|
/// <summary>
|
||||||
[DisplayName("退出系统")]
|
/// 退出系统 🔖
|
||||||
public async Task Logout()
|
/// </summary>
|
||||||
{
|
[DisplayName("退出系统")]
|
||||||
var httpContext = _httpContextAccessor.HttpContext ?? throw Oops.Oh(ErrorCodeEnum.D1016);
|
public async Task Logout()
|
||||||
|
{
|
||||||
var userId = httpContext.User.FindFirst(ClaimConst.UserId)?.Value;
|
var httpContext = _httpContextAccessor.HttpContext ?? throw Oops.Oh(ErrorCodeEnum.D1016);
|
||||||
var version = httpContext.User.FindFirst(ClaimConst.TokenVersion)?.Value;
|
|
||||||
if (string.IsNullOrWhiteSpace(userId) || string.IsNullOrWhiteSpace(version) || string.IsNullOrWhiteSpace(_userManager.Account))
|
var userId = httpContext.User.FindFirst(ClaimConst.UserId)?.Value;
|
||||||
throw Oops.Oh(ErrorCodeEnum.D1011);
|
var version = httpContext.User.FindFirst(ClaimConst.TokenVersion)?.Value;
|
||||||
|
if (string.IsNullOrWhiteSpace(userId) || string.IsNullOrWhiteSpace(version) || string.IsNullOrWhiteSpace(_userManager.Account))
|
||||||
// 写入Token黑名单
|
throw Oops.Oh(ErrorCodeEnum.D1011);
|
||||||
var tokenExpire = await _sysConfigService.GetTokenExpire();
|
|
||||||
_sysCacheService.Set($"{CacheConst.KeyTokenBlacklist}:{userId}:{version}", _userManager.Account, TimeSpan.FromMinutes(tokenExpire));
|
// 写入Token黑名单
|
||||||
|
var tokenExpire = await _sysConfigService.GetTokenExpire();
|
||||||
// 发布系统退出事件
|
_sysCacheService.Set($"{CacheConst.KeyTokenBlacklist}:{userId}:{version}", _userManager.Account, TimeSpan.FromMinutes(tokenExpire));
|
||||||
await _eventPublisher.PublishAsync(UserEventTypeEnum.Logout, _userManager);
|
|
||||||
_userManager.RemoveSession(_userManager.UserId);
|
// 发布系统退出事件
|
||||||
|
await _eventPublisher.PublishAsync(UserEventTypeEnum.Logout, _userManager);
|
||||||
// 退出Swagger/设置无效Token响应头
|
_userManager.RemoveSession(_userManager.UserId);
|
||||||
_httpContextAccessor.HttpContext.SignoutToSwagger();
|
|
||||||
}
|
// 退出Swagger/设置无效Token响应头
|
||||||
|
_httpContextAccessor.HttpContext.SignoutToSwagger();
|
||||||
/// <summary>
|
}
|
||||||
/// 获取验证码 🔖
|
|
||||||
/// </summary>
|
/// <summary>
|
||||||
/// <returns></returns>
|
/// 获取验证码 🔖
|
||||||
[SuppressMonitor]
|
/// </summary>
|
||||||
[DisplayName("获取验证码")]
|
/// <returns></returns>
|
||||||
[AllowAnonymous]
|
[SuppressMonitor]
|
||||||
public CaptchaOutput GetCaptcha()
|
[DisplayName("获取验证码")]
|
||||||
{
|
[AllowAnonymous]
|
||||||
var codeId = YitIdHelper.NextId().ToString();
|
public CaptchaOutput GetCaptcha()
|
||||||
var captcha = _captcha.Generate(codeId);
|
{
|
||||||
var expirySeconds = App.GetOptions<CaptchaOptions>()?.ExpirySeconds ?? 60;
|
var codeId = YitIdHelper.NextId().ToString();
|
||||||
return new CaptchaOutput { Id = codeId, Img = captcha.Base64, ExpirySeconds = expirySeconds };
|
var captcha = _captcha.Generate(codeId);
|
||||||
}
|
var expirySeconds = App.GetOptions<CaptchaOptions>()?.ExpirySeconds ?? 60;
|
||||||
|
return new CaptchaOutput { Id = codeId, Img = captcha.Base64, ExpirySeconds = expirySeconds };
|
||||||
/// <summary>
|
}
|
||||||
/// Swagger登录检查 🔖
|
|
||||||
/// </summary>
|
/// <summary>
|
||||||
/// <returns></returns>
|
/// Swagger登录检查 🔖
|
||||||
[Route("/api/swagger/checkUrl"), NonUnify]
|
/// </summary>
|
||||||
[ApiDescriptionSettings(Description = "Swagger登录检查", DisableInherite = true)]
|
/// <returns></returns>
|
||||||
[AllowAnonymous]
|
[Route("/api/swagger/checkUrl"), NonUnify]
|
||||||
public int SwaggerCheckUrl()
|
[ApiDescriptionSettings(Description = "Swagger登录检查", DisableInherite = true)]
|
||||||
{
|
[AllowAnonymous]
|
||||||
return _httpContextAccessor.HttpContext.User.Identity.IsAuthenticated ? 200 : 401;
|
public int SwaggerCheckUrl()
|
||||||
}
|
{
|
||||||
|
return _httpContextAccessor.HttpContext.User.Identity.IsAuthenticated ? 200 : 401;
|
||||||
/// <summary>
|
}
|
||||||
/// Swagger登录提交 🔖
|
|
||||||
/// </summary>
|
/// <summary>
|
||||||
/// <param name="auth"></param>
|
/// Swagger登录提交 🔖
|
||||||
/// <returns></returns>
|
/// </summary>
|
||||||
[Route("/api/swagger/submitUrl"), NonUnify]
|
/// <param name="auth"></param>
|
||||||
[ApiDescriptionSettings(Description = "Swagger登录提交", DisableInherite = true)]
|
/// <returns></returns>
|
||||||
[AllowAnonymous]
|
[Route("/api/swagger/submitUrl"), NonUnify]
|
||||||
public async Task<int> SwaggerSubmitUrl([FromForm] SpecificationAuth auth)
|
[ApiDescriptionSettings(Description = "Swagger登录提交", DisableInherite = true)]
|
||||||
{
|
[AllowAnonymous]
|
||||||
try
|
public async Task<int> SwaggerSubmitUrl([FromForm] SpecificationAuth auth)
|
||||||
{
|
{
|
||||||
// 关闭默认租户验证码验证
|
try
|
||||||
var tenantList = _sysCacheService.Get<List<SysTenant>>(CacheConst.KeyTenant);
|
{
|
||||||
var tenant = tenantList.FirstOrDefault(u => u.Id == SqlSugarConst.DefaultTenantId);
|
// 关闭默认租户验证码验证
|
||||||
var tmpCaptcha = tenant.Captcha;
|
var tenantList = _sysCacheService.Get<List<SysTenant>>(CacheConst.KeyTenant);
|
||||||
tenant.Captcha = false;
|
var tenant = tenantList.FirstOrDefault(u => u.Id == SqlSugarConst.DefaultTenantId);
|
||||||
_sysCacheService.Set(CacheConst.KeyTenant, tenantList);
|
var tmpCaptcha = tenant.Captcha;
|
||||||
|
tenant.Captcha = false;
|
||||||
await Login(new LoginInput
|
_sysCacheService.Set(CacheConst.KeyTenant, tenantList);
|
||||||
{
|
|
||||||
Account = auth.UserName,
|
await Login(new LoginInput
|
||||||
Password = CryptogramHelper.SM2Encrypt(auth.Password),
|
{
|
||||||
TenantId = SqlSugarConst.DefaultTenantId
|
Account = auth.UserName,
|
||||||
});
|
Password = CryptogramHelper.SM2Encrypt(auth.Password),
|
||||||
|
TenantId = SqlSugarConst.DefaultTenantId
|
||||||
// 恢复默认租户验证码状态
|
});
|
||||||
tenant.Captcha = tmpCaptcha;
|
|
||||||
_sysCacheService.Set(CacheConst.KeyTenant, tenantList);
|
// 恢复默认租户验证码状态
|
||||||
|
tenant.Captcha = tmpCaptcha;
|
||||||
return 200;
|
_sysCacheService.Set(CacheConst.KeyTenant, tenantList);
|
||||||
}
|
|
||||||
catch (Exception)
|
return 200;
|
||||||
{
|
}
|
||||||
return 401;
|
catch (Exception)
|
||||||
}
|
{
|
||||||
}
|
return 401;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -35,7 +35,7 @@ public class SysDictDataService : IDynamicApiController, ITransient
|
|||||||
{
|
{
|
||||||
return await _sysDictDataRep.AsQueryable()
|
return await _sysDictDataRep.AsQueryable()
|
||||||
.Where(u => u.DictTypeId == input.DictTypeId)
|
.Where(u => u.DictTypeId == input.DictTypeId)
|
||||||
.WhereIF(!string.IsNullOrEmpty(input.Value?.Trim()), u => u.Code.Contains(input.Value))
|
.WhereIF(!string.IsNullOrEmpty(input.Value?.Trim()), u => u.Code.Contains(input.Value))
|
||||||
.WhereIF(!string.IsNullOrEmpty(input.Code?.Trim()), u => u.Code.Contains(input.Code))
|
.WhereIF(!string.IsNullOrEmpty(input.Code?.Trim()), u => u.Code.Contains(input.Code))
|
||||||
.WhereIF(!string.IsNullOrEmpty(input.Label?.Trim()), u => u.Label.Contains(input.Label))
|
.WhereIF(!string.IsNullOrEmpty(input.Label?.Trim()), u => u.Label.Contains(input.Label))
|
||||||
.OrderBy(u => new { u.OrderNo, u.Code })
|
.OrderBy(u => new { u.OrderNo, u.Code })
|
||||||
|
|||||||
@ -468,6 +468,18 @@ public class SysUserService : IDynamicApiController, ITransient
|
|||||||
return await _sysUserExtOrgService.GetUserExtOrgList(userId);
|
return await _sysUserExtOrgService.GetUserExtOrgList(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 重置用户Token版本
|
||||||
|
/// </summary>
|
||||||
|
[NonAction]
|
||||||
|
public void ResetTokenVersion()
|
||||||
|
{
|
||||||
|
_sysUserRep.AsUpdateable()
|
||||||
|
.Where(u => u.TokenVersion > int.MaxValue / 2)
|
||||||
|
.UpdateColumns(u => u.TokenVersion)
|
||||||
|
.ExecuteCommand();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 强制下线账号和失效Token
|
/// 强制下线账号和失效Token
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
222
Admin.NET/Admin.NET.Core/Utils/CustomJson/CustomJsonConverter.cs
Normal file
222
Admin.NET/Admin.NET.Core/Utils/CustomJson/CustomJsonConverter.cs
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||||
|
//
|
||||||
|
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||||
|
//
|
||||||
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||||
|
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Admin.NET.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自定义JSON转换器
|
||||||
|
/// </summary>
|
||||||
|
public class CustomJsonConverter : JsonConverterFactory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 判断是否可以转换指定类型
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="typeToConvert">要转换的类型</param>
|
||||||
|
/// <returns>是否可以转换</returns>
|
||||||
|
public override bool CanConvert(Type typeToConvert)
|
||||||
|
{
|
||||||
|
return typeToConvert.GetProperties()
|
||||||
|
.Any(p => p.GetCustomAttribute<CustomJsonPropertyAttribute>() != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建指定类型的转换器实例
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">要转换的类型</param>
|
||||||
|
/// <param name="options">JSON序列化选项</param>
|
||||||
|
/// <returns>JSON转换器实例</returns>
|
||||||
|
public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
var converterType = typeof(CustomConverter<>).MakeGenericType(type);
|
||||||
|
return (JsonConverter)Activator.CreateInstance(converterType, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自定义转换器实现类
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">要转换的类型</typeparam>
|
||||||
|
private class CustomConverter<T> : JsonConverter<T>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 基础JSON序列化选项
|
||||||
|
/// </summary>
|
||||||
|
private readonly JsonSerializerOptions _baseOptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JSON属性名到对象属性的映射字典
|
||||||
|
/// </summary>
|
||||||
|
private readonly Dictionary<string, PropertyInfo> _jsonToPropertyMap;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="baseOptions">基础JSON序列化选项</param>
|
||||||
|
public CustomConverter(JsonSerializerOptions baseOptions)
|
||||||
|
{
|
||||||
|
_baseOptions = new JsonSerializerOptions(baseOptions);
|
||||||
|
_jsonToPropertyMap = CreatePropertyMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建属性映射字典
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>属性映射字典</returns>
|
||||||
|
private Dictionary<string, PropertyInfo> CreatePropertyMap()
|
||||||
|
{
|
||||||
|
var map = new Dictionary<string, PropertyInfo>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var prop in typeof(T).GetProperties())
|
||||||
|
{
|
||||||
|
var customAttr = prop.GetCustomAttribute<CustomJsonPropertyAttribute>();
|
||||||
|
if (customAttr != null)
|
||||||
|
{
|
||||||
|
map[customAttr.Name] = prop;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var jsonName = _baseOptions.PropertyNamingPolicy?.ConvertName(prop.Name) ?? prop.Name;
|
||||||
|
map[jsonName] = prop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 反序列化JSON为对象
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reader">JSON读取器</param>
|
||||||
|
/// <param name="typeToConvert">要转换的类型</param>
|
||||||
|
/// <param name="options">JSON序列化选项</param>
|
||||||
|
/// <returns>反序列化后的对象</returns>
|
||||||
|
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.Null) return default;
|
||||||
|
|
||||||
|
// 创建实例并设置默认值
|
||||||
|
var instance = Activator.CreateInstance<T>();
|
||||||
|
object GetDefaultValue(Type type) => type.IsValueType ? Activator.CreateInstance(type) : null;
|
||||||
|
foreach (var prop in typeof(T).GetProperties().Where(p => p.CanWrite))
|
||||||
|
prop.SetValue(instance, GetDefaultValue(prop.PropertyType));
|
||||||
|
|
||||||
|
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.PropertyName)
|
||||||
|
{
|
||||||
|
var propertyName = reader.GetString();
|
||||||
|
if (_jsonToPropertyMap.TryGetValue(propertyName!, out var prop))
|
||||||
|
{
|
||||||
|
reader.Read();
|
||||||
|
var value = DeserializeWithCustomFormat(ref reader, prop, _baseOptions);
|
||||||
|
prop.SetValue(instance, value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
reader.Skip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 序列化对象为JSON
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="writer">JSON写入器</param>
|
||||||
|
/// <param name="value">要序列化的对象</param>
|
||||||
|
/// <param name="options">JSON序列化选项</param>
|
||||||
|
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteStartObject();
|
||||||
|
|
||||||
|
foreach (var prop in typeof(T).GetProperties())
|
||||||
|
{
|
||||||
|
var propValue = prop.GetValue(value);
|
||||||
|
if (propValue == null && _baseOptions.DefaultIgnoreCondition == JsonIgnoreCondition.WhenWritingNull)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var customAttr = prop.GetCustomAttribute<CustomJsonPropertyAttribute>();
|
||||||
|
var jsonName = customAttr?.Name ?? ToCamelCase(prop.Name);
|
||||||
|
|
||||||
|
writer.WritePropertyName(jsonName);
|
||||||
|
|
||||||
|
// 处理日期格式化
|
||||||
|
if (!string.IsNullOrEmpty(customAttr?.DateFormat))
|
||||||
|
{
|
||||||
|
if (propValue is DateTime dateTime)
|
||||||
|
{
|
||||||
|
writer.WriteStringValue(dateTime.ToString(customAttr.DateFormat));
|
||||||
|
}
|
||||||
|
else if (prop.PropertyType == typeof(DateTime?) && propValue != null)
|
||||||
|
{
|
||||||
|
var nullableDateTime = (DateTime?)propValue;
|
||||||
|
writer.WriteStringValue(nullableDateTime.Value.ToString(customAttr.DateFormat));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
JsonSerializer.Serialize(writer, propValue, prop.PropertyType, _baseOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
JsonSerializer.Serialize(writer, propValue, prop.PropertyType, _baseOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteEndObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转换属性名
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private string ToCamelCase(string name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(name)) return name;
|
||||||
|
|
||||||
|
return char.ToLowerInvariant(name[0]) + name.Substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 反序列化JSON为对象,处理自定义格式
|
||||||
|
/// </summary>
|
||||||
|
private object DeserializeWithCustomFormat(ref Utf8JsonReader reader, PropertyInfo prop, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
var customAttr = prop.GetCustomAttribute<CustomJsonPropertyAttribute>();
|
||||||
|
|
||||||
|
// 特殊处理日期类型
|
||||||
|
if (!string.IsNullOrEmpty(customAttr?.DateFormat))
|
||||||
|
{
|
||||||
|
if (prop.PropertyType == typeof(DateTime) && reader.TokenType == JsonTokenType.String)
|
||||||
|
{
|
||||||
|
var dateString = reader.GetString();
|
||||||
|
if (DateTime.TryParseExact(dateString, customAttr.DateFormat, null, DateTimeStyles.None, out var dateTime))
|
||||||
|
{
|
||||||
|
return dateTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (prop.PropertyType == typeof(DateTime?) && reader.TokenType == JsonTokenType.String)
|
||||||
|
{
|
||||||
|
var dateString = reader.GetString();
|
||||||
|
if (DateTime.TryParseExact(dateString, customAttr.DateFormat, null, DateTimeStyles.None, out var dateTime))
|
||||||
|
{
|
||||||
|
return (DateTime?)dateTime;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认处理
|
||||||
|
return JsonSerializer.Deserialize(ref reader, prop.PropertyType, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||||
|
//
|
||||||
|
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||||
|
//
|
||||||
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||||
|
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Admin.NET.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自定义Json序列化工具
|
||||||
|
/// </summary>
|
||||||
|
public static class CustomJsonHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 序列化对象
|
||||||
|
/// </summary>
|
||||||
|
public static string Serialize(object obj, JsonSerializerOptions globalOptions = null)
|
||||||
|
{
|
||||||
|
var options = CreateOptions(globalOptions);
|
||||||
|
return JsonSerializer.Serialize(obj, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 反序列化对象
|
||||||
|
/// </summary>
|
||||||
|
public static T Deserialize<T>(string json, JsonSerializerOptions globalOptions = null)
|
||||||
|
{
|
||||||
|
var options = CreateOptions(globalOptions);
|
||||||
|
return JsonSerializer.Deserialize<T>(json, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建序列化配置
|
||||||
|
/// </summary>
|
||||||
|
private static JsonSerializerOptions CreateOptions(JsonSerializerOptions globalOptions)
|
||||||
|
{
|
||||||
|
JsonSerializerOptions options = globalOptions != null ? new(globalOptions) : new();
|
||||||
|
options.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
|
||||||
|
options.ReferenceHandler = ReferenceHandler.IgnoreCycles;
|
||||||
|
options.Converters.Add(new JsonStringEnumConverter());
|
||||||
|
options.Converters.Add(new CustomJsonConverter());
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,225 +0,0 @@
|
|||||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
|
||||||
//
|
|
||||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
|
||||||
//
|
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
|
||||||
|
|
||||||
using System.Text.Encodings.Web;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using System.Text.Unicode;
|
|
||||||
|
|
||||||
namespace Admin.NET.Core;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 自定义JSON属性名称转换器
|
|
||||||
/// </summary>
|
|
||||||
public class CustomJsonPropertyConverter : JsonConverter<object>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 共享配置选项
|
|
||||||
/// </summary>
|
|
||||||
public static readonly JsonSerializerOptions Options = new()
|
|
||||||
{
|
|
||||||
Converters = { new CustomJsonPropertyConverter() },
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
||||||
PropertyNameCaseInsensitive = true,
|
|
||||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
|
||||||
ReferenceHandler = ReferenceHandler.IgnoreCycles,
|
|
||||||
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 缓存类型属性元数据
|
|
||||||
/// </summary>
|
|
||||||
private static readonly ConcurrentDictionary<Type, IReadOnlyList<PropertyMeta>> PropertyCache = new();
|
|
||||||
|
|
||||||
private readonly string _dateTimeFormat;
|
|
||||||
|
|
||||||
public CustomJsonPropertyConverter(string dateTimeFormat = "yyyy-MM-dd HH:mm:ss")
|
|
||||||
{
|
|
||||||
_dateTimeFormat = dateTimeFormat;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 检查类型是否包含自定义属性
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="typeToConvert"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public override bool CanConvert(Type typeToConvert)
|
|
||||||
{
|
|
||||||
return PropertyCache.GetOrAdd(typeToConvert, type =>
|
|
||||||
type.GetProperties()
|
|
||||||
.Where(p => p.GetCustomAttribute<CustomJsonPropertyAttribute>() != null)
|
|
||||||
.Select(p => new PropertyMeta(p))
|
|
||||||
.ToList().AsReadOnly()
|
|
||||||
).Count > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// JSON反序列化
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="reader"></param>
|
|
||||||
/// <param name="typeToConvert"></param>
|
|
||||||
/// <param name="options"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
var jsonDoc = JsonDocument.ParseValue(ref reader);
|
|
||||||
var instance = Activator.CreateInstance(typeToConvert);
|
|
||||||
var properties = PropertyCache.GetOrAdd(typeToConvert, BuildPropertyMeta);
|
|
||||||
|
|
||||||
foreach (var prop in properties)
|
|
||||||
{
|
|
||||||
if (jsonDoc.RootElement.TryGetProperty(prop.JsonName, out var value))
|
|
||||||
{
|
|
||||||
object propertyValue = prop.PropertyType switch
|
|
||||||
{
|
|
||||||
Type t when IsDateTimeType(t) => HandleDateTimeValue(value, t),
|
|
||||||
_ => JsonSerializer.Deserialize(value.GetRawText(), prop.PropertyType, options)
|
|
||||||
};
|
|
||||||
|
|
||||||
prop.SetValue(instance, propertyValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// JSON序列化
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="writer"></param>
|
|
||||||
/// <param name="value"></param>
|
|
||||||
/// <param name="options"></param>
|
|
||||||
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
writer.WriteStartObject();
|
|
||||||
var properties = PropertyCache.GetOrAdd(value.GetType(), BuildPropertyMeta);
|
|
||||||
|
|
||||||
foreach (var prop in properties)
|
|
||||||
{
|
|
||||||
var propertyValue = prop.GetValue(value);
|
|
||||||
writer.WritePropertyName(prop.JsonName);
|
|
||||||
|
|
||||||
if (propertyValue != null && IsDateTimeType(prop.PropertyType))
|
|
||||||
{
|
|
||||||
writer.WriteStringValue(FormatDateTime(propertyValue));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
JsonSerializer.Serialize(writer, propertyValue, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.WriteEndObject();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 构建属性元数据缓存
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="type"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
private static IReadOnlyList<PropertyMeta> BuildPropertyMeta(Type type)
|
|
||||||
{
|
|
||||||
return type.GetProperties()
|
|
||||||
.Where(p => p.GetCustomAttribute<CustomJsonPropertyAttribute>() != null)
|
|
||||||
.Select(p => new PropertyMeta(p))
|
|
||||||
.ToList().AsReadOnly();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 处理DateTime类型值
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value"></param>
|
|
||||||
/// <param name="targetType"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
private object HandleDateTimeValue(JsonElement value, Type targetType)
|
|
||||||
{
|
|
||||||
var dateStr = value.GetString();
|
|
||||||
if (string.IsNullOrEmpty(dateStr)) return null;
|
|
||||||
|
|
||||||
var date = DateTime.Parse(dateStr);
|
|
||||||
return targetType == typeof(DateTimeOffset) ? new DateTimeOffset(date) : date;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 格式化DateTime输出
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dateTime"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
private string FormatDateTime(object dateTime)
|
|
||||||
{
|
|
||||||
return dateTime switch
|
|
||||||
{
|
|
||||||
DateTime dt => dt.ToString(_dateTimeFormat),
|
|
||||||
DateTimeOffset dto => dto.ToString(_dateTimeFormat),
|
|
||||||
_ => dateTime?.ToString()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 检查是否为DateTime类型
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="type"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
private static bool IsDateTimeType(Type type)
|
|
||||||
{
|
|
||||||
var actualType = Nullable.GetUnderlyingType(type) ?? type;
|
|
||||||
return actualType == typeof(DateTime) || actualType == typeof(DateTimeOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 属性元数据包装类
|
|
||||||
/// </summary>
|
|
||||||
private class PropertyMeta
|
|
||||||
{
|
|
||||||
private readonly PropertyInfo _property;
|
|
||||||
private readonly Func<object, object> _getter;
|
|
||||||
private readonly Action<object, object> _setter;
|
|
||||||
|
|
||||||
public string JsonName { get; }
|
|
||||||
public Type PropertyType => _property.PropertyType;
|
|
||||||
|
|
||||||
public PropertyMeta(PropertyInfo property)
|
|
||||||
{
|
|
||||||
_property = property;
|
|
||||||
// 获取自定义属性名或使用原属性名
|
|
||||||
JsonName = property.GetCustomAttribute<CustomJsonPropertyAttribute>()?.Name ?? property.Name;
|
|
||||||
|
|
||||||
// 编译表达式树优化属性访问性能
|
|
||||||
var instanceParam = Expression.Parameter(typeof(object), "instance");
|
|
||||||
|
|
||||||
// Getter表达式编译
|
|
||||||
var getterExpr = Expression.Lambda<Func<object, object>>(
|
|
||||||
Expression.Convert(
|
|
||||||
Expression.Property(
|
|
||||||
Expression.Convert(instanceParam, property.DeclaringType),
|
|
||||||
property),
|
|
||||||
typeof(object)),
|
|
||||||
instanceParam);
|
|
||||||
_getter = getterExpr.Compile();
|
|
||||||
|
|
||||||
// Setter表达式编译(如果属性可写)
|
|
||||||
if (property.CanWrite)
|
|
||||||
{
|
|
||||||
var valueParam = Expression.Parameter(typeof(object), "value");
|
|
||||||
var setterExpr = Expression.Lambda<Action<object, object>>(
|
|
||||||
Expression.Assign(
|
|
||||||
Expression.Property(
|
|
||||||
Expression.Convert(instanceParam, property.DeclaringType),
|
|
||||||
property),
|
|
||||||
Expression.Convert(valueParam, property.PropertyType)),
|
|
||||||
instanceParam, valueParam);
|
|
||||||
_setter = setterExpr.Compile();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public object GetValue(object instance) => _getter(instance);
|
|
||||||
|
|
||||||
public void SetValue(object instance, object value)
|
|
||||||
{
|
|
||||||
_setter?.Invoke(instance, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -19,4 +19,4 @@
|
|||||||
<ProjectReference Include="..\Admin.NET.Application\Admin.NET.Application.csproj" />
|
<ProjectReference Include="..\Admin.NET.Application\Admin.NET.Application.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@ -70,10 +70,7 @@ namespace Admin.NET.Web.Core
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证Token版本号
|
if (!string.IsNullOrWhiteSpace(userId))
|
||||||
var tokenVersion1 = httpContext.User.FindFirst(ClaimConst.TokenVersion)?.Value;
|
|
||||||
var tokenVersion2 = sysCacheService.Get<string>($"{CacheConst.KeyUserToken}{userId}");
|
|
||||||
if (string.IsNullOrWhiteSpace(tokenVersion2) && !string.IsNullOrWhiteSpace(userId))
|
|
||||||
{
|
{
|
||||||
// 查库并缓存用户Token版本
|
// 查库并缓存用户Token版本
|
||||||
var user = await serviceScope.ServiceProvider.GetRequiredService<ISqlSugarClient>().Queryable<SysUser>().FirstAsync(u => u.Id == long.Parse(userId));
|
var user = await serviceScope.ServiceProvider.GetRequiredService<ISqlSugarClient>().Queryable<SysUser>().FirstAsync(u => u.Id == long.Parse(userId));
|
||||||
@ -85,14 +82,6 @@ namespace Admin.NET.Web.Core
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sysCacheService.Set($"{CacheConst.KeyUserToken}{user.Id}", $"{user.TokenVersion}");
|
sysCacheService.Set($"{CacheConst.KeyUserToken}{user.Id}", $"{user.TokenVersion}");
|
||||||
tokenVersion2 = user.TokenVersion.ToString();
|
|
||||||
}
|
|
||||||
if (string.IsNullOrWhiteSpace(tokenVersion1) || tokenVersion1 != tokenVersion2)
|
|
||||||
{
|
|
||||||
context.Fail(new AuthorizationFailureReason(this, "令牌已失效,请重新登录。"));
|
|
||||||
context.StatusCode(StatusCodes.Status401Unauthorized);
|
|
||||||
context.GetCurrentHttpContext().SignoutToSwagger();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证租户有效期
|
// 验证租户有效期
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,25 +0,0 @@
|
|||||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
|
||||||
//
|
|
||||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
|
||||||
//
|
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
|
||||||
|
|
||||||
using Furion.ConfigurableOptions;
|
|
||||||
|
|
||||||
namespace Admin.NET.Plugin.WorkWeixin.Option;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 企业微信配置项
|
|
||||||
/// </summary>
|
|
||||||
public class WorkWeixinOptions : IConfigurableOptions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 企业ID
|
|
||||||
/// </summary>
|
|
||||||
public string CorpId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 企业微信凭证密钥
|
|
||||||
/// </summary>
|
|
||||||
public string CorpSecret { get; set; }
|
|
||||||
}
|
|
||||||
@ -1,425 +0,0 @@
|
|||||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
|
||||||
//
|
|
||||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
|
||||||
//
|
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
|
||||||
|
|
||||||
namespace Admin.NET.Plugin.WorkWeixin.Proxy;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 创建群聊会话输入参数
|
|
||||||
/// </summary>
|
|
||||||
public class CreatAppChatInput
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 群名称
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("name")]
|
|
||||||
[JsonPropertyName("name")]
|
|
||||||
[Required(ErrorMessage = "群名称不能为空"), MaxLength(50, ErrorMessage = "群名称最多不能超过50个字符")]
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 群主Id
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("owner")]
|
|
||||||
[JsonPropertyName("owner")]
|
|
||||||
[Required(ErrorMessage = "群主Id不能为空")]
|
|
||||||
public string Owner { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 群成员Id列表
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("userlist")]
|
|
||||||
[JsonPropertyName("userlist")]
|
|
||||||
[NotEmpty(ErrorMessage = "群成员列表不能为空")]
|
|
||||||
public List<string> UserList { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 群Id
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("chatid")]
|
|
||||||
[JsonPropertyName("chatid")]
|
|
||||||
[Required(ErrorMessage = "群Id不能为空"), MaxLength(32, ErrorMessage = "群Id最多不能超过32个字符")]
|
|
||||||
public string ChatId { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 修改群聊会话输入参数
|
|
||||||
/// </summary>
|
|
||||||
public class UpdateAppChatInput
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 群Id
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("chatid")]
|
|
||||||
[JsonPropertyName("chatid")]
|
|
||||||
[Required(ErrorMessage = "群Id不能为空"), MaxLength(32, ErrorMessage = "群Id最多不能超过32个字符")]
|
|
||||||
public string ChatId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 群名称
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("name")]
|
|
||||||
[JsonPropertyName("name")]
|
|
||||||
[Required(ErrorMessage = "群名称不能为空"), MaxLength(50, ErrorMessage = "群名称最多不能超过50个字符")]
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 群主Id
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("owner")]
|
|
||||||
[JsonPropertyName("owner")]
|
|
||||||
[Required(ErrorMessage = "群主Id不能为空")]
|
|
||||||
public string Owner { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 添加成员的id列表
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("add_user_list")]
|
|
||||||
[JsonPropertyName("add_user_list")]
|
|
||||||
public List<string> AddUserList { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 踢出成员的id列表
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("del_user_list")]
|
|
||||||
[JsonPropertyName("del_user_list")]
|
|
||||||
public List<string> DelUserList { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 应用消息推送输入基类参数
|
|
||||||
/// </summary>
|
|
||||||
public class SendBaseAppChatInput
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 群Id
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("chatid")]
|
|
||||||
[JsonPropertyName("chatid")]
|
|
||||||
[Required(ErrorMessage = "群Id不能为空"), MaxLength(32, ErrorMessage = "群Id最多不能超过32个字符")]
|
|
||||||
public string ChatId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 消息类型
|
|
||||||
/// </summary>
|
|
||||||
/// <example>text:文本消息</example>
|
|
||||||
/// <example>image:图片消息</example>
|
|
||||||
/// <example>voice:图片消息</example>
|
|
||||||
/// <example>video:视频消息</example>
|
|
||||||
/// <example>file:文件消息</example>
|
|
||||||
/// <example>textcard:文本卡片</example>
|
|
||||||
/// <example>news:图文消息</example>
|
|
||||||
/// <example>mpnews:图文消息(存储在企业微信)</example>
|
|
||||||
/// <example>markdown:markdown消息</example>
|
|
||||||
[JsonProperty("msgtype")]
|
|
||||||
[JsonPropertyName("msgtype")]
|
|
||||||
[Required(ErrorMessage = "消息类型不能为空")]
|
|
||||||
protected string MsgType { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 是否是保密消息
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("safe")]
|
|
||||||
[JsonPropertyName("safe")]
|
|
||||||
[Required(ErrorMessage = "消息类型不能为空")]
|
|
||||||
public int Safe { get; set; }
|
|
||||||
|
|
||||||
public SendBaseAppChatInput(string chatId, string msgType, bool safe = false)
|
|
||||||
{
|
|
||||||
ChatId = chatId;
|
|
||||||
MsgType = msgType;
|
|
||||||
Safe = safe ? 1 : 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 推送文本消息输入参数
|
|
||||||
/// </summary>
|
|
||||||
public class SendTextAppChatInput : SendBaseAppChatInput
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 消息内容
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("text")]
|
|
||||||
[JsonPropertyName("text")]
|
|
||||||
public object Text { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 文本消息
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="chatId"></param>
|
|
||||||
/// <param name="content"></param>
|
|
||||||
/// <param name="safe"></param>
|
|
||||||
public SendTextAppChatInput(string chatId, string content, bool safe = false) : base(chatId, "text", safe)
|
|
||||||
{
|
|
||||||
Text = new { content };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 推送图片消息输入参数
|
|
||||||
/// </summary>
|
|
||||||
public class SendImageAppChatInput : SendBaseAppChatInput
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 消息内容
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("image")]
|
|
||||||
[JsonPropertyName("image")]
|
|
||||||
public object Image { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 图片消息
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="chatId"></param>
|
|
||||||
/// <param name="mediaId"></param>
|
|
||||||
/// <param name="safe"></param>
|
|
||||||
public SendImageAppChatInput(string chatId, string mediaId, bool safe = false) : base(chatId, "image", safe)
|
|
||||||
{
|
|
||||||
Image = new { media_id = mediaId };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 推送语音消息输入参数
|
|
||||||
/// </summary>
|
|
||||||
public class SendVoiceAppChatInput : SendBaseAppChatInput
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 消息内容
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("voice")]
|
|
||||||
[JsonPropertyName("voice")]
|
|
||||||
public object Voice { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 语音消息
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="chatId"></param>
|
|
||||||
/// <param name="mediaId"></param>
|
|
||||||
/// <param name="safe"></param>
|
|
||||||
public SendVoiceAppChatInput(string chatId, string mediaId, bool safe = false) : base(chatId, "voice", safe)
|
|
||||||
{
|
|
||||||
Voice = new { media_id = mediaId };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 推送视频消息输入参数
|
|
||||||
/// </summary>
|
|
||||||
public class SendVideoAppChatInput : SendBaseAppChatInput
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 消息内容
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("video")]
|
|
||||||
[JsonPropertyName("video")]
|
|
||||||
public object Video { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 视频消息
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="chatId"></param>
|
|
||||||
/// <param name="title"></param>
|
|
||||||
/// <param name="description"></param>
|
|
||||||
/// <param name="mediaId"></param>
|
|
||||||
/// <param name="safe"></param>
|
|
||||||
public SendVideoAppChatInput(string chatId, string title, string description, string mediaId, bool safe = false) : base(chatId, "video", safe)
|
|
||||||
{
|
|
||||||
Video = new
|
|
||||||
{
|
|
||||||
media_id = mediaId,
|
|
||||||
description,
|
|
||||||
title
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 推送视频消息输入参数
|
|
||||||
/// </summary>
|
|
||||||
public class SendFileAppChatInput : SendBaseAppChatInput
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 消息内容
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("file")]
|
|
||||||
[JsonPropertyName("file")]
|
|
||||||
public object File { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 文件消息
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="chatId"></param>
|
|
||||||
/// <param name="mediaId"></param>
|
|
||||||
/// <param name="safe"></param>
|
|
||||||
public SendFileAppChatInput(string chatId, string mediaId, bool safe = false) : base(chatId, "video", safe)
|
|
||||||
{
|
|
||||||
File = new { media_id = mediaId };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 推送文本卡片消息输入参数
|
|
||||||
/// </summary>
|
|
||||||
public class SendTextCardAppChatInput : SendBaseAppChatInput
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 消息内容
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("textcard")]
|
|
||||||
[JsonPropertyName("textcard")]
|
|
||||||
public object TextCard { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 文本卡片消息
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="chatId"></param>
|
|
||||||
/// <param name="title">标题</param>
|
|
||||||
/// <param name="description">描述</param>
|
|
||||||
/// <param name="url">点击后跳转的链接</param>
|
|
||||||
/// <param name="btnTxt">按钮文字</param>
|
|
||||||
/// <param name="safe"></param>
|
|
||||||
public SendTextCardAppChatInput(string chatId, string title, string description, string url, string btnTxt, bool safe = false) : base(chatId, "textcard", safe)
|
|
||||||
{
|
|
||||||
TextCard = new
|
|
||||||
{
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
url,
|
|
||||||
btntxt = btnTxt
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 图文消息项
|
|
||||||
/// </summary>
|
|
||||||
public class SendNewsItem
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 标题
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("title")]
|
|
||||||
[JsonPropertyName("title")]
|
|
||||||
public string Title { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 描述
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("description")]
|
|
||||||
[JsonPropertyName("description")]
|
|
||||||
public string Description { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 描述
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("url")]
|
|
||||||
[JsonPropertyName("url")]
|
|
||||||
public string Url { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 图文消息的图片链接(推荐大图1068 * 455,小图150 * 150)
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("picurl")]
|
|
||||||
[JsonPropertyName("picurl")]
|
|
||||||
public string PicUrl { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 推送图文消息输入参数
|
|
||||||
/// </summary>
|
|
||||||
public class SendNewsAppChatInput : SendBaseAppChatInput
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 消息内容
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("news")]
|
|
||||||
[JsonPropertyName("news")]
|
|
||||||
public object News { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 图文消息
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="chatId"></param>
|
|
||||||
/// <param name="newsList">图文消息列表</param>
|
|
||||||
/// <param name="safe"></param>
|
|
||||||
public SendNewsAppChatInput(string chatId, List<SendNewsItem> newsList, bool safe = false) : base(chatId, "news", safe)
|
|
||||||
{
|
|
||||||
News = new { articles = newsList };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 图文消息项
|
|
||||||
/// </summary>
|
|
||||||
public class SendMpNewsItem
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 标题
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("title")]
|
|
||||||
[JsonPropertyName("title")]
|
|
||||||
public string Title { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 缩略图media_id
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("thumb_media_id")]
|
|
||||||
[JsonPropertyName("thumb_media_id")]
|
|
||||||
public string ThumbMediaId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 作者
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("author")]
|
|
||||||
[JsonPropertyName("author")]
|
|
||||||
public string Author { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 点击“阅读原文”之后的页面链接
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("content_source_url")]
|
|
||||||
[JsonPropertyName("content_source_url")]
|
|
||||||
public string ContentSourceUrl { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 图文消息的内容
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("content")]
|
|
||||||
[JsonPropertyName("content")]
|
|
||||||
public string Content { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 图文消息的描述
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("digest")]
|
|
||||||
[JsonPropertyName("digest")]
|
|
||||||
public string Digest { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 推送图文消息(存储在企业微信)输入参数
|
|
||||||
/// </summary>
|
|
||||||
public class SendMpNewsAppChatInput : SendBaseAppChatInput
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 消息内容
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("mpnews")]
|
|
||||||
[JsonPropertyName("mpnews")]
|
|
||||||
public object MpNews { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 图文消息
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="chatId"></param>
|
|
||||||
/// <param name="mpNewsList">图文消息列表</param>
|
|
||||||
/// <param name="safe"></param>
|
|
||||||
public SendMpNewsAppChatInput(string chatId, List<SendMpNewsItem> mpNewsList, bool safe = false) : base(chatId, "mpnews", safe)
|
|
||||||
{
|
|
||||||
MpNews = new { articles = mpNewsList };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
|
||||||
//
|
|
||||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
|
||||||
//
|
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
|
||||||
|
|
||||||
namespace Admin.NET.Plugin.WorkWeixin.Proxy;
|
|
||||||
|
|
||||||
public class CreatAppChatOutput : BaseWorkOutput
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 群聊的唯一标志
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("chatid")]
|
|
||||||
[JsonPropertyName("chatid")]
|
|
||||||
public string ChatId { get; set; }
|
|
||||||
}
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
|
||||||
//
|
|
||||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
|
||||||
//
|
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
|
||||||
|
|
||||||
namespace Admin.NET.Plugin.WorkWeixin.Proxy.AppChat;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 群聊会话远程调用服务
|
|
||||||
/// </summary>
|
|
||||||
public interface IWorkWeixinAppChatHttp : IHttpDeclarative
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 创建群聊会话
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="accessToken"></param>
|
|
||||||
/// <param name="body"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
/// <inheritdoc cref="https://developer.work.weixin.qq.com/document/path/90245"/>
|
|
||||||
[Post("https://qyapi.weixin.qq.com/cgi-bin/appchat/create")]
|
|
||||||
Task<CreatAppChatOutput> Create([Query("access_token")] string accessToken, [Body] CreatAppChatInput body);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 修改群聊会话
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="accessToken"></param>
|
|
||||||
/// <param name="body"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
/// <inheritdoc cref="https://developer.work.weixin.qq.com/document/path/98913"/>
|
|
||||||
[Post("https://qyapi.weixin.qq.com/cgi-bin/appchat/update")]
|
|
||||||
Task<CreatAppChatOutput> Update([Query("access_token")] string accessToken, [Body] UpdateAppChatInput body);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取群聊会话
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="accessToken"></param>
|
|
||||||
/// <param name="chatId"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
/// <inheritdoc cref="https://developer.work.weixin.qq.com/document/path/98914"/>
|
|
||||||
[Get("https://qyapi.weixin.qq.com/cgi-bin/appchat/get")]
|
|
||||||
Task<CreatAppChatOutput> Get([Query("access_token")] string accessToken, [Query("chatid")] string chatId);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 应用推送消息
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="accessToken"></param>
|
|
||||||
/// <param name="body"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
/// <inheritdoc cref="https://developer.work.weixin.qq.com/document/path/90248"/>
|
|
||||||
[Post("https://qyapi.weixin.qq.com/cgi-bin/appchat/send")]
|
|
||||||
Task<BaseWorkOutput> Send([Query("access_token")] string accessToken, [Body] SendBaseAppChatInput body);
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
|
||||||
//
|
|
||||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
|
||||||
//
|
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
|
||||||
|
|
||||||
namespace Admin.NET.Plugin.WorkWeixin.Proxy;
|
|
||||||
|
|
||||||
public class AuthAccessTokenHttpOutput : BaseWorkOutput
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 获取到的凭证
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("access_token")]
|
|
||||||
[JsonPropertyName("access_token")]
|
|
||||||
public string AccessToken { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 凭证的有效时间(秒)
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("expires_in")]
|
|
||||||
[JsonPropertyName("expires_in")]
|
|
||||||
public int ExpiresIn { get; set; }
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
|
||||||
//
|
|
||||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
|
||||||
//
|
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
|
||||||
|
|
||||||
namespace Admin.NET.Plugin.WorkWeixin.Proxy.AppChat;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 授权会话远程服务
|
|
||||||
/// </summary>
|
|
||||||
public interface IWorkWeixinAuthHttp : IHttpDeclarative
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 获取接口凭证
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="corpId">企业ID</param>
|
|
||||||
/// <param name="corpSecret">应用的凭证密钥</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
/// <inheritdoc cref="https://developer.work.weixin.qq.com/document/path/91039"/>
|
|
||||||
[Post("https://qyapi.weixin.qq.com/cgi-bin/gettoken")]
|
|
||||||
Task<AuthAccessTokenHttpOutput> GetToken([Query("corpid")] string corpId, [Query("corpsecret")] string corpSecret);
|
|
||||||
}
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
|
||||||
//
|
|
||||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
|
||||||
//
|
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
|
||||||
|
|
||||||
namespace Admin.NET.Plugin.WorkWeixin.Proxy;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 创建部门输入参数
|
|
||||||
/// </summary>
|
|
||||||
public class DepartmentHttpInput
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 部门名称
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("id")]
|
|
||||||
[JsonPropertyName("id")]
|
|
||||||
public long? Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 父部门id
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("parentid")]
|
|
||||||
[JsonPropertyName("parentid")]
|
|
||||||
public long? ParentId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 部门名称
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("name")]
|
|
||||||
[JsonPropertyName("name")]
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 英文名称
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("name_en")]
|
|
||||||
[JsonPropertyName("name_en")]
|
|
||||||
public string NameEn { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 序号
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("order")]
|
|
||||||
[JsonPropertyName("order")]
|
|
||||||
public int? Order { get; set; }
|
|
||||||
}
|
|
||||||
@ -1,95 +0,0 @@
|
|||||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
|
||||||
//
|
|
||||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
|
||||||
//
|
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
|
||||||
|
|
||||||
namespace Admin.NET.Plugin.WorkWeixin.Proxy;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 部门Id列表输出参数
|
|
||||||
/// </summary>
|
|
||||||
public class DepartmentIdOutput : BaseWorkOutput
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// id
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("department_id")]
|
|
||||||
[JsonPropertyName("department_id")]
|
|
||||||
public List<DepartmentItemOutput> DepartmentList { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 部门Id输出参数
|
|
||||||
/// </summary>
|
|
||||||
public class DepartmentItemOutput
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 部门名称
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("id")]
|
|
||||||
[JsonPropertyName("id")]
|
|
||||||
public long? Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 父部门id
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("parentid")]
|
|
||||||
[JsonPropertyName("parentid")]
|
|
||||||
public long? ParentId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 序号
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("order")]
|
|
||||||
[JsonPropertyName("order")]
|
|
||||||
public int? Order { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 部门输出参数
|
|
||||||
/// </summary>
|
|
||||||
public class DepartmentOutput
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 部门名称
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("id")]
|
|
||||||
[JsonPropertyName("id")]
|
|
||||||
public long? Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 父部门id
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("parentid")]
|
|
||||||
[JsonPropertyName("parentid")]
|
|
||||||
public long? ParentId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 部门名称
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("name")]
|
|
||||||
[JsonPropertyName("name")]
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 英文名称
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("name_en")]
|
|
||||||
[JsonPropertyName("name_en")]
|
|
||||||
public string NameEn { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 部门负责人列表
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("department_leader")]
|
|
||||||
[JsonPropertyName("department_leader")]
|
|
||||||
public List<string> Leaders { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 序号
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("order")]
|
|
||||||
[JsonPropertyName("order")]
|
|
||||||
public int? Order { get; set; }
|
|
||||||
}
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
|
||||||
//
|
|
||||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
|
||||||
//
|
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
|
||||||
|
|
||||||
namespace Admin.NET.Plugin.WorkWeixin.Proxy.AppChat;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 部门远程调用服务
|
|
||||||
/// </summary>
|
|
||||||
public interface IDepartmentHttp : IHttpDeclarative
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 创建部门
|
|
||||||
/// https://developer.work.weixin.qq.com/document/path/90205
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="accessToken"></param>
|
|
||||||
/// <param name="body"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[Post("https://qyapi.weixin.qq.com/cgi-bin/department/create")]
|
|
||||||
Task<BaseWorkIdOutput> Create([Query("access_token")] string accessToken, [Body] DepartmentHttpInput body);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 修改部门
|
|
||||||
/// https://developer.work.weixin.qq.com/document/path/90206
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="accessToken"></param>
|
|
||||||
/// <param name="body"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[Post("https://qyapi.weixin.qq.com/cgi-bin/department/update")]
|
|
||||||
Task<BaseWorkOutput> Update([Query("access_token")] string accessToken, [Body] DepartmentHttpInput body);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 删除部门
|
|
||||||
/// https://developer.work.weixin.qq.com/document/path/90207
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="accessToken"></param>
|
|
||||||
/// <param name="id"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[Get("https://qyapi.weixin.qq.com/cgi-bin/department/delete")]
|
|
||||||
Task<BaseWorkOutput> Delete([Query("access_token")] string accessToken, [Query] long id);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取部门Id列表
|
|
||||||
/// https://developer.work.weixin.qq.com/document/path/90208
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="accessToken"></param>
|
|
||||||
/// <param name="id"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[Get("https://qyapi.weixin.qq.com/cgi-bin/department/simplelist")]
|
|
||||||
Task<DepartmentIdOutput> SimpleList([Query("access_token")] string accessToken, [Query] long id);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取部门详情
|
|
||||||
/// https://developer.work.weixin.qq.com/document/path/90208
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="accessToken"></param>
|
|
||||||
/// <param name="id"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[Get("https://qyapi.weixin.qq.com/cgi-bin/department/get")]
|
|
||||||
Task<DepartmentOutput> Get([Query("access_token")] string accessToken, [Query] long id);
|
|
||||||
}
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
|
||||||
//
|
|
||||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
|
||||||
//
|
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
|
||||||
|
|
||||||
namespace Admin.NET.Plugin.WorkWeixin.Proxy;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 标签输入参数
|
|
||||||
/// </summary>
|
|
||||||
public class TagHttpInput
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 标签id
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("tagid")]
|
|
||||||
[JsonPropertyName("tagid")]
|
|
||||||
public long? TagId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 标签名称
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("tagname")]
|
|
||||||
[JsonPropertyName("tagname")]
|
|
||||||
public string TagName { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 增加标签成员输入参数
|
|
||||||
/// </summary>
|
|
||||||
public class TagUsersTagInput
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 标签id
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("tagid")]
|
|
||||||
[JsonPropertyName("tagid")]
|
|
||||||
public long TagId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 企业成员ID列表
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("userlist")]
|
|
||||||
[JsonPropertyName("userlist")]
|
|
||||||
public List<string> UserList { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 企业部门ID列表
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("partylist")]
|
|
||||||
[JsonPropertyName("partylist")]
|
|
||||||
public List<long> PartyList { get; set; }
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
|
||||||
//
|
|
||||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
|
||||||
//
|
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
|
||||||
|
|
||||||
namespace Admin.NET.Plugin.WorkWeixin.Proxy;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 新增标签输出参数
|
|
||||||
/// </summary>
|
|
||||||
public class TagIdHttpOutput : BaseWorkOutput
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 标签Id
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("tagid")]
|
|
||||||
[JsonPropertyName("tagid")]
|
|
||||||
public long? TagId { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 标签列表输出参数
|
|
||||||
/// </summary>
|
|
||||||
public class TagListHttpOutput : BaseWorkOutput
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 标签Id
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("taglist")]
|
|
||||||
[JsonPropertyName("taglist")]
|
|
||||||
public List<TagHttpInput> TagList { get; set; }
|
|
||||||
}
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
|
||||||
//
|
|
||||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
|
||||||
//
|
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
|
||||||
|
|
||||||
namespace Admin.NET.Plugin.WorkWeixin.Proxy;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 标签远程调用服务
|
|
||||||
/// </summary>
|
|
||||||
public interface ITagHttp : IHttpDeclarative
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 创建标签
|
|
||||||
/// https://developer.work.weixin.qq.com/document/path/90210
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="accessToken"></param>
|
|
||||||
/// <param name="body"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[Post("https://qyapi.weixin.qq.com/cgi-bin/tag/create")]
|
|
||||||
Task<BaseWorkIdOutput> Create([Query("access_token")] string accessToken, [Body] TagHttpInput body);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 更新标签名字
|
|
||||||
/// https://developer.work.weixin.qq.com/document/path/90211
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="accessToken"></param>
|
|
||||||
/// <param name="body"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[Post("https://qyapi.weixin.qq.com/cgi-bin/tag/update")]
|
|
||||||
Task<TagIdHttpOutput> Update([Query("access_token")] string accessToken, [Body] TagHttpInput body);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 删除标签
|
|
||||||
/// https://developer.work.weixin.qq.com/document/path/90212
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="accessToken"></param>
|
|
||||||
/// <param name="tagId"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[Get("https://qyapi.weixin.qq.com/cgi-bin/tag/delete")]
|
|
||||||
Task<BaseWorkOutput> Delete([Query("access_token")] string accessToken, [Query("tagid")] long tagId);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取标签详情
|
|
||||||
/// https://developer.work.weixin.qq.com/document/path/90213
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="accessToken"></param>
|
|
||||||
/// <param name="tagId"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[Get("https://qyapi.weixin.qq.com/cgi-bin/tag/get")]
|
|
||||||
Task<DepartmentOutput> Get([Query("access_token")] string accessToken, [Query("tagid")] long tagId);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 增加标签成员
|
|
||||||
/// https://developer.work.weixin.qq.com/document/path/90214
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="accessToken"></param>
|
|
||||||
/// <param name="body"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[Post("https://qyapi.weixin.qq.com/cgi-bin/tag/addtagusers")]
|
|
||||||
Task<DepartmentOutput> AddTagUsers([Query("access_token")] string accessToken, [Body] TagUsersTagInput body);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 删除标签成员
|
|
||||||
/// https://developer.work.weixin.qq.com/document/path/90215
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="accessToken"></param>
|
|
||||||
/// <param name="body"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[Post("https://qyapi.weixin.qq.com/cgi-bin/tag/deltagusers")]
|
|
||||||
Task<DepartmentOutput> DelTagUsers([Query("access_token")] string accessToken, [Body] TagUsersTagInput body);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取标签列表
|
|
||||||
/// https://developer.work.weixin.qq.com/document/path/90216
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="accessToken"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[Get("https://qyapi.weixin.qq.com/cgi-bin/tag/list")]
|
|
||||||
Task<TagListHttpOutput> List([Query("access_token")] string accessToken);
|
|
||||||
}
|
|
||||||
@ -5,12 +5,11 @@
|
|||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||||
|
|
||||||
|
|
||||||
using Admin.NET.Plugin.WorkWeixin.Const;
|
|
||||||
using Furion.FriendlyException;
|
|
||||||
using Furion.JsonSerialization;
|
|
||||||
using Furion.Logging;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using Admin.NET.Plugin.WorkWeixin.Const;
|
||||||
|
using Furion.FriendlyException;
|
||||||
|
using Furion.Logging;
|
||||||
|
|
||||||
namespace Admin.NET.Plugin.WorkWeixin;
|
namespace Admin.NET.Plugin.WorkWeixin;
|
||||||
|
|
||||||
@ -22,7 +21,7 @@ public class WorkWxBaseService(
|
|||||||
SysConfigService sysConfigService,
|
SysConfigService sysConfigService,
|
||||||
IHttpRemoteService httpRemoteService) : ITransient
|
IHttpRemoteService httpRemoteService) : ITransient
|
||||||
{
|
{
|
||||||
private static readonly Lazy<HttpRemoteItem> _options = new(() => App.GetOptions<HttpRemotesOptions>().WorkWeixin);
|
private static readonly Lazy<HttpRemoteItem> Options = new(() => App.GetOptions<HttpRemotesOptions>().WorkWeixin);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取企业微信接口凭证
|
/// 获取企业微信接口凭证
|
||||||
@ -54,14 +53,14 @@ public class WorkWxBaseService(
|
|||||||
/// <returns>返回结果</returns>
|
/// <returns>返回结果</returns>
|
||||||
public async Task<R> SendAsync<T, R>(T input) where R : BaseWorkWxOutput
|
public async Task<R> SendAsync<T, R>(T input) where R : BaseWorkWxOutput
|
||||||
{
|
{
|
||||||
if (_options.Value == null || string.IsNullOrWhiteSpace(_options.Value.BaseAddress)) throw Oops.Oh("[企业微信] 服务配置缺失");
|
if (Options.Value == null || string.IsNullOrWhiteSpace(Options.Value.BaseAddress)) throw Oops.Oh("[企业微信] 服务配置缺失");
|
||||||
|
|
||||||
var attr = typeof(T).GetCustomAttribute<HttpRemoteApiAttribute>();
|
var attr = typeof(T).GetCustomAttribute<HttpRemoteApiAttribute>();
|
||||||
if (attr == null || string.IsNullOrWhiteSpace(attr.Action as string))
|
if (attr == null || string.IsNullOrWhiteSpace(attr.Action as string))
|
||||||
throw Oops.Oh("[企业微信] 接口入参未正确配置[HttpRemoteApi]特性");
|
throw Oops.Oh("[企业微信] 接口入参未正确配置[HttpRemoteApi]特性");
|
||||||
|
|
||||||
// 拼接请求地址,并设置token
|
// 拼接请求地址,并设置token
|
||||||
var url = _options.Value.BaseAddress + $"/cgi-bin/{attr.Action}?";
|
var url = Options.Value.BaseAddress + $"/cgi-bin/{attr.Action}?";
|
||||||
if (input is AuthWorkWxInput)
|
if (input is AuthWorkWxInput)
|
||||||
{
|
{
|
||||||
// 重试3次
|
// 重试3次
|
||||||
@ -82,10 +81,10 @@ public class WorkWxBaseService(
|
|||||||
{
|
{
|
||||||
HttpMethodEnum.Get => await httpRemoteService.GetAsync(
|
HttpMethodEnum.Get => await httpRemoteService.GetAsync(
|
||||||
url + input.ToCustomJsonPropertyQueryString(),
|
url + input.ToCustomJsonPropertyQueryString(),
|
||||||
builder => builder.SetHttpClientName(_options.Value.HttpName).SetRemoteApiAttr(attr)),
|
builder => builder.SetHttpClientName(Options.Value.HttpName).SetReqRemoteApiAttr(attr)),
|
||||||
HttpMethodEnum.Post => await httpRemoteService.PostAsync(url,
|
HttpMethodEnum.Post => await httpRemoteService.PostAsync(url,
|
||||||
builder => builder.SetHttpClientName(_options.Value.HttpName).SetRemoteApiAttr(attr)
|
builder => builder.SetHttpClientName(Options.Value.HttpName).SetReqRemoteApiAttr(attr)
|
||||||
.SetContent(new StringContent(JSON.Serialize(input), Encoding.UTF8, "application/json"))),
|
.SetContent(new StringContent(CustomJsonHelper.Serialize(input), Encoding.UTF8, "application/json"))),
|
||||||
_ => throw Oops.Oh($"[企业微信] 不支持的请求方式:{attr.HttpMethod.ToString()}:({typeof(T).FullName})"),
|
_ => throw Oops.Oh($"[企业微信] 不支持的请求方式:{attr.HttpMethod.ToString()}:({typeof(T).FullName})"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -112,7 +111,7 @@ public class WorkWxBaseService(
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var resp = JSON.Deserialize<R>(responseContent);
|
var resp = CustomJsonHelper.Deserialize<R>(responseContent);
|
||||||
if (resp?.ErrCode == 0) return resp;
|
if (resp?.ErrCode == 0) return resp;
|
||||||
throw Oops.Oh("[企业微信] 请求失败:" + resp?.ErrMsg);
|
throw Oops.Oh("[企业微信] 请求失败:" + resp?.ErrMsg);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,40 +0,0 @@
|
|||||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
|
||||||
//
|
|
||||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
|
||||||
//
|
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
|
||||||
|
|
||||||
namespace Admin.NET.Plugin.WorkWeixin;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 企业微信接口输出基类
|
|
||||||
/// </summary>
|
|
||||||
public class BaseWorkOutput
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 返回码
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("errcode")]
|
|
||||||
[JsonPropertyName("errcode")]
|
|
||||||
public int ErrCode { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 对返回码的文本描述内容
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("errmsg")]
|
|
||||||
[JsonPropertyName("errmsg")]
|
|
||||||
public string ErrMsg { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 带id的输出参数
|
|
||||||
/// </summary>
|
|
||||||
public class BaseWorkIdOutput : BaseWorkOutput
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// id
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("id")]
|
|
||||||
[JsonPropertyName("id")]
|
|
||||||
public long? Id { get; set; }
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user