Merge pull request '新增 badgeTabs 组件、新增DateTimeRange参数校验特性、重构企业微信插件、 优化Http日志记录' (#411) from jasondom/Admin.NET.Pro:v2-1 into v2

Reviewed-on: https://code.adminnet.top/Admin.NET/Admin.NET.Pro/pulls/411
This commit is contained in:
zuohuaijun 2025-08-23 12:57:06 +08:00
commit 0290bcbdef
57 changed files with 4899 additions and 998 deletions

View File

@ -42,6 +42,7 @@
<ProjectReference Include="..\Admin.NET.Core\Admin.NET.Core.csproj" />
<ProjectReference Include="..\Plugins\Admin.NET.Plugin.GoView\Admin.NET.Plugin.GoView.csproj" />
<ProjectReference Include="..\Plugins\Admin.NET.Plugin.ReZero\Admin.NET.Plugin.ReZero.csproj" />
<ProjectReference Include="..\Plugins\Admin.NET.Plugin.WorkWeixin\Admin.NET.Plugin.WorkWeixin.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@ -9,7 +9,7 @@
"Account": "",
"Password": ""
},
"HttpBin": { //
"HttpBin": { // Ip
"EnabledLog": true,
"UseCookies": false,
"EnabledProxy": false,
@ -22,6 +22,17 @@
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
},
"Timeout": 5
},
"WorkWeixin": { //
"EnabledLog": true,
"UseCookies": false,
"EnabledProxy": false,
"HttpName": "WORK_WEIXIN",
"BaseAddress": "https://qyapi.weixin.qq.com",
"Headers": {
"User-Agent": "WORK_WEIXIN"
},
"Timeout": 60
}
}
}

View File

@ -4,6 +4,7 @@
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using Admin.NET.Plugin.WorkWeixin;
using Furion.Localization;
using Furion.Logging;
using Microsoft.Extensions.Logging;
@ -19,11 +20,13 @@ public class TestService : IDynamicApiController
private readonly UserManager _userManager;
private readonly IEventPublisher _eventPublisher;
private readonly ILogger _logger;
private readonly WorkWxUserService _workWxUserService;
public TestService(UserManager userManager, IEventPublisher eventPublisher, ILoggerFactory loggerFactory)
public TestService(UserManager userManager, IEventPublisher eventPublisher, WorkWxUserService workWxUserService, ILoggerFactory loggerFactory)
{
_userManager = userManager;
_eventPublisher = eventPublisher;
_workWxUserService = workWxUserService;
_logger = loggerFactory.CreateLogger(CommonConst.SysLogCategoryName); // 日志过滤标识(会写入数据库)
}
@ -68,6 +71,15 @@ public class TestService : IDynamicApiController
_logger.LogWarning($"信息{DateTime.Now}");
}
/// <summary>
/// 企业微信测试
/// </summary>
/// <returns></returns>
public async Task<List<UserIdListWorkWxOutput.DeptUserInfo>> UserIdList()
{
return (await _workWxUserService.GetUserIdList())?.DeptUser;
}
/// <summary>
/// 匿名上传文件测试
/// </summary>

View File

@ -4,14 +4,16 @@
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Plugin.WorkWeixin.Proxy;
namespace Admin.NET.Core;
public class CreatAppChatOutput : BaseWorkOutput
/// <summary>
/// 自定义Json转换字段名
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class CustomJsonPropertyAttribute(string name) : Attribute
{
/// <summary>
/// 群聊的唯一标志
/// 序列化名称
/// </summary>
[JsonProperty("chatid")]
[JsonPropertyName("chatid")]
public string ChatId { get; set; }
public string Name { get; } = name;
}

View File

@ -0,0 +1,277 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
/// <summary>
/// 用于校验 List&lt;DateTime&gt; 时间范围参数的自定义验证特性
/// 要求列表包含两个有效 DateTime 值,构成一个时间范围
/// </summary>
/// <example>
/// <para><b>示例 1基础时间范围起始 &lt; 结束)</b></para>
/// <code>
/// public class QueryModel
/// {
/// [DateTimeRange]
/// public List&lt;DateTime&gt; TimeRange { get; set; }
/// }
/// // 合法值: [2025-01-01 00:00:00, 2025-01-07 23:59:59]
/// </code>
///
/// <para><b>示例 2仅允许日期时间部分必须为 00:00:00</b></para>
/// <code>
/// [DateTimeRange(DateOnly = true)]
/// public List&lt;DateTime&gt; DateRange { get; set; }
/// // 合法值: [2025-01-01 00:00:00, 2025-01-31 00:00:00]
/// // 非法值: [2025-01-01 08:00:00, ...] → 报错
/// </code>
///
/// <para><b>示例 3最小间隔 1 小时60分钟</b></para>
/// <code>
/// [DateTimeRange(MinInterval = 60)]
/// public List&lt;DateTime&gt; Duration { get; set; }
/// // 合法值: 间隔 ≥ 1小时如 2小时 → "2小时"
/// // 错误提示: 时间间隔必须至少为 1小时
/// </code>
///
/// <para><b>示例 4最大间隔 7 天</b></para>
/// <code>
/// [DateTimeRange(MaxInterval = 7 * 24 * 60)] // 7天 = 10080分钟
/// public List&lt;DateTime&gt; WeeklyRange { get; set; }
/// // 合法值: 间隔 ≤ 7天
/// // 错误提示: 时间间隔不能超过 7天
/// </code>
///
/// <para><b>示例 5时间范围必须在过去且可包含当前时间</b></para>
/// <code>
/// [DateTimeRange(DirectionEnum = TimeDirectionEnum.Past, AllowNow = true)]
/// public List&lt;DateTime&gt; PastEvents { get; set; }
/// // 合法值: [2025-08-01, 2025-08-20](若今天是 2025-08-20
/// // 非法值: [2025-08-21, ...] → 完全在未来
/// </code>
///
/// <para><b>示例 6时间范围必须在未来不能包含当前时间</b></para>
/// <code>
/// [DateTimeRange(DirectionEnum = TimeDirectionEnum.Future, AllowNow = false)]
/// public List&lt;DateTime&gt; FutureTasks { get; set; }
/// // 合法值: [2025-08-21 00:00, 2025-08-30 23:59]
/// // 非法值: [2025-08-20 10:00, ...] → 包含当前时间
/// </code>
///
/// <para><b>示例 7允许起止时间相等单日查询</b></para>
/// <code>
/// [DateTimeRange(AllowEqual = true, DateOnly = true)]
/// public List&lt;DateTime&gt; SingleDay { get; set; }
/// // 合法值: [2025-08-20 00:00:00, 2025-08-20 00:00:00]
/// </code>
///
/// <para><b>示例 8单元测试中固定“当前时间”</b></para>
/// <code>
/// // 用于测试未来/过去逻辑
/// var attribute = new DateTimeRangeAttribute
/// {
/// DirectionEnum = TimeDirectionEnum.Future,
/// NowProvider = () => new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero)
/// };
/// </code>
/// </example>
[SuppressSniffer]
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public class DateTimeRangeAttribute : ValidationAttribute
{
/// <summary>
/// 获取或设置时间方向约束
/// </summary>
public TimeDirectionEnum DirectionEnum { get; set; } = TimeDirectionEnum.None;
/// <summary>
/// 最小时间间隔(分钟) 0表示无限制
/// </summary>
public int MinInterval { get; set; } = 0;
/// <summary>
/// 最大时间间隔(分钟) 0表示无限制
/// </summary>
public int MaxInterval { get; set; } = 0;
/// <summary>
/// 是否仅允许日期类型(时间部分必须为 00:00:00
/// </summary>
public bool DateOnly { get; set; } = false;
/// <summary>
/// 是否允许起始时间等于结束时间(即零间隔)
/// 默认为 false即起始时间必须小于结束时间
/// </summary>
public bool AllowEqual { get; set; } = false;
/// <summary>
/// 是否允许范围包含当前时间
/// 当 Direction 为 Future 或 Past 时,此属性可覆盖默认行为
/// </summary>
public bool AllowNow { get; set; } = true;
/// <summary>
/// 获取或设置自定义的“现在”时间提供器,用于单元测试或特定时区场景
/// 默认为 DateTimeOffset.Now
/// </summary>
public Func<DateTimeOffset> NowProvider { get; set; } = () => DateTimeOffset.Now;
/// <summary>
/// 获取格式化的当前时间字符串(用于错误消息)
/// </summary>
private string NowString => NowProvider().ToString("yyyy-MM-dd HH:mm:ss");
/// <summary>
/// 执行验证逻辑
/// </summary>
protected override ValidationResult IsValid(object? value, ValidationContext validationContext)
{
if (value == null) return ValidationResult.Success;
var list = value as List<DateTime>;
if (list == null) return new ValidationResult($"'{validationContext.DisplayName}' 必须是日期时间列表类型");
if (list.Count != 2) return new ValidationResult($"'{validationContext.DisplayName}' 必须包含两个日期时间值");
var start = list[0];
var end = list[1];
// 验证 DateTime 值的有效性
if (!IsValidDateTime(start) || !IsValidDateTime(end)) return new ValidationResult($"'{validationContext.DisplayName}' 包含无效的日期时间值");
// 检查 DateOnly 约束
if (DateOnly)
{
if (start.TimeOfDay != TimeSpan.Zero) return new ValidationResult($"'{validationContext.DisplayName}' 起始时间需为日期格式00:00:00当前为 {start:yyyy-MM-dd HH:mm:ss}");
if (end.TimeOfDay != TimeSpan.Zero) return new ValidationResult($"'{validationContext.DisplayName}' 结束时间需为日期格式00:00:00当前为 {end:yyyy-MM-dd HH:mm:ss}");
}
// 检查时间顺序
if (AllowEqual)
{
if (start > end) return new ValidationResult($"'{validationContext.DisplayName}' 起始时间不能晚于结束时间");
}
else
{
if (start >= end) return new ValidationResult($"'{validationContext.DisplayName}' 起始时间必须早于结束时间");
}
// 检查时间方向
var now = NowProvider().DateTime;
var directionResult = ValidateDirection(start, end, now, validationContext.DisplayName);
if (directionResult != null) return directionResult;
// 检查最小和最大间隔
var intervalResult = ValidateTimeIntervals(start, end, validationContext.DisplayName);
if (intervalResult != null) return intervalResult;
return ValidationResult.Success;
}
/// <summary>
/// 验证时间方向约束
/// </summary>
private ValidationResult? ValidateDirection(DateTime start, DateTime end, DateTime now, string? displayName)
{
return DirectionEnum switch
{
TimeDirectionEnum.Future when !AllowNow && (start <= now || end <= now) =>
new ValidationResult($"'{displayName}' 必须在 {NowString} 之后"),
TimeDirectionEnum.Future when AllowNow && (start < now && end < now) =>
new ValidationResult($"'{displayName}' 必须是未来时间"),
TimeDirectionEnum.Past when !AllowNow && (start >= now || end >= now) =>
new ValidationResult($"'{displayName}' 必须在 {NowString} 之前"),
TimeDirectionEnum.Past when AllowNow && (start > now && end > now) =>
new ValidationResult($"'{displayName}' 必须是过去时间"),
_ => null
};
}
/// <summary>
/// 验证最小和最大时间间隔
/// </summary>
private ValidationResult? ValidateTimeIntervals(DateTime start, DateTime end, string? displayName)
{
var interval = end - start;
var totalMinutes = (int)interval.TotalMinutes;
// 验证最小间隔
if (MinInterval > 0)
{
if (MinInterval < 0) return new ValidationResult($"'{displayName}' 最小间隔不能为负数");
if (totalMinutes < MinInterval)
{
var required = TimeSpan.FromMinutes(MinInterval);
var actual = TimeSpan.FromMinutes(totalMinutes);
return new ValidationResult($"'{displayName}'至少间隔 {required.FormatTimeSpanText(2)}");
}
}
// 验证最大间隔
if (MaxInterval > 0)
{
if (MaxInterval < 0) return new ValidationResult($"'{displayName}'最大间隔不能为负数");
if (totalMinutes > MaxInterval)
{
var allowed = TimeSpan.FromMinutes(MaxInterval);
var actual = TimeSpan.FromMinutes(totalMinutes);
return new ValidationResult($"'{displayName}'最多间隔 {allowed.FormatTimeSpanText(2)}");
}
}
// 验证最小不能大于最大
if (MinInterval > 0 && MaxInterval > 0 && MinInterval > MaxInterval)
{
var minSpan = TimeSpan.FromMinutes(MinInterval);
var maxSpan = TimeSpan.FromMinutes(MaxInterval);
return new ValidationResult($"'{displayName}' 最小间隔({minSpan.FormatTimeSpanText(2)})不能大于最大间隔({maxSpan.FormatTimeSpanText(2)})");
}
return null;
}
/// <summary>
/// 验证 DateTime 值是否在合理范围内
/// </summary>
private static bool IsValidDateTime(DateTime dt)
{
return dt >= new DateTime(1900, 1, 1) && dt <= new DateTime(2100, 12, 31);
}
}
/// <summary>
/// 指定范围必须是未来时间、过去时间,还是不限制
/// </summary>
[SuppressSniffer]
[Description("时间方向枚举")]
public enum TimeDirectionEnum
{
/// <summary>
/// 不限制时间方向
/// </summary>
[Description("不限")]
None,
/// <summary>
/// 范围必须在当前时间之后(未来)
/// </summary>
[Description("未来")]
Future,
/// <summary>
/// 范围必须在当前时间之前(过去)
/// </summary>
[Description("过去")]
Past
}

View File

@ -0,0 +1,61 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
/// <summary>
/// 远程请求接口特性
/// </summary>
[SuppressSniffer]
[AttributeUsage(AttributeTargets.Class)]
public class HttpRemoteApiAttribute : Attribute
{
/// <summary>
/// 方法名
/// </summary>
[Required]
public string Action { get; set; }
/// <summary>
/// 描述
/// </summary>
[Required]
public string Desc { get; set; }
/// <summary>
/// 请求方式
/// </summary>
[Required]
public HttpMethodEnum HttpMethod { get; set; } = HttpMethodEnum.Post;
/// <summary>
/// 参数类型
/// </summary>
public HttpParameterTypeEnum? Type { get; set; }
}
/// <summary>
/// 远程请求接口参数类型枚举
/// </summary>
[SuppressSniffer]
[Description("远程请求接口参数类型枚举")]
public enum HttpParameterTypeEnum
{
[Description("查询")]
Query = 1,
[Description("表单")]
FormData = 2,
[Description("JSON")]
Json = 3,
[Description("XML")]
Xml = 4,
[Description("自定义")]
Custom = 5
}

View File

@ -36,4 +36,15 @@ public class CommonConst
/// 事件-发送异常邮件
/// </summary>
public const string SendErrorMail = "Send:ErrorMail";
/// <summary>
/// 远程请求配置客户端名称键名
/// </summary>
public const string HttpRemoteClientName = "__HTTP_CLIENT_NAME__";
/// <summary>
/// 远程请求请求头接口描述键值
/// </summary>
public const string HttpRemoteApiDescHeaderName = "__HTTP_CLIENT_API_DESC__";
}

View File

@ -16,6 +16,20 @@ namespace Admin.NET.Core;
[LogTable]
public partial class SysLogHttp : EntityBaseId
{
/// <summary>
/// 客户端名称
/// </summary>
[SugarColumn(ColumnDescription = "客户端名称", Length = 32)]
[MaxLength(32)]
public string? HttpClientName { get; set; }
/// <summary>
/// 接口描述
/// </summary>
[SugarColumn(ColumnDescription = "接口描述", Length = 32)]
[MaxLength(32)]
public string? HttpApiDesc { get; set; }
/// <summary>
/// 请求方式
/// </summary>

View File

@ -34,4 +34,14 @@ public static class HttpRemotesExtension {
}
return services;
}
/// <summary>
/// 设置Http远程服务选项
/// </summary>
public static HttpRequestBuilder SetHttpOptions(this HttpRequestBuilder builder, HttpRemoteItem option, string apiDesc = "")
{
builder.SetHttpClientName(option.HttpName);
builder.WithHeader(CommonConst.HttpRemoteApiDescHeaderName, apiDesc, replace:true);
return builder;
}
}

View File

@ -55,7 +55,28 @@ public static partial class ObjectExtension
/// <returns></returns>
public static string ToQueryString(this Dictionary<string, string> dict, bool urlEncode = true)
{
return string.Join("&", dict.Select(p => $"{(urlEncode ? p.Key?.UrlEncode() : "")}={(urlEncode ? p.Value?.UrlEncode() : "")}"));
return string.Join("&", dict.Select(p => $"{(urlEncode ? p.Key?.UrlEncode() : p.Key)}={(urlEncode ? p.Value?.UrlEncode() : p.Value)}"));
}
/// <summary>
/// 根据[CustomJsonProperty]特性转换属性为Query参数
/// </summary>
/// <param name="obj"></param>
/// <param name="urlEncode"></param>
/// <returns></returns>
public static string ToCustomJsonPropertyQueryString(this object obj, bool urlEncode = true)
{
var properties = obj.GetType().GetProperties();
return string.Join("&", properties.Select(p =>
{
var name = p.GetCustomAttribute<CustomJsonPropertyAttribute>()?.Name ?? p.Name.ToFirstLetterLowerCase();
var val = p.GetValue(obj)?.ToString();
if (!urlEncode) return $"{name}={val}";
val = val?.UrlEncode();
name = name?.UrlEncode();
return $"{name}={val}";
}));
}
/// <summary>

View File

@ -11,7 +11,6 @@ namespace Admin.NET.Core;
/// </summary>
public class HttpLoggingHandler : DelegatingHandler, ITransient
{
private const string HttpName = "__HTTP_CLIENT_NAME__";
private readonly Dictionary<string, bool> _enabledLogMap;
private readonly SysConfigService _sysConfigService;
private readonly IEventPublisher _eventPublisher;
@ -35,11 +34,17 @@ public class HttpLoggingHandler : DelegatingHandler, ITransient
if (!enabledLog) return await base.SendAsync(request, cancellationToken);
// 判断当前配置日志开关
request.Options.TryGetValue<string>(HttpName, out var clientName);
request.Options.TryGetValue<string>(CommonConst.HttpRemoteClientName, out var clientName);
if (!string.IsNullOrWhiteSpace(clientName)) enabledLog = _enabledLogMap.GetOrDefault(clientName);
if (!enabledLog) return await base.SendAsync(request, cancellationToken);
var sysLogHttp = new SysLogHttp();
sysLogHttp.HttpClientName = clientName;
// 获取接口描述,并移除
sysLogHttp.HttpApiDesc = request.Headers.FirstOrDefault(u => u.Key == CommonConst.HttpRemoteApiDescHeaderName).Value?.FirstOrDefault();
request.Headers.Remove(CommonConst.HttpRemoteApiDescHeaderName);
sysLogHttp.HttpMethod = request.Method.Method;
sysLogHttp.RequestUrl = request.RequestUri?.ToString();
sysLogHttp.RequestHeaders = request.Headers.ToDictionary(u => u.Key, u => u.Value.Join(";")).ToJson();

View File

@ -20,6 +20,11 @@ public sealed class HttpRemotesOptions : IConfigurableOptions
/// 获取Ip地址接口
/// </summary>
public HttpRemoteItem HttpBin { get; set; }
/// <summary>
/// 企业微信接口配置
/// </summary>
public HttpRemoteItem WorkWeixin { get; set; }
}
/// <summary>

View File

@ -18,6 +18,11 @@ public class ConstOutput
/// </summary>
public dynamic Code { get; set; }
/// <summary>
/// 描述
/// </summary>
public string Desc { get; set; }
/// <summary>
/// 扩展字段
/// </summary>

View File

@ -64,6 +64,7 @@ public class SysConstService : IDynamicApiController, ITransient
.Select(u => new ConstOutput
{
Name = u.Name,
Desc = u.GetCustomAttribute<DescriptionAttribute>()?.Description,
Code = isEnum ? (int)u.GetValue(BindingFlags.Instance)! : u.GetValue(BindingFlags.Instance)
}).ToList();
_sysCacheService.Set(key, constList);

View File

@ -87,6 +87,16 @@ public class PageLogHttpInput : BasePageInput
/// </summary>
public string SearchKey { get; set; }
/// <summary>
/// 客户端名称
/// </summary>
public string HttpClientName { get; set; }
/// <summary>
/// 请求接口描述
/// </summary>
public string HttpApiDesc { get; set; }
/// <summary>
/// 请求方式
/// </summary>

View File

@ -16,6 +16,16 @@ public class PageLogHttpOutput
/// </summary>
public long? Id { get; set; }
/// <summary>
/// 客户端名称
/// </summary>
public string HttpClientName { get; set; }
/// <summary>
/// 请求接口描述
/// </summary>
public string HttpApiDesc { get; set; }
/// <summary>
/// 请求方式
/// </summary>

View File

@ -32,7 +32,9 @@ public class SysLogHttpService : IDynamicApiController, ITransient
{
input.Keyword = input.Keyword?.Trim();
var query = _sysLogHttpRep.AsQueryable()
.WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), u => u.RequestUrl.Contains(input.Keyword) || u.RequestBody.Contains(input.Keyword) || u.ResponseBody.Contains(input.Keyword) || u.Exception.Contains(input.Keyword))
.WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), u => u.HttpClientName.Contains(input.Keyword) || u.HttpApiDesc.Contains(input.Keyword) || u.RequestUrl.Contains(input.Keyword) || u.RequestBody.Contains(input.Keyword) || u.ResponseBody.Contains(input.Keyword) || u.Exception.Contains(input.Keyword))
.WhereIF(!string.IsNullOrWhiteSpace(input.HttpClientName), u => u.HttpClientName.Contains(input.HttpClientName.Trim()))
.WhereIF(!string.IsNullOrWhiteSpace(input.HttpApiDesc), u => u.HttpApiDesc.Contains(input.HttpApiDesc.Trim()))
.WhereIF(!string.IsNullOrWhiteSpace(input.RequestUrl), u => u.RequestUrl.Contains(input.RequestUrl.Trim()))
.WhereIF(!string.IsNullOrWhiteSpace(input.RequestBody), u => u.RequestBody.Contains(input.RequestBody.Trim()))
.WhereIF(!string.IsNullOrWhiteSpace(input.ResponseBody), u => u.ResponseBody.Contains(input.ResponseBody.Trim()))
@ -55,6 +57,9 @@ public class SysLogHttpService : IDynamicApiController, ITransient
public async Task<List<SysLogHttp>> GetList([FromQuery] PageLogHttpInput input)
{
return await _sysLogHttpRep.AsQueryable()
.WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), u => u.HttpClientName.Contains(input.Keyword) || u.HttpApiDesc.Contains(input.Keyword) || u.RequestUrl.Contains(input.Keyword) || u.RequestBody.Contains(input.Keyword) || u.ResponseBody.Contains(input.Keyword) || u.Exception.Contains(input.Keyword))
.WhereIF(!string.IsNullOrWhiteSpace(input.HttpClientName), u => u.HttpClientName.Contains(input.HttpClientName.Trim()))
.WhereIF(!string.IsNullOrWhiteSpace(input.HttpApiDesc), u => u.HttpApiDesc.Contains(input.HttpApiDesc.Trim()))
.WhereIF(!string.IsNullOrWhiteSpace(input.RequestUrl?.Trim()), u => u.RequestUrl.Contains(input.RequestUrl.Trim()))
.WhereIF(!string.IsNullOrWhiteSpace(input.RequestBody?.Trim()), u => u.RequestBody.Contains(input.RequestBody.Trim()))
.WhereIF(!string.IsNullOrWhiteSpace(input.ResponseBody?.Trim()), u => u.ResponseBody.Contains(input.ResponseBody.Trim()))

View File

@ -0,0 +1,225 @@
// 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);
}
}
}

View File

@ -231,6 +231,56 @@ public static class DateTimeFormatExtensions
return $"{sDay} 天 {sHour} 小时 {sMinute} 分 {sSecond} 秒 {sMilliSecond} 毫秒";
}
/// <summary>
/// 将 TimeSpan 格式化为 y年M月d天h小时m分钟 的字符串值为0的单位不显示
/// 注意此方法对年、月的计算是基于近似值1年≈365.25天1月≈30.44天),适用于显示目的,不保证绝对精确
/// </summary>
/// <param name="timeSpan">要格式化的 TimeSpan</param>
/// <param name="maxUnits">保留的最大时间单位数量</param>
/// <returns>格式化后的字符串</returns>
public static string FormatTimeSpanText(this TimeSpan timeSpan, int maxUnits = int.MaxValue)
{
if (timeSpan < TimeSpan.Zero) timeSpan = TimeSpan.Zero; // 确保非负
if (maxUnits <= 0) throw new ArgumentException("maxUnits 必须大于0", nameof(maxUnits));
long totalMinutes = (long)timeSpan.TotalMinutes;
if (totalMinutes == 0) return "0分钟";
// 计算年、月、日、小时、分钟
// 使用近似值进行计算
const double minutesInYear = 365 * 24 * 60;
const double minutesInMonth = 30 * 24 * 60; // 平均每月天数
const long minutesInDay = 24 * 60;
const long minutesInHour = 60;
int years = (int)(totalMinutes / minutesInYear);
totalMinutes %= (long)minutesInYear;
int months = (int)(totalMinutes / minutesInMonth);
totalMinutes %= (long)minutesInMonth;
int days = (int)(totalMinutes / minutesInDay);
totalMinutes %= minutesInDay;
int hours = (int)(totalMinutes / minutesInHour);
int minutes = (int)(totalMinutes % minutesInHour);
var parts = new List<string>();
if (years > 0) parts.Add($"{years}年");
if (months > 0) parts.Add($"{months}个月");
if (days > 0) parts.Add($"{days}天");
if (hours > 0) parts.Add($"{hours}小时");
if (minutes > 0) parts.Add($"{minutes}分钟");
// 如果指定了最大单位数量,则截取前 maxUnits 个非零单位
if (maxUnits < int.MaxValue && parts.Count > maxUnits)
{
parts = parts.Take(maxUnits).ToList();
}
return parts.Count == 0 ? "0分钟" : string.Join("", parts);
}
/// <summary>
/// 时间转换简易字符串
/// </summary>

View File

@ -1,8 +0,0 @@
{
"$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json",
"WorkWeixin": {
"CorpId": "xxxx", // ID
"CorpSecret": "xxxx" //
}
}

View File

@ -9,7 +9,22 @@ namespace Admin.NET.Plugin.WorkWeixin.Const;
public class WorkWeixinConst
{
/// <summary>
/// API分组名称
/// 企业微信缓存建
/// </summary>
public const string GroupName = "WorkWeixin";
public const string KeyWorkWeixinToken = "work_weixin_token";
/// <summary>
/// 企业微信CorpId
/// </summary>
public const string WorkWeixinCorpId = "work_weixin_corp_id";
/// <summary>
/// 企业微信CorpSecret
/// </summary>
public const string WorkWeixinCorpSecret = "work_weixin_corp_secret";
/// <summary>
/// 企业微信分布式锁
/// </summary>
public const string KeyLockWorkWeixin = "dis_lock_work_weixin";
}

View File

@ -5,8 +5,14 @@
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
global using Admin.NET.Core;
global using Admin.NET.Core.Service;
global using Furion;
global using Furion.DependencyInjection;
global using Furion.EventBus;
global using Furion.HttpRemote;
global using Lazy.Captcha.Core;
global using Microsoft.AspNetCore.Http;
global using Microsoft.AspNetCore.Mvc;
global using Newtonsoft.Json;
global using System.ComponentModel.DataAnnotations;
global using System.Text.Json.Serialization;

View File

@ -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; }
}

View File

@ -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>markdownmarkdown消息</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 };
}
}

View File

@ -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);
}

View File

@ -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; }
}

View File

@ -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);
}

View File

@ -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; }
}

View File

@ -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; }
}

View File

@ -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);
}

View File

@ -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; }
}

View File

@ -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; }
}

View File

@ -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);
}

View File

@ -0,0 +1,29 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using Admin.NET.Plugin.WorkWeixin.Const;
namespace Admin.NET.Plugin.WorkWeixin;
/// <summary>
/// 系统配置表种子数据
/// </summary>
[IgnoreUpdateSeed]
public class SysConfigSeedData : ISqlSugarEntitySeedData<SysConfig>
{
/// <summary>
/// 种子数据
/// </summary>
/// <returns></returns>
public IEnumerable<SysConfig> HasData()
{
return
[
new SysConfig{ Id=1330000000001, Name="企业微信CorpId", Code=WorkWeixinConst.WorkWeixinCorpId, Value="", SysFlag=YesNoEnum.Y, Remark= "企业微信接口获取凭证的CorpId", OrderNo=1000, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2025-04-10 00:00:00") },
new SysConfig{ Id=1330000000002, Name="企业微信CorpSecret", Code=WorkWeixinConst.WorkWeixinCorpSecret, Value="", SysFlag=YesNoEnum.Y, Remark= "企业微信接口获取凭证的CorpSecret", OrderNo=1000, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2025-04-10 00:00:00") },
];
}
}

View File

@ -0,0 +1,94 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Plugin.WorkWeixin;
/// <summary>
/// 需授权基类,继承此类,自动追加令牌
/// </summary>
public class AuthWorkWxInput
{
}
/// <summary>
/// 企业微信接口输出基类
/// </summary>
public class BaseWorkWxOutput
{
/// <summary>
/// 返回码
/// </summary>
[CustomJsonProperty("errcode")]
public int ErrCode { get; set; }
/// <summary>
/// 对返回码的文本描述内容
/// </summary>
[CustomJsonProperty("errmsg")]
public string ErrMsg { get; set; }
}
/// <summary>
/// 企业微信接口输出基类
/// </summary>
public class BasePageWorkWxOutput : BaseWorkWxOutput
{
/// <summary>
/// 分页游标,下次请求时填写以获取之后分页的记录。如果该字段返回空则表示已没有更多数据
/// </summary>
[CustomJsonProperty("next_cursor")]
public string NextCursor { get; set; }
}
/// <summary>
/// 带id的输出参数
/// </summary>
public class BaseIdWorkWxOutput : BaseWorkWxOutput
{
/// <summary>
/// id
/// </summary>
[CustomJsonProperty("id")]
public long? Id { get; set; }
}
/// <summary>
/// 获取接口凭证输入参数
/// </summary>
/// https://developer.work.weixin.qq.com/document/path/91039
[HttpRemoteApi(Action = "gettoken", Desc = "获取接口凭证", HttpMethod = HttpMethodEnum.Get)]
public class TokenWorkWxInput
{
/// <summary>
/// 企业标识
/// </summary>
[CustomJsonProperty("corpid")]
public string CorpId { get; set; }
/// <summary>
/// 企业密钥
/// </summary>
[CustomJsonProperty("corpsecret")]
public string CorpSecret { get; set; }
}
/// <summary>
/// 获取接口凭证输出参数
/// </summary>
public class TokenWorkWxOutput : BaseWorkWxOutput
{
/// <summary>
/// 获取到的凭证
/// </summary>
[CustomJsonProperty("access_token")]
public string AccessToken { get; set; }
/// <summary>
/// 凭证的有效时间(秒)
/// </summary>
[CustomJsonProperty("expires_in")]
public int ExpiresIn { get; set; }
}

View File

@ -0,0 +1,102 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Plugin.WorkWeixin;
/// <summary>
/// 应用推送消息工厂类
/// </summary>
public static class AppChatMessageFactory
{
/// <summary>
/// 文本消息
/// </summary>
public static TextAppChatMessage CreateTextMessage(string chatId, string content, int? safe = null)
{
return new TextAppChatMessage
{
ChatId = chatId,
Text = new TextContent { Content = content },
Safe = safe
};
}
/// <summary>
/// 图片消息
/// </summary>
public static ImageAppChatMessage CreateImageMessage(string chatId, string mediaId, int? safe = null)
{
return new ImageAppChatMessage
{
ChatId = chatId,
Image = new MediaContent { MediaId = mediaId },
Safe = safe
};
}
/// <summary>
/// 语音消息
/// </summary>
public static VoiceAppChatMessage CreateVoiceMessage(string chatId, string mediaId)
{
return new VoiceAppChatMessage
{
ChatId = chatId,
Voice = new MediaContent { MediaId = mediaId }
};
}
/// <summary>
/// 视频消息
/// </summary>
public static VideoAppChatMessage CreateVideoMessage(string chatId, string mediaId, string title = null, string description = null, int? safe = null)
{
return new VideoAppChatMessage
{
ChatId = chatId,
Video = new VideoContent { MediaId = mediaId, Title = title, Description = description },
Safe = safe
};
}
/// <summary>
/// 文件消息
/// </summary>
public static FileAppChatMessage CreateFileMessage(string chatId, string mediaId, int? safe = null)
{
return new FileAppChatMessage
{
ChatId = chatId,
File = new MediaContent { MediaId = mediaId },
Safe = safe
};
}
/// <summary>
/// 文本卡片消息
/// </summary>
public static TextCardAppChatMessage CreateTextCardMessage(string chatId, string title, string description, string url, string btnTxt = null, int? safe = null)
{
return new TextCardAppChatMessage
{
ChatId = chatId,
TextCard = new TextCardContent { Title = title, Description = description, Url = url, BtnTxt = btnTxt },
Safe = safe
};
}
/// <summary>
/// Markdown消息
/// </summary>
public static MarkdownAppChatMessage CreateMarkdownMessage(string chatId, string content)
{
return new MarkdownAppChatMessage
{
ChatId = chatId,
Markdown = new MarkdownContent { Content = content }
};
}
}

View File

@ -0,0 +1,410 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Plugin.WorkWeixin;
/// <summary>
/// 应用推送消息基类
/// </summary>
[HttpRemoteApi(Action = "appchat/send", Desc = "应用消息推送", HttpMethod = HttpMethodEnum.Post)]
public abstract class AppChatMessageInput : AuthWorkWxInput
{
/// <summary>
/// 群聊id
/// </summary>
/// <remarks>是否必填:是</remarks>
[CustomJsonProperty("chatid")]
[Required(ErrorMessage = "群聊ID不能为空")]
public string ChatId { get; set; }
/// <summary>
/// 消息类型
/// </summary>
/// <remarks>是否必填:是</remarks>
[CustomJsonProperty("msgtype")]
[Required(ErrorMessage = "消息类型不能为空")]
public abstract string MsgType { get; }
/// <summary>
/// 表示是否是保密消息0表示否1表示是默认0
/// </summary>
/// <remarks>是否必填:否</remarks>
[CustomJsonProperty("safe")]
[Range(0, 1, ErrorMessage = "安全标识只能是0或1")]
public int? Safe { get; set; }
}
/// <summary>
/// 文本消息
/// </summary>
/// <remarks>
/// <br/>特殊说明content字段可以支持换行换行符请用转义过的'\n'
/// </remarks>
public class TextAppChatMessage : AppChatMessageInput
{
public override string MsgType => "text";
/// <summary>
/// 文本消息内容
/// </summary>
[CustomJsonProperty("text")]
[Required(ErrorMessage = "文本消息内容不能为空")]
public TextContent Text { get; set; } = new TextContent();
}
/// <summary>
/// 图片消息
/// </summary>
public class ImageAppChatMessage : AppChatMessageInput
{
public override string MsgType => "image";
/// <summary>
/// 图片消息内容
/// </summary>
[CustomJsonProperty("image")]
[Required(ErrorMessage = "图片消息内容不能为空")]
public MediaContent Image { get; set; } = new MediaContent();
}
/// <summary>
/// 语音消息
/// </summary>
public class VoiceAppChatMessage : AppChatMessageInput
{
public override string MsgType => "voice";
/// <summary>
/// 语音消息内容
/// </summary>
[CustomJsonProperty("voice")]
[Required(ErrorMessage = "语音消息内容不能为空")]
public MediaContent Voice { get; set; } = new MediaContent();
}
/// <summary>
/// 视频消息
/// </summary>
public class VideoAppChatMessage : AppChatMessageInput
{
public override string MsgType => "video";
/// <summary>
/// 视频消息内容
/// </summary>
[CustomJsonProperty("video")]
[Required(ErrorMessage = "视频消息内容不能为空")]
public VideoContent Video { get; set; } = new VideoContent();
}
/// <summary>
/// 文件消息
/// </summary>
public class FileAppChatMessage : AppChatMessageInput
{
public override string MsgType => "file";
/// <summary>
/// 文件消息内容
/// </summary>
[CustomJsonProperty("file")]
[Required(ErrorMessage = "文件消息内容不能为空")]
public MediaContent File { get; set; } = new MediaContent();
}
/// <summary>
/// 文本卡片消息
/// </summary>
public class TextCardAppChatMessage : AppChatMessageInput
{
public override string MsgType => "textcard";
/// <summary>
/// 文本卡片消息内容
/// </summary>
[CustomJsonProperty("textcard")]
[Required(ErrorMessage = "文本卡片消息内容不能为空")]
public TextCardContent TextCard { get; set; } = new TextCardContent();
}
/// <summary>
/// 图文消息
/// </summary>
public class NewsAppChatMessage : AppChatMessageInput
{
public override string MsgType => "news";
/// <summary>
/// 图文消息内容
/// </summary>
[CustomJsonProperty("news")]
[Required(ErrorMessage = "图文消息内容不能为空")]
public NewsContent News { get; set; } = new NewsContent();
}
/// <summary>
/// 图文消息mpnews
/// </summary>
public class MpNewsAppChatMessage : AppChatMessageInput
{
public override string MsgType => "mpnews";
/// <summary>
/// 图文消息内容
/// </summary>
[CustomJsonProperty("mpnews")]
[Required(ErrorMessage = "图文消息内容不能为空")]
public MpNewsContent MpNews { get; set; } = new MpNewsContent();
}
/// <summary>
/// Markdown消息
/// </summary>
public class MarkdownAppChatMessage : AppChatMessageInput
{
public override string MsgType => "markdown";
/// <summary>
/// Markdown消息内容
/// </summary>
[CustomJsonProperty("markdown")]
[Required(ErrorMessage = "Markdown消息内容不能为空")]
public MarkdownContent Markdown { get; set; } = new MarkdownContent();
}
/// <summary>
/// 文本消息内容
/// </summary>
public class TextContent
{
/// <summary>
/// 消息内容最长不超过2048个字节
/// </summary>
/// <remarks>是否必填:是</remarks>
[CustomJsonProperty("content")]
[Required(ErrorMessage = "消息内容不能为空")]
[StringLength(2048, ErrorMessage = "消息内容长度不能超过2048个字节")]
public string Content { get; set; }
}
/// <summary>
/// 媒体内容基类
/// </summary>
public class MediaContent
{
/// <summary>
/// 媒体文件id可以调用上传临时素材接口获取
/// </summary>
/// <remarks>是否必填:是</remarks>
[CustomJsonProperty("media_id")]
[Required(ErrorMessage = "媒体文件ID不能为空")]
public string MediaId { get; set; }
}
/// <summary>
/// 视频消息内容
/// </summary>
public class VideoContent : MediaContent
{
/// <summary>
/// 视频消息的标题不超过128个字节超过会自动截断
/// </summary>
/// <remarks>是否必填:否</remarks>
[CustomJsonProperty("title")]
[StringLength(128, ErrorMessage = "视频标题长度不能超过128个字节")]
public string Title { get; set; }
/// <summary>
/// 视频消息的描述不超过512个字节超过会自动截断
/// </summary>
/// <remarks>是否必填:否</remarks>
[CustomJsonProperty("description")]
[StringLength(512, ErrorMessage = "视频描述长度不能超过512个字节")]
public string Description { get; set; }
}
/// <summary>
/// 文本卡片消息内容
/// </summary>
public class TextCardContent
{
/// <summary>
/// 标题不超过128个字节超过会自动截断
/// </summary>
/// <remarks>是否必填:是</remarks>
[CustomJsonProperty("title")]
[Required(ErrorMessage = "标题不能为空")]
[StringLength(128, ErrorMessage = "标题长度不能超过128个字节")]
public string Title { get; set; }
/// <summary>
/// 描述不超过512个字节超过会自动截断
/// </summary>
/// <remarks>是否必填:是</remarks>
[CustomJsonProperty("description")]
[Required(ErrorMessage = "描述不能为空")]
[StringLength(512, ErrorMessage = "描述长度不能超过512个字节")]
public string Description { get; set; }
/// <summary>
/// 点击后跳转的链接
/// </summary>
/// <remarks>是否必填:是</remarks>
[CustomJsonProperty("url")]
[Required(ErrorMessage = "跳转链接不能为空")]
[Url(ErrorMessage = "链接格式不正确")]
public string Url { get; set; }
/// <summary>
/// 按钮文字。默认为"详情"不超过4个文字超过自动截断
/// </summary>
/// <remarks>是否必填:否</remarks>
[CustomJsonProperty("btntxt")]
[StringLength(4, ErrorMessage = "按钮文字长度不能超过4个字符")]
public string BtnTxt { get; set; }
}
/// <summary>
/// 图文消息文章
/// </summary>
public class Article
{
/// <summary>
/// 标题不超过128个字节超过会自动截断
/// </summary>
/// <remarks>是否必填:是</remarks>
[CustomJsonProperty("title")]
[Required(ErrorMessage = "文章标题不能为空")]
[StringLength(128, ErrorMessage = "文章标题长度不能超过128个字节")]
public string Title { get; set; }
/// <summary>
/// 描述不超过512个字节超过会自动截断
/// </summary>
/// <remarks>是否必填:否</remarks>
[CustomJsonProperty("description")]
[StringLength(512, ErrorMessage = "文章描述长度不能超过512个字节")]
public string Description { get; set; }
/// <summary>
/// 点击后跳转的链接
/// </summary>
/// <remarks>是否必填:是</remarks>
[CustomJsonProperty("url")]
[Required(ErrorMessage = "文章链接不能为空")]
[Url(ErrorMessage = "文章链接格式不正确")]
public string Url { get; set; }
/// <summary>
/// 图文消息的图片链接支持JPG、PNG格式
/// </summary>
/// <remarks>是否必填:否</remarks>
[CustomJsonProperty("picurl")]
[Url(ErrorMessage = "图片链接格式不正确")]
public string PicUrl { get; set; }
}
/// <summary>
/// 图文消息内容
/// </summary>
public class NewsContent
{
/// <summary>
/// 图文消息一个图文消息支持1到8条图文
/// </summary>
/// <remarks>是否必填:是</remarks>
[CustomJsonProperty("articles")]
[Required(ErrorMessage = "图文消息不能为空")]
[MinLength(1, ErrorMessage = "至少需要1条图文消息")]
[MaxLength(8, ErrorMessage = "最多只能有8条图文消息")]
public List<Article> Articles { get; set; } = new List<Article>();
}
/// <summary>
/// 图文消息mpnews文章
/// </summary>
public class MpArticle
{
/// <summary>
/// 标题不超过128个字节超过会自动截断
/// </summary>
/// <remarks>是否必填:是</remarks>
[CustomJsonProperty("title")]
[Required(ErrorMessage = "文章标题不能为空")]
[StringLength(128, ErrorMessage = "文章标题长度不能超过128个字节")]
public string Title { get; set; }
/// <summary>
/// 图文消息缩略图的media_id可以通过素材管理接口获得
/// </summary>
/// <remarks>是否必填:是</remarks>
[CustomJsonProperty("thumb_media_id")]
[Required(ErrorMessage = "缩略图媒体ID不能为空")]
public string ThumbMediaId { get; set; }
/// <summary>
/// 图文消息的作者不超过64个字节
/// </summary>
/// <remarks>是否必填:否</remarks>
[CustomJsonProperty("author")]
[StringLength(64, ErrorMessage = "作者长度不能超过64个字节")]
public string Author { get; set; }
/// <summary>
/// 图文消息点击"阅读原文"之后的页面链接
/// </summary>
/// <remarks>是否必填:否</remarks>
[CustomJsonProperty("content_source_url")]
[Url(ErrorMessage = "原文链接格式不正确")]
public string ContentSourceUrl { get; set; }
/// <summary>
/// 图文消息的内容支持html标签不超过666K个字节
/// </summary>
/// <remarks>是否必填:是</remarks>
[CustomJsonProperty("content")]
[Required(ErrorMessage = "文章内容不能为空")]
public string Content { get; set; }
/// <summary>
/// 图文消息的描述不超过512个字节超过会自动截断
/// </summary>
/// <remarks>是否必填:否</remarks>
[CustomJsonProperty("digest")]
[StringLength(512, ErrorMessage = "摘要长度不能超过512个字节")]
public string Digest { get; set; }
}
/// <summary>
/// 图文消息mpnews内容
/// </summary>
public class MpNewsContent
{
/// <summary>
/// 图文消息一个图文消息支持1到8条图文
/// </summary>
/// <remarks>是否必填:是</remarks>
[CustomJsonProperty("articles")]
[Required(ErrorMessage = "图文消息不能为空")]
[MinLength(1, ErrorMessage = "至少需要1条图文消息")]
[MaxLength(8, ErrorMessage = "最多只能有8条图文消息")]
public List<MpArticle> Articles { get; set; } = new List<MpArticle>();
}
/// <summary>
/// Markdown消息内容
/// </summary>
public class MarkdownContent
{
/// <summary>
/// markdown内容最长不超过2048个字节必须是utf8编码
/// </summary>
/// <remarks>是否必填:是</remarks>
[CustomJsonProperty("content")]
[Required(ErrorMessage = "Markdown内容不能为空")]
[StringLength(2048, ErrorMessage = "Markdown内容长度不能超过2048个字节")]
public string Content { get; set; }
}

View File

@ -0,0 +1,148 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Plugin.WorkWeixin;
/// <summary>
/// 创建群聊会话输入参数
/// </summary>
/// <remarks>
/// <br/>最后更新2024/11/29
/// <br/>请求方式POSTHTTPS
/// <br/>请求地址https://qyapi.weixin.qq.com/cgi-bin/appchat/create?access_token=ACCESS_TOKEN
/// <br/>权限说明:只允许企业自建应用调用,且应用的可见范围必须是根部门
/// <br/>限制说明:群成员人数不可超过管理端配置的"群成员人数上限"最大不可超过2000人每企业创建群数不可超过1000/天
/// <br/>文档地址https://developer.work.weixin.qq.com/document/path/90245
/// </remarks>
[HttpRemoteApi(Action = "appchat/create", Desc = "创建群聊会话", HttpMethod = HttpMethodEnum.Post)]
public class CreateChatWorkWxInput : AuthWorkWxInput
{
/// <summary>
/// 群聊名最多50个utf8字符超过将截断
/// </summary>
/// <remarks>是否必填:否</remarks>
[CustomJsonProperty("name")]
[StringLength(50, ErrorMessage = "群聊名长度不能超过50个字符")]
public string Name { get; set; }
/// <summary>
/// 指定群主的id。如果不指定系统会随机从userlist中选一人作为群主
/// </summary>
/// <remarks>是否必填:否</remarks>
[Required(ErrorMessage = "群主id不能为空")]
[CustomJsonProperty("owner")]
public string Owner { get; set; }
/// <summary>
/// 群成员id列表。至少2人至多2000人
/// </summary>
/// <remarks>是否必填:是</remarks>
[CustomJsonProperty("userlist")]
[NotEmpty(ErrorMessage = "群成员列表不能为空")]
[MinLength(2, ErrorMessage = "群成员至少需要2人")]
[MaxLength(2000, ErrorMessage = "群成员最多不能超过2000人")]
public List<string> UserList { get; set; }
/// <summary>
/// 群聊的唯一标志不能与已有的群重复字符串类型最长32个字符。只允许字符0-9及字母a-zA-Z。如果不填系统会随机生成群id
/// </summary>
/// <remarks>是否必填:否</remarks>
[CustomJsonProperty("chatid")]
[RegularExpression("^[a-zA-Z0-9]{0,32}$", ErrorMessage = "ID只能包含字母和数字32")]
public string ChatId { get; set; }
}
/// <summary>
/// 修改群聊会话输入参数
/// </summary>
/// <remarks>
/// <br/>最后更新2023/04/18
/// <br/>请求方式POSTHTTPS
/// <br/>请求地址https://qyapi.weixin.qq.com/cgi-bin/appchat/update?access_token=ACCESS_TOKEN
/// <br/>权限说明:只允许企业自建应用调用,且应用的可见范围必须是根部门
/// <br/>限制说明chatid所代表的群必须是该应用所创建群成员人数不可超过2000人每企业变更群的次数不可超过1000次/小时
/// <br/>文档地址https://developer.work.weixin.qq.com/document/path/98913
/// </remarks>
[HttpRemoteApi(Action = "appchat/update", Desc = "修改群聊会话", HttpMethod = HttpMethodEnum.Post)]
public class UpdateChatWorkWxInput : AuthWorkWxInput
{
/// <summary>
/// 群聊id
/// </summary>
/// <remarks>是否必填:是</remarks>
[CustomJsonProperty("chatid")]
[Required(ErrorMessage = "群聊ID不能为空")]
public string ChatId { get; set; }
/// <summary>
/// 新的群聊名。若不需更新请忽略此参数。最多50个utf8字符超过将截断
/// </summary>
/// <remarks>是否必填:否</remarks>
[CustomJsonProperty("name")]
[StringLength(50, ErrorMessage = "群聊名长度不能超过50个字符")]
public string Name { get; set; }
/// <summary>
/// 新群主的id。若不需更新请忽略此参数。课程群聊群主必须拥有课程群创建权限del_user_list包含群主时本字段必填
/// </summary>
/// <remarks>是否必填:否</remarks>
[CustomJsonProperty("owner")]
public string Owner { get; set; }
/// <summary>
/// 添加成员的id列表
/// </summary>
/// <remarks>是否必填:否</remarks>
[CustomJsonProperty("add_user_list")]
[MaxLength(2000, ErrorMessage = "添加成员列表不能超过2000人")]
public List<string> AddUserList { get; set; }
/// <summary>
/// 踢出成员的id列表
/// </summary>
/// <remarks>是否必填:否</remarks>
[CustomJsonProperty("del_user_list")]
[MaxLength(2000, ErrorMessage = "踢出成员列表不能超过2000人")]
public List<string> DelUserList { get; set; }
}
/// <summary>
/// 获取群聊会话输入参数
/// </summary>
/// <remarks>
/// <br/>最后更新2023/04/20
/// <br/>请求方式GETHTTPS
/// <br/>请求地址https://qyapi.weixin.qq.com/cgi-bin/appchat/get?access_token=ACCESS_TOKEN&amp;chatid=CHATID
/// <br/>权限说明只允许企业自建应用调用且应用的可见范围必须是根部门chatid所代表的群必须是该应用所创建第三方不可调用
/// <br/>文档地址https://developer.work.weixin.qq.com/document/path/98914
/// </remarks>
[HttpRemoteApi(Action = "appchat/get", Desc = "获取群聊会话", HttpMethod = HttpMethodEnum.Get)]
public class GetChatWorkWxInput : AuthWorkWxInput
{
/// <summary>
/// 群聊id
/// </summary>
/// <remarks>是否必填:是</remarks>
[CustomJsonProperty("chatid")]
[Required(ErrorMessage = "群聊ID不能为空")]
public string ChatId { get; set; }
}
/// <summary>
/// 应用消息推送输入参数
/// </summary>
/// <remarks>
/// <br/>最后更新2024/07/11
/// <br/>请求方式POSTHTTPS
/// <br/>请求地址https://qyapi.weixin.qq.com/cgi-bin/appchat/send?access_token=ACCESS_TOKEN
/// <br/>权限说明:只允许企业自建应用调用,且应用的可见范围必须是根部门
/// <br/>限制说明chatid所代表的群必须是该应用所创建消息发送量有限制成员接收消息有限制
/// <br/>文档地址https://developer.work.weixin.qq.com/document/path/90248
/// </remarks>
[HttpRemoteApi(Action = "appchat/send", Desc = "应用消息推送", HttpMethod = HttpMethodEnum.Post)]
public class SendChatWorkWxInput
{
}

View File

@ -0,0 +1,67 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Plugin.WorkWeixin;
/// <summary>
/// 创建群聊会话输出参数
/// </summary>
public class CreateChatWorkWxOutput : BaseWorkWxOutput
{
/// <summary>
/// 群聊的唯一标志
/// </summary>
[CustomJsonProperty("chatid")]
public string ChatId { get; set; }
}
/// <summary>
/// 获取群聊会话输出参数
/// </summary>
public class GetChatWorkWxOutput : BaseWorkWxOutput
{
/// <summary>
/// 群聊信息
/// </summary>
[CustomJsonProperty("chat_info")]
public ChatInfoDto ChatInfo { get; set; }
/// <summary>
/// 群聊信息
/// </summary>
public class ChatInfoDto
{
/// <summary>
/// 群聊唯一标志
/// </summary>
[CustomJsonProperty("chatid")]
public string ChatId { get; set; }
/// <summary>
/// 群聊名
/// </summary>
[CustomJsonProperty("name")]
public string Name { get; set; }
/// <summary>
/// 群主id
/// </summary>
[CustomJsonProperty("owner")]
public string Owner { get; set; }
/// <summary>
/// 群成员id列表
/// </summary>
[CustomJsonProperty("userlist")]
public List<string> UserList { get; set; }
/// <summary>
/// 群聊类型0表示普通群聊1表示课程群聊
/// </summary>
[CustomJsonProperty("chat_type")]
public int ChatType { get; set; }
}
}

View File

@ -0,0 +1,121 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Plugin.WorkWeixin;
/// <summary>
/// 企业微信群聊会话服务 🧩
/// </summary>
public class WorkWxAppChatService(WorkWxBaseService baseService) : ITransient
{
/// <summary>
/// 创建群聊会话
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<CreateChatWorkWxOutput> CreateChat(CreateChatWorkWxInput input)
{
return await baseService.SendAsync<CreateChatWorkWxInput, CreateChatWorkWxOutput>(input);
}
/// <summary>
/// 修改群聊会话
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<BaseWorkWxOutput> UpdateChat(UpdateChatWorkWxInput input)
{
return await baseService.SendAsync<UpdateChatWorkWxInput, BaseWorkWxOutput>(input);
}
/// <summary>
/// 推送纯文本消息
/// </summary>
/// <param name="chatId">群聊Id</param>
/// <param name="content">内容</param>
/// <param name="safe">安全标识</param>
/// <returns></returns>
public async Task<BaseWorkWxOutput> SendText(string chatId, string content, int? safe = null)
{
return await baseService.SendAsync<TextAppChatMessage, BaseWorkWxOutput>(AppChatMessageFactory.CreateTextMessage(chatId, content, safe));
}
/// <summary>
/// 推送文本卡片消息
/// </summary>
/// <param name="chatId">群聊Id</param>
/// <param name="title">标题</param>
/// <param name="description">内容</param>
/// <param name="url">跳转链接</param>
/// <param name="btnTxt">按钮文本</param>
/// <param name="safe">安全标识</param>
/// <returns></returns>
public async Task<BaseWorkWxOutput> SendTextCard(string chatId, string title, string description, string url, string btnTxt = null, int? safe = null)
{
return await baseService.SendAsync<TextCardAppChatMessage, BaseWorkWxOutput>(AppChatMessageFactory.CreateTextCardMessage(chatId, title, description, url, btnTxt, safe));
}
/// <summary>
/// 推送Markdown消息
/// </summary>
/// <param name="chatId">群聊Id</param>
/// <param name="content">内容</param>
/// <returns></returns>
public async Task<BaseWorkWxOutput> SendMarkdown(string chatId, string content)
{
return await baseService.SendAsync<MarkdownAppChatMessage, BaseWorkWxOutput>(AppChatMessageFactory.CreateMarkdownMessage(chatId, content));
}
/// <summary>
/// 推送图片消息
/// </summary>
/// <param name="chatId">群聊Id</param>
/// <param name="mediaId">媒体文件ID</param>
/// <param name="safe">安全标识</param>
/// <returns></returns>
public async Task<BaseWorkWxOutput> SendImage(string chatId, string mediaId, int? safe = null)
{
return await baseService.SendAsync<ImageAppChatMessage, BaseWorkWxOutput>(AppChatMessageFactory.CreateImageMessage(chatId, mediaId, safe));
}
/// <summary>
/// 推送语音消息
/// </summary>
/// <param name="chatId">群聊Id</param>
/// <param name="mediaId">媒体文件ID</param>
/// <returns></returns>
public async Task<BaseWorkWxOutput> SendVoice(string chatId, string mediaId)
{
return await baseService.SendAsync<VoiceAppChatMessage, BaseWorkWxOutput>(AppChatMessageFactory.CreateVoiceMessage(chatId, mediaId));
}
/// <summary>
/// 推送视频消息
/// </summary>
/// <param name="chatId">群聊Id</param>
/// <param name="mediaId">媒体文件ID</param>
/// <param name="title">标题</param>
/// <param name="description">描述</param>
/// <param name="safe">安全标识</param>
/// <returns></returns>
public async Task<BaseWorkWxOutput> SendVideo(string chatId, string mediaId, string title = null, string description = null, int? safe = null)
{
return await baseService.SendAsync<VideoAppChatMessage, BaseWorkWxOutput>(AppChatMessageFactory.CreateVideoMessage(chatId, mediaId, title, description, safe));
}
/// <summary>
/// 推送文件消息
/// </summary>
/// <param name="chatId">群聊Id</param>
/// <param name="mediaId">媒体文件ID</param>
/// <param name="safe">安全标识</param>
/// <returns></returns>
public async Task<BaseWorkWxOutput> SendFile(string chatId, string mediaId, int? safe = null)
{
return await baseService.SendAsync<FileAppChatMessage, BaseWorkWxOutput>(AppChatMessageFactory.CreateFileMessage(chatId, mediaId, safe));
}
}

View File

@ -0,0 +1,168 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Plugin.WorkWeixin;
/// <summary>
/// 创建部门
/// </summary>
/// <remarks>
/// <br/>最后更新2023/08/15
/// <br/>请求方式POSTHTTPS
/// <br/>请求地址https://qyapi.weixin.qq.com/cgi-bin/department/create?access_token=ACCESS_TOKEN
/// <br/>权限说明:第三方仅通讯录应用可以调用
/// <br/>限制说明部门最大层级15层部门总数不超过3万个每个部门下节点不超过3万个
/// <br/>文档地址https://developer.work.weixin.qq.com/document/path/90205
/// </remarks>
[HttpRemoteApi(Action = "department/create", Desc = "创建部门", HttpMethod = HttpMethodEnum.Post)]
public class CreateDeptWorkWxInput : AuthWorkWxInput
{
/// <summary>
/// 部门名称。同一个层级的部门名称不能重复。长度限制为1~64个UTF-8字符字符不能包括\:*?"<>
/// </summary>
/// <remarks>是否必填:是</remarks>
[CustomJsonProperty("name")]
[Required(ErrorMessage = "部门名称不能为空")]
[StringLength(64, MinimumLength = 1, ErrorMessage = "部门名称长度必须在1-64个字符之间")]
[RegularExpression(@"^[^\\:*?""<>]+$", ErrorMessage = "\\:*?\"<>|字符")]
public string Name { get; set; }
/// <summary>
/// 英文名称。同一个层级的部门名称不能重复。需要在管理后台开启多语言支持才能生效
/// </summary>
/// <remarks>是否必填:否</remarks>
[CustomJsonProperty("name_en")]
[StringLength(64, MinimumLength = 1, ErrorMessage = "英文名称长度必须在1-64个字符之间")]
[RegularExpression(@"^[^\\:*?""<>]*$", ErrorMessage = "\\:*?\"<>|字符")]
public string NameEn { get; set; }
/// <summary>
/// 父部门id32位整型
/// </summary>
/// <remarks>是否必填:是</remarks>
[CustomJsonProperty("parentid")]
[Required(ErrorMessage = "父部门ID不能为空")]
[Range(1, long.MaxValue, ErrorMessage = "父部门ID必须大于0")]
public long ParentId { get; set; }
/// <summary>
/// 在父部门中的次序值。order值大的排序靠前
/// </summary>
/// <remarks>是否必填:否</remarks>
[CustomJsonProperty("order")]
[Range(0, 4294967295, ErrorMessage = "排序值必须在0-4294967295之间")]
public long? Order { get; set; }
/// <summary>
/// 部门id32位整型指定时必须大于1
/// </summary>
/// <remarks>是否必填:否</remarks>
[CustomJsonProperty("id")]
[Range(2, long.MaxValue, ErrorMessage = "部门ID必须大于1")]
public long? Id { get; set; }
}
/// <summary>
/// 更新部门
/// </summary>
/// <remarks>
/// <br/>最后更新2023/08/15
/// <br/>请求方式POSTHTTPS
/// <br/>请求地址https://qyapi.weixin.qq.com/cgi-bin/department/update?access_token=ACCESS_TOKEN
/// <br/>权限说明:应用须拥有指定部门的管理权限,第三方仅通讯录应用可以调用
/// <br/>限制说明部门最大层级15层部门总数不超过3万个每个部门下节点不超过3万个
/// <br/>文档地址https://developer.work.weixin.qq.com/document/path/90206
/// </remarks>
[HttpRemoteApi(Action = "department/update", Desc = "更新部门", HttpMethod = HttpMethodEnum.Post)]
public class UpdateDeptWorkWxInput : AuthWorkWxInput
{
/// <summary>
/// 部门id
/// </summary>
/// <remarks>是否必填:是</remarks>
[CustomJsonProperty("id")]
[Required(ErrorMessage = "部门ID不能为空")]
[Range(1, long.MaxValue, ErrorMessage = "部门ID必须大于0")]
public long Id { get; set; }
/// <summary>
/// 部门名称。长度限制为1~64个UTF-8字符字符不能包括\:*?"<>
/// </summary>
/// <remarks>是否必填:否</remarks>
[CustomJsonProperty("name")]
[StringLength(64, MinimumLength = 1, ErrorMessage = "部门名称长度必须在1-64个字符之间")]
[RegularExpression(@"^[^\\:*?""<>]+$", ErrorMessage = "\\:*?\"<>|字符")]
public string Name { get; set; }
/// <summary>
/// 英文名称,需要在管理后台开启多语言支持才能生效
/// </summary>
/// <remarks>是否必填:否</remarks>
[CustomJsonProperty("name_en")]
[StringLength(64, MinimumLength = 1, ErrorMessage = "英文名称长度必须在1-64个字符之间")]
[RegularExpression(@"^[^\\:*?""<>]*$", ErrorMessage = "\\:*?\"<>|字符")]
public string NameEn { get; set; }
/// <summary>
/// 父部门id
/// </summary>
/// <remarks>是否必填:否</remarks>
[CustomJsonProperty("parentid")]
[Range(1, long.MaxValue, ErrorMessage = "父部门ID必须大于0")]
public long? ParentId { get; set; }
/// <summary>
/// 在父部门中的次序值。order值大的排序靠前
/// </summary>
/// <remarks>是否必填:否</remarks>
[CustomJsonProperty("order")]
[Range(0, 4294967295, ErrorMessage = "排序值必须在0-4294967295之间")]
public long? Order { get; set; }
}
/// <summary>
/// 删除部门
/// </summary>
/// <remarks>
/// <br/>最后更新2020/03/30
/// <br/>请求方式GETHTTPS
/// <br/>请求地址https://qyapi.weixin.qq.com/cgi-bin/department/delete?access_token=ACCESS_TOKEN&amp;id=ID
/// <br/>权限说明:应用须拥有指定部门的管理权限,第三方仅通讯录应用可以调用
/// <br/>限制说明:不能删除根部门;不能删除含有子部门、成员的部门
/// <br/>文档地址https://developer.work.weixin.qq.com/document/path/90207
/// </remarks>
[HttpRemoteApi(Action = "department/delete", Desc = "删除部门", HttpMethod = HttpMethodEnum.Get)]
public class DeleteDeptWorkWxInput : AuthWorkWxInput
{
/// <summary>
/// 部门id。不能删除根部门不能删除含有子部门、成员的部门
/// </summary>
/// <remarks>是否必填:是</remarks>
[CustomJsonProperty("id")]
[Required(ErrorMessage = "部门ID不能为空")]
[Range(2, long.MaxValue, ErrorMessage = "部门ID必须大于1不能删除根部门")]
public long Id { get; set; }
}
/// <summary>
/// 获取子部门ID列表
/// </summary>
/// <remarks>
/// <br/>最后更新2023/09/06
/// <br/>请求方式GETHTTPS
/// <br/>请求地址https://qyapi.weixin.qq.com/cgi-bin/department/simplelist?access_token=ACCESS_TOKEN&amp;id=ID
/// <br/>文档地址https://developer.work.weixin.qq.com/document/path/95350
/// </remarks>
[HttpRemoteApi(Action = "department/simplelist", Desc = "获取子部门ID列表", HttpMethod = HttpMethodEnum.Get)]
public class DeptSimpleListWorkWxInput : AuthWorkWxInput
{
/// <summary>
/// 部门id。获取指定部门及其下的子部门递归
/// </summary>
/// <remarks>是否必填:否</remarks>
[CustomJsonProperty("id")]
public long? Id { get; set; }
}

View File

@ -0,0 +1,86 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Plugin.WorkWeixin;
/// <summary>
/// 创建部门输出参数
/// </summary>
public class CreateDepartmentWorkWxOutput : BaseWorkWxOutput
{
/// <summary>
/// 创建的部门id
/// </summary>
[CustomJsonProperty("id")]
public long Id { get; set; }
}
/// <summary>
/// 部门基础信息
/// </summary>
public class DeptWorkWxBaseInfo
{
/// <summary>
/// 部门id
/// </summary>
[CustomJsonProperty("id")]
public long Id { get; set; }
/// <summary>
/// 父部门id
/// </summary>
[CustomJsonProperty("parentid")]
public long ParentId { get; set; }
/// <summary>
/// 排序值
/// </summary>
[CustomJsonProperty("order")]
public long Order { get; set; }
}
/// <summary>
/// 获取子部门ID列表输出参数
/// </summary>
public class DeptSimpleListWorkWxOutput : BaseWorkWxOutput
{
/// <summary>
/// 部门列表数据
/// </summary>
[CustomJsonProperty("department_id")]
public List<DeptWorkWxBaseInfo> DeptList { get; set; } = new List<DeptWorkWxBaseInfo>();
/// <summary>
/// 部门数量
/// </summary>
public int Count => DeptList?.Count ?? 0;
/// <summary>
/// 获取部门ID列表
/// </summary>
public List<long> DeptIds => DeptList?.Select(d => d.Id).ToList() ?? new List<long>();
/// <summary>
/// 按父部门分组
/// </summary>
public Dictionary<long, List<DeptWorkWxBaseInfo>> GroupByParent()
{
var result = new Dictionary<long, List<DeptWorkWxBaseInfo>>();
if (DeptList == null) return result;
foreach (var dept in DeptList)
{
if (!result.ContainsKey(dept.ParentId))
{
result[dept.ParentId] = new List<DeptWorkWxBaseInfo>();
}
result[dept.ParentId].Add(dept);
}
return result;
}
}

View File

@ -0,0 +1,54 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Plugin.WorkWeixin;
/// <summary>
/// 企业微信部门服务 🧩
/// </summary>
public class WorkWxDeptService(WorkWxBaseService baseService) : ITransient
{
/// <summary>
/// 创建部门
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<BaseIdWorkWxOutput> Create(CreateDeptWorkWxInput input)
{
return await baseService.SendAsync<CreateDeptWorkWxInput, BaseIdWorkWxOutput>(input);
}
/// <summary>
/// 更新部门
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<BaseWorkWxOutput> Update(UpdateDeptWorkWxInput input)
{
return await baseService.SendAsync<UpdateDeptWorkWxInput, BaseWorkWxOutput>(input);
}
/// <summary>
/// 删除部门
/// </summary>
/// <param name="id">部门id</param>
/// <returns></returns>
public async Task<BaseWorkWxOutput> Delete(long id)
{
return await baseService.SendAsync<DeleteDeptWorkWxInput, BaseWorkWxOutput>(new() { Id = id });
}
/// <summary>
/// 获取子部门ID列表
/// </summary>
/// <param name="id">部门id。获取指定部门及其下的子部门递归</param>
/// <returns></returns>
public async Task<DeptSimpleListWorkWxOutput> Get(long id)
{
return await baseService.SendAsync<DeptSimpleListWorkWxInput, DeptSimpleListWorkWxOutput>(new() { Id = id });
}
}

View File

@ -0,0 +1,166 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Plugin.WorkWeixin;
/// <summary>
/// 创建标签
/// </summary>
/// <remarks>
/// <br/>最后更新2020/06/08
/// <br/>请求方式POSTHTTPS
/// <br/>请求地址https://qyapi.weixin.qq.com/cgi-bin/tag/create?access_token=ACCESS_TOKEN
/// <br/>权限说明创建的标签属于该应用只有该应用的secret才可以增删成员
/// <br/>限制说明标签总数不能超过3000个
/// <br/>文档地址https://developer.work.weixin.qq.com/document/path/90210
/// </remarks>
[HttpRemoteApi(Action = "tag/create", Desc = "创建标签", HttpMethod = HttpMethodEnum.Post)]
public class CreateTagWorkWxInput : AuthWorkWxInput
{
/// <summary>
/// 标签名称长度限制为32个字以内汉字或英文字母标签名不可与其他标签重名
/// </summary>
[CustomJsonProperty("tagname")]
[Required]
[StringLength(32, MinimumLength = 1)]
public string TagName { get; set; }
/// <summary>
/// 标签id非负整型指定此参数时新增的标签会生成对应的标签id不指定时则以目前最大的id自增
/// </summary>
[CustomJsonProperty("tagid")]
public long? TagId { get; set; }
}
/// <summary>
/// 更新标签名字
/// </summary>
/// <remarks>
/// <br/>最后更新2020/04/02
/// <br/>请求方式POSTHTTPS
/// <br/>请求地址https://qyapi.weixin.qq.com/cgi-bin/tag/update?access_token=ACCESS_TOKEN
/// <br/>权限说明:调用的应用必须是指定标签的创建者
/// <br/>文档地址https://developer.work.weixin.qq.com/document/path/90211
/// </remarks>
[HttpRemoteApi(Action = "tag/update", Desc = "更新标签名字", HttpMethod = HttpMethodEnum.Post)]
public class UpdateTagWorkWxInput : AuthWorkWxInput
{
/// <summary>
/// 标签ID
/// </summary>
[CustomJsonProperty("tagid")]
[Required]
public long TagId { get; set; }
/// <summary>
/// 标签名称长度限制为32个字汉字或英文字母标签不可与其他标签重名
/// </summary>
[CustomJsonProperty("tagname")]
[Required]
[StringLength(32, MinimumLength = 1)]
public string TagName { get; set; }
}
/// <summary>
/// 删除标签
/// </summary>
/// <remarks>
/// <br/>最后更新2020/04/02
/// <br/>请求方式GETHTTPS
/// <br/>请求地址https://qyapi.weixin.qq.com/cgi-bin/tag/delete?access_token=ACCESS_TOKEN&amp;tagid=TAGID
/// <br/>权限说明:调用的应用必须是指定标签的创建者
/// <br/>文档地址https://developer.work.weixin.qq.com/document/path/90212
/// </remarks>
[HttpRemoteApi(Action = "tag/delete", Desc = "删除标签", HttpMethod = HttpMethodEnum.Get)]
public class DeleteTagWorkWxInput : AuthWorkWxInput
{
/// <summary>
/// 标签ID
/// </summary>
/// <remarks>是否必填:是</remarks>
[CustomJsonProperty("tagid")]
[Required(ErrorMessage = "标签ID不能为空")]
[Range(1, long.MaxValue, ErrorMessage = "标签ID必须大于0")]
public long TagId { get; set; }
}
/// <summary>
/// 获取标签成员
/// </summary>
[HttpRemoteApi(Action = "tag/get", Desc = "获取标签成员", HttpMethod = HttpMethodEnum.Get)]
public class TagMembersWorkWxInput : AuthWorkWxInput
{
/// <summary>
/// 标签ID
/// </summary>
[CustomJsonProperty("tagid")]
[Required]
public long TagId { get; set; }
}
/// <summary>
/// 增加标签成员
/// </summary>
[HttpRemoteApi(Action = "tag/addtagusers", Desc = "增加标签成员", HttpMethod = HttpMethodEnum.Post)]
public class AddTagMembersWorkWxInput : AuthWorkWxInput
{
/// <summary>
/// 标签ID
/// </summary>
[CustomJsonProperty("tagid")]
[Required]
public long TagId { get; set; }
/// <summary>
/// 企业成员ID列表
/// </summary>
[CustomJsonProperty("userlist")]
[MaxLength(1000)]
public List<string> UserList { get; set; }
/// <summary>
/// 企业部门ID列表
/// </summary>
[CustomJsonProperty("partylist")]
[MaxLength(100)]
public List<long> PartyList { get; set; }
}
/// <summary>
/// 删除标签成员
/// </summary>
[HttpRemoteApi(Action = "tag/deltagusers", Desc = "删除标签成员", HttpMethod = HttpMethodEnum.Post)]
public class DeleteTagMembersWorkWxInput : AuthWorkWxInput
{
/// <summary>
/// 标签ID
/// </summary>
[CustomJsonProperty("tagid")]
[Required]
public long TagId { get; set; }
/// <summary>
/// 企业成员ID列表
/// </summary>
[CustomJsonProperty("userlist")]
[MaxLength(1000)]
public List<string> UserList { get; set; }
/// <summary>
/// 企业部门ID列表
/// </summary>
[CustomJsonProperty("partylist")]
[MaxLength(100)]
public List<long> PartyList { get; set; }
}
/// <summary>
/// 获取标签列表
/// </summary>
[HttpRemoteApi(Action = "tag/list", Desc = "获取标签列表", HttpMethod = HttpMethodEnum.Get)]
public class TagListWorkWxInput : AuthWorkWxInput
{
}

View File

@ -0,0 +1,127 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Plugin.WorkWeixin;
/// <summary>
/// 创建标签输出
/// </summary>
public class CreateTagWorkWxOutput : BaseWorkWxOutput
{
/// <summary>
/// 标签id
/// </summary>
[CustomJsonProperty("tagid")]
public long TagId { get; set; }
}
/// <summary>
/// 标签成员信息
/// </summary>
public class TagMemberWorkWxInfo
{
/// <summary>
/// 成员账号
/// </summary>
[CustomJsonProperty("userid")]
public string UserId { get; set; }
/// <summary>
/// 成员名称
/// </summary>
[CustomJsonProperty("name")]
public string Name { get; set; }
}
/// <summary>
/// 获取标签成员输出
/// </summary>
public class TagMembersWorkWxOutput : BaseWorkWxOutput
{
/// <summary>
/// 标签名
/// </summary>
[CustomJsonProperty("tagname")]
public string TagName { get; set; }
/// <summary>
/// 标签中包含的成员列表
/// </summary>
[CustomJsonProperty("userlist")]
public List<TagMemberWorkWxInfo> UserList { get; set; } = new List<TagMemberWorkWxInfo>();
/// <summary>
/// 标签中包含的部门id列表
/// </summary>
[CustomJsonProperty("partylist")]
public List<long> PartyList { get; set; } = new List<long>();
}
/// <summary>
/// 增加标签成员输出
/// </summary>
public class AddTagMembersWorkWxOutput : BaseWorkWxOutput
{
/// <summary>
/// 非法的成员账号列表
/// </summary>
[CustomJsonProperty("invalidlist")]
public string InvalidList { get; set; }
/// <summary>
/// 非法的部门id列表
/// </summary>
[CustomJsonProperty("invalidparty")]
public List<long> InvalidParty { get; set; } = new List<long>();
}
/// <summary>
/// 删除标签成员输出
/// </summary>
public class DeleteTagMembersWorkWxOutput : BaseWorkWxOutput
{
/// <summary>
/// 非法的成员账号列表
/// </summary>
[CustomJsonProperty("invalidlist")]
public string InvalidList { get; set; }
/// <summary>
/// 非法的部门id列表
/// </summary>
[CustomJsonProperty("invalidparty")]
public List<long> InvalidParty { get; set; } = new List<long>();
}
/// <summary>
/// 标签基本信息
/// </summary>
public class TagInfoWorkWx
{
/// <summary>
/// 标签id
/// </summary>
[CustomJsonProperty("tagid")]
public long TagId { get; set; }
/// <summary>
/// 标签名
/// </summary>
[CustomJsonProperty("tagname")]
public string TagName { get; set; }
}
/// <summary>
/// 获取标签列表输出
/// </summary>
public class TagListWorkWxOutput : BaseWorkWxOutput
{
/// <summary>
/// 标签列表
/// </summary>
[CustomJsonProperty("taglist")]
public List<TagInfoWorkWx> TagList { get; set; } = new List<TagInfoWorkWx>();
}

View File

@ -0,0 +1,83 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Plugin.WorkWeixin;
/// <summary>
/// 企业微信标签服务 🧩
/// </summary>
public class WorkWxTagService(WorkWxBaseService baseService) : ITransient
{
/// <summary>
/// 创建标签
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<CreateTagWorkWxOutput> Create(CreateTagWorkWxInput input)
{
return await baseService.SendAsync<CreateTagWorkWxInput, CreateTagWorkWxOutput>(input);
}
/// <summary>
/// 更新标签
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<BaseWorkWxOutput> Update(UpdateTagWorkWxInput input)
{
return await baseService.SendAsync<UpdateTagWorkWxInput, BaseWorkWxOutput>(input);
}
/// <summary>
/// 删除标签
/// </summary>
/// <param name="id">标签ID</param>
/// <returns></returns>
public async Task<BaseWorkWxOutput> Delete(long id)
{
return await baseService.SendAsync<DeleteTagWorkWxInput, BaseWorkWxOutput>(new(){ TagId = id });
}
/// <summary>
/// 获取标签成员
/// </summary>
/// <param name="id">标签ID</param>
/// <returns></returns>
public async Task<TagMembersWorkWxOutput> Get(long id)
{
return await baseService.SendAsync<TagMembersWorkWxInput, TagMembersWorkWxOutput>(new(){ TagId = id });
}
/// <summary>
/// 增加标签成员
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<AddTagMembersWorkWxOutput> AddTagMembers(AddTagMembersWorkWxInput input)
{
return await baseService.SendAsync<AddTagMembersWorkWxInput, AddTagMembersWorkWxOutput>(input);
}
/// <summary>
/// 删除标签成员
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<DeleteTagMembersWorkWxOutput> DeleteTagMembers(DeleteTagMembersWorkWxInput input)
{
return await baseService.SendAsync<DeleteTagMembersWorkWxInput, DeleteTagMembersWorkWxOutput>(input);
}
/// <summary>
/// 获取标签列表
/// </summary>
/// <returns></returns>
public async Task<TagListWorkWxOutput> GetList()
{
return await baseService.SendAsync<TagListWorkWxInput, TagListWorkWxOutput>(new());
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,899 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using System.Text;
using System.Text.RegularExpressions;
namespace Admin.NET.Plugin.WorkWeixin;
/// <summary>
/// 创建成员输出参数
/// </summary>
public class CreateUserWorkWxOutput : BaseWorkWxOutput
{
/// <summary>
/// 因填写不存在的部门,新增的部门列表
/// </summary>
[CustomJsonProperty("created_department_list")]
public CreatedDepartmentListDto CreatedDepartmentList { get; set; }
/// <summary>
/// 新增部门列表
/// </summary>
public class CreatedDepartmentListDto
{
/// <summary>
/// 部门信息列表
/// </summary>
[CustomJsonProperty("department_info")]
public List<DepartmentInfo> DepartmentInfo { get; set; }
}
/// <summary>
/// 部门信息
/// </summary>
public class DepartmentInfo
{
/// <summary>
/// 部门名称
/// </summary>
[CustomJsonProperty("name")]
public string Name { get; set; }
/// <summary>
/// 部门ID
/// </summary>
[CustomJsonProperty("id")]
public long Id { get; set; }
}
}
/// <summary>
/// 读取成员输出参数
/// </summary>
/// <remarks>
/// <br/>注意:应用只能获取可见范围内的成员信息,且每种应用获取的字段有所不同
/// <br/>从2022年6月20号开始新创建的自建应用与代开发应用不再返回敏感字段
/// </remarks>
public class UserWorkWxOutput : BaseWorkWxOutput
{
/// <summary>
/// 成员UserID。对应管理端的账号企业内必须唯一。不区分大小写长度为1~64个字节第三方应用返回的值为open_userid
/// </summary>
[CustomJsonProperty("userid")]
public string UserId { get; set; }
/// <summary>
/// 成员名称第三方不可获取调用时返回userid以代替name代开发自建应用需要管理员授权才返回
/// </summary>
[CustomJsonProperty("name")]
public string Name { get; set; }
/// <summary>
/// 成员所属部门id列表仅返回该应用有查看权限的部门id
/// </summary>
[CustomJsonProperty("department")]
public List<long> Department { get; set; }
/// <summary>
/// 部门内的排序值默认为0。数量必须和department一致数值越大排序越前面
/// </summary>
[CustomJsonProperty("order")]
public List<long> Order { get; set; }
/// <summary>
/// 职务信息;代开发自建应用需要管理员授权才返回
/// </summary>
[CustomJsonProperty("position")]
public string Position { get; set; }
/// <summary>
/// 手机号码代开发自建应用需要管理员授权且成员oauth2授权获取
/// </summary>
[CustomJsonProperty("mobile")]
public string Mobile { get; set; }
/// <summary>
/// 性别。0表示未定义1表示男性2表示女性
/// </summary>
[CustomJsonProperty("gender")]
public int? Gender { get; set; }
/// <summary>
/// 性别描述
/// </summary>
public string GenderDesc => Gender switch
{
1 => "男",
2 => "女",
0 => "未定义",
_ => "未知"
};
/// <summary>
/// 邮箱代开发自建应用需要管理员授权且成员oauth2授权获取
/// </summary>
[CustomJsonProperty("email")]
public string Email { get; set; }
/// <summary>
/// 企业邮箱代开发自建应用需要管理员授权且成员oauth2授权获取
/// </summary>
[CustomJsonProperty("biz_mail")]
public string BizMail { get; set; }
/// <summary>
/// 表示在所在的部门内是否为部门负责人数量与department一致
/// </summary>
[CustomJsonProperty("is_leader_in_dept")]
public List<int> IsLeaderInDept { get; set; }
/// <summary>
/// 直属上级UserID返回在应用可见范围内的直属上级列表最多有1个直属上级
/// </summary>
[CustomJsonProperty("direct_leader")]
public List<string> DirectLeader { get; set; }
/// <summary>
/// 头像url。代开发自建应用需要管理员授权且成员oauth2授权获取
/// </summary>
[CustomJsonProperty("avatar")]
public string Avatar { get; set; }
/// <summary>
/// 头像缩略图url
/// </summary>
[CustomJsonProperty("thumb_avatar")]
public string ThumbAvatar { get; set; }
/// <summary>
/// 座机。代开发自建应用需要管理员授权才返回
/// </summary>
[CustomJsonProperty("telephone")]
public string Telephone { get; set; }
/// <summary>
/// 别名
/// </summary>
[CustomJsonProperty("alias")]
public string Alias { get; set; }
/// <summary>
/// 地址。代开发自建应用需要管理员授权且成员oauth2授权获取
/// </summary>
[CustomJsonProperty("address")]
public string Address { get; set; }
/// <summary>
/// 全局唯一ID。对于同一个服务商不同应用获取到企业内同一个成员的open_userid是相同的
/// </summary>
[CustomJsonProperty("open_userid")]
public string OpenUserId { get; set; }
/// <summary>
/// 主部门,仅当应用对主部门有查看权限时返回
/// </summary>
[CustomJsonProperty("main_department")]
public long? MainDepartment { get; set; }
/// <summary>
/// 扩展属性
/// </summary>
[CustomJsonProperty("extattr")]
public CreateUserWorkWxInput.ExtAttrDto ExtAttr { get; set; }
/// <summary>
/// 激活状态: 1=已激活2=已禁用4=未激活5=退出企业
/// </summary>
[CustomJsonProperty("status")]
public int? Status { get; set; }
/// <summary>
/// 状态描述
/// </summary>
public string StatusDesc => Status switch
{
1 => "已激活",
2 => "已禁用",
4 => "未激活",
5 => "退出企业",
_ => "未知状态"
};
/// <summary>
/// 员工个人二维码URL
/// </summary>
[CustomJsonProperty("qr_code")]
public string QrCode { get; set; }
/// <summary>
/// 对外职务
/// </summary>
[CustomJsonProperty("external_position")]
public string ExternalPosition { get; set; }
/// <summary>
/// 成员对外属性
/// </summary>
[CustomJsonProperty("external_profile")]
public CreateUserWorkWxInput.ExternalProfileDto ExternalProfile { get; set; }
/// <summary>
/// 是否为部门负责人(主部门)
/// </summary>
public bool IsLeader
{
get
{
if (IsLeaderInDept == null || Department == null || MainDepartment == null)
return false;
var mainDeptIndex = Department.IndexOf(MainDepartment.Value);
return mainDeptIndex >= 0 && mainDeptIndex < IsLeaderInDept.Count && IsLeaderInDept[mainDeptIndex] == 1;
}
}
/// <summary>
/// 是否已激活
/// </summary>
public bool IsActive => Status == 1;
/// <summary>
/// 获取主部门名称(需要外部传入部门映射)
/// </summary>
public string GetMainDepartmentName(Dictionary<long, string> departmentMap)
{
if (MainDepartment.HasValue && departmentMap != null && departmentMap.TryGetValue(MainDepartment.Value, out var name))
return name;
return null;
}
/// <summary>
/// 视频号信息(响应)
/// </summary>
public class WechatChannelsResponse
{
/// <summary>
/// 视频号名称
/// </summary>
[CustomJsonProperty("nickname")]
public string Nickname { get; set; }
/// <summary>
/// 视频号状态
/// </summary>
[CustomJsonProperty("status")]
public int? Status { get; set; }
}
}
/// <summary>
/// 获取部门成员输出参数
/// </summary>
public class DeptUserSimpleListWorkWxOutput : BaseWorkWxOutput
{
/// <summary>
/// 成员列表
/// </summary>
[CustomJsonProperty("userlist")]
public List<DepartmentUserSimpleInfo> UserList { get; set; } = new List<DepartmentUserSimpleInfo>();
/// <summary>
/// 成员数量
/// </summary>
public int Count => UserList?.Count ?? 0;
/// <summary>
/// 是否包含成员
/// </summary>
public bool HasUsers => Count > 0;
/// <summary>
/// 获取用户ID列表
/// </summary>
public List<string> UserIds => UserList?.Select(u => u.UserId).ToList() ?? new List<string>();
/// <summary>
/// 获取OpenUserID列表
/// </summary>
public List<string> OpenUserIds => UserList?.Where(u => !string.IsNullOrEmpty(u.OpenUserId))
.Select(u => u.OpenUserId)
.ToList() ?? new List<string>();
/// <summary>
/// 部门成员简略信息
/// </summary>
public class DepartmentUserSimpleInfo
{
/// <summary>
/// 成员UserID。对应管理端的账号
/// </summary>
[CustomJsonProperty("userid")]
public string UserId { get; set; }
/// <summary>
/// 成员名称第三方应用可能返回userid代替name
/// </summary>
[CustomJsonProperty("name")]
public string Name { get; set; }
/// <summary>
/// 成员所属部门列表。列表项为部门ID32位整型
/// </summary>
[CustomJsonProperty("department")]
public List<long> Department { get; set; }
/// <summary>
/// 全局唯一ID。对于同一个服务商不同应用获取到企业内同一个成员的open_userid是相同的
/// </summary>
[CustomJsonProperty("open_userid")]
public string OpenUserId { get; set; }
/// <summary>
/// 获取显示名称优先显示name如果没有则显示userid
/// </summary>
public string DisplayName => !string.IsNullOrEmpty(Name) ? Name : UserId;
/// <summary>
/// 是否包含指定部门
/// </summary>
public bool ContainsDepartment(long departmentId)
{
return Department?.Contains(departmentId) ?? false;
}
/// <summary>
/// 获取主部门(第一个部门)
/// </summary>
public long? MainDepartment => Department?.FirstOrDefault();
}
}
/// <summary>
/// 获取部门成员详情输出参数
/// </summary>
public class DeptUserDetailListWorkWxOutput : BaseWorkWxOutput
{
/// <summary>
/// 成员列表
/// </summary>
[CustomJsonProperty("userlist")]
public List<DepartmentUserDetailInfo> UserList { get; set; } = new List<DepartmentUserDetailInfo>();
/// <summary>
/// 成员数量
/// </summary>
public int Count => UserList?.Count ?? 0;
/// <summary>
/// 是否包含成员
/// </summary>
public bool HasUsers => Count > 0;
/// <summary>
/// 获取已激活的成员列表
/// </summary>
public List<DepartmentUserDetailInfo> ActiveUsers => UserList?.Where(u => u.IsActive).ToList() ?? new List<DepartmentUserDetailInfo>();
/// <summary>
/// 获取部门负责人列表
/// </summary>
public List<DepartmentUserDetailInfo> LeaderUsers => UserList?.Where(u => u.IsLeader).ToList() ?? new List<DepartmentUserDetailInfo>();
/// <summary>
/// 获取用户ID列表
/// </summary>
public List<string> UserIds => UserList?.Select(u => u.UserId).ToList() ?? new List<string>();
/// <summary>
/// 按状态分组统计
/// </summary>
public Dictionary<string, int> GetStatusStatistics()
{
return UserList?
.GroupBy(u => u.StatusDesc)
.ToDictionary(g => g.Key, g => g.Count()) ?? new Dictionary<string, int>();
}
/// <summary>
/// 部门成员详细信息
/// </summary>
public class DepartmentUserDetailInfo
{
/// <summary>
/// 成员UserID。对应管理端的账号
/// </summary>
[CustomJsonProperty("userid")]
public string UserId { get; set; }
/// <summary>
/// 成员名称第三方不可获取调用时返回userid以代替name
/// </summary>
[CustomJsonProperty("name")]
public string Name { get; set; }
/// <summary>
/// 英文名
/// </summary>
[CustomJsonProperty("english_name")]
public string EnglishName { get; set; }
/// <summary>
/// 成员所属部门id列表仅返回该应用有查看权限的部门id
/// </summary>
[CustomJsonProperty("department")]
public List<long> Department { get; set; }
/// <summary>
/// 部门内的排序值默认为0。数量必须和department一致
/// </summary>
[CustomJsonProperty("order")]
public List<long> Order { get; set; }
/// <summary>
/// 职务信息;代开发自建应用需要管理员授权才返回
/// </summary>
[CustomJsonProperty("position")]
public string Position { get; set; }
/// <summary>
/// 手机号码,代开发自建应用需要管理员授权才返回
/// </summary>
[CustomJsonProperty("mobile")]
public string Mobile { get; set; }
/// <summary>
/// 性别。0表示未定义1表示男性2表示女性
/// </summary>
[CustomJsonProperty("gender")]
public int? Gender { get; set; }
/// <summary>
/// 性别描述
/// </summary>
public string GenderDesc => Gender switch
{
1 => "男",
2 => "女",
0 => "未定义",
_ => "未知"
};
/// <summary>
/// 邮箱,代开发自建应用需要管理员授权才返回
/// </summary>
[CustomJsonProperty("email")]
public string Email { get; set; }
/// <summary>
/// 企业邮箱,代开发自建应用不返回
/// </summary>
[CustomJsonProperty("biz_mail")]
public string BizMail { get; set; }
/// <summary>
/// 表示在所在的部门内是否为部门负责人。0-否1-是
/// </summary>
[CustomJsonProperty("is_leader_in_dept")]
public List<int> IsLeaderInDept { get; set; }
/// <summary>
/// 直属上级UserID返回在应用可见范围内的直属上级列表
/// </summary>
[CustomJsonProperty("direct_leader")]
public List<string> DirectLeader { get; set; }
/// <summary>
/// 头像url。第三方仅通讯录应用可获取
/// </summary>
[CustomJsonProperty("avatar")]
public string Avatar { get; set; }
/// <summary>
/// 头像缩略图url。第三方仅通讯录应用可获取
/// </summary>
[CustomJsonProperty("thumb_avatar")]
public string ThumbAvatar { get; set; }
/// <summary>
/// 座机。代开发自建应用需要管理员授权才返回
/// </summary>
[CustomJsonProperty("telephone")]
public string Telephone { get; set; }
/// <summary>
/// 别名;第三方仅通讯录应用可获取
/// </summary>
[CustomJsonProperty("alias")]
public string Alias { get; set; }
/// <summary>
/// 扩展属性
/// </summary>
[CustomJsonProperty("extattr")]
public CreateUserWorkWxInput.ExtAttrDto ExtAttr { get; set; }
/// <summary>
/// 激活状态: 1=已激活2=已禁用4=未激活5=退出企业
/// </summary>
[CustomJsonProperty("status")]
public int? Status { get; set; }
/// <summary>
/// 状态描述
/// </summary>
public string StatusDesc => Status switch
{
1 => "已激活",
2 => "已禁用",
4 => "未激活",
5 => "退出企业",
_ => "未知状态"
};
/// <summary>
/// 地址。代开发自建应用需要管理员授权才返回
/// </summary>
[CustomJsonProperty("address")]
public string Address { get; set; }
/// <summary>
/// 全局唯一ID。仅第三方应用可获取
/// </summary>
[CustomJsonProperty("open_userid")]
public string OpenUserId { get; set; }
/// <summary>
/// 主部门,仅当应用对主部门有查看权限时返回
/// </summary>
[CustomJsonProperty("main_department")]
public long? MainDepartment { get; set; }
/// <summary>
/// 员工个人二维码URL
/// </summary>
[CustomJsonProperty("qr_code")]
public string QrCode { get; set; }
/// <summary>
/// 对外职务
/// </summary>
[CustomJsonProperty("external_position")]
public string ExternalPosition { get; set; }
/// <summary>
/// 成员对外属性
/// </summary>
[CustomJsonProperty("external_profile")]
public CreateUserWorkWxInput.ExternalProfileDto ExternalProfile { get; set; }
/// <summary>
/// 获取显示名称优先显示name如果没有则显示userid
/// </summary>
public string DisplayName => !string.IsNullOrEmpty(Name) ? Name : UserId;
/// <summary>
/// 是否已激活
/// </summary>
public bool IsActive => Status == 1;
/// <summary>
/// 是否为部门负责人(主部门)
/// </summary>
public bool IsLeader
{
get
{
if (IsLeaderInDept == null || Department == null || MainDepartment == null)
return false;
var mainDeptIndex = Department.IndexOf(MainDepartment.Value);
return mainDeptIndex >= 0 && mainDeptIndex < IsLeaderInDept.Count && IsLeaderInDept[mainDeptIndex] == 1;
}
}
/// <summary>
/// 是否包含指定部门
/// </summary>
public bool ContainsDepartment(long departmentId)
{
return Department?.Contains(departmentId) ?? false;
}
/// <summary>
/// 获取主部门排序值
/// </summary>
public long? GetMainDepartmentOrder()
{
if (Order == null || Department == null || MainDepartment == null)
return null;
var mainDeptIndex = Department.IndexOf(MainDepartment.Value);
return mainDeptIndex >= 0 && mainDeptIndex < Order.Count ? Order[mainDeptIndex] : (long?)null;
}
}
}
/// <summary>
/// userid转openid输出参数
/// </summary>
public class ConvToOpenIdWorkWxOutput : BaseWorkWxOutput
{
/// <summary>
/// 企业微信成员userid对应的openid
/// </summary>
[CustomJsonProperty("openid")]
public string OpenId { get; set; }
}
/// <summary>
/// openid转userid输出参数
/// </summary>
public class ConvToUserIdWorkWxOutput : BaseWorkWxOutput
{
/// <summary>
/// 该openid在企业微信对应的成员userid
/// </summary>
[CustomJsonProperty("userid")]
public string UserId { get; set; }
}
/// <summary>
/// 邀请成员输出参数
/// </summary>
public class InviteUserWorkWxOutput : BaseWorkWxOutput
{
/// <summary>
/// 非法成员列表
/// </summary>
[CustomJsonProperty("invaliduser")]
public List<string> InvalidUser { get; set; } = new List<string>();
/// <summary>
/// 非法部门列表
/// </summary>
[CustomJsonProperty("invalidparty")]
public List<long> InvalidParty { get; set; } = new List<long>();
/// <summary>
/// 非法标签列表
/// </summary>
[CustomJsonProperty("invalidtag")]
public List<long> InvalidTag { get; set; } = new List<long>();
/// <summary>
/// 有效成员数量
/// </summary>
public int ValidUserCount => (InputUserCount - InvalidUserCount);
/// <summary>
/// 有效部门数量
/// </summary>
public int ValidPartyCount => (InputPartyCount - InvalidPartyCount);
/// <summary>
/// 有效标签数量
/// </summary>
public int ValidTagCount => (InputTagCount - InvalidTagCount);
/// <summary>
/// 非法成员数量
/// </summary>
public int InvalidUserCount => InvalidUser?.Count ?? 0;
/// <summary>
/// 非法部门数量
/// </summary>
public int InvalidPartyCount => InvalidParty?.Count ?? 0;
/// <summary>
/// 非法标签数量
/// </summary>
public int InvalidTagCount => InvalidTag?.Count ?? 0;
/// <summary>
/// 输入成员数量(需要在调用时设置)
/// </summary>
[System.Text.Json.Serialization.JsonIgnore]
[Newtonsoft.Json.JsonIgnore]
public int InputUserCount { get; set; }
/// <summary>
/// 输入部门数量(需要在调用时设置)
/// </summary>
[System.Text.Json.Serialization.JsonIgnore]
[Newtonsoft.Json.JsonIgnore]
public int InputPartyCount { get; set; }
/// <summary>
/// 输入标签数量(需要在调用时设置)
/// </summary>
[System.Text.Json.Serialization.JsonIgnore]
[Newtonsoft.Json.JsonIgnore]
public int InputTagCount { get; set; }
/// <summary>
/// 是否全部有效
/// </summary>
public bool IsAllValid => InvalidUserCount == 0 && InvalidPartyCount == 0 && InvalidTagCount == 0;
/// <summary>
/// 获取邀请结果摘要
/// </summary>
public string GetInviteSummary()
{
var summary = new StringBuilder();
summary.Append("邀请结果: ");
if (InvalidUserCount > 0)
{
summary.Append($"{InvalidUserCount}个无效成员");
}
if (InvalidPartyCount > 0)
{
if (summary.Length > 12) summary.Append(", ");
summary.Append($"{InvalidPartyCount}个无效部门");
}
if (InvalidTagCount > 0)
{
if (summary.Length > 12) summary.Append(", ");
summary.Append($"{InvalidTagCount}个无效标签");
}
if (IsAllValid)
{
summary.Append("全部有效");
}
return summary.ToString();
}
}
/// <summary>
/// 获取加入企业二维码输出参数
/// </summary>
public class JoinQrcodeWorkWxOutput : BaseWorkWxOutput
{
/// <summary>
/// 二维码链接有效期7天
/// </summary>
[CustomJsonProperty("join_qrcode")]
public string JoinQrcode { get; set; }
/// <summary>
/// 二维码尺寸描述
/// </summary>
public string QrcodeSizeDesc => SizeType switch
{
1 => "171x171",
2 => "399x399",
3 => "741x741",
4 => "2052x2052",
_ => "未知尺寸"
};
/// <summary>
/// 尺寸类型从URL中解析
/// </summary>
[System.Text.Json.Serialization.JsonIgnore]
[Newtonsoft.Json.JsonIgnore]
public int? SizeType
{
get
{
if (string.IsNullOrEmpty(JoinQrcode))return null;
var match = Regex.Match(JoinQrcode, @"qr_size=(\d)");
return match.Success ? int.Parse(match.Groups[1].Value) : null;
}
}
}
/// <summary>
/// 成员Id输出参数
/// </summary>
public class UserIdWorkWxOutput : BaseWorkWxOutput
{
/// <summary>
/// 用户userid
/// </summary>
[CustomJsonProperty("userid")]
public string UserId { get; set; }
}
/// <summary>
/// 获取成员ID列表输出参数
/// </summary>
public class UserIdListWorkWxOutput : BaseWorkWxOutput
{
/// <summary>
/// 分页游标,下次请求时填写以获取之后分页的记录
/// </summary>
[CustomJsonProperty("next_cursor")]
public string NextCursor { get; set; }
/// <summary>
/// 用户-部门关系列表
/// </summary>
[CustomJsonProperty("dept_user")]
public List<DeptUserInfo> DeptUser { get; set; } = new List<DeptUserInfo>();
/// <summary>
/// 用户数量
/// </summary>
public int UserCount => DeptUser?.Count ?? 0;
/// <summary>
/// 是否还有更多数据
/// </summary>
public bool HasMore => !string.IsNullOrEmpty(NextCursor);
/// <summary>
/// 获取去重后的用户ID列表
/// </summary>
public List<string> DistinctUserIds => DeptUser?.Select(d => d.UserId).Distinct().ToList() ?? new List<string>();
/// <summary>
/// 按部门分组用户
/// </summary>
public Dictionary<long, List<string>> GroupUsersByDepartment()
{
var result = new Dictionary<long, List<string>>();
if (DeptUser == null) return result;
foreach (var item in DeptUser)
{
if (!result.ContainsKey(item.Department))
{
result[item.Department] = new List<string>();
}
result[item.Department].Add(item.UserId);
}
return result;
}
/// <summary>
/// 按用户分组部门
/// </summary>
public Dictionary<string, List<long>> GroupDepartmentsByUser()
{
var result = new Dictionary<string, List<long>>();
if (DeptUser == null) return result;
foreach (var item in DeptUser)
{
if (!result.ContainsKey(item.UserId))
{
result[item.UserId] = new List<long>();
}
result[item.UserId].Add(item.Department);
}
return result;
}
/// <summary>
/// 用户-部门关系信息
/// </summary>
public class DeptUserInfo
{
/// <summary>
/// 用户userid当用户在多个部门下时会有多条记录
/// </summary>
[CustomJsonProperty("userid")]
public string UserId { get; set; }
/// <summary>
/// 用户所属部门
/// </summary>
[CustomJsonProperty("department")]
public long Department { get; set; }
}
}

View File

@ -0,0 +1,55 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Plugin.WorkWeixin;
/// <summary>
/// 企业微信成员工具类
/// </summary>
public class WorkWxUserHelper
{
/// <summary>
/// 检查是否包含敏感字段(根据应用类型判断)
/// </summary>
public static bool HasSensitiveFields(UserWorkWxOutput user, bool isNewApp)
{
if (isNewApp)
{
// 新应用无法获取的敏感字段
return string.IsNullOrEmpty(user.Avatar) &&
string.IsNullOrEmpty(user.Mobile) &&
string.IsNullOrEmpty(user.Email) &&
string.IsNullOrEmpty(user.BizMail) &&
string.IsNullOrEmpty(user.QrCode) &&
string.IsNullOrEmpty(user.Address) &&
(user.Gender == null || user.Gender == 0);
}
return true;
}
/// <summary>
/// 获取显示名称优先显示name如果没有则显示userid
/// </summary>
public static string GetDisplayName(UserWorkWxOutput user)
{
return !string.IsNullOrEmpty(user.Name) ? user.Name : user.UserId;
}
/// <summary>
/// 获取完整的部门信息
/// </summary>
public static string GetDepartmentInfo(UserWorkWxOutput user, Dictionary<long, string> departmentMap)
{
if (user.Department == null || departmentMap == null)
return string.Empty;
var departments = user.Department
.Select(deptId => departmentMap.TryGetValue(deptId, out var name) ? $"{name}({deptId})" : deptId.ToString())
.ToList();
return string.Join(", ", departments);
}
}

View File

@ -0,0 +1,166 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Plugin.WorkWeixin;
/// <summary>
/// 企业微信成员服务 🧩
/// </summary>
public class WorkWxUserService(WorkWxBaseService baseService) : ITransient
{
/// <summary>
/// 创建成员
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<CreateUserWorkWxOutput> Create(CreateUserWorkWxInput input)
{
return await baseService.SendAsync<CreateUserWorkWxInput, CreateUserWorkWxOutput>(input);
}
/// <summary>
/// 读取成员
/// </summary>
/// <param name="userId">成员UserID。对应管理端的账号企业内必须唯一。不区分大小写长度为1~64个字节</param>
/// <returns></returns>
public async Task<UserWorkWxOutput> Get(string userId)
{
return await baseService.SendAsync<UserWorkWxInput, UserWorkWxOutput>(new() { UserId = userId });
}
/// <summary>
/// 修改成员
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<BaseWorkWxOutput> Update(UpdateUserWorkWxInput input)
{
return await baseService.SendAsync<UpdateUserWorkWxInput, BaseWorkWxOutput>(input);
}
/// <summary>
/// 删除成员
/// </summary>
/// <param name="userId">成员UserID。对应管理端的账号</param>
/// <returns></returns>
public async Task<BaseWorkWxOutput> Delete(string userId)
{
return await baseService.SendAsync<DeleteUserWorkWxInput, BaseWorkWxOutput>(new() { UserId = userId });
}
/// <summary>
/// 批量删除成员
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<BaseWorkWxOutput> DeleteUser(BatchDeleteUserWorkWxInput input)
{
return await baseService.SendAsync<BatchDeleteUserWorkWxInput, BaseWorkWxOutput>(input);
}
/// <summary>
/// 获取部门成员
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<DeptUserSimpleListWorkWxOutput> SimpleListDept(DeptUserSimpleListWorkWxInput input)
{
return await baseService.SendAsync<DeptUserSimpleListWorkWxInput, DeptUserSimpleListWorkWxOutput>(input);
}
/// <summary>
/// 获取部门成员详情
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<DeptUserDetailListWorkWxOutput> DeptUserDetail(DeptUserDetailListWorkWxInput input)
{
return await baseService.SendAsync<DeptUserDetailListWorkWxInput, DeptUserDetailListWorkWxOutput>(input);
}
/// <summary>
/// userid转openid
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<ConvToOpenIdWorkWxOutput> UserIdToOpenId(ConvToOpenIdWorkWxInput input)
{
return await baseService.SendAsync<ConvToOpenIdWorkWxInput, ConvToOpenIdWorkWxOutput>(input);
}
/// <summary>
/// openid转userid
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<ConvToUserIdWorkWxOutput> OpenIdToUserId(ConvToUserIdWorkWxInput input)
{
return await baseService.SendAsync<ConvToUserIdWorkWxInput, ConvToUserIdWorkWxOutput>(input);
}
/// <summary>
/// 登录二次验证
/// </summary>
/// <param name="userId">成员UserID。对应管理端的账号</param>
/// <returns></returns>
public async Task<BaseWorkWxOutput> AuthSucc(string userId)
{
return await baseService.SendAsync<UserAuthSuccessWorkWxInput, BaseWorkWxOutput>(new() { UserId = userId });
}
/// <summary>
/// 邀请成员
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<InviteUserWorkWxOutput> InviteUser(InviteUserWorkWxInput input)
{
return await baseService.SendAsync<InviteUserWorkWxInput, InviteUserWorkWxOutput>(input);
}
/// <summary>
/// 获取加入企业二维码
/// </summary>
/// <param name="sizeType">qrcode尺寸类型1: 171 x 171; 2: 399 x 399; 3: 741 x 741; 4: 2052 x 2052</param>
/// <returns></returns>
public async Task<JoinQrcodeWorkWxOutput> GetJoinQrcode(int? sizeType)
{
return await baseService.SendAsync<JoinQrcodeWorkWxInput, JoinQrcodeWorkWxOutput>(new() { SizeType = sizeType });
}
/// <summary>
/// 通过手机号获取成员ID
/// </summary>
/// <param name="mobile">用户在企业微信通讯录中的手机号码。长度为5~32个字节</param>
/// <returns></returns>
public async Task<UserIdWorkWxOutput> GetUserIdByMobile(string mobile)
{
return await baseService.SendAsync<UserIdByMobileWorkWxInput, UserIdWorkWxOutput>(new() { Mobile = mobile });
}
/// <summary>
/// 通过邮箱获取成员ID
/// </summary>
/// <param name="email">邮箱</param>
/// <param name="emailType">邮箱类型1-企业邮箱默认2-个人邮箱</param>
/// <returns></returns>
public async Task<UserIdWorkWxOutput> GetUserIdByEmail(string email, int? emailType = 1)
{
return await baseService.SendAsync<UserIdByEmailWorkWxInput, UserIdWorkWxOutput>(new(){ Email = email, EmailType = emailType });
}
/// <summary>
/// 获取成员ID列表
/// </summary>
/// <param name="cursor">用于分页查询的游标,字符串类型,由上一次调用返回,首次调用不填</param>
/// <param name="limit">分页,预期请求的数据量,取值范围 1 ~ 10000</param>
/// <returns></returns>
public async Task<UserIdListWorkWxOutput> GetUserIdList(string cursor = null, int? limit = 20)
{
return await baseService.SendAsync<UserIdListWorkWxInput, UserIdListWorkWxOutput>(new(){ Cursor = cursor, Limit = limit });
}
}

View File

@ -0,0 +1,92 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Plugin.WorkWeixin;
/// <summary>
/// 企业微信成员更新工具类
/// </summary>
public class WorkWxUserUpdateHelper
{
/// <summary>
/// 从现有成员信息创建更新对象
/// </summary>
public static UpdateUserWorkWxInput CreateFromExisting(UserWorkWxOutput existingUser)
{
return new UpdateUserWorkWxInput
{
UserId = existingUser.UserId,
Name = existingUser.Name,
Alias = existingUser.Alias,
Mobile = existingUser.Mobile,
Email = existingUser.Email,
Department = existingUser.Department,
Order = existingUser.Order,
Position = existingUser.Position,
Gender = existingUser.Gender,
Telephone = existingUser.Telephone,
IsLeaderInDept = existingUser.IsLeaderInDept,
DirectLeader = existingUser.DirectLeader,
Enable = existingUser.Status == 1 ? 1 : (existingUser.Status == 2 ? 0 : (int?)null),
ExtAttr = existingUser.ExtAttr,
ExternalPosition = existingUser.ExternalPosition,
ExternalProfile = existingUser.ExternalProfile,
Address = existingUser.Address,
MainDepartment = existingUser.MainDepartment
};
}
/// <summary>
/// 创建部门更新信息
/// </summary>
public static UpdateUserWorkWxInput CreateDepartmentUpdate(string userId, List<long> departments, List<long> orders = null, List<int> isLeaders = null)
{
var input = new UpdateUserWorkWxInput
{
UserId = userId,
Department = departments
};
if (orders != null && orders.Count == departments.Count)
{
input.Order = orders;
}
if (isLeaders != null && isLeaders.Count == departments.Count)
{
input.IsLeaderInDept = isLeaders;
}
return input;
}
/// <summary>
/// 创建基础信息更新
/// </summary>
public static UpdateUserWorkWxInput CreateBasicInfoUpdate(string userId, string name = null, string mobile = null, string email = null, string position = null)
{
return new UpdateUserWorkWxInput
{
UserId = userId,
Name = name,
Mobile = mobile,
Email = email,
Position = position
};
}
/// <summary>
/// 创建状态更新
/// </summary>
public static UpdateUserWorkWxInput CreateStatusUpdate(string userId, bool enable)
{
return new UpdateUserWorkWxInput
{
UserId = userId,
Enable = enable ? 1 : 0
};
}
}

View File

@ -0,0 +1,111 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using System.Reflection;
using System.Text;
using Admin.NET.Plugin.WorkWeixin.Const;
using Furion.FriendlyException;
using Furion.JsonSerialization;
using Furion.Logging;
using Microsoft.Extensions.Options;
namespace Admin.NET.Plugin.WorkWeixin;
/// <summary>
/// 企业微信接口基类服务 🧩
/// </summary>
public class WorkWxBaseService(
SysCacheService sysCacheService,
SysConfigService sysConfigService,
IHttpRemoteService httpRemoteService,
IOptions<HttpRemotesOptions> options) : ITransient
{
/// <summary>
/// 发起请求
/// </summary>
/// <typeparam name="T">输入类型</typeparam>
/// <typeparam name="R">返回类型</typeparam>
/// <param name="input">输入参数</param>
/// <returns>返回结果</returns>
public async Task<R> SendAsync<T, R>(T input) where R : BaseWorkWxOutput
{
var attr = typeof(T).GetCustomAttribute<HttpRemoteApiAttribute>();
if (attr == null || string.IsNullOrWhiteSpace(attr.Action) || string.IsNullOrWhiteSpace(attr.Desc))
throw Oops.Oh($"接口入参类型({typeof(T).FullName})未正确配置[HttpRemoteApi]特性");
// 拼接请求地址并设置token
var url = options.Value.WorkWeixin.BaseAddress + $"/cgi-bin/{attr.Action}?";
if (input is AuthWorkWxInput)
{
var token = sysCacheService.Get<string>(WorkWeixinConst.KeyWorkWeixinToken) ?? await GetTokenAsync();
url += "access_token=" + token;
}
HttpResponseMessage response;
try
{
response = attr.HttpMethod switch
{
HttpMethodEnum.Get => await httpRemoteService.GetAsync(
url + input.ToCustomJsonPropertyQueryString(),
builder => builder.SetHttpOptions(options.Value.WorkWeixin, attr.Desc)),
HttpMethodEnum.Post => await httpRemoteService.PostAsync(url,
builder => builder.SetHttpOptions(options.Value.WorkWeixin, attr.Desc)
.SetContent(new StringContent(JSON.Serialize(input, CustomJsonPropertyConverter.Options), Encoding.UTF8, "application/json"))),
_ => throw Oops.Oh($"[企业微信] 不支持的请求方式{attr.HttpMethod.ToString()}({typeof(T).FullName})"),
};
}
catch (Exception ex)
{
Log.Error(ex.Message);
throw Oops.Oh("[企业微信] 服务不可用,请检查网路是否正常" + ex.Message);
}
return await HandleHttpResponseAsync<R>(response);
}
/// <summary>
/// 处理HTTP响应
/// </summary>
/// <param name="respMsg"></param>
/// <returns></returns>
/// <exception cref="HttpRequestException"></exception>
private async Task<R> HandleHttpResponseAsync<R>(HttpResponseMessage respMsg) where R : BaseWorkWxOutput
{
if (!respMsg.IsSuccessStatusCode) throw Oops.Oh("[企业微信] 请求失败");
var responseContent = await respMsg.Content.ReadAsStringAsync();
if (string.IsNullOrWhiteSpace(responseContent)) throw Oops.Oh("[企业微信] 响应体为空");
try
{
var resp = JSON.Deserialize<R>(responseContent, CustomJsonPropertyConverter.Options);
if (resp?.ErrCode == 0) return resp;
throw Oops.Oh("[企业微信] 请求失败:" + resp?.ErrMsg);
}
catch (Exception ex)
{
Log.Error(ex.Message);
throw Oops.Oh((ex is AppFriendlyException ? "" : "[企业微信] 序列化失败:") + ex.Message);
}
}
/// <summary>
/// 获取企业微信接口凭证
/// </summary>
/// <returns></returns>
private async Task<string> GetTokenAsync()
{
using var disposable = sysCacheService.BeginCacheLock(WorkWeixinConst.KeyLockWorkWeixin);
var result = await SendAsync<TokenWorkWxInput, TokenWorkWxOutput>(new()
{
CorpId = await sysConfigService.GetConfigValueByCode(WorkWeixinConst.WorkWeixinCorpId),
CorpSecret = await sysConfigService.GetConfigValueByCode(WorkWeixinConst.WorkWeixinCorpSecret)
});
sysCacheService.Set(WorkWeixinConst.KeyWorkWeixinToken, result.AccessToken, TimeSpan.FromSeconds(result.ExpiresIn));
return result.AccessToken;
}
}

View File

@ -4,7 +4,6 @@
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using Admin.NET.Plugin.WorkWeixin.Option;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
@ -16,7 +15,6 @@ public class Startup : AppStartup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddConfigurableOptions<WorkWeixinOptions>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

View File

@ -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; }
}

View File

@ -0,0 +1,113 @@
<script setup lang="ts">
import { PropType, reactive, watch } from 'vue';
import { number } from 'echarts';
import { auth } from '/@/utils/authFunction';
const emit = defineEmits(['update:modelValue', 'change']);
type BadgeTabsItem = {
label: string; //
value: string; //
count?: number; //
visible?: boolean; //
disabled?: boolean; //
};
/**
* 组件属性定义
*/
const props = defineProps({
/**
* 绑定值
* @type {string|number|boolean}
* @required
* @example
* <BadgeTabs v-model="state.active" :data="[
* { label: '履约中', value: 10, count: state.total?.ly ?? 0 },
* { label: '解约申请', value: 20, count: state.total?.jysq ?? 0 },
* { label: '解约审核', value: 21, count: state.total?.jysh ?? 0, visible: auth('jtysqyQyd/approve') },
* { label: '已解约', value: 22, count: state.total?.yjy ?? 0 },
* { label: '已超期', value: 11, count: state.total?.ycq ?? 0 },
* { label: '已失效', value: 199, count: state.total?.ysx ?? 0 },
* ]" @@change="handleQuery(true)">
* <el-table ref="tableRef" :data="state.tableData">
* <el-table-column label="序号" width="50"></el-table-column>
* <el-table-column label="合同编号"></el-table-column>
* </el-table>
* </BadgeTabs>
*/
modelValue: {
type: [String, Number, Boolean],
required: true,
},
/**
* 字典编码用于从字典中获取数据
* @type {string}
* @required
* @default []
* @example []
*/
data: {
type: Array as PropType<BadgeTabsItem[]>,
required: true,
default: [],
},
/**
* badge 最大值
* @type {number}
* @default 99
* @example 99
*/
max: {
type: Number,
default: 99,
},
/**
* badge 的偏移量
* @type [int, int]
* @default [5, -2]
* @example [5, -2]
*/
offset: {
type: Array as PropType<[int, int]>,
default: [5, -2],
},
/**
* 隐藏小于等于0的badge
* @type {boolean}
* @default true
* @example true
*/
hideZero: {
type: Boolean,
default: true,
},
});
const state = reactive({
active: props.modelValue,
data: props.data,
});
watch(() => state.active, (newValue) => {
emit('update:modelValue', newValue);
emit('change', newValue, state.data);
}, { immediate: true })
watch(() => props.data, (newValue) => {
state.data = newValue ?? [];
console.log(state.data);
}, { immediate: true })
</script>
<template>
<el-tabs v-model="state.active" class="ml5 mt5" v-bind="$attrs">
<el-tab-pane v-for="(item, index) in state.data" :key="index" :label="item.label" :name="item.value" :disabled="item.disabled" v-show="item.visible === undefined || item.visible">
<template #label v-if="(props.hideZero && item.count || !props.hideZero)">
<el-badge :value="item.count ?? 0" :max="props.max" :offset="props.offset">{{ item.label }}</el-badge>
</template>
</el-tab-pane>
<slot></slot>
</el-tabs>
</template>
<style scoped lang="scss">
</style>

View File

@ -10,18 +10,18 @@
<el-form :model="data" label-width="auto">
<el-tabs v-model="state.selectedTabName">
<el-tab-pane label="请求信息">
<el-form-item label="客户端">
{{ data.httpClientName }}
</el-form-item>
<el-form-item label="接口描述">
{{ data.httpApiDesc }}
</el-form-item>
<el-form-item label="请求地址">
{{ data.requestUrl?.indexOf('?') == -1 ? data.requestUrl : data.requestUrl?.substring(0, data.requestUrl.indexOf('?')) }}
</el-form-item>
<el-form-item label="Query参数" v-if="data.requestUrl?.indexOf('?') != -1">
<el-row v-for="(value, key, index) in queryObject">
<el-col :span="4">
<el-input :value="key" readonly>
</el-input>
</el-col>
<el-col :span="20">
<vue-json-pretty :data="tryJsonParse(value)" showLength showIcon showSelectController />
</el-col>
<span class="query-key">{{ key }}</span> = <span class="query-value">{{ value }}</span>&
</el-row>
</el-form-item>
<el-form-item label="请求头">
@ -83,4 +83,12 @@ const tryJsonParse = (str: string) => {
defineExpose({
openDialog,
});
</script>
</script>
<style lang="less" scoped>
.query-value {
color: #13ce66;
}
.query-key {
color: #d55fde;
}
</style>

View File

@ -6,6 +6,16 @@
<el-card shadow="hover" :body-style="{ padding: '5px', display: 'flex', width: '100%', height: '100%', alignItems: 'start' }">
<el-form :model="state.queryParams" ref="queryForm" :show-message="false" :inlineMessage="true" label-width="auto" style="flex: 1 1 0%">
<el-row :gutter="10">
<el-col class="mb5" :sm="12" :md="8" :lg="6" :xl="6">
<el-form-item label="客户端" prop="httpClientName">
<el-input v-model="state.queryParams.httpClientName" placeholder="请输入客户端" clearable @keyup.enter.native="handleQuery(true)" />
</el-form-item>
</el-col>
<el-col class="mb5" :sm="12" :md="8" :lg="6" :xl="6">
<el-form-item label="接口描述" prop="httpApiDesc">
<el-input v-model="state.queryParams.httpApiDesc" placeholder="请输入接口描述" clearable @keyup.enter.native="handleQuery(true)" />
</el-form-item>
</el-col>
<el-col class="mb5" :sm="12" :md="8" :lg="6" :xl="6">
<el-form-item label="请求方式" prop="httpMethod">
<el-input v-model="state.queryParams.httpMethod" placeholder="请输入请求方式" clearable @keyup.enter.native="handleQuery(true)" />
@ -144,8 +154,10 @@ const options = useVxeTable<PageLogHttpOutput>(
// { type: 'checkbox', width: 40, fixed: 'left' },
{ field: 'seq', type: 'seq', title: '序号', width: 60, fixed: 'left' },
{ field: 'createTime', title: '创建时间', minWidth: 150, showOverflow: 'tooltip' },
{ field: 'httpClientName', title: '客户端', minWidth: 110, showOverflow: 'tooltip' },
{ field: 'httpMethod', title: '请求方式', minWidth: 60, showOverflow: 'tooltip' },
{ field: 'isSuccessStatusCode', title: '是否成功', minWidth: 60, showOverflow: 'tooltip', slots: { default: 'row_isSuccessStatusCode' } },
{ field: 'httpApiDesc', title: '接口描述', minWidth: 150, showOverflow: 'tooltip' },
{ field: 'requestUrl', title: '请求地址', minWidth: 150, align: 'left', showOverflow: 'tooltip', slots: { default: 'row_requestUrl' } },
{ field: 'requestHeaders', title: '请求头', minWidth: 150, showOverflow: 'tooltip' },
{ field: 'requestBody', title: '请求体', minWidth: 150, showOverflow: 'tooltip', slots: { default: 'row_requestBody' } },