diff --git a/.gitignore b/.gitignore
index 9818dfc3f..c58f02a99 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@ obj
bin
Logs
appsettings.Production.json
+appsettings.secrets.json
tempkey.jwk
.vs
Publish
diff --git a/aspnet-core/LINGYUN.MicroService.All.sln b/aspnet-core/LINGYUN.MicroService.All.sln
index 568fb4098..fc125f4eb 100644
--- a/aspnet-core/LINGYUN.MicroService.All.sln
+++ b/aspnet-core/LINGYUN.MicroService.All.sln
@@ -649,6 +649,26 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.OssManagement.F
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.OssManagement.FileSystem.Imaging.ImageSharp", "modules\oss-management\LINGYUN.Abp.OssManagement.FileSystem.Imaging.ImageSharp\LINGYUN.Abp.OssManagement.FileSystem.Imaging.ImageSharp.csproj", "{5177C729-7666-4A6C-9D54-D7E5DEF0E857}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.WeChat.Work", "modules\wechat\LINGYUN.Abp.WeChat.Work\LINGYUN.Abp.WeChat.Work.csproj", "{E4CEED06-B8E9-41FA-82BF-5401AE101C4B}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.WeChat.Work.Tests", "tests\LINGYUN.Abp.WeChat.Work.Tests\LINGYUN.Abp.WeChat.Work.Tests.csproj", "{9BDE2F3E-6A95-47AC-9BBC-EC8570F2925A}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.WeChat.Work.AspNetCore", "modules\wechat\LINGYUN.Abp.WeChat.Work.AspNetCore\LINGYUN.Abp.WeChat.Work.AspNetCore.csproj", "{0C7B2C1B-CB57-4A89-B73F-ECB579FFBC81}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.WeChat.Work.Application.Contracts", "modules\wechat\LINGYUN.Abp.WeChat.Work.Application.Contracts\LINGYUN.Abp.WeChat.Work.Application.Contracts.csproj", "{B4C8056F-7325-4DB1-9F09-A6F37B052192}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.WeChat.Work.Application", "modules\wechat\LINGYUN.Abp.WeChat.Work.Application\LINGYUN.Abp.WeChat.Work.Application.csproj", "{D5AEBB8E-713C-4DD2-BA18-7B0B48489901}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.WeChat.Work.HttpApi", "modules\wechat\LINGYUN.Abp.WeChat.Work.HttpApi\LINGYUN.Abp.WeChat.Work.HttpApi.csproj", "{CC07A6C2-CD79-4A1E-BE65-C6444AC89C2C}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.IdentityServer.WeChat.Work", "modules\identityServer\LINGYUN.Abp.IdentityServer.WeChat.Work\LINGYUN.Abp.IdentityServer.WeChat.Work.csproj", "{DEDB69A9-657F-4B8B-81A7-4ADB19664F35}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.OpenIddict.WeChat.Work", "modules\openIddict\LINGYUN.Abp.OpenIddict.WeChat.Work\LINGYUN.Abp.OpenIddict.WeChat.Work.csproj", "{2C86306D-D626-41F8-BA3C-5C9B4123CE7D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.Notifications.WeChat.Work", "modules\wechat\LINGYUN.Abp.Notifications.WeChat.Work\LINGYUN.Abp.Notifications.WeChat.Work.csproj", "{2DC43D15-F20F-44EC-B3A3-47BD8BBB50CA}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.Identity.WeChat.Work", "modules\wechat\LINGYUN.Abp.Identity.WeChat.Work\LINGYUN.Abp.Identity.WeChat.Work.csproj", "{3E32DBDA-1C63-42B4-85D1-E84BBD072D89}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1663,6 +1683,46 @@ Global
{5177C729-7666-4A6C-9D54-D7E5DEF0E857}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5177C729-7666-4A6C-9D54-D7E5DEF0E857}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5177C729-7666-4A6C-9D54-D7E5DEF0E857}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E4CEED06-B8E9-41FA-82BF-5401AE101C4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E4CEED06-B8E9-41FA-82BF-5401AE101C4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E4CEED06-B8E9-41FA-82BF-5401AE101C4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E4CEED06-B8E9-41FA-82BF-5401AE101C4B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9BDE2F3E-6A95-47AC-9BBC-EC8570F2925A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9BDE2F3E-6A95-47AC-9BBC-EC8570F2925A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9BDE2F3E-6A95-47AC-9BBC-EC8570F2925A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9BDE2F3E-6A95-47AC-9BBC-EC8570F2925A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0C7B2C1B-CB57-4A89-B73F-ECB579FFBC81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0C7B2C1B-CB57-4A89-B73F-ECB579FFBC81}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0C7B2C1B-CB57-4A89-B73F-ECB579FFBC81}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0C7B2C1B-CB57-4A89-B73F-ECB579FFBC81}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B4C8056F-7325-4DB1-9F09-A6F37B052192}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B4C8056F-7325-4DB1-9F09-A6F37B052192}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B4C8056F-7325-4DB1-9F09-A6F37B052192}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B4C8056F-7325-4DB1-9F09-A6F37B052192}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D5AEBB8E-713C-4DD2-BA18-7B0B48489901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D5AEBB8E-713C-4DD2-BA18-7B0B48489901}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D5AEBB8E-713C-4DD2-BA18-7B0B48489901}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D5AEBB8E-713C-4DD2-BA18-7B0B48489901}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CC07A6C2-CD79-4A1E-BE65-C6444AC89C2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CC07A6C2-CD79-4A1E-BE65-C6444AC89C2C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CC07A6C2-CD79-4A1E-BE65-C6444AC89C2C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CC07A6C2-CD79-4A1E-BE65-C6444AC89C2C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DEDB69A9-657F-4B8B-81A7-4ADB19664F35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DEDB69A9-657F-4B8B-81A7-4ADB19664F35}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DEDB69A9-657F-4B8B-81A7-4ADB19664F35}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DEDB69A9-657F-4B8B-81A7-4ADB19664F35}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2C86306D-D626-41F8-BA3C-5C9B4123CE7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2C86306D-D626-41F8-BA3C-5C9B4123CE7D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2C86306D-D626-41F8-BA3C-5C9B4123CE7D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2C86306D-D626-41F8-BA3C-5C9B4123CE7D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2DC43D15-F20F-44EC-B3A3-47BD8BBB50CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2DC43D15-F20F-44EC-B3A3-47BD8BBB50CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2DC43D15-F20F-44EC-B3A3-47BD8BBB50CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2DC43D15-F20F-44EC-B3A3-47BD8BBB50CA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3E32DBDA-1C63-42B4-85D1-E84BBD072D89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3E32DBDA-1C63-42B4-85D1-E84BBD072D89}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3E32DBDA-1C63-42B4-85D1-E84BBD072D89}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3E32DBDA-1C63-42B4-85D1-E84BBD072D89}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1977,6 +2037,16 @@ Global
{2F49E870-DAE2-4D89-98CA-46BBD91C68E2} = {59627844-A66A-46AC-B882-E8F302D0EC24}
{6C8489F4-68B5-4CBC-8463-010C71C23245} = {B05CB08F-C088-4D6D-97EE-A94A5D1AE4A6}
{5177C729-7666-4A6C-9D54-D7E5DEF0E857} = {B05CB08F-C088-4D6D-97EE-A94A5D1AE4A6}
+ {E4CEED06-B8E9-41FA-82BF-5401AE101C4B} = {DD9BE9E7-F6BF-4869-BCD2-82F5072BDA21}
+ {9BDE2F3E-6A95-47AC-9BBC-EC8570F2925A} = {370D7CD5-1E17-4F3D-BBFA-03429F6D4F2F}
+ {0C7B2C1B-CB57-4A89-B73F-ECB579FFBC81} = {DD9BE9E7-F6BF-4869-BCD2-82F5072BDA21}
+ {B4C8056F-7325-4DB1-9F09-A6F37B052192} = {DD9BE9E7-F6BF-4869-BCD2-82F5072BDA21}
+ {D5AEBB8E-713C-4DD2-BA18-7B0B48489901} = {DD9BE9E7-F6BF-4869-BCD2-82F5072BDA21}
+ {CC07A6C2-CD79-4A1E-BE65-C6444AC89C2C} = {DD9BE9E7-F6BF-4869-BCD2-82F5072BDA21}
+ {DEDB69A9-657F-4B8B-81A7-4ADB19664F35} = {0439B173-F41E-4CE0-A44A-CCB70328F272}
+ {2C86306D-D626-41F8-BA3C-5C9B4123CE7D} = {83E698F6-F8CD-4604-AB80-01A203389501}
+ {2DC43D15-F20F-44EC-B3A3-47BD8BBB50CA} = {DD9BE9E7-F6BF-4869-BCD2-82F5072BDA21}
+ {3E32DBDA-1C63-42B4-85D1-E84BBD072D89} = {DD9BE9E7-F6BF-4869-BCD2-82F5072BDA21}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C95FDF91-16F2-4A8B-A4BE-0E62D1B66718}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Notifications.Core/LINGYUN/Abp/Notifications/NotificationProviderNames.cs b/aspnet-core/modules/common/LINGYUN.Abp.Notifications.Core/LINGYUN/Abp/Notifications/NotificationProviderNames.cs
index 20c19ab30..b69382d57 100644
--- a/aspnet-core/modules/common/LINGYUN.Abp.Notifications.Core/LINGYUN/Abp/Notifications/NotificationProviderNames.cs
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Notifications.Core/LINGYUN/Abp/Notifications/NotificationProviderNames.cs
@@ -21,4 +21,8 @@ public static class NotificationProviderNames
/// 微信小程序模板通知
///
public const string WechatMiniProgram = "WeChat.MiniProgram";
+ ///
+ /// 企业微信应用消息
+ ///
+ public const string WechatWork = "WeChat.Work";
}
diff --git a/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.WeChat.Work/FodyWeavers.xml b/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.WeChat.Work/FodyWeavers.xml
new file mode 100644
index 000000000..1715698cc
--- /dev/null
+++ b/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.WeChat.Work/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.WeChat.Work/FodyWeavers.xsd b/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.WeChat.Work/FodyWeavers.xsd
new file mode 100644
index 000000000..3f3946e28
--- /dev/null
+++ b/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.WeChat.Work/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.WeChat.Work/LINGYUN.Abp.IdentityServer.WeChat.Work.csproj b/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.WeChat.Work/LINGYUN.Abp.IdentityServer.WeChat.Work.csproj
new file mode 100644
index 000000000..8dad38e96
--- /dev/null
+++ b/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.WeChat.Work/LINGYUN.Abp.IdentityServer.WeChat.Work.csproj
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ net7.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.WeChat.Work/LINGYUN/Abp/IdentityServer/WeChat/Work/AbpIdentityServerWeChatWorkModule.cs b/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.WeChat.Work/LINGYUN/Abp/IdentityServer/WeChat/Work/AbpIdentityServerWeChatWorkModule.cs
new file mode 100644
index 000000000..0f702528b
--- /dev/null
+++ b/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.WeChat.Work/LINGYUN/Abp/IdentityServer/WeChat/Work/AbpIdentityServerWeChatWorkModule.cs
@@ -0,0 +1,40 @@
+using LINGYUN.Abp.WeChat.Work;
+using LINGYUN.Abp.WeChat.Work.Localization;
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.Identity.Localization;
+using Volo.Abp.IdentityServer;
+using Volo.Abp.Localization;
+using Volo.Abp.Modularity;
+using Volo.Abp.VirtualFileSystem;
+
+namespace LINGYUN.Abp.IdentityServer.WeChat.Work;
+
+[DependsOn(
+ typeof(AbpIdentityServerDomainModule),
+ typeof(AbpWeChatWorkModule))]
+public class AbpIdentityServerWeChatWorkModule : AbpModule
+{
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ PreConfigure(builder =>
+ {
+ builder.AddExtensionGrantValidator();
+ });
+ }
+
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ Configure(options =>
+ {
+ options.FileSets.AddEmbedded();
+ });
+
+ Configure(options =>
+ {
+ options.Resources
+ .Get()
+ .AddBaseTypes(typeof(IdentityResource))
+ .AddVirtualJson("/LINGYUN/Abp/IdentityServer/WeChat/Work/Localization/Resources");
+ });
+ }
+}
diff --git a/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.WeChat.Work/LINGYUN/Abp/IdentityServer/WeChat/Work/Localization/Resources/en.json b/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.WeChat.Work/LINGYUN/Abp/IdentityServer/WeChat/Work/Localization/Resources/en.json
new file mode 100644
index 000000000..d78678160
--- /dev/null
+++ b/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.WeChat.Work/LINGYUN/Abp/IdentityServer/WeChat/Work/Localization/Resources/en.json
@@ -0,0 +1,8 @@
+{
+ "culture": "en",
+ "texts": {
+ "InvalidGrant:GrantTypeInvalid": "The authorization type that is not allowed!",
+ "InvalidGrant:AgentIdOrCodeNotFound": "Enterprise identity(AgentId) not found or user cancelled login!",
+ "InvalidGrant:UserIdNotRegister": "Enterprise users are not registered!"
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.WeChat.Work/LINGYUN/Abp/IdentityServer/WeChat/Work/Localization/Resources/zh-Hans.json b/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.WeChat.Work/LINGYUN/Abp/IdentityServer/WeChat/Work/Localization/Resources/zh-Hans.json
new file mode 100644
index 000000000..63e03849c
--- /dev/null
+++ b/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.WeChat.Work/LINGYUN/Abp/IdentityServer/WeChat/Work/Localization/Resources/zh-Hans.json
@@ -0,0 +1,8 @@
+{
+ "culture": "zh-Hans",
+ "texts": {
+ "InvalidGrant:GrantTypeInvalid": "不被允许的授权类型!",
+ "InvalidGrant:AgentIdOrCodeNotFound": "企业标识未找到或用户取消登录!",
+ "InvalidGrant:UserIdNotRegister": "企业用户未注册!"
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.WeChat.Work/LINGYUN/Abp/IdentityServer/WeChat/Work/WeChatWorkGrantValidator.cs b/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.WeChat.Work/LINGYUN/Abp/IdentityServer/WeChat/Work/WeChatWorkGrantValidator.cs
new file mode 100644
index 000000000..0e79edea5
--- /dev/null
+++ b/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.WeChat.Work/LINGYUN/Abp/IdentityServer/WeChat/Work/WeChatWorkGrantValidator.cs
@@ -0,0 +1,201 @@
+using IdentityModel;
+using IdentityServer4.Events;
+using IdentityServer4.Models;
+using IdentityServer4.Services;
+using IdentityServer4.Validation;
+using LINGYUN.Abp.WeChat.Work;
+using LINGYUN.Abp.WeChat.Work.Authorize;
+using LINGYUN.Abp.WeChat.Work.Localization;
+using LINGYUN.Abp.WeChat.Work.Settings;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.Extensions.Localization;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Volo.Abp.Guids;
+using Volo.Abp.Identity;
+using Volo.Abp.IdentityServer;
+using Volo.Abp.MultiTenancy;
+using Volo.Abp.Security.Claims;
+using Volo.Abp.Settings;
+using Volo.Abp.Uow;
+using IdentityUser = Volo.Abp.Identity.IdentityUser;
+
+namespace LINGYUN.Abp.IdentityServer.WeChat.Work;
+public class WeChatWorkGrantValidator : IExtensionGrantValidator
+{
+ public string GrantType => AbpWeChatWorkGlobalConsts.GrantType;
+
+ protected ILogger Logger { get; }
+ protected IEventService EventService { get; }
+ protected UserManager UserManager { get; }
+ protected IdentitySecurityLogManager IdentitySecurityLogManager { get; }
+ protected ICurrentTenant CurrentTenant { get; }
+ protected ISettingProvider SettingProvider { get; }
+ protected IGuidGenerator GuidGenerator { get; }
+ protected IStringLocalizer WeChatWorkLocalizer { get; }
+ protected IWeChatWorkUserFinder WeChatWorkUserFinder { get; }
+
+ public WeChatWorkGrantValidator(
+ IEventService eventService,
+ ICurrentTenant currentTenant,
+ IGuidGenerator guidGenerator,
+ ISettingProvider settingProvider,
+ UserManager userManager,
+ IdentitySecurityLogManager identitySecurityLogManager,
+ IStringLocalizer weChatWorkLocalizer,
+ IWeChatWorkUserFinder weChatWorkUserFinder,
+ ILogger logger)
+ {
+ Logger = logger;
+ EventService = eventService;
+ CurrentTenant = currentTenant;
+ GuidGenerator = guidGenerator;
+ SettingProvider = settingProvider;
+ UserManager = userManager;
+ IdentitySecurityLogManager = identitySecurityLogManager;
+ WeChatWorkLocalizer = weChatWorkLocalizer;
+ WeChatWorkUserFinder = weChatWorkUserFinder;
+ }
+
+ [UnitOfWork]
+ public async virtual Task ValidateAsync(ExtensionGrantValidationContext context)
+ {
+ var raw = context.Request.Raw;
+ var credential = raw.Get(OidcConstants.TokenRequest.GrantType);
+ if (credential == null || !credential.Equals(GrantType))
+ {
+ Logger.LogInformation("Invalid grant type: not allowed");
+ context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, WeChatWorkLocalizer["InvalidGrant:GrantTypeInvalid"]);
+ return;
+ }
+
+ var agentId = raw.Get(AbpWeChatWorkGlobalConsts.AgentId);
+ var code = raw.Get(AbpWeChatWorkGlobalConsts.Code);
+ if (agentId.IsNullOrWhiteSpace() || code.IsNullOrWhiteSpace())
+ {
+ Logger.LogInformation("Invalid grant type: agentId or code not found");
+ context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, WeChatWorkLocalizer["InvalidGrant:AgentIdOrCodeNotFound"]);
+ return;
+ }
+
+ try
+ {
+ var userInfo = await WeChatWorkUserFinder.GetUserInfoAsync(agentId, code);
+ var currentUser = await UserManager.FindByLoginAsync(AbpWeChatWorkGlobalConsts.ProviderName, userInfo.UserId);
+
+ if (currentUser == null)
+ {
+ // TODO 检查启用用户注册是否有必要引用账户模块
+ if (!await SettingProvider.IsTrueAsync("Abp.Account.IsSelfRegistrationEnabled") ||
+ !await SettingProvider.IsTrueAsync(WeChatWorkSettingNames.EnabledQuickLogin))
+ {
+ Logger.LogWarning("Invalid grant type: wechat work user not register", userInfo.UserId);
+ context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, WeChatWorkLocalizer["InvalidGrant:UserIdNotRegister"]);
+
+ return;
+ }
+ var userName = "wxid-work" + userInfo.UserId.ToMd5().ToLower();
+ var userEmail = $"{userName}@{CurrentTenant.Name ?? "default"}.io";
+
+ currentUser = new IdentityUser(GuidGenerator.Create(), userName, userEmail, CurrentTenant.Id);
+
+ (await UserManager.CreateAsync(currentUser)).CheckErrors();
+ (await UserManager.AddLoginAsync(
+ currentUser,
+ new UserLoginInfo(
+ AbpWeChatWorkGlobalConsts.ProviderName,
+ userInfo.UserId,
+ AbpWeChatWorkGlobalConsts.DisplayName))).CheckErrors();
+ }
+
+ if (await UserManager.IsLockedOutAsync(currentUser))
+ {
+ Logger.LogInformation("Authentication failed for username: {username}, reason: locked out", currentUser.UserName);
+ context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, WeChatWorkLocalizer["Volo.Abp.Identity:UserLockedOut"]);
+
+ await SaveSecurityLogAsync(context, currentUser, IdentityServerSecurityLogActionConsts.LoginLockedout);
+
+ return;
+ }
+
+ await EventService.RaiseAsync(new UserLoginSuccessEvent(AbpWeChatWorkGlobalConsts.ProviderName, userInfo.UserId, null));
+
+ // 登录之后需要更新安全令牌
+ (await UserManager.UpdateSecurityStampAsync(currentUser)).CheckErrors();
+
+ await SetSuccessResultAsync(context, currentUser);
+ }
+ catch (AbpWeChatWorkException wwe)
+ {
+ Logger.LogInformation("Invalid get user info: {message}", wwe.Message);
+ var error = WeChatWorkLocalizer[wwe.Code];
+ context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, error.ResourceNotFound ? wwe.Code : error.Value);
+ return;
+ }
+ }
+
+ protected async virtual Task SetSuccessResultAsync(ExtensionGrantValidationContext context, IdentityUser user)
+ {
+ var sub = await UserManager.GetUserIdAsync(user);
+
+ Logger.LogInformation("Credentials validated for username: {username}", user.UserName);
+
+ var additionalClaims = new List();
+
+ await AddCustomClaimsAsync(additionalClaims, user, context);
+
+ context.Result = new GrantValidationResult(
+ sub,
+ AbpWeChatWorkGlobalConsts.AuthenticationMethod,
+ additionalClaims.ToArray()
+ );
+
+ await SaveSecurityLogAsync(
+ context,
+ user,
+ IdentityServerSecurityLogActionConsts.LoginSucceeded);
+ }
+
+ protected async virtual Task SaveSecurityLogAsync(
+ ExtensionGrantValidationContext context,
+ IdentityUser user,
+ string action)
+ {
+ var logContext = new IdentitySecurityLogContext
+ {
+ Identity = IdentityServerSecurityLogIdentityConsts.IdentityServer,
+ Action = action,
+ UserName = user.UserName,
+ ClientId = await FindClientIdAsync(context)
+ };
+ logContext.WithProperty("GrantType", GrantType);
+
+ await IdentitySecurityLogManager.SaveAsync(logContext);
+ }
+
+ protected virtual Task FindClientIdAsync(ExtensionGrantValidationContext context)
+ {
+ return Task.FromResult(context.Request?.Client?.ClientId);
+ }
+
+ protected virtual Task AddCustomClaimsAsync(
+ List customClaims,
+ IdentityUser user,
+ ExtensionGrantValidationContext context)
+ {
+ if (user.TenantId.HasValue)
+ {
+ customClaims.Add(
+ new Claim(
+ AbpClaimTypes.TenantId,
+ user.TenantId?.ToString()
+ )
+ );
+ }
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.WeChat.Work/README.md b/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.WeChat.Work/README.md
new file mode 100644
index 000000000..67be60641
--- /dev/null
+++ b/aspnet-core/modules/identityServer/LINGYUN.Abp.IdentityServer.WeChat.Work/README.md
@@ -0,0 +1,25 @@
+# LINGYUN.Abp.IdentityServer.WeChat.Work
+
+企业微信扩展登录集成
+
+
+## 配置使用
+
+```csharp
+[DependsOn(typeof(AbpIdentityServerWeChatWorkModule))]
+public class YouProjectModule : AbpModule
+{
+ // other
+}
+```
+
+```shell
+
+curl -X POST "http://127.0.0.1:44385/connect/token" \
+--header 'Content-Type: application/x-www-form-urlencoded' \
+--data-urlencode 'grant_type=wx-work' \
+--data-urlencode 'client_id=你的客户端标识' \
+--data-urlencode 'client_secret=你的客户端密钥' \
+--data-urlencode 'agent_id=你的企业微信应用标识' \
+--data-urlencode 'code=用户扫描登录二维码后重定向页面携带的code标识, 换取用户信息的关键' \
+```
diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.WeChat.Work/FodyWeavers.xml b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.WeChat.Work/FodyWeavers.xml
new file mode 100644
index 000000000..1715698cc
--- /dev/null
+++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.WeChat.Work/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.WeChat.Work/FodyWeavers.xsd b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.WeChat.Work/FodyWeavers.xsd
new file mode 100644
index 000000000..3f3946e28
--- /dev/null
+++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.WeChat.Work/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.WeChat.Work/LINGYUN.Abp.OpenIddict.WeChat.Work.csproj b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.WeChat.Work/LINGYUN.Abp.OpenIddict.WeChat.Work.csproj
new file mode 100644
index 000000000..df6ed9755
--- /dev/null
+++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.WeChat.Work/LINGYUN.Abp.OpenIddict.WeChat.Work.csproj
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ net7.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.WeChat.Work/LINGYUN/Abp/OpenIddict/WeChat/Work/AbpOpenIddictWeChatWorkModule.cs b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.WeChat.Work/LINGYUN/Abp/OpenIddict/WeChat/Work/AbpOpenIddictWeChatWorkModule.cs
new file mode 100644
index 000000000..2447af85c
--- /dev/null
+++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.WeChat.Work/LINGYUN/Abp/OpenIddict/WeChat/Work/AbpOpenIddictWeChatWorkModule.cs
@@ -0,0 +1,49 @@
+using LINGYUN.Abp.WeChat.Work;
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.Localization;
+using Volo.Abp.Modularity;
+using Volo.Abp.OpenIddict;
+using Volo.Abp.OpenIddict.ExtensionGrantTypes;
+using Volo.Abp.OpenIddict.Localization;
+using Volo.Abp.VirtualFileSystem;
+
+namespace LINGYUN.Abp.OpenIddict.WeChat.Work;
+
+[DependsOn(
+ typeof(AbpWeChatWorkModule),
+ typeof(AbpOpenIddictAspNetCoreModule))]
+public class AbpOpenIddictWeChatWorkModule : AbpModule
+{
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ PreConfigure(builder =>
+ {
+ builder
+ .AllowWeChatWorkFlow()
+ .RegisterWeChatWorkScopes()
+ .RegisterWeChatWorkClaims();
+ });
+ }
+
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ Configure(options =>
+ {
+ options.Grants.TryAdd(
+ AbpWeChatWorkGlobalConsts.GrantType,
+ new WeChatWorkTokenExtensionGrant());
+ });
+
+ Configure(options =>
+ {
+ options.FileSets.AddEmbedded();
+ });
+
+ Configure(options =>
+ {
+ options.Resources
+ .Get()
+ .AddVirtualJson("/LINGYUN/Abp/OpenIddict/WeChat/Work/Localization/Resources");
+ });
+ }
+}
diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.WeChat.Work/LINGYUN/Abp/OpenIddict/WeChat/Work/Localization/Resources/en.json b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.WeChat.Work/LINGYUN/Abp/OpenIddict/WeChat/Work/Localization/Resources/en.json
new file mode 100644
index 000000000..2f8fdd3b4
--- /dev/null
+++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.WeChat.Work/LINGYUN/Abp/OpenIddict/WeChat/Work/Localization/Resources/en.json
@@ -0,0 +1,12 @@
+{
+ "culture": "en",
+ "texts": {
+ "MiniProgramAuthorizationDisabledMessage": "Applet authorization is not enabled for the application",
+ "OfficialAuthorizationDisabledMessage": "Official authorization is not enabled for the application",
+ "SelfRegistrationDisabledMessage": "Self-registration is disabled for this application. Please contact the application administrator to register a new user.",
+ "InvalidGrant:GrantTypeInvalid": "The type of authorization that is not allowed!",
+ "InvalidGrant:WeChatTokenInvalid": "WeChat authentication failed!",
+ "InvalidGrant:WeChatCodeNotFound": "The code obtained when WeChat is logged in is empty or does not exist!",
+ "InvalidGrant:WeChatNotRegister": "User WeChat account not registed!"
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.WeChat.Work/LINGYUN/Abp/OpenIddict/WeChat/Work/Localization/Resources/zh-Hans.json b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.WeChat.Work/LINGYUN/Abp/OpenIddict/WeChat/Work/Localization/Resources/zh-Hans.json
new file mode 100644
index 000000000..b07fdf6f7
--- /dev/null
+++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.WeChat.Work/LINGYUN/Abp/OpenIddict/WeChat/Work/Localization/Resources/zh-Hans.json
@@ -0,0 +1,12 @@
+{
+ "culture": "zh-Hans",
+ "texts": {
+ "MiniProgramAuthorizationDisabledMessage": "应用程序未开放小程序授权",
+ "OfficialAuthorizationDisabledMessage": "应用程序未开放公众平台授权",
+ "SelfRegistrationDisabledMessage": "应用程序未开放注册,请联系管理员添加新用户.",
+ "InvalidGrant:GrantTypeInvalid": "不被允许的授权类型!",
+ "InvalidGrant:WeChatTokenInvalid": "微信认证失败!",
+ "InvalidGrant:WeChatCodeNotFound": "微信登录时获取的 code 为空或不存在!",
+ "InvalidGrant:WeChatNotRegister": "用户微信账号未绑定!"
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.WeChat.Work/LINGYUN/Abp/OpenIddict/WeChat/Work/WeChatWorkTokenExtensionGrant.cs b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.WeChat.Work/LINGYUN/Abp/OpenIddict/WeChat/Work/WeChatWorkTokenExtensionGrant.cs
new file mode 100644
index 000000000..d70f54e0e
--- /dev/null
+++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.WeChat.Work/LINGYUN/Abp/OpenIddict/WeChat/Work/WeChatWorkTokenExtensionGrant.cs
@@ -0,0 +1,233 @@
+using LINGYUN.Abp.WeChat.Work;
+using LINGYUN.Abp.WeChat.Work.Authorize;
+using LINGYUN.Abp.WeChat.Work.Security.Claims;
+using LINGYUN.Abp.WeChat.Work.Settings;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Localization;
+using Microsoft.Extensions.Logging;
+using OpenIddict.Abstractions;
+using OpenIddict.Server.AspNetCore;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Volo.Abp.Guids;
+using Volo.Abp.Identity;
+using Volo.Abp.Identity.Localization;
+using Volo.Abp.MultiTenancy;
+using Volo.Abp.OpenIddict;
+using Volo.Abp.OpenIddict.ExtensionGrantTypes;
+using Volo.Abp.OpenIddict.Localization;
+using Volo.Abp.Settings;
+using IdentityUser = Volo.Abp.Identity.IdentityUser;
+using SignInResult = Microsoft.AspNetCore.Mvc.SignInResult;
+
+namespace LINGYUN.Abp.OpenIddict.WeChat.Work;
+
+public class WeChatWorkTokenExtensionGrant : ITokenExtensionGrant
+{
+ public string Name => AbpWeChatWorkGlobalConsts.GrantType;
+
+ public async virtual Task HandleAsync(ExtensionGrantContext context)
+ {
+ await CheckFeatureAsync(context);
+
+ return await HandleWeChatAsync(context);
+ }
+
+ protected async virtual Task HandleWeChatAsync(ExtensionGrantContext context)
+ {
+ var logger = GetRequiredService>(context);
+ var localizer = GetRequiredService>(context);
+
+ var agentId = context.Request.GetParameter(AbpWeChatWorkGlobalConsts.AgentId)?.ToString();
+ var code = context.Request.GetParameter(AbpWeChatWorkGlobalConsts.Code)?.ToString();
+ if (agentId.IsNullOrWhiteSpace() || code.IsNullOrWhiteSpace())
+ {
+ logger.LogWarning("Invalid grant type: agentId or code not found");
+
+ var properties = new AuthenticationProperties(new Dictionary
+ {
+ [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant,
+ [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = localizer["InvalidGrant:AgentIdOrCodeNotFound"]
+ });
+
+ return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
+ }
+
+ var userFinder = GetRequiredService(context);
+
+ try
+ {
+ var userInfo = await userFinder.GetUserInfoAsync(agentId, code);
+
+ var userManager = GetRequiredService(context);
+ var currentUser = await userManager.FindByLoginAsync(AbpWeChatWorkGlobalConsts.ProviderName, userInfo.UserId);
+
+ if (currentUser == null)
+ {
+ var currentTenant = GetRequiredService(context);
+ var settingProvider = GetRequiredService(context);
+ var guidGenerator = GetRequiredService(context);
+ // TODO 检查启用用户注册是否有必要引用账户模块
+ if (!await settingProvider.IsTrueAsync("Abp.Account.IsSelfRegistrationEnabled") ||
+ !await settingProvider.IsTrueAsync(WeChatWorkSettingNames.EnabledQuickLogin))
+ {
+ logger.LogWarning("Invalid grant type: wechat work user not register", userInfo.UserId);
+
+ var properties = new AuthenticationProperties(new Dictionary
+ {
+ [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant,
+ [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = localizer["InvalidGrant:UserIdNotRegister"]
+ });
+
+ return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
+ }
+ var userName = "wxid-work" + userInfo.UserId.ToMd5().ToLower();
+ var userEmail = $"{userName}@{currentTenant.Name ?? "default"}.io";
+
+ currentUser = new IdentityUser(guidGenerator.Create(), userName, userEmail, currentTenant.Id);
+
+ (await userManager.CreateAsync(currentUser)).CheckErrors();
+ (await userManager.AddLoginAsync(
+ currentUser,
+ new UserLoginInfo(
+ AbpWeChatWorkGlobalConsts.ProviderName,
+ userInfo.UserId,
+ AbpWeChatWorkGlobalConsts.DisplayName))).CheckErrors();
+ }
+
+ // 检查是否已锁定
+ if (await userManager.IsLockedOutAsync(currentUser))
+ {
+ var identityLocalizer = GetRequiredService>(context);
+
+ logger.LogInformation("Authentication failed for username: {username}, reason: locked out", currentUser.UserName);
+
+ var properties = new AuthenticationProperties(new Dictionary
+ {
+ [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant,
+ [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = identityLocalizer["Volo.Abp.Identity:UserLockedOut"]
+ });
+
+ await SaveSecurityLogAsync(context, currentUser, OpenIddictSecurityLogActionConsts.LoginLockedout);
+
+ return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
+ }
+
+ // 登录之后需要更新安全令牌
+ (await userManager.UpdateSecurityStampAsync(currentUser)).CheckErrors();
+
+ return await SetSuccessResultAsync(context, currentUser, userInfo.UserId, logger);
+ }
+ catch (AbpWeChatWorkException wwe)
+ {
+ var properties = new AuthenticationProperties(new Dictionary
+ {
+ [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant,
+ [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = wwe.Code
+ });
+
+ return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
+ }
+ }
+
+ protected virtual Task CheckFeatureAsync(ExtensionGrantContext context)
+ {
+ return Task.CompletedTask;
+ }
+
+ protected virtual T GetRequiredService(ExtensionGrantContext context)
+ {
+ return context.HttpContext.RequestServices.GetRequiredService();
+ }
+
+ protected async virtual Task SetSuccessResultAsync(
+ ExtensionGrantContext context,
+ IdentityUser user,
+ string userId,
+ ILogger logger)
+ {
+ logger.LogInformation("Credentials validated for username: {username}", user.UserName);
+
+ var signInManager = GetRequiredService>(context);
+
+ var principal = await signInManager.CreateUserPrincipalAsync(user);
+
+ principal.SetScopes(context.Request.GetScopes());
+ principal.SetResources(await GetResourcesAsync(context));
+
+ principal.AddClaim(AbpWeChatWorkClaimTypes.UserId, userId);
+
+ await SetClaimsDestinationsAsync(context, principal);
+
+ await SaveSecurityLogAsync(
+ context,
+ user,
+ OpenIddictSecurityLogActionConsts.LoginSucceeded);
+
+ return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, principal);
+ }
+
+ protected async virtual Task SaveSecurityLogAsync(
+ ExtensionGrantContext context,
+ IdentityUser user,
+ string action)
+ {
+ var logContext = new IdentitySecurityLogContext
+ {
+ Identity = OpenIddictSecurityLogIdentityConsts.OpenIddict,
+ Action = action,
+ UserName = user.UserName,
+ ClientId = await FindClientIdAsync(context)
+ };
+ logContext.WithProperty("GrantType", Name);
+ logContext.WithProperty("Provider", AbpWeChatWorkGlobalConsts.ProviderName);
+ logContext.WithProperty("Method", AbpWeChatWorkGlobalConsts.AuthenticationMethod);
+
+ var identitySecurityLogManager = GetRequiredService(context);
+
+ await identitySecurityLogManager.SaveAsync(logContext);
+ }
+
+ protected virtual Task FindClientIdAsync(ExtensionGrantContext context)
+ {
+ return Task.FromResult(context.Request.ClientId);
+ }
+
+ protected async virtual Task SetClaimsDestinationsAsync(ExtensionGrantContext context, ClaimsPrincipal principal)
+ {
+ var openIddictClaimsPrincipalManager = GetRequiredService(context);
+
+ await openIddictClaimsPrincipalManager.HandleAsync(context.Request, principal);
+ }
+
+ protected async virtual Task> GetResourcesAsync(ExtensionGrantContext context)
+ {
+ var scopes = context.Request.GetScopes();
+ var resources = new List();
+ if (!scopes.Any())
+ {
+ return resources;
+ }
+
+ var scopeManager = GetRequiredService(context);
+
+ await foreach (var resource in scopeManager.ListResourcesAsync(scopes))
+ {
+ resources.Add(resource);
+ }
+ return resources;
+ }
+
+ public virtual ForbidResult Forbid(AuthenticationProperties properties, params string[] authenticationSchemes)
+ {
+ return new ForbidResult(
+ authenticationSchemes,
+ properties);
+ }
+}
diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.WeChat.Work/Microsoft/Extensions/DependencyInjection/WeChatWorkOpenIddictServerBuilderExtensions.cs b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.WeChat.Work/Microsoft/Extensions/DependencyInjection/WeChatWorkOpenIddictServerBuilderExtensions.cs
new file mode 100644
index 000000000..8b024a7b4
--- /dev/null
+++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.WeChat.Work/Microsoft/Extensions/DependencyInjection/WeChatWorkOpenIddictServerBuilderExtensions.cs
@@ -0,0 +1,29 @@
+using LINGYUN.Abp.WeChat.Work;
+using LINGYUN.Abp.WeChat.Work.Security.Claims;
+
+namespace Microsoft.Extensions.DependencyInjection;
+
+public static class WeChatWorkOpenIddictServerBuilderExtensions
+{
+ public static OpenIddictServerBuilder AllowWeChatWorkFlow(this OpenIddictServerBuilder builder)
+ {
+ return builder
+ .AllowCustomFlow(AbpWeChatWorkGlobalConsts.GrantType);
+ }
+
+ public static OpenIddictServerBuilder RegisterWeChatWorkScopes(this OpenIddictServerBuilder builder)
+ {
+ return builder.RegisterScopes(new[]
+ {
+ AbpWeChatWorkGlobalConsts.ProfileKey,
+ });
+ }
+
+ public static OpenIddictServerBuilder RegisterWeChatWorkClaims(this OpenIddictServerBuilder builder)
+ {
+ return builder.RegisterClaims(new[]
+ {
+ AbpWeChatWorkClaimTypes.UserId,
+ });
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.Identity.WeChat.Work/FodyWeavers.xml b/aspnet-core/modules/wechat/LINGYUN.Abp.Identity.WeChat.Work/FodyWeavers.xml
new file mode 100644
index 000000000..1715698cc
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.Identity.WeChat.Work/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.Identity.WeChat.Work/FodyWeavers.xsd b/aspnet-core/modules/wechat/LINGYUN.Abp.Identity.WeChat.Work/FodyWeavers.xsd
new file mode 100644
index 000000000..3f3946e28
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.Identity.WeChat.Work/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.Identity.WeChat.Work/LINGYUN.Abp.Identity.WeChat.Work.csproj b/aspnet-core/modules/wechat/LINGYUN.Abp.Identity.WeChat.Work/LINGYUN.Abp.Identity.WeChat.Work.csproj
new file mode 100644
index 000000000..8ec08c2e2
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.Identity.WeChat.Work/LINGYUN.Abp.Identity.WeChat.Work.csproj
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ netstandard2.1
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.Identity.WeChat.Work/LINGYUN/Abp/Identity/WeChat/Work/AbpIdentityWeChatWorkModule.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.Identity.WeChat.Work/LINGYUN/Abp/Identity/WeChat/Work/AbpIdentityWeChatWorkModule.cs
new file mode 100644
index 000000000..5344bf07f
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.Identity.WeChat.Work/LINGYUN/Abp/Identity/WeChat/Work/AbpIdentityWeChatWorkModule.cs
@@ -0,0 +1,13 @@
+using LINGYUN.Abp.WeChat.Work;
+using Volo.Abp.Identity;
+using Volo.Abp.Modularity;
+
+namespace LINGYUN.Abp.Identity.WeChat.Work;
+
+[DependsOn(
+ typeof(AbpWeChatWorkModule),
+ typeof(AbpIdentityDomainModule))]
+public class AbpIdentityWeChatWorkModule : AbpModule
+{
+
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.Identity.WeChat.Work/LINGYUN/Abp/Identity/WeChat/Work/WeChatWorkInternalUserFinder.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.Identity.WeChat.Work/LINGYUN/Abp/Identity/WeChat/Work/WeChatWorkInternalUserFinder.cs
new file mode 100644
index 000000000..691a7a86e
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.Identity.WeChat.Work/LINGYUN/Abp/Identity/WeChat/Work/WeChatWorkInternalUserFinder.cs
@@ -0,0 +1,59 @@
+using LINGYUN.Abp.WeChat.Work;
+using LINGYUN.Abp.WeChat.Work.Authorize;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Identity;
+
+namespace LINGYUN.Abp.Identity.WeChat.Work
+{
+ [Dependency(ServiceLifetime.Transient, ReplaceServices = true)]
+ [ExposeServices(typeof(IWeChatWorkInternalUserFinder))]
+ public class WeChatWorkInternalUserFinder : IWeChatWorkInternalUserFinder
+ {
+ protected IdentityUserManager UserManager { get; }
+
+ public WeChatWorkInternalUserFinder(
+ IdentityUserManager userManager)
+ {
+ UserManager = userManager;
+ }
+
+ protected string GetUserOpenIdOrNull(IdentityUser user, string provider)
+ {
+ // 微信扩展登录后openid存储在Login中
+ var userLogin = user?.Logins
+ .Where(login => login.LoginProvider == provider)
+ .FirstOrDefault();
+
+ return userLogin?.ProviderKey;
+ }
+
+ public async virtual Task FindUserIdentifierAsync(string agentId, Guid userId, CancellationToken cancellationToken = default)
+ {
+ var user = await UserManager.FindByIdAsync(userId.ToString());
+
+ return GetUserOpenIdOrNull(user, AbpWeChatWorkGlobalConsts.ProviderName);
+ }
+
+ public async virtual Task> FindUserIdentifierListAsync(string agentId, IEnumerable userIdList, CancellationToken cancellationToken = default)
+ {
+ var userIdentifiers = new List();
+ foreach (var userId in userIdList)
+ {
+ var user = await UserManager.FindByIdAsync(userId.ToString());
+ var weChatWorkUserId = GetUserOpenIdOrNull(user, AbpWeChatWorkGlobalConsts.ProviderName);
+ if (!weChatWorkUserId.IsNullOrWhiteSpace())
+ {
+ userIdentifiers.Add(weChatWorkUserId);
+ }
+ }
+
+ return userIdentifiers;
+ }
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.MiniProgram/LINGYUN.Abp.Notifications.WeChat.MiniProgram.csproj b/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.MiniProgram/LINGYUN.Abp.Notifications.WeChat.MiniProgram.csproj
index 96e0bf39b..06244fe00 100644
--- a/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.MiniProgram/LINGYUN.Abp.Notifications.WeChat.MiniProgram.csproj
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.MiniProgram/LINGYUN.Abp.Notifications.WeChat.MiniProgram.csproj
@@ -5,6 +5,7 @@
netstandard2.0
+
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.Work/FodyWeavers.xml b/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.Work/FodyWeavers.xml
new file mode 100644
index 000000000..1715698cc
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.Work/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.Work/FodyWeavers.xsd b/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.Work/FodyWeavers.xsd
new file mode 100644
index 000000000..3f3946e28
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.Work/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.Work/LINGYUN.Abp.Notifications.WeChat.Work.csproj b/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.Work/LINGYUN.Abp.Notifications.WeChat.Work.csproj
new file mode 100644
index 000000000..3ef768264
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.Work/LINGYUN.Abp.Notifications.WeChat.Work.csproj
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.Work/LINGYUN/Abp/Notifications/NotificationDataWeChatWorkExtensions.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.Work/LINGYUN/Abp/Notifications/NotificationDataWeChatWorkExtensions.cs
new file mode 100644
index 000000000..10be3480e
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.Work/LINGYUN/Abp/Notifications/NotificationDataWeChatWorkExtensions.cs
@@ -0,0 +1,83 @@
+namespace LINGYUN.Abp.Notifications;
+public static class NotificationDataWeChatWorkExtensions
+{
+ private const string Prefix = "wx-work:";
+ private const string AgentIdKey = Prefix + "agent_id";
+ private const string ToTagKey = Prefix + "to_tag";
+ private const string ToPartyKey = Prefix + "to_party";
+
+ ///
+ /// 设定发送到所有应用
+ ///
+ ///
+ ///
+ public static void WithAllAgent(
+ this NotificationData notificationData)
+ {
+ notificationData.SetAgentId("@all");
+ }
+
+ ///
+ /// 设定消息应用标识
+ ///
+ ///
+ ///
+ ///
+ public static void SetAgentId(
+ this NotificationData notificationData,
+ string agentId)
+ {
+ notificationData.TrySetData(AgentIdKey, agentId);
+ }
+ ///
+ /// 获取消息应用标识
+ ///
+ ///
+ public static string GetAgentIdOrNull(
+ this NotificationData notificationData)
+ {
+ return notificationData.TryGetData(AgentIdKey)?.ToString();
+ }
+ ///
+ /// 指定接收消息的标签,标签ID列表,多个接收者用‘|’分隔,最多支持100个。
+ ///
+ ///
+ ///
+ ///
+ public static void SetTag(
+ this NotificationData notificationData,
+ string tag)
+ {
+ notificationData.TrySetData(ToTagKey, tag);
+ }
+ ///
+ /// 获取接收消息的标签
+ ///
+ ///
+ public static string GetTagOrNull(
+ this NotificationData notificationData)
+ {
+ return notificationData.TryGetData(ToTagKey)?.ToString();
+ }
+ ///
+ /// 指定接收消息的部门,部门ID列表,多个接收者用‘|’分隔,最多支持100个。
+ ///
+ ///
+ ///
+ ///
+ public static void SetParty(
+ this NotificationData notificationData,
+ string party)
+ {
+ notificationData.TrySetData(ToPartyKey, party);
+ }
+ ///
+ /// 获取接收消息的部门
+ ///
+ ///
+ public static string GetPartyOrNull(
+ this NotificationData notificationData)
+ {
+ return notificationData.TryGetData(ToPartyKey)?.ToString();
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.Work/LINGYUN/Abp/Notifications/NotificationDefinitionWeChatWorkExtensions.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.Work/LINGYUN/Abp/Notifications/NotificationDefinitionWeChatWorkExtensions.cs
new file mode 100644
index 000000000..58d4b2223
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.Work/LINGYUN/Abp/Notifications/NotificationDefinitionWeChatWorkExtensions.cs
@@ -0,0 +1,103 @@
+namespace LINGYUN.Abp.Notifications;
+public static class NotificationDefinitionWeChatWorkExtensions
+{
+ private const string Prefix = "wx-work:";
+ private const string AgentIdKey = Prefix + "agent_id";
+ private const string ToTagKey = Prefix + "to_tag";
+ private const string ToPartyKey = Prefix + "to_party";
+
+ ///
+ /// 设定发送到所有应用
+ ///
+ ///
+ ///
+ public static NotificationDefinition WithAllAgent(
+ this NotificationDefinition notification)
+ {
+ return notification.WithAgentId("@all");
+ }
+
+ ///
+ /// 设定消息应用标识
+ ///
+ ///
+ ///
+ ///
+ public static NotificationDefinition WithAgentId(
+ this NotificationDefinition notification,
+ string agentId)
+ {
+ return notification.WithProperty(AgentIdKey, agentId);
+ }
+
+ ///
+ /// 获取消息应用标识
+ ///
+ ///
+ public static string GetAgentIdOrNull(
+ this NotificationDefinition notification)
+ {
+ if (notification.Properties.TryGetValue(AgentIdKey, out var agentIdDefine))
+ {
+ return agentIdDefine.ToString();
+ }
+
+ return null;
+ }
+
+ ///
+ /// 指定接收消息的标签,标签ID列表,多个接收者用‘|’分隔,最多支持100个。
+ ///
+ ///
+ ///
+ ///
+ public static NotificationDefinition WithTag(
+ this NotificationDefinition notification,
+ string tag)
+ {
+ return notification.WithProperty(ToTagKey, tag);
+ }
+
+ ///
+ /// 获取接收消息的标签
+ ///
+ ///
+ public static string GetTagOrNull(
+ this NotificationDefinition notification)
+ {
+ if (notification.Properties.TryGetValue(ToTagKey, out var tagDefine))
+ {
+ return tagDefine.ToString();
+ }
+
+ return null;
+ }
+
+ ///
+ /// 指定接收消息的部门,部门ID列表,多个接收者用‘|’分隔,最多支持100个。
+ ///
+ ///
+ ///
+ ///
+ public static NotificationDefinition WithParty(
+ this NotificationDefinition notification,
+ string party)
+ {
+ return notification.WithProperty(ToPartyKey, party);
+ }
+
+ ///
+ /// 获取接收消息的部门
+ ///
+ ///
+ public static string GetPartyOrNull(
+ this NotificationDefinition notification)
+ {
+ if (notification.Properties.TryGetValue(ToPartyKey, out var partyDefine))
+ {
+ return partyDefine.ToString();
+ }
+
+ return null;
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.Work/LINGYUN/Abp/Notifications/WeChat/Work/AbpNotificationsWeChatWorkModule.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.Work/LINGYUN/Abp/Notifications/WeChat/Work/AbpNotificationsWeChatWorkModule.cs
new file mode 100644
index 000000000..56f73d5ee
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.Work/LINGYUN/Abp/Notifications/WeChat/Work/AbpNotificationsWeChatWorkModule.cs
@@ -0,0 +1,18 @@
+using LINGYUN.Abp.WeChat.Work;
+using Volo.Abp.Modularity;
+
+namespace LINGYUN.Abp.Notifications.WeChat.Work;
+
+[DependsOn(
+ typeof(AbpWeChatWorkModule),
+ typeof(AbpNotificationsModule))]
+public class AbpNotificationsWeChatWorkModule : AbpModule
+{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ Configure(options =>
+ {
+ options.PublishProviders.Add();
+ });
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.Work/LINGYUN/Abp/Notifications/WeChat/Work/WeChatWorkNotificationPublishProvider.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.Work/LINGYUN/Abp/Notifications/WeChat/Work/WeChatWorkNotificationPublishProvider.cs
new file mode 100644
index 000000000..a323ab2e4
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.Work/LINGYUN/Abp/Notifications/WeChat/Work/WeChatWorkNotificationPublishProvider.cs
@@ -0,0 +1,159 @@
+using LINGYUN.Abp.RealTime.Localization;
+using LINGYUN.Abp.WeChat.Work;
+using LINGYUN.Abp.WeChat.Work.Authorize;
+using LINGYUN.Abp.WeChat.Work.Message;
+using LINGYUN.Abp.WeChat.Work.Message.Models;
+using Microsoft.Extensions.Localization;
+using Microsoft.Extensions.Options;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Volo.Abp.Features;
+
+namespace LINGYUN.Abp.Notifications.WeChat.Work;
+public class WeChatWorkNotificationPublishProvider : NotificationPublishProvider
+{
+
+ public const string ProviderName = NotificationProviderNames.WechatWork;
+ public override string Name => ProviderName;
+ protected IFeatureChecker FeatureChecker { get; }
+ protected IStringLocalizerFactory LocalizerFactory { get; }
+ protected IWeChatWorkMessageSender WeChatWorkMessageSender { get; }
+ protected IWeChatWorkInternalUserFinder WeChatWorkInternalUserFinder { get; }
+ protected INotificationDefinitionManager NotificationDefinitionManager { get; }
+ protected WeChatWorkOptions WeChatWorkOptions { get; }
+ public WeChatWorkNotificationPublishProvider(
+ IFeatureChecker featureChecker,
+ IStringLocalizerFactory localizerFactory,
+ IWeChatWorkMessageSender weChatWorkMessageSender,
+ IWeChatWorkInternalUserFinder weChatWorkInternalUserFinder,
+ INotificationDefinitionManager notificationDefinitionManager,
+ IOptionsMonitor weChatWorkOptions)
+ {
+ FeatureChecker = featureChecker;
+ LocalizerFactory = localizerFactory;
+ WeChatWorkMessageSender = weChatWorkMessageSender;
+ WeChatWorkInternalUserFinder = weChatWorkInternalUserFinder;
+ NotificationDefinitionManager = notificationDefinitionManager;
+ WeChatWorkOptions = weChatWorkOptions.CurrentValue;
+ }
+
+ protected async override Task PublishAsync(
+ NotificationInfo notification,
+ IEnumerable identifiers,
+ CancellationToken cancellationToken = default)
+ {
+ var sendToAgentIds = new List();
+ var notificationDefine = await NotificationDefinitionManager.GetOrNullAsync(notification.Name);
+ var agentId = notification.Data.GetAgentIdOrNull() ?? notificationDefine?.GetAgentIdOrNull();
+ if (agentId.IsNullOrWhiteSpace())
+ {
+ return;
+ }
+ // 发送到所有应用
+ if (agentId.Contains("@all"))
+ {
+ foreach (var application in WeChatWorkOptions.Applications)
+ {
+ sendToAgentIds.Add(application.Key);
+ }
+ }
+ else
+ {
+ sendToAgentIds.AddRange(agentId.Split(';'));
+ }
+
+ var title = "";
+ var message = "";
+ var description = "";
+ var toTag = notification.Data.GetTagOrNull() ?? notificationDefine?.GetTagOrNull();
+ var toParty = notification.Data.GetPartyOrNull() ?? notificationDefine?.GetPartyOrNull();
+
+ if (!notification.Data.NeedLocalizer())
+ {
+ title = notification.Data.TryGetData("title").ToString();
+ message = notification.Data.TryGetData("message").ToString();
+ description = notification.Data.TryGetData("description")?.ToString() ?? "";
+ }
+ else
+ {
+ var titleInfo = notification.Data.TryGetData("title").As();
+ var titleLocalizer = await LocalizerFactory.CreateByResourceNameAsync(titleInfo.ResourceName);
+ title = titleLocalizer[titleInfo.Name, titleInfo.Values].Value;
+
+ var messageInfo = notification.Data.TryGetData("message").As();
+ var messageLocalizer = await LocalizerFactory.CreateByResourceNameAsync(messageInfo.ResourceName);
+ message = messageLocalizer[messageInfo.Name, messageInfo.Values].Value;
+
+ var descriptionInfo = notification.Data.TryGetData("description")?.As();
+ if (descriptionInfo != null)
+ {
+ var descriptionLocalizer = await LocalizerFactory.CreateByResourceNameAsync(descriptionInfo.ResourceName);
+ description = descriptionLocalizer[descriptionInfo.Name, descriptionInfo.Values].Value;
+ }
+ }
+
+ foreach (var sendToAgentId in sendToAgentIds)
+ {
+ var findUserList = await WeChatWorkInternalUserFinder
+ .FindUserIdentifierListAsync(sendToAgentId, identifiers.Select(id => id.UserId));
+
+ if (!findUserList.Any())
+ {
+ continue;
+ }
+
+ await PublishToAgentAsync(
+ sendToAgentId,
+ notification,
+ findUserList.JoinAsString("|"),
+ title,
+ message,
+ description,
+ toParty,
+ toTag,
+ cancellationToken);
+ }
+ }
+
+ protected async virtual Task PublishToAgentAsync(
+ string agentId,
+ NotificationInfo notification,
+ string toUser,
+ string title,
+ string content,
+ string description = "",
+ string toParty = null,
+ string toTag = null,
+ CancellationToken cancellationToken = default)
+ {
+ WeChatWorkMessage message = null;
+
+ switch (notification.ContentType)
+ {
+ case NotificationContentType.Text:
+ message = new WeChatWorkTextMessage(agentId, new TextMessage(content));
+ break;
+ case NotificationContentType.Html:
+ message = new WeChatWorkTextCardMessage(agentId, new TextCardMessage(title, content, "javascript(0);"));
+ break;
+ case NotificationContentType.Markdown:
+ message = new WeChatWorkMarkdownMessage(agentId, new MarkdownMessage(content));
+ break;
+ default:
+ break;
+ }
+
+ if (message == null)
+ {
+ return;
+ }
+
+ message.ToTag = toTag;
+ message.ToParty = toParty;
+
+ await WeChatWorkMessageSender.SendAsync(message, cancellationToken);
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/FodyWeavers.xml b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/FodyWeavers.xml
new file mode 100644
index 000000000..1715698cc
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/FodyWeavers.xsd b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/FodyWeavers.xsd
new file mode 100644
index 000000000..3f3946e28
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN.Abp.WeChat.Work.Application.Contracts.csproj b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN.Abp.WeChat.Work.Application.Contracts.csproj
new file mode 100644
index 000000000..20daae9cc
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN.Abp.WeChat.Work.Application.Contracts.csproj
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkApplicationContractsModule.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkApplicationContractsModule.cs
new file mode 100644
index 000000000..26530114b
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkApplicationContractsModule.cs
@@ -0,0 +1,11 @@
+using Volo.Abp.Application;
+using Volo.Abp.Modularity;
+
+namespace LINGYUN.Abp.WeChat.Work;
+
+[DependsOn(
+ typeof(AbpDddApplicationContractsModule))]
+public class AbpWeChatWorkApplicationContractsModule : AbpModule
+{
+
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkRemoteServiceConsts.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkRemoteServiceConsts.cs
new file mode 100644
index 000000000..90d33ea95
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkRemoteServiceConsts.cs
@@ -0,0 +1,8 @@
+namespace LINGYUN.Abp.WeChat.Work;
+
+public class AbpWeChatWorkRemoteServiceConsts
+{
+ public const string RemoteServiceName = "AbpWeChatWork";
+
+ public const string ModuleName = "wechat-work";
+}
\ No newline at end of file
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkAuthorizeAppService.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkAuthorizeAppService.cs
new file mode 100644
index 000000000..d6a029606
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkAuthorizeAppService.cs
@@ -0,0 +1,18 @@
+using System.Threading.Tasks;
+using Volo.Abp.Application.Services;
+
+namespace LINGYUN.Abp.WeChat.Work.Authorize;
+public interface IWeChatWorkAuthorizeAppService : IApplicationService
+{
+ Task GenerateOAuth2AuthorizeAsync(
+ string agentid,
+ string redirectUri,
+ string responseType = "code",
+ string scope = "snsapi_base");
+
+ Task GenerateOAuth2LoginAsync(
+ string appid,
+ string redirectUri,
+ string loginType = "ServiceApp",
+ string agentid = "");
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/Message/Dto/MessageHandleInput.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/Message/Dto/MessageHandleInput.cs
new file mode 100644
index 000000000..7898cfccf
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/Message/Dto/MessageHandleInput.cs
@@ -0,0 +1,10 @@
+using LINGYUN.Abp.WeChat.Work.Models;
+using System;
+
+namespace LINGYUN.Abp.WeChat.Work.Message;
+
+[Serializable]
+public class MessageHandleInput : WeChatWorkMessage
+{
+ public string Data { get; set; }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/Message/Dto/MessageValidationInput.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/Message/Dto/MessageValidationInput.cs
new file mode 100644
index 000000000..0a00b1b71
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/Message/Dto/MessageValidationInput.cs
@@ -0,0 +1,12 @@
+using LINGYUN.Abp.WeChat.Work.Models;
+using System.Text.Json.Serialization;
+
+namespace LINGYUN.Abp.WeChat.Work.Message;
+public class MessageValidationInput : WeChatWorkMessage
+{
+ ///
+ /// 加密的字符串。需要解密得到消息内容明文,解密后有random、msg_len、msg、receiveid四个字段,其中msg即为消息内容明文
+ ///
+ [JsonPropertyName("echostr")]
+ public string EchoStr { get; set; }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/Message/IWeChatWorkMessageAppService.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/Message/IWeChatWorkMessageAppService.cs
new file mode 100644
index 000000000..334215892
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/Message/IWeChatWorkMessageAppService.cs
@@ -0,0 +1,30 @@
+using System.Threading.Tasks;
+using Volo.Abp.Application.Services;
+
+namespace LINGYUN.Abp.WeChat.Work.Message;
+///
+/// 企业微信消息接口
+///
+public interface IWeChatWorkMessageAppService : IApplicationService
+{
+ ///
+ /// 校验企业微信消息
+ ///
+ ///
+ /// 参考文档:
+ ///
+ ///
+ ///
+ ///
+ Task Handle(string agentId, MessageValidationInput input);
+ ///
+ /// 处理企业微信消息
+ ///
+ ///
+ /// 参考文档:
+ ///
+ ///
+ ///
+ ///
+ Task Handle(string agentId, MessageHandleInput input);
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/Models/WeChatWorkMessage.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/Models/WeChatWorkMessage.cs
new file mode 100644
index 000000000..fa13e322a
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/Models/WeChatWorkMessage.cs
@@ -0,0 +1,25 @@
+using System.Text.Json.Serialization;
+
+namespace LINGYUN.Abp.WeChat.Work.Models;
+public class WeChatWorkMessage
+{
+ ///
+ /// 企业微信加密签名,
+ /// msg_signature计算结合了企业填写的token、请求中的timestamp、nonce、加密的消息体
+ ///
+ ///
+ /// 签名计算方法参考: https://developer.work.weixin.qq.com/document/path/90930#12976/%E6%B6%88%E6%81%AF%E4%BD%93%E7%AD%BE%E5%90%8D%E6%A0%A1%E9%AA%8C
+ ///
+ [JsonPropertyName("msg_signature")]
+ public string Msg_Signature { get; set; }
+ ///
+ /// 时间戳。与nonce结合使用,用于防止请求重放攻击。
+ ///
+ [JsonPropertyName("timestamp")]
+ public int TimeStamp { get; set; }
+ ///
+ /// 随机数。与timestamp结合使用,用于防止请求重放攻击。
+ ///
+ [JsonPropertyName("nonce")]
+ public string Nonce { get; set; }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application/FodyWeavers.xml b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application/FodyWeavers.xml
new file mode 100644
index 000000000..1715698cc
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application/FodyWeavers.xsd b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application/FodyWeavers.xsd
new file mode 100644
index 000000000..3f3946e28
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN.Abp.WeChat.Work.Application.csproj b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN.Abp.WeChat.Work.Application.csproj
new file mode 100644
index 000000000..26b7c7a0e
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN.Abp.WeChat.Work.Application.csproj
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkApplicationModule.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkApplicationModule.cs
new file mode 100644
index 000000000..0eb380c9c
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkApplicationModule.cs
@@ -0,0 +1,13 @@
+using Volo.Abp.Application;
+using Volo.Abp.Modularity;
+
+namespace LINGYUN.Abp.WeChat.Work;
+
+[DependsOn(
+ typeof(AbpWeChatWorkApplicationContractsModule),
+ typeof(AbpWeChatWorkModule),
+ typeof(AbpDddApplicationModule))]
+public class AbpWeChatWorkApplicationModule : AbpModule
+{
+
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeAppService.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeAppService.cs
new file mode 100644
index 000000000..dea24c8f5
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeAppService.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Threading.Tasks;
+using System.Web;
+using Volo.Abp;
+using Volo.Abp.Application.Services;
+using Volo.Abp.Security.Encryption;
+
+namespace LINGYUN.Abp.WeChat.Work.Authorize;
+
+[IntegrationService]
+public class WeChatWorkAuthorizeAppService : ApplicationService, IWeChatWorkAuthorizeAppService
+{
+ private readonly IStringEncryptionService _encryptionService;
+ private readonly IWeChatWorkAuthorizeGenerator _authorizeGenerator;
+
+ public WeChatWorkAuthorizeAppService(
+ IStringEncryptionService encryptionService,
+ IWeChatWorkAuthorizeGenerator authorizeGenerator)
+ {
+ _encryptionService = encryptionService;
+ _authorizeGenerator = authorizeGenerator;
+ }
+
+ public async virtual Task GenerateOAuth2AuthorizeAsync(string agentid, string redirectUri, string responseType = "code", string scope = "snsapi_base")
+ {
+ var state = _encryptionService.Encrypt($"agentid={agentid}&redirectUri={redirectUri}&responseType={responseType}&scope={scope}&random={Guid.NewGuid():D}").ToMd5();
+
+ return await _authorizeGenerator.GenerateOAuth2AuthorizeAsync(agentid, HttpUtility.UrlEncode(redirectUri), state, responseType, scope);
+ }
+
+ public async virtual Task GenerateOAuth2LoginAsync(string appid, string redirectUri, string loginType = "ServiceApp", string agentid = "")
+ {
+ var state = _encryptionService.Encrypt($"agentid={agentid}&redirectUri={redirectUri}&loginType={loginType}&agentid={agentid}&random={Guid.NewGuid():D}").ToMd5();
+
+ return await _authorizeGenerator.GenerateOAuth2LoginAsync(agentid, HttpUtility.UrlEncode(redirectUri), state, loginType, agentid);
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMessageAppService.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMessageAppService.cs
new file mode 100644
index 000000000..52ea56f5d
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMessageAppService.cs
@@ -0,0 +1,62 @@
+using LINGYUN.Abp.WeChat.Work.Security;
+using LINGYUN.Abp.WeChat.Work.Settings;
+using Microsoft.Extensions.Options;
+using System.Threading.Tasks;
+using Volo.Abp;
+using Volo.Abp.Application.Services;
+
+namespace LINGYUN.Abp.WeChat.Work.Message;
+public class WeChatWorkMessageAppService : ApplicationService, IWeChatWorkMessageAppService
+{
+ private readonly IWeChatWorkCryptoService _cryptoService;
+ private readonly WeChatWorkOptions _options;
+
+ public WeChatWorkMessageAppService(
+ IWeChatWorkCryptoService cryptoService,
+ IOptionsMonitor options)
+ {
+ _cryptoService = cryptoService;
+ _options = options.CurrentValue;
+ }
+
+ public async virtual Task Handle(string agentId, MessageValidationInput input)
+ {
+ var corpId = await SettingProvider.GetOrNullAsync(WeChatWorkSettingNames.Connection.CorpId);
+ Check.NotNullOrEmpty(corpId, nameof(corpId));
+
+ var applicationConfiguration = _options.Applications.GetConfiguration(agentId);
+ var cryptoConfiguration = applicationConfiguration.GetCryptoConfiguration("Message");
+ var echoData = new WeChatWorkCryptoEchoData(
+ input.EchoStr,
+ corpId,
+ cryptoConfiguration.Token,
+ cryptoConfiguration.EncodingAESKey,
+ input.Msg_Signature,
+ input.TimeStamp.ToString(),
+ input.Nonce);
+
+ var echoStr = _cryptoService.Validation(echoData);
+
+ return echoStr;
+ }
+
+ public async virtual Task Handle(string agentId, MessageHandleInput input)
+ {
+ var corpId = await SettingProvider.GetOrNullAsync(WeChatWorkSettingNames.Connection.CorpId);
+ Check.NotNullOrEmpty(corpId, nameof(corpId));
+
+ var applicationConfiguration = _options.Applications.GetConfiguration(agentId);
+ var cryptoConfiguration = applicationConfiguration.GetCryptoConfiguration("Message");
+ var decryptData = new WeChatWorkCryptoDecryptData(
+ input.Data,
+ corpId,
+ cryptoConfiguration.Token,
+ cryptoConfiguration.EncodingAESKey,
+ input.Msg_Signature,
+ input.TimeStamp.ToString(),
+ input.Nonce);
+
+ var msg = _cryptoService.Decrypt(decryptData);
+ return msg;
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/FodyWeavers.xml b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/FodyWeavers.xml
new file mode 100644
index 000000000..1715698cc
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/FodyWeavers.xsd b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/FodyWeavers.xsd
new file mode 100644
index 000000000..3f3946e28
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/LINGYUN.Abp.WeChat.Work.HttpApi.csproj b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/LINGYUN.Abp.WeChat.Work.HttpApi.csproj
new file mode 100644
index 000000000..4ae9ee2b3
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/LINGYUN.Abp.WeChat.Work.HttpApi.csproj
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ net7.0
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkHttpApiModule.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkHttpApiModule.cs
new file mode 100644
index 000000000..6aa96be6f
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkHttpApiModule.cs
@@ -0,0 +1,26 @@
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.AspNetCore.Mvc;
+using Volo.Abp.Modularity;
+
+namespace LINGYUN.Abp.WeChat.Work;
+
+[DependsOn(
+ typeof(AbpWeChatWorkApplicationContractsModule),
+ typeof(AbpAspNetCoreMvcModule))]
+public class AbpWeChatWorkHttpApiModule : AbpModule
+{
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ PreConfigure(mvcBuilder =>
+ {
+ mvcBuilder.AddApplicationPartIfNotExists(typeof(AbpWeChatWorkHttpApiModule).Assembly);
+ });
+
+ //PreConfigure(options =>
+ //{
+ // options.AddAssemblyResource(
+ // typeof(AbpTextTemplatingResource),
+ // typeof(AbpWeChatWorkApplicationContractsModule).Assembly);
+ //});
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeController.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeController.cs
new file mode 100644
index 000000000..0030893bb
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeController.cs
@@ -0,0 +1,42 @@
+using Microsoft.AspNetCore.Mvc;
+using System.Threading.Tasks;
+using Volo.Abp;
+using Volo.Abp.AspNetCore.Mvc;
+
+namespace LINGYUN.Abp.WeChat.Work.Authorize;
+
+[Controller]
+[RemoteService(Name = AbpWeChatWorkRemoteServiceConsts.RemoteServiceName)]
+[Area(AbpWeChatWorkRemoteServiceConsts.ModuleName)]
+[Route("api/wechat/work/authorize")]
+public class WeChatWorkAuthorizeController : AbpControllerBase, IWeChatWorkAuthorizeAppService
+{
+ private readonly IWeChatWorkAuthorizeAppService _service;
+
+ public WeChatWorkAuthorizeController(IWeChatWorkAuthorizeAppService service)
+ {
+ _service = service;
+ }
+
+ [HttpGet]
+ [Route("oauth2")]
+ public virtual Task GenerateOAuth2AuthorizeAsync(
+ [FromQuery(Name = "agent_id")] string agentid,
+ [FromQuery(Name = "redirect_uri")] string redirectUri,
+ [FromQuery(Name = "response_type")] string responseType = "code",
+ [FromQuery] string scope = "snsapi_base")
+ {
+ return _service.GenerateOAuth2AuthorizeAsync(agentid, redirectUri, responseType, scope);
+ }
+
+ [HttpGet]
+ [Route("oauth2/login")]
+ public virtual Task GenerateOAuth2LoginAsync(
+ [FromQuery] string appid,
+ [FromQuery(Name = "redirect_uri")] string redirectUri,
+ [FromQuery(Name = "login_type")] string loginType = "ServiceApp",
+ [FromQuery(Name = "agent_id")] string agentid = "")
+ {
+ return _service.GenerateOAuth2LoginAsync(agentid, redirectUri, loginType, agentid);
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMessageController.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMessageController.cs
new file mode 100644
index 000000000..15a167607
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMessageController.cs
@@ -0,0 +1,41 @@
+using Microsoft.AspNetCore.Mvc;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Volo.Abp;
+using Volo.Abp.AspNetCore.Mvc;
+
+namespace LINGYUN.Abp.WeChat.Work.Message;
+
+[Controller]
+[RemoteService(Name = AbpWeChatWorkRemoteServiceConsts.RemoteServiceName)]
+[Area(AbpWeChatWorkRemoteServiceConsts.ModuleName)]
+[Route("api/wechat/work/messages")]
+public class WeChatWorkMessageController : AbpControllerBase, IWeChatWorkMessageAppService
+{
+ private readonly IWeChatWorkMessageAppService _service;
+
+ public WeChatWorkMessageController(IWeChatWorkMessageAppService service)
+ {
+ _service = service;
+ }
+
+ [HttpGet]
+ [Route("{agentId}")]
+ public virtual Task Handle([FromRoute] string agentId, [FromQuery] MessageValidationInput input)
+ {
+ return _service.Handle(agentId, input);
+ }
+
+ [HttpPost]
+ [Route("{agentId}")]
+ public async virtual Task Handle([FromRoute] string agentId, [FromQuery] MessageHandleInput input)
+ {
+ using var reader = new StreamReader(Request.Body, Encoding.UTF8);
+ var content = await reader.ReadToEndAsync();
+
+ input.Data = content;
+
+ return await _service.Handle(agentId, input);
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/FodyWeavers.xml b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/FodyWeavers.xml
new file mode 100644
index 000000000..1715698cc
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/FodyWeavers.xsd b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/FodyWeavers.xsd
new file mode 100644
index 000000000..3f3946e28
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN.Abp.WeChat.Work.csproj b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN.Abp.WeChat.Work.csproj
new file mode 100644
index 000000000..b3aa6da4a
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN.Abp.WeChat.Work.csproj
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkException.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkException.cs
new file mode 100644
index 000000000..cbad5bf4d
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkException.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Runtime.Serialization;
+using Volo.Abp;
+
+namespace LINGYUN.Abp.WeChat.Work;
+
+public class AbpWeChatWorkException : BusinessException
+{
+ public AbpWeChatWorkException()
+ {
+ }
+
+ public AbpWeChatWorkException(
+ string code = null,
+ string message = null,
+ string details = null,
+ Exception innerException = null)
+ : base(code, message, details, innerException)
+ {
+ }
+
+ public AbpWeChatWorkException(SerializationInfo serializationInfo, StreamingContext context)
+ : base(serializationInfo, context)
+ {
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkGlobalConsts.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkGlobalConsts.cs
new file mode 100644
index 000000000..7c8fa9867
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkGlobalConsts.cs
@@ -0,0 +1,42 @@
+namespace LINGYUN.Abp.WeChat.Work
+{
+ public class AbpWeChatWorkGlobalConsts
+ {
+ ///
+ /// 企业微信对应的Provider名称
+ ///
+ public static string ProviderName { get; set; } = "WeChat.Work";
+ ///
+ /// 企业微信授权类型
+ ///
+ public static string GrantType { get; set; } = "wx-work";
+ ///
+ /// 企业微信授权名称
+ ///
+ public static string AuthenticationScheme { get; set; }= "WeCom";
+ ///
+ /// 企业微信个人信息标识
+ ///
+ public static string ProfileKey { get; set; } = "wecom.profile";
+ ///
+ /// 企业微信授权应用标识参数
+ ///
+ public static string AgentId { get; set; } = "agent_id";
+ ///
+ /// 企业微信授权Code参数
+ ///
+ public static string Code { get; set; }= "code";
+ ///
+ /// 企业微信授权显示名称
+ ///
+ public static string DisplayName { get; set; } = "企业微信";
+ ///
+ ///企业微信授权方法名称
+ ///
+ public static string AuthenticationMethod { get; set; } = "wecom";
+
+ internal static string ApiClient { get; set; } = "Abp.WeChat.Work";
+ internal static string OAuthClient { get; set; } = "Abp.WeChat.Work.OAuth";
+ internal static string LoginClient { get; set; } = "Abp.WeChat.Work.Login";
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkModule.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkModule.cs
new file mode 100644
index 000000000..1cc684752
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkModule.cs
@@ -0,0 +1,58 @@
+using LINGYUN.Abp.Features.LimitValidation;
+using LINGYUN.Abp.WeChat.Work.Localization;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using Volo.Abp.Caching;
+using Volo.Abp.Localization;
+using Volo.Abp.Localization.ExceptionHandling;
+using Volo.Abp.Modularity;
+using Volo.Abp.Settings;
+using Volo.Abp.VirtualFileSystem;
+
+namespace LINGYUN.Abp.WeChat.Work;
+
+[DependsOn(
+ typeof(AbpCachingModule),
+ typeof(AbpFeaturesLimitValidationModule),
+ typeof(AbpSettingsModule))]
+public class AbpWeChatWorkModule : AbpModule
+{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ var configuration = context.Services.GetConfiguration();
+ Configure(configuration.GetSection("WeChat:Work"));
+
+ Configure(options =>
+ {
+ options.FileSets.AddEmbedded();
+ });
+
+ Configure(options =>
+ {
+ options.Resources
+ .Add("zh-Hans")
+ .AddVirtualJson("/LINGYUN/Abp/WeChat/Work/Localization/Resources");
+ });
+
+ Configure(options =>
+ {
+ options.MapCodeNamespace(WeChatWorkErrorCodes.Namespace, typeof(WeChatWorkResource));
+ });
+
+ context.Services.AddHttpClient(AbpWeChatWorkGlobalConsts.ApiClient,
+ options =>
+ {
+ options.BaseAddress = new Uri("https://qyapi.weixin.qq.com");
+ });
+ context.Services.AddHttpClient(AbpWeChatWorkGlobalConsts.OAuthClient,
+ options =>
+ {
+ options.BaseAddress = new Uri("https://open.weixin.qq.com");
+ });
+ context.Services.AddHttpClient(AbpWeChatWorkGlobalConsts.LoginClient,
+ options =>
+ {
+ options.BaseAddress = new Uri("https://login.work.weixin.qq.com");
+ });
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkAuthorizeGenerator.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkAuthorizeGenerator.cs
new file mode 100644
index 000000000..5e84a2cec
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkAuthorizeGenerator.cs
@@ -0,0 +1,41 @@
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.WeChat.Work.Authorize;
+public interface IWeChatWorkAuthorizeGenerator
+{
+ ///
+ /// 构造网页授权链接
+ ///
+ ///
+ /// 参考:https://developer.work.weixin.qq.com/document/path/91022
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task GenerateOAuth2AuthorizeAsync(
+ string agentid,
+ string redirectUri,
+ string state,
+ string responseType = "code",
+ string scope = "snsapi_base");
+ ///
+ /// 构建网页登录链接
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task GenerateOAuth2LoginAsync(
+ string appid,
+ string redirectUri,
+ string state,
+ string loginType = "ServiceApp",
+ string agentid = "",
+ string lang = "zh");
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkInternalUserFinder.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkInternalUserFinder.cs
new file mode 100644
index 000000000..e9154070a
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkInternalUserFinder.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.WeChat.Work.Authorize;
+public interface IWeChatWorkInternalUserFinder
+{
+ ///
+ /// 通过用户标识查询企业微信用户标识
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task FindUserIdentifierAsync(
+ string agentId,
+ Guid userId,
+ CancellationToken cancellationToken = default);
+ ///
+ /// 通过用户标识列表查询企业微信用户标识列表
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task> FindUserIdentifierListAsync(
+ string agentId,
+ IEnumerable userIdList,
+ CancellationToken cancellationToken = default);
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkUserFinder.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkUserFinder.cs
new file mode 100644
index 000000000..9339095b9
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkUserFinder.cs
@@ -0,0 +1,20 @@
+using LINGYUN.Abp.WeChat.Work.Authorize.Models;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.WeChat.Work.Authorize;
+///
+/// 企业微信用户信息查询接口
+///
+public interface IWeChatWorkUserFinder
+{
+ Task GetUserInfoAsync(
+ string agentId,
+ string code,
+ CancellationToken cancellationToken = default);
+
+ Task GetUserDetailAsync(
+ string agentId,
+ string userTicket,
+ CancellationToken cancellationToken = default);
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/Models/WeChatWorkGender.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/Models/WeChatWorkGender.cs
new file mode 100644
index 000000000..348174374
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/Models/WeChatWorkGender.cs
@@ -0,0 +1,19 @@
+namespace LINGYUN.Abp.WeChat.Work.Authorize.Models;
+///
+/// 性别
+///
+public enum WeChatWorkGender
+{
+ ///
+ /// 未定义
+ ///
+ None = 0,
+ ///
+ /// 男性
+ ///
+ Man = 1,
+ ///
+ /// 女性
+ ///
+ Women = 2
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/Models/WeChatWorkUserDetail.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/Models/WeChatWorkUserDetail.cs
new file mode 100644
index 000000000..09ad39787
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/Models/WeChatWorkUserDetail.cs
@@ -0,0 +1,77 @@
+using JetBrains.Annotations;
+using Newtonsoft.Json;
+using System.Text.Json.Serialization;
+
+namespace LINGYUN.Abp.WeChat.Work.Authorize.Models;
+///
+/// 企业微信用户详情
+///
+public class WeChatWorkUserDetail
+{
+ ///
+ /// 成员UserID
+ ///
+ [NotNull]
+ [JsonProperty("userid")]
+ [JsonPropertyName("userid")]
+ public string UserId { get; set; }
+ ///
+ /// 性别。
+ /// 0表示未定义,
+ /// 1表示男性,
+ /// 2表示女性。
+ /// 仅在用户同意snsapi_privateinfo授权时返回真实值,否则返回0
+ ///
+ [CanBeNull]
+ [JsonProperty("gender")]
+ [JsonPropertyName("gender")]
+ public WeChatWorkGender Gender { get; set; }
+ ///
+ /// 头像url。
+ /// 仅在用户同意snsapi_privateinfo授权时返回真实头像,否则返回默认头像
+ ///
+ [CanBeNull]
+ [JsonProperty("avatar")]
+ [JsonPropertyName("avatar")]
+ public string Avatar { get; set; }
+ ///
+ /// 员工个人二维码(扫描可添加为外部联系人)
+ /// 仅在用户同意snsapi_privateinfo授权时返回
+ ///
+ [CanBeNull]
+ [JsonProperty("qr_code")]
+ [JsonPropertyName("qr_code")]
+ public string QrCode { get; set; }
+ ///
+ /// 手机
+ /// 仅在用户同意snsapi_privateinfo授权时返回,第三方应用不可获取
+ ///
+ [CanBeNull]
+ [JsonProperty("mobile")]
+ [JsonPropertyName("mobile")]
+ public string Mobile { get; set; }
+ ///
+ /// 邮箱
+ /// 仅在用户同意snsapi_privateinfo授权时返回,第三方应用不可获取
+ ///
+ [CanBeNull]
+ [JsonProperty("email")]
+ [JsonPropertyName("email")]
+ public string Email { get; set; }
+ ///
+ /// 企业邮箱
+ /// 仅在用户同意snsapi_privateinfo授权时返回,第三方应用不可获取
+ ///
+ [CanBeNull]
+ [JsonProperty("biz_mail")]
+ [JsonPropertyName("biz_mail")]
+ public string WorkEmail { get; set; }
+ ///
+ /// 地址
+ /// 仅在用户同意snsapi_privateinfo授权时返回,第三方应用不可获取
+ ///
+ [CanBeNull]
+ [JsonProperty("address")]
+ [JsonPropertyName("address")]
+ public string Address { get; set; }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/Models/WeChatWorkUserInfo.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/Models/WeChatWorkUserInfo.cs
new file mode 100644
index 000000000..f86cbac8f
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/Models/WeChatWorkUserInfo.cs
@@ -0,0 +1,25 @@
+using JetBrains.Annotations;
+using Newtonsoft.Json;
+using System.Text.Json.Serialization;
+
+namespace LINGYUN.Abp.WeChat.Work.Authorize.Models;
+///
+/// 企业微信用户信息
+///
+public class WeChatWorkUserInfo
+{
+ ///
+ /// 成员UserID
+ ///
+ [NotNull]
+ [JsonProperty("userid")]
+ [JsonPropertyName("userid")]
+ public string UserId { get; set; }
+ ///
+ /// 成员票据,最大为512字节,有效期为1800s
+ ///
+ [NotNull]
+ [JsonProperty("user_ticket")]
+ [JsonPropertyName("user_ticket")]
+ public string UserTicket { get; set; }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/NullWeChatWorkInternalUserFinder.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/NullWeChatWorkInternalUserFinder.cs
new file mode 100644
index 000000000..a946574e2
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/NullWeChatWorkInternalUserFinder.cs
@@ -0,0 +1,30 @@
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+
+namespace LINGYUN.Abp.WeChat.Work.Authorize;
+
+[Dependency(ServiceLifetime.Singleton, TryRegister = true)]
+public class NullWeChatWorkInternalUserFinder : IWeChatWorkInternalUserFinder
+{
+ public readonly static IWeChatWorkInternalUserFinder Instance = new NullWeChatWorkInternalUserFinder();
+ public Task FindUserIdentifierAsync(
+ string agentId,
+ Guid userId,
+ CancellationToken cancellationToken = default)
+ {
+ string findUserId = null;
+ return Task.FromResult(findUserId);
+ }
+
+ public Task> FindUserIdentifierListAsync(
+ string agentId,
+ IEnumerable userIdList,
+ CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(new List());
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/Request/WeChatWorkUserDetailRequest.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/Request/WeChatWorkUserDetailRequest.cs
new file mode 100644
index 000000000..189e64de4
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/Request/WeChatWorkUserDetailRequest.cs
@@ -0,0 +1,23 @@
+using JetBrains.Annotations;
+using Newtonsoft.Json;
+using System.Text.Json.Serialization;
+using Volo.Abp;
+
+namespace LINGYUN.Abp.WeChat.Work.Authorize.Request;
+
+public class WeChatWorkUserDetailRequest
+{
+ [NotNull]
+ [JsonProperty("user_ticket")]
+ [JsonPropertyName("user_ticket")]
+ public string UserTicket { get; set; }
+ public WeChatWorkUserDetailRequest([NotNull] string userTicket)
+ {
+ UserTicket = Check.NotNullOrWhiteSpace(userTicket, nameof(userTicket), 512);
+ }
+
+ public virtual string SerializeToJson()
+ {
+ return JsonConvert.SerializeObject(this);
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/Response/WeChatWorkResponseExtensions.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/Response/WeChatWorkResponseExtensions.cs
new file mode 100644
index 000000000..9c5284b3a
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/Response/WeChatWorkResponseExtensions.cs
@@ -0,0 +1,35 @@
+using LINGYUN.Abp.WeChat.Work.Authorize.Models;
+
+namespace LINGYUN.Abp.WeChat.Work.Authorize.Response;
+internal static class WeChatWorkResponseExtensions
+{
+ public static WeChatWorkUserInfo ToUserInfo(
+ this WeChatWorkUserInfoResponse response)
+ {
+ response.ThrowIfNotSuccess();
+
+ return new WeChatWorkUserInfo
+ {
+ UserId = response.UserId,
+ UserTicket = response.UserTicket,
+ };
+ }
+
+ public static WeChatWorkUserDetail ToUserDetail(
+ this WeChatWorkUserDetailResponse response)
+ {
+ response.ThrowIfNotSuccess();
+
+ return new WeChatWorkUserDetail
+ {
+ UserId = response.UserId,
+ Address = response.Address,
+ Avatar = response.Avatar,
+ QrCode = response.QrCode,
+ Email = response.Email,
+ Gender = response.Gender,
+ Mobile = response.Mobile,
+ WorkEmail = response.WorkEmail,
+ };
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/Response/WeChatWorkUserDetailResponse.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/Response/WeChatWorkUserDetailResponse.cs
new file mode 100644
index 000000000..e783e4211
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/Response/WeChatWorkUserDetailResponse.cs
@@ -0,0 +1,69 @@
+using LINGYUN.Abp.WeChat.Work.Authorize.Models;
+using Newtonsoft.Json;
+using System.Text.Json.Serialization;
+
+namespace LINGYUN.Abp.WeChat.Work.Authorize.Response;
+///
+/// 企业微信用户详情响应
+///
+public class WeChatWorkUserDetailResponse : WeChatWorkResponse
+{
+ ///
+ /// 成员UserID
+ ///
+ [JsonProperty("userid")]
+ [JsonPropertyName("userid")]
+ public string UserId { get; set; }
+ ///
+ /// 性别。
+ /// 0表示未定义,
+ /// 1表示男性,
+ /// 2表示女性。
+ /// 仅在用户同意snsapi_privateinfo授权时返回真实值,否则返回0
+ ///
+ [JsonProperty("gender")]
+ [JsonPropertyName("gender")]
+ public WeChatWorkGender Gender { get; set; }
+ ///
+ /// 头像url。
+ /// 仅在用户同意snsapi_privateinfo授权时返回真实头像,否则返回默认头像
+ ///
+ [JsonProperty("avatar")]
+ [JsonPropertyName("avatar")]
+ public string Avatar { get; set; }
+ ///
+ /// 员工个人二维码(扫描可添加为外部联系人)
+ /// 仅在用户同意snsapi_privateinfo授权时返回
+ ///
+ [JsonProperty("qr_code")]
+ [JsonPropertyName("qr_code")]
+ public string QrCode { get; set; }
+ ///
+ /// 手机
+ /// 仅在用户同意snsapi_privateinfo授权时返回,第三方应用不可获取
+ ///
+ [JsonProperty("mobile")]
+ [JsonPropertyName("mobile")]
+ public string Mobile { get; set; }
+ ///
+ /// 邮箱
+ /// 仅在用户同意snsapi_privateinfo授权时返回,第三方应用不可获取
+ ///
+ [JsonProperty("email")]
+ [JsonPropertyName("email")]
+ public string Email { get; set; }
+ ///
+ /// 企业邮箱
+ /// 仅在用户同意snsapi_privateinfo授权时返回,第三方应用不可获取
+ ///
+ [JsonProperty("biz_mail")]
+ [JsonPropertyName("biz_mail")]
+ public string WorkEmail { get; set; }
+ ///
+ /// 地址
+ /// 仅在用户同意snsapi_privateinfo授权时返回,第三方应用不可获取
+ ///
+ [JsonProperty("address")]
+ [JsonPropertyName("address")]
+ public string Address { get; set; }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/Response/WeChatWorkUserInfoResponse.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/Response/WeChatWorkUserInfoResponse.cs
new file mode 100644
index 000000000..d964b1d3d
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/Response/WeChatWorkUserInfoResponse.cs
@@ -0,0 +1,22 @@
+using Newtonsoft.Json;
+using System.Text.Json.Serialization;
+
+namespace LINGYUN.Abp.WeChat.Work.Authorize.Response;
+///
+/// 企业微信用户信息响应
+///
+public class WeChatWorkUserInfoResponse : WeChatWorkResponse
+{
+ ///
+ /// 成员UserID
+ ///
+ [JsonProperty("userid")]
+ [JsonPropertyName("userid")]
+ public string UserId { get; set; }
+ ///
+ /// 成员票据,最大为512字节,有效期为1800s
+ ///
+ [JsonProperty("user_ticket")]
+ [JsonPropertyName("user_ticket")]
+ public string UserTicket { get; set; }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeGenerator.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeGenerator.cs
new file mode 100644
index 000000000..168e4313d
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeGenerator.cs
@@ -0,0 +1,78 @@
+using LINGYUN.Abp.WeChat.Work.Settings;
+using System;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using System.Web;
+using Volo.Abp;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Settings;
+
+namespace LINGYUN.Abp.WeChat.Work.Authorize;
+public class WeChatWorkAuthorizeGenerator : IWeChatWorkAuthorizeGenerator, ISingletonDependency
+{
+ protected ISettingProvider SettingProvider { get; }
+ protected IHttpClientFactory HttpClientFactory { get; }
+
+ public WeChatWorkAuthorizeGenerator(
+ ISettingProvider settingProvider,
+ IHttpClientFactory httpClientFactory)
+ {
+ SettingProvider = settingProvider;
+ HttpClientFactory = httpClientFactory;
+ }
+
+ public async virtual Task GenerateOAuth2AuthorizeAsync(
+ string agentid,
+ string redirectUri,
+ string state,
+ string responseType = "code",
+ string scope = "snsapi_base")
+ {
+ var corpId = await SettingProvider.GetOrNullAsync(WeChatWorkSettingNames.Connection.CorpId);
+
+ Check.NotNullOrEmpty(corpId, nameof(corpId));
+
+ var client = HttpClientFactory.CreateClient(AbpWeChatWorkGlobalConsts.OAuthClient);
+
+ var generatedUrlBuilder = new StringBuilder();
+
+ generatedUrlBuilder
+ .Append(client.BaseAddress.AbsoluteUri.EnsureEndsWith('/'))
+ .Append("connect/oauth2/authorize")
+ .AppendFormat("?appid={0}", corpId)
+ .AppendFormat("&redirect_uri={0}", HttpUtility.UrlEncode(redirectUri))
+ .AppendFormat("&response_type={0}", responseType)
+ .AppendFormat("&scope={0}", scope)
+ .AppendFormat("&state={0}", state)
+ .AppendFormat("&agentid={0}", agentid)
+ .Append("#wechat_redirect");
+
+ return generatedUrlBuilder.ToString();
+ }
+
+ public virtual Task GenerateOAuth2LoginAsync(
+ string appid,
+ string redirectUri,
+ string state,
+ string loginType = "ServiceApp",
+ string agentid = "",
+ string lang = "zh")
+ {
+ var client = HttpClientFactory.CreateClient(AbpWeChatWorkGlobalConsts.LoginClient);
+
+ var generatedUrlBuilder = new StringBuilder();
+
+ generatedUrlBuilder
+ .Append(client.BaseAddress.AbsoluteUri.EnsureEndsWith('/'))
+ .Append("wwlogin/sso/login")
+ .AppendFormat("?login_type={0}", loginType)
+ .AppendFormat("&appid={0}", appid)
+ .AppendFormat("&agentid={0}", agentid)
+ .AppendFormat("&redirect_uri={0}", HttpUtility.UrlEncode(redirectUri))
+ .AppendFormat("&state={0}", state)
+ .AppendFormat("&lang={0}", lang);
+
+ return Task.FromResult(generatedUrlBuilder.ToString());
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkUserFinder.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkUserFinder.cs
new file mode 100644
index 000000000..18211b226
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkUserFinder.cs
@@ -0,0 +1,55 @@
+using LINGYUN.Abp.WeChat.Work.Authorize.Models;
+using LINGYUN.Abp.WeChat.Work.Authorize.Request;
+using LINGYUN.Abp.WeChat.Work.Authorize.Response;
+using LINGYUN.Abp.WeChat.Work.Token;
+using Newtonsoft.Json;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+
+namespace LINGYUN.Abp.WeChat.Work.Authorize;
+public class WeChatWorkUserFinder : IWeChatWorkUserFinder, ISingletonDependency
+{
+ protected IHttpClientFactory HttpClientFactory { get; }
+ protected IWeChatWorkTokenProvider WeChatWorkTokenProvider { get; }
+
+ public WeChatWorkUserFinder(
+ IHttpClientFactory httpClientFactory,
+ IWeChatWorkTokenProvider weChatWorkTokenProvider)
+ {
+ HttpClientFactory = httpClientFactory;
+ WeChatWorkTokenProvider = weChatWorkTokenProvider;
+ }
+
+ public async virtual Task GetUserInfoAsync(
+ string agentId,
+ string code,
+ CancellationToken cancellationToken = default)
+ {
+ var token = await WeChatWorkTokenProvider.GetTokenAsync(agentId, cancellationToken);
+ var client = HttpClientFactory.CreateClient(AbpWeChatWorkGlobalConsts.ApiClient);
+
+ using var response = await client.GetUserInfoAsync(token.AccessToken, code, cancellationToken);
+ var responseContent = await response.Content.ReadAsStringAsync();
+ var userInfoResponse = JsonConvert.DeserializeObject(responseContent);
+
+ return userInfoResponse.ToUserInfo();
+ }
+
+ public async virtual Task GetUserDetailAsync(
+ string agentId,
+ string userTicket,
+ CancellationToken cancellationToken = default)
+ {
+ var token = await WeChatWorkTokenProvider.GetTokenAsync(agentId, cancellationToken);
+ var client = HttpClientFactory.CreateClient(AbpWeChatWorkGlobalConsts.ApiClient);
+
+ var request = new WeChatWorkUserDetailRequest(userTicket);
+ using var response = await client.GetUserDetailAsync(token.AccessToken, request, cancellationToken);
+ var responseContent = await response.Content.ReadAsStringAsync();
+ var userDetailResponse = JsonConvert.DeserializeObject(responseContent);
+
+ return userDetailResponse.ToUserDetail();
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Features/WeChatWorkFeatureDefinitionProvider.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Features/WeChatWorkFeatureDefinitionProvider.cs
new file mode 100644
index 000000000..f65a38cb5
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Features/WeChatWorkFeatureDefinitionProvider.cs
@@ -0,0 +1,46 @@
+using LINGYUN.Abp.WeChat.Work.Localization;
+using Volo.Abp.Features;
+using Volo.Abp.Localization;
+using Volo.Abp.Validation.StringValues;
+
+namespace LINGYUN.Abp.WeChat.Work.Features;
+public class WeChatWorkFeatureDefinitionProvider : FeatureDefinitionProvider
+{
+ public override void Define(IFeatureDefinitionContext context)
+ {
+ var group = context.AddGroup(
+ name: WeChatWorkFeatureNames.GroupName,
+ displayName: L("Features:WeChatWork"));
+
+ var weChatWorkEnableFeature = group.AddFeature(
+ name: WeChatWorkFeatureNames.Enable,
+ defaultValue: "false",
+ displayName: L("Features:WeChatWorkEnable"),
+ description: L("Features:WeChatWorkEnableDesc"),
+ valueType: new ToggleStringValueType(new BooleanValueValidator()));
+
+ var messageEnableFeature = weChatWorkEnableFeature.CreateChild(
+ name: WeChatWorkFeatureNames.Message.Enable,
+ defaultValue: "false",
+ displayName: L("Features:MessageEnable"),
+ description: L("Features:MessageEnableDesc"),
+ valueType: new ToggleStringValueType(new BooleanValueValidator()));
+ messageEnableFeature.CreateChild(
+ name: WeChatWorkFeatureNames.Message.SendLimit,
+ defaultValue: "20000",
+ displayName: L("Features:Message.SendLimit"),
+ description: L("Features:Message.SendLimitDesc"),
+ valueType: new FreeTextStringValueType(new NumericValueValidator(1, 100000)));
+ messageEnableFeature.CreateChild(
+ name: WeChatWorkFeatureNames.Message.SendLimitInterval,
+ defaultValue: "1",
+ displayName: L("Features:Message.SendLimitInterval"),
+ description: L("Features:Message.SendLimitIntervalDesc"),
+ valueType: new FreeTextStringValueType(new NumericValueValidator(1, 1)));
+ }
+
+ private static LocalizableString L(string name)
+ {
+ return LocalizableString.Create(name);
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Features/WeChatWorkFeatureNames.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Features/WeChatWorkFeatureNames.cs
new file mode 100644
index 000000000..2f7c47d01
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Features/WeChatWorkFeatureNames.cs
@@ -0,0 +1,27 @@
+namespace LINGYUN.Abp.WeChat.Work.Features;
+
+public static class WeChatWorkFeatureNames
+{
+ public const string GroupName = "WeChat.Work";
+ ///
+ /// 启用企业微信
+ ///
+ public const string Enable = GroupName + ".Enable";
+
+ public static class Message
+ {
+ public const string GroupName = WeChatWorkFeatureNames.GroupName + ".Message";
+ ///
+ /// 启用消息推送
+ ///
+ public const string Enable = GroupName + ".Enable";
+ ///
+ /// 发送次数上限
+ ///
+ public const string SendLimit = GroupName + ".SendLimit";
+ ///
+ /// 发送次数上限时长
+ ///
+ public const string SendLimitInterval = GroupName + ".SendLimitInterval";
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Localization/Resources/en.json b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Localization/Resources/en.json
new file mode 100644
index 000000000..9b014f7e2
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Localization/Resources/en.json
@@ -0,0 +1,56 @@
+{
+ "culture": "en",
+ "texts": {
+ "Features:WeChatWork": "WeCom",
+ "Features:WeChatWorkEnable": "Enable WeCom",
+ "Features:WeChatWorkEnableDesc": "Enable to enable the application to have WeCom capabilities.",
+ "Features:Message": "WeCom message push",
+ "Features:MessageEnable": "Enable WeCom message push",
+ "Features:MessageEnableDesc": "Enable so that the app will have the ability to push messages to the app via WeCom.",
+ "Features:Message.SendLimit": "Restrictions on WeCom message push,",
+ "Features:Message.SendLimitDesc": "Set to limit the WeCom application message push limit.",
+ "Features:Message.SendLimitInterval": "WeCom message limit period",
+ "Features:Message.SendLimitIntervalDesc": "Set the limit period of WeCom messages (time scale: days). Each application cannot exceed the limit number of accounts *200 people/day.",
+ "DisplayName:WeChatWork.Connection.CorpId": "Corp Id",
+ "Description:WeChatWork.Connection.CorpId": "Each enterprise has a unique corpid, to obtain this information, you can view \"Enterprise ID\" under \"My Enterprise\" - \"Enterprise Information\" in the management background (requires administrator permissions).",
+ "DisplayName:WeChatWork.EnabledQuickLogin": "Enabled Quick Login",
+ "Description:WeChatWork.EnabledQuickLogin": "Users can log in directly by scanning the code obtained when they are not registered.",
+ "WeChatWork:100400": "处理企业微信服务器消息失败,请检查应用签名配置!",
+ "WeChatWork:100404": "没有找到标识 {AgentId} 的内部应用配置!",
+ "WeChatWork:101404": "没有找到应用 {AgentId} 所属功能 {Feture} 的加解密配置!",
+ "WeChatWork:-31020": "redirect_uri 与配置的登录授权调域名不一致",
+ "WeChatWork:-31027": "appid 参数错误",
+ "WeChatWork:-31028": "agentid 参数错误",
+ "WeChatWork:-31033": "校验请求来源错误",
+ "WeChatWork:-31034": "该企业不是服务商",
+ "WeChatWork:-31035": "redirect_uri 不能为空",
+ "WeChatWork:-31037": "appid 非登录授权应用",
+ "WeChatWork:-31039": "redirect_uri 与配置的可信域名不一致",
+ "WeChatWork:-31040": "login_type 参数错误",
+ "WeChatWork:-40001": "签名验证错误",
+ "WeChatWork:-40002": "xml/json解析失败",
+ "WeChatWork:-40003": "sha加密生成签名失败",
+ "WeChatWork:-40004": "AESKey 非法",
+ "WeChatWork:-40005": "ReceiveId 校验错误",
+ "WeChatWork:-40006": "AES 加密失败",
+ "WeChatWork:-40007": "AES 解密失败",
+ "WeChatWork:-40008": "解密后得到的buffer非法",
+ "WeChatWork:-40009": "base64加密失败",
+ "WeChatWork:-40010": "base64解密失败",
+ "WeChatWork:-40011": "生成xml/json失败",
+ "WeChatWork:-1": "系统繁忙, 服务器暂不可用,建议稍候重试。建议重试次数不超过3次!",
+ "WeChatWork:0": "请求成功",
+ "WeChatWork:6000": "数据版本冲突,可能有多个调用端同时修改数据,稍后重试!",
+ "WeChatWork:40001": "不合法的secret参数,参考:https://developer.work.weixin.qq.com/document/path/90313#%E9%94%99%E8%AF%AF%E7%A0%81%EF%BC%9A40001",
+ "WeChatWork:40003": "无效的UserID,参考:https://developer.work.weixin.qq.com/document/path/90313#10649/%E9%94%99%E8%AF%AF%E7%A0%81%EF%BC%9A40003",
+ "WeChatWork:40004": "不合法的媒体文件类型,参考:https://developer.work.weixin.qq.com/document/path/90313#10112",
+ "WeChatWork:40005": "不合法的type参数, 参考:https://developer.work.weixin.qq.com/document/path/90313#10112",
+ "WeChatWork:40006": "不合法的文件大小,参考:https://developer.work.weixin.qq.com/document/path/90313#10112",
+ "WeChatWork:40007": "不合法的media_id参数,参考:https://developer.work.weixin.qq.com/document/path/90313#10649/%E9%94%99%E8%AF%AF%E7%A0%81%EF%BC%9A40007",
+ "WeChatWork:40008": "不合法的msgtype参数,参考:https://developer.work.weixin.qq.com/document/path/90313#10167",
+ "WeChatWork:40009": "上传图片大小不是有效值,参考:https://developer.work.weixin.qq.com/document/path/90313#10112/%E4%B8%8A%E4%BC%A0%E7%9A%84%E5%AA%92%E4%BD%93%E6%96%87%E4%BB%B6%E9%99%90%E5%88%B6",
+ "WeChatWork:40011": "上传视频大小不是有效值,参考:https://developer.work.weixin.qq.com/document/path/90313#10112/%E4%B8%8A%E4%BC%A0%E7%9A%84%E5%AA%92%E4%BD%93%E6%96%87%E4%BB%B6%E9%99%90%E5%88%B6",
+ "WeChatWork:40013": "不合法的CorpID",
+ "WeChatWork:40014": "不合法的access_token,参考:https://developer.work.weixin.qq.com/document/path/90313#10649/%E9%94%99%E8%AF%AF%E7%A0%81%EF%BC%9A40014"
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Localization/Resources/zh-Hans.json b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Localization/Resources/zh-Hans.json
new file mode 100644
index 000000000..a1335a61b
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Localization/Resources/zh-Hans.json
@@ -0,0 +1,56 @@
+{
+ "culture": "zh-Hans",
+ "texts": {
+ "Features:WeChatWork": "企业微信",
+ "Features:WeChatWorkEnable": "启用企业微信",
+ "Features:WeChatWorkEnableDesc": "启用以使应用拥有企业微信的能力.",
+ "Features:Message": "企业微信消息推送",
+ "Features:MessageEnable": "启用企业微信消息推送",
+ "Features:MessageEnableDesc": "启用以使应用将拥有通过企业微信推送到应用消息的能力.",
+ "Features:Message.SendLimit": "企业微信消息推送限制",
+ "Features:Message.SendLimitDesc": "设置以限制企业微信应用消息推送上限.",
+ "Features:Message.SendLimitInterval": "企业微信消息限制周期",
+ "Features:Message.SendLimitIntervalDesc": "设置企业微信消息限制周期(时间刻度: 天).每应用不可超过账号上限数*200人次/天.",
+ "DisplayName:WeChatWork.Connection.CorpId": "企业Id",
+ "Description:WeChatWork.Connection.CorpId": "每个企业都拥有唯一的corpid,获取此信息可在管理后台“我的企业”-“企业信息”下查看“企业ID”(需要有管理员权限)",
+ "DisplayName:WeChatWork.EnabledQuickLogin": "启用快捷登录",
+ "Description:WeChatWork.EnabledQuickLogin": "用户可在未注册时通过扫码得到的code直接登录",
+ "WeChatWork:100400": "处理企业微信服务器消息失败,请检查应用签名配置!",
+ "WeChatWork:100404": "没有找到标识 {AgentId} 的内部应用配置!",
+ "WeChatWork:101404": "没有找到应用 {AgentId} 所属功能 {Feture} 的加解密配置!",
+ "WeChatWork:-31020": "redirect_uri 与配置的登录授权调域名不一致",
+ "WeChatWork:-31027": "appid 参数错误",
+ "WeChatWork:-31028": "agentid 参数错误",
+ "WeChatWork:-31033": "校验请求来源错误",
+ "WeChatWork:-31034": "该企业不是服务商",
+ "WeChatWork:-31035": "redirect_uri 不能为空",
+ "WeChatWork:-31037": "appid 非登录授权应用",
+ "WeChatWork:-31039": "redirect_uri 与配置的可信域名不一致",
+ "WeChatWork:-31040": "login_type 参数错误",
+ "WeChatWork:-40001": "签名验证错误",
+ "WeChatWork:-40002": "xml/json解析失败",
+ "WeChatWork:-40003": "sha加密生成签名失败",
+ "WeChatWork:-40004": "AESKey 非法",
+ "WeChatWork:-40005": "ReceiveId 校验错误",
+ "WeChatWork:-40006": "AES 加密失败",
+ "WeChatWork:-40007": "AES 解密失败",
+ "WeChatWork:-40008": "解密后得到的buffer非法",
+ "WeChatWork:-40009": "base64加密失败",
+ "WeChatWork:-40010": "base64解密失败",
+ "WeChatWork:-40011": "生成xml/json失败",
+ "WeChatWork:-1": "系统繁忙, 服务器暂不可用,建议稍候重试。建议重试次数不超过3次!",
+ "WeChatWork:0": "请求成功",
+ "WeChatWork:6000": "数据版本冲突,可能有多个调用端同时修改数据,稍后重试!",
+ "WeChatWork:40001": "不合法的secret参数,参考:https://developer.work.weixin.qq.com/document/path/90313#%E9%94%99%E8%AF%AF%E7%A0%81%EF%BC%9A40001",
+ "WeChatWork:40003": "无效的UserID,参考:https://developer.work.weixin.qq.com/document/path/90313#10649/%E9%94%99%E8%AF%AF%E7%A0%81%EF%BC%9A40003",
+ "WeChatWork:40004": "不合法的媒体文件类型,参考:https://developer.work.weixin.qq.com/document/path/90313#10112",
+ "WeChatWork:40005": "不合法的type参数, 参考:https://developer.work.weixin.qq.com/document/path/90313#10112",
+ "WeChatWork:40006": "不合法的文件大小,参考:https://developer.work.weixin.qq.com/document/path/90313#10112",
+ "WeChatWork:40007": "不合法的media_id参数,参考:https://developer.work.weixin.qq.com/document/path/90313#10649/%E9%94%99%E8%AF%AF%E7%A0%81%EF%BC%9A40007",
+ "WeChatWork:40008": "不合法的msgtype参数,参考:https://developer.work.weixin.qq.com/document/path/90313#10167",
+ "WeChatWork:40009": "上传图片大小不是有效值,参考:https://developer.work.weixin.qq.com/document/path/90313#10112/%E4%B8%8A%E4%BC%A0%E7%9A%84%E5%AA%92%E4%BD%93%E6%96%87%E4%BB%B6%E9%99%90%E5%88%B6",
+ "WeChatWork:40011": "上传视频大小不是有效值,参考:https://developer.work.weixin.qq.com/document/path/90313#10112/%E4%B8%8A%E4%BC%A0%E7%9A%84%E5%AA%92%E4%BD%93%E6%96%87%E4%BB%B6%E9%99%90%E5%88%B6",
+ "WeChatWork:40013": "不合法的CorpID",
+ "WeChatWork:40014": "不合法的access_token,参考:https://developer.work.weixin.qq.com/document/path/90313#10649/%E9%94%99%E8%AF%AF%E7%A0%81%EF%BC%9A40014"
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Localization/WeChatWorkResource.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Localization/WeChatWorkResource.cs
new file mode 100644
index 000000000..ff459d036
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Localization/WeChatWorkResource.cs
@@ -0,0 +1,8 @@
+using Volo.Abp.Localization;
+
+namespace LINGYUN.Abp.WeChat.Work.Localization;
+
+[LocalizationResourceName("AbpWeChatWork")]
+public class WeChatWorkResource
+{
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Media/IWeChatWorkMediaProvider.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Media/IWeChatWorkMediaProvider.cs
new file mode 100644
index 000000000..b00e7cbd1
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Media/IWeChatWorkMediaProvider.cs
@@ -0,0 +1,59 @@
+using System.Threading;
+using System.Threading.Tasks;
+using Volo.Abp.Content;
+
+namespace LINGYUN.Abp.WeChat.Work.Media;
+///
+/// 素材管理接口
+///
+///
+/// API:
+///
+public interface IWeChatWorkMediaProvider
+{
+ ///
+ /// 上传临时素材
+ ///
+ ///
+ /// API:
+ ///
+ /// 应用标识
+ /// 媒体文件类型
+ /// 待上传文件
+ ///
+ ///
+ Task UploadAsync(
+ string agentId,
+ string type,
+ IRemoteStreamContent media,
+ CancellationToken cancellationToken = default);
+ ///
+ /// 获取临时素材
+ ///
+ ///
+ /// API:
+ ///
+ /// 应用标识
+ /// 媒体文件id
+ ///
+ ///
+ ///
+ Task GetAsync(
+ string agentId,
+ string mediaId,
+ CancellationToken cancellationToken = default);
+ ///
+ /// 上传图片
+ ///
+ ///
+ /// API:
+ ///
+ /// 应用标识
+ /// 待上传图片
+ ///
+ ///
+ Task UploadImageAsync(
+ string agentId,
+ IRemoteStreamContent image,
+ CancellationToken cancellationToken = default);
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Media/WeChatWorkImageResponse.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Media/WeChatWorkImageResponse.cs
new file mode 100644
index 000000000..2306c16c4
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Media/WeChatWorkImageResponse.cs
@@ -0,0 +1,11 @@
+using Newtonsoft.Json;
+
+namespace LINGYUN.Abp.WeChat.Work.Media;
+public class WeChatWorkImageResponse : WeChatWorkResponse
+{
+ ///
+ /// 上传后得到的图片URL。永久有效
+ ///
+ [JsonProperty("url")]
+ public string Url { get; set; }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Media/WeChatWorkMediaProvider.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Media/WeChatWorkMediaProvider.cs
new file mode 100644
index 000000000..09f95dda8
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Media/WeChatWorkMediaProvider.cs
@@ -0,0 +1,117 @@
+using LINGYUN.Abp.WeChat.Work.Token;
+using Newtonsoft.Json;
+using System;
+using System.Linq;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Volo.Abp.Content;
+using Volo.Abp.DependencyInjection;
+
+namespace LINGYUN.Abp.WeChat.Work.Media;
+public class WeChatWorkMediaProvider : IWeChatWorkMediaProvider, ISingletonDependency
+{
+ protected IHttpClientFactory HttpClientFactory { get; }
+ protected IWeChatWorkTokenProvider WeChatWorkTokenProvider { get; }
+
+ public WeChatWorkMediaProvider(
+ IHttpClientFactory httpClientFactory,
+ IWeChatWorkTokenProvider weChatWorkTokenProvider)
+ {
+ HttpClientFactory = httpClientFactory;
+ WeChatWorkTokenProvider = weChatWorkTokenProvider;
+ }
+
+ public async virtual Task GetAsync(
+ string agentId,
+ string mediaId,
+ CancellationToken cancellationToken = default)
+ {
+ var token = await WeChatWorkTokenProvider.GetTokenAsync(agentId, cancellationToken);
+ var client = HttpClientFactory.CreateClient(AbpWeChatWorkGlobalConsts.ApiClient);
+
+ using var response = await client.GetMediaAsync(
+ token.AccessToken,
+ mediaId,
+ cancellationToken);
+ if (!response.IsSuccessStatusCode)
+ {
+ var responseContent = await response.Content.ReadAsStringAsync();
+ var errorResponse = JsonConvert.DeserializeObject(responseContent);
+ errorResponse.ThrowIfNotSuccess();
+ }
+
+ var mediaStream = await response.Content.ReadAsStreamAsync();
+ string fileName = null;
+ string contentType = null;
+
+ if (response.Headers.TryGetValues("Content-Disposition", out var contentDispositions))
+ {
+ var contentDisposition = contentDispositions.FirstOrDefault();
+ if (!contentDisposition.IsNullOrWhiteSpace())
+ {
+ var startIndex = contentDisposition.IndexOf("filename=", StringComparison.OrdinalIgnoreCase);
+ if (startIndex >= 0)
+ {
+ startIndex += "filename=".Length;
+ var endIndex = contentDisposition.IndexOf(";", startIndex);
+ if (endIndex < 0)
+ {
+ endIndex = contentDisposition.Length;
+ }
+ fileName = contentDisposition.Substring(startIndex, endIndex - startIndex).Trim('\"');
+ }
+ }
+ }
+
+ if (response.Headers.TryGetValues("Content-Type", out var contentTypes))
+ {
+ contentType = contentTypes.FirstOrDefault();
+ }
+
+ return new RemoteStreamContent(
+ mediaStream,
+ fileName: fileName,
+ contentType: contentType);
+ }
+
+ public async virtual Task UploadAsync(
+ string agentId,
+ string type,
+ IRemoteStreamContent media,
+ CancellationToken cancellationToken = default)
+ {
+ var token = await WeChatWorkTokenProvider.GetTokenAsync(agentId, cancellationToken);
+ var client = HttpClientFactory.CreateClient(AbpWeChatWorkGlobalConsts.ApiClient);
+
+ var request = new WeChatWorkMediaRequest(
+ token.AccessToken,
+ media);
+
+ using var response = await client.UploadMediaAsync(type, request, cancellationToken);
+ var responseContent = await response.Content.ReadAsStringAsync();
+ var mediaRespose = JsonConvert.DeserializeObject(responseContent);
+ mediaRespose.ThrowIfNotSuccess();
+
+ return mediaRespose;
+ }
+
+ public async virtual Task UploadImageAsync(
+ string agentId,
+ IRemoteStreamContent image,
+ CancellationToken cancellationToken = default)
+ {
+ var token = await WeChatWorkTokenProvider.GetTokenAsync(agentId, cancellationToken);
+ var client = HttpClientFactory.CreateClient(AbpWeChatWorkGlobalConsts.ApiClient);
+ var request = new WeChatWorkMediaRequest(
+ token.AccessToken,
+ image);
+
+ using var response = await client.UploadImageAsync(request, cancellationToken);
+ var responseContent = await response.Content.ReadAsStringAsync();
+ var mediaRespose = JsonConvert.DeserializeObject(responseContent);
+ mediaRespose.ThrowIfNotSuccess();
+
+ return mediaRespose;
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Media/WeChatWorkMediaRequest.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Media/WeChatWorkMediaRequest.cs
new file mode 100644
index 000000000..f258b0cc5
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Media/WeChatWorkMediaRequest.cs
@@ -0,0 +1,15 @@
+using Volo.Abp.Content;
+
+namespace LINGYUN.Abp.WeChat.Work.Media;
+public class WeChatWorkMediaRequest
+{
+ public string AccessToken { get; set; }
+ public IRemoteStreamContent Content { get; set; }
+ public WeChatWorkMediaRequest(
+ string accessToken,
+ IRemoteStreamContent content)
+ {
+ AccessToken = accessToken;
+ Content = content;
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Media/WeChatWorkMediaResponse.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Media/WeChatWorkMediaResponse.cs
new file mode 100644
index 000000000..e29f0e0bd
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Media/WeChatWorkMediaResponse.cs
@@ -0,0 +1,27 @@
+using Newtonsoft.Json;
+
+namespace LINGYUN.Abp.WeChat.Work.Media;
+public class WeChatWorkMediaResponse : WeChatWorkResponse
+{
+ ///
+ /// 媒体文件类型
+ ///
+ ///
+ /// 图片(image)
+ /// 语音(voice)
+ /// 视频(video)
+ /// 普通文件(file)
+ ///
+ [JsonProperty("type")]
+ public string Type { get; set; }
+ ///
+ /// 媒体文件上传后获取的唯一标识,3天内有效
+ ///
+ [JsonProperty("media_id")]
+ public string MediaId { get; set; }
+ ///
+ /// 媒体文件上传时间戳
+ ///
+ [JsonProperty("created_at")]
+ public string CreatedAt { get; set; }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/IWeChatWorkMessageSender.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/IWeChatWorkMessageSender.cs
new file mode 100644
index 000000000..672f7365b
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/IWeChatWorkMessageSender.cs
@@ -0,0 +1,19 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.WeChat.Work.Message;
+///
+/// 消息发送接口
+///
+public interface IWeChatWorkMessageSender
+{
+ ///
+ /// 发送消息
+ ///
+ /// 继承自 的企业微信消息载体
+ ///
+ ///
+ Task SendAsync(
+ WeChatWorkMessage message,
+ CancellationToken cancellationToken = default);
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/Models/MarkdownMessage.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/Models/MarkdownMessage.cs
new file mode 100644
index 000000000..8bca936fa
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/Models/MarkdownMessage.cs
@@ -0,0 +1,22 @@
+using JetBrains.Annotations;
+using Newtonsoft.Json;
+using System.Text.Json.Serialization;
+
+namespace LINGYUN.Abp.WeChat.Work.Message.Models;
+///
+/// markdown消息
+///
+public class MarkdownMessage
+{
+ ///
+ /// markdown内容,最长不超过2048个字节,必须是utf8编码
+ ///
+ [NotNull]
+ [JsonProperty("content")]
+ [JsonPropertyName("content")]
+ public string Content { get; set; }
+ public MarkdownMessage(string content)
+ {
+ Content = content;
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/Models/MediaMessage.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/Models/MediaMessage.cs
new file mode 100644
index 000000000..21356887b
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/Models/MediaMessage.cs
@@ -0,0 +1,22 @@
+using JetBrains.Annotations;
+using Newtonsoft.Json;
+using System.Text.Json.Serialization;
+
+namespace LINGYUN.Abp.WeChat.Work.Message.Models;
+///
+/// 媒体文件消息
+///
+public class MediaMessage
+{
+ ///
+ ///媒体文件id,可以调用上传临时素材接口获取
+ ///
+ [NotNull]
+ [JsonProperty("media_id")]
+ [JsonPropertyName("media_id")]
+ public string MediaId { get; set; }
+ public MediaMessage(string mediaId)
+ {
+ MediaId = mediaId;
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/Models/MpNewMessage.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/Models/MpNewMessage.cs
new file mode 100644
index 000000000..ac619a940
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/Models/MpNewMessage.cs
@@ -0,0 +1,87 @@
+using JetBrains.Annotations;
+using Newtonsoft.Json;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace LINGYUN.Abp.WeChat.Work.Message.Models;
+///
+/// 图文消息(mp)载体
+///
+public class MpNewMessagePayload
+{
+ ///
+ /// 图文消息(mp)列表
+ ///
+ [NotNull]
+ [JsonProperty("articles")]
+ [JsonPropertyName("articles")]
+ public List Articles { get; set; }
+ public MpNewMessagePayload(List articles)
+ {
+ Articles = articles;
+ }
+}
+///
+/// 图文消息(mp)
+///
+public class MpNewMessage
+{
+ public MpNewMessage(
+ string title,
+ string thumbMediaId,
+ string content,
+ string author = "",
+ string contentSourceUrl = "",
+ string description = "")
+ {
+ Title = title;
+ ThumbMediaId = thumbMediaId;
+ Author = author;
+ ContentSourceUrl = contentSourceUrl;
+ Content = content;
+ Description = description;
+ }
+
+ ///
+ /// 标题,不超过128个字节,超过会自动截断(支持id转译)
+ ///
+ [NotNull]
+ [JsonProperty("title")]
+ [JsonPropertyName("title")]
+ public string Title { get; set; }
+ ///
+ /// 图文消息缩略图的media_id, 可以通过素材管理接口获得。此处thumb_media_id即上传接口返回的media_id
+ ///
+ [NotNull]
+ [JsonProperty("thumb_media_id")]
+ [JsonPropertyName("thumb_media_id")]
+ public string ThumbMediaId { get; set; }
+ ///
+ /// 图文消息的作者,不超过64个字节
+ ///
+ [CanBeNull]
+ [JsonProperty("author")]
+ [JsonPropertyName("author")]
+ public string Author { get; set; }
+ ///
+ /// 图文消息点击“阅读原文”之后的页面链接
+ ///
+ [CanBeNull]
+ [JsonProperty("content_source_url")]
+ [JsonPropertyName("content_source_url")]
+ public string ContentSourceUrl { get; set; }
+ ///
+ /// 图文消息的内容,支持html标签,不超过666 K个字节(支持id转译)
+ ///
+ [NotNull]
+ [JsonProperty("content")]
+ [JsonPropertyName("content")]
+ public string Content { get; set; }
+ ///
+ /// 图文消息的描述,不超过512个字节,超过会自动截断(支持id转译)
+ ///
+ [CanBeNull]
+ [JsonProperty("digest")]
+ [JsonPropertyName("digest")]
+ public string Description { get; set; }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/Models/NewMessage.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/Models/NewMessage.cs
new file mode 100644
index 000000000..8c6d5f451
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/Models/NewMessage.cs
@@ -0,0 +1,88 @@
+using JetBrains.Annotations;
+using Newtonsoft.Json;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace LINGYUN.Abp.WeChat.Work.Message.Models;
+///
+/// 图文消息载体
+///
+public class NewMessagePayload
+{
+ ///
+ /// 图文消息列表
+ ///
+ [NotNull]
+ [JsonProperty("articles")]
+ [JsonPropertyName("articles")]
+ public List Articles { get; set; }
+ public NewMessagePayload(List articles)
+ {
+ Articles = articles;
+ }
+}
+///
+/// 图文消息
+///
+public class NewMessage
+{
+ public NewMessage(
+ string title,
+ string description = "",
+ string url = "",
+ string pictureUrl = "",
+ string appId = "",
+ string pagePath = "")
+ {
+ Title = title;
+ Description = description;
+ Url = url;
+ PictureUrl = pictureUrl;
+ AppId = appId;
+ PagePath = pagePath;
+ }
+
+ ///
+ /// 标题,不超过128个字节,超过会自动截断(支持id转译)
+ ///
+ [NotNull]
+ [JsonProperty("title")]
+ [JsonPropertyName("title")]
+ public string Title { get; set; }
+ ///
+ /// 描述,不超过512个字节,超过会自动截断(支持id转译)
+ ///
+ [CanBeNull]
+ [JsonProperty("description")]
+ [JsonPropertyName("description")]
+ public string Description { get; set; }
+ ///
+ /// 点击后跳转的链接。
+ /// 最长2048字节,请确保包含了协议头(http/https),小程序或者url必须填写一个
+ ///
+ [CanBeNull]
+ [JsonProperty("url")]
+ [JsonPropertyName("url")]
+ public string Url { get; set; }
+ ///
+ /// 图文消息的图片链接,最长2048字节,支持JPG、PNG格式,较好的效果为大图 1068*455,小图150*150。
+ ///
+ [CanBeNull]
+ [JsonProperty("picurl")]
+ [JsonPropertyName("picurl")]
+ public string PictureUrl { get; set; }
+ ///
+ /// 小程序appid,必须是与当前应用关联的小程序,appid和pagepath必须同时填写,填写后会忽略url字段
+ ///
+ [CanBeNull]
+ [JsonProperty("appid")]
+ [JsonPropertyName("appid")]
+ public string AppId { get; set; }
+ ///
+ /// 点击消息卡片后的小程序页面,最长128字节,仅限本小程序内的页面。appid和pagepath必须同时填写,填写后会忽略url字段
+ ///
+ [CanBeNull]
+ [JsonProperty("pagepath")]
+ [JsonPropertyName("pagepath")]
+ public string PagePath { get; set; }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/Models/TextCardMessage.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/Models/TextCardMessage.cs
new file mode 100644
index 000000000..5f7dc231c
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/Models/TextCardMessage.cs
@@ -0,0 +1,51 @@
+using JetBrains.Annotations;
+using Newtonsoft.Json;
+using System.Text.Json.Serialization;
+
+namespace LINGYUN.Abp.WeChat.Work.Message.Models;
+///
+/// 文本卡片消息
+///
+public class TextCardMessage
+{
+ public TextCardMessage(
+ string title,
+ string description,
+ string url,
+ string buttonText = "")
+ {
+ Title = title;
+ Description = description;
+ Url = url;
+ ButtonText = buttonText;
+ }
+
+ ///
+ /// 标题,不超过128个字节,超过会自动截断(支持id转译)
+ ///
+ [NotNull]
+ [JsonProperty("title")]
+ [JsonPropertyName("title")]
+ public string Title { get; set; }
+ ///
+ /// 描述,不超过512个字节,超过会自动截断(支持id转译)
+ ///
+ [NotNull]
+ [JsonProperty("description")]
+ [JsonPropertyName("description")]
+ public string Description { get; set; }
+ ///
+ /// 点击后跳转的链接。最长2048字节,请确保包含了协议头(http/https)
+ ///
+ [NotNull]
+ [JsonProperty("url")]
+ [JsonPropertyName("url")]
+ public string Url { get; set; }
+ ///
+ /// 按钮文字。 默认为“详情”, 不超过4个文字,超过自动截断。
+ ///
+ [CanBeNull]
+ [JsonProperty("btntxt")]
+ [JsonPropertyName("btntxt")]
+ public string ButtonText { get; set; }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/Models/TextMessage.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/Models/TextMessage.cs
new file mode 100644
index 000000000..12b22bfae
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/Models/TextMessage.cs
@@ -0,0 +1,23 @@
+using JetBrains.Annotations;
+using Newtonsoft.Json;
+using System.Text.Json.Serialization;
+
+namespace LINGYUN.Abp.WeChat.Work.Message.Models;
+///
+/// 文本消息
+///
+public class TextMessage
+{
+ public TextMessage(string content)
+ {
+ Content = content;
+ }
+
+ ///
+ /// 消息内容,最长不超过2048个字节,超过将截断(支持id转译)
+ ///
+ [NotNull]
+ [JsonProperty("content")]
+ [JsonPropertyName("content")]
+ public string Content { get; set; }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/Models/VideoMessage.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/Models/VideoMessage.cs
new file mode 100644
index 000000000..7e2ff5902
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/Models/VideoMessage.cs
@@ -0,0 +1,42 @@
+using JetBrains.Annotations;
+using Newtonsoft.Json;
+using System.Text.Json.Serialization;
+
+namespace LINGYUN.Abp.WeChat.Work.Message.Models;
+///
+/// 视频消息
+///
+public class VideoMessage
+{
+ public VideoMessage(
+ string mediaId,
+ string title = "",
+ string description = "")
+ {
+ Title = title;
+ Description = description;
+ MediaId = mediaId;
+ }
+
+ ///
+ /// 视频消息的标题,不超过128个字节,超过会自动截断
+ ///
+ [CanBeNull]
+ [JsonProperty("title")]
+ [JsonPropertyName("title")]
+ public string Title { get; set; }
+ ///
+ /// 视频消息的描述,不超过512个字节,超过会自动截断
+ ///
+ [CanBeNull]
+ [JsonProperty("description")]
+ [JsonPropertyName("description")]
+ public string Description { get; set; }
+ ///
+ /// 视频媒体文件id,可以调用上传临时素材接口获取
+ ///
+ [NotNull]
+ [JsonProperty("media_id")]
+ [JsonPropertyName("media_id")]
+ public string MediaId { get; set; }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkFileMessage.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkFileMessage.cs
new file mode 100644
index 000000000..132cafbfd
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkFileMessage.cs
@@ -0,0 +1,46 @@
+using JetBrains.Annotations;
+using LINGYUN.Abp.WeChat.Work.Message.Models;
+using Newtonsoft.Json;
+using System.Text.Json.Serialization;
+
+namespace LINGYUN.Abp.WeChat.Work.Message;
+///
+/// 企业微信文件消息
+///
+public class WeChatWorkFileMessage : WeChatWorkMessage
+{
+ public WeChatWorkFileMessage(
+ string agentId,
+ MediaMessage file) : base(agentId, "file")
+ {
+ File = file;
+ }
+ ///
+ /// 媒体文件
+ ///
+ [NotNull]
+ [JsonProperty("file")]
+ [JsonPropertyName("file")]
+ public MediaMessage File { get; set; }
+ ///
+ /// 表示是否是保密消息,
+ /// 0表示可对外分享,
+ /// 1表示不能分享且内容显示水印,
+ /// 默认为0
+ ///
+ [JsonProperty("safe")]
+ [JsonPropertyName("safe")]
+ public int Safe { get; set; }
+ ///
+ /// 表示是否开启重复消息检查,0表示否,1表示是,默认0
+ ///
+ [JsonProperty("enable_duplicate_check")]
+ [JsonPropertyName("enable_duplicate_check")]
+ public byte EnableDuplicateCheck { get; set; }
+ ///
+ /// 表示是否重复消息检查的时间间隔,默认1800s,最大不超过4小时
+ ///
+ [JsonProperty("duplicate_check_interval")]
+ [JsonPropertyName("duplicate_check_interval")]
+ public int DuplicateCheckInterval { get; set; } = 1800;
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkImageMessage.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkImageMessage.cs
new file mode 100644
index 000000000..4fe42e8f6
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkImageMessage.cs
@@ -0,0 +1,54 @@
+using JetBrains.Annotations;
+using LINGYUN.Abp.WeChat.Work.Message.Models;
+using Newtonsoft.Json;
+using System.Text.Json.Serialization;
+
+namespace LINGYUN.Abp.WeChat.Work.Message;
+///
+/// 企业微信图片消息
+///
+public class WeChatWorkImageMessage : WeChatWorkMessage
+{
+ public WeChatWorkImageMessage(
+ string agentId,
+ MediaMessage image) : base(agentId, "image")
+ {
+ Image = image;
+ }
+ ///
+ /// 图片媒体文件
+ ///
+ [NotNull]
+ [JsonProperty("image")]
+ [JsonPropertyName("image")]
+ public MediaMessage Image { get; set; }
+ ///
+ /// 表示是否是保密消息,
+ /// 0表示可对外分享,
+ /// 1表示不能分享且内容显示水印,
+ /// 默认为0
+ ///
+ [JsonProperty("safe")]
+ [JsonPropertyName("safe")]
+ public int Safe { get; set; }
+ ///
+ /// 表示是否开启id转译,0表示否,1表示是,默认0。
+ /// 仅第三方应用需要用到
+ /// 企业自建应用可以忽略。
+ ///
+ [JsonProperty("enable_id_trans")]
+ [JsonPropertyName("enable_id_trans")]
+ public byte EnableIdTrans { get; set; }
+ ///
+ /// 表示是否开启重复消息检查,0表示否,1表示是,默认0
+ ///
+ [JsonProperty("enable_duplicate_check")]
+ [JsonPropertyName("enable_duplicate_check")]
+ public byte EnableDuplicateCheck { get; set; }
+ ///
+ /// 表示是否重复消息检查的时间间隔,默认1800s,最大不超过4小时
+ ///
+ [JsonProperty("duplicate_check_interval")]
+ [JsonPropertyName("duplicate_check_interval")]
+ public int DuplicateCheckInterval { get; set; } = 1800;
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMarkdownMessage.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMarkdownMessage.cs
new file mode 100644
index 000000000..9fcf261db
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMarkdownMessage.cs
@@ -0,0 +1,37 @@
+using JetBrains.Annotations;
+using LINGYUN.Abp.WeChat.Work.Message.Models;
+using Newtonsoft.Json;
+using System.Text.Json.Serialization;
+
+namespace LINGYUN.Abp.WeChat.Work.Message;
+///
+/// 企业微信markdown消息
+///
+public class WeChatWorkMarkdownMessage : WeChatWorkMessage
+{
+ public WeChatWorkMarkdownMessage(
+ string agentId,
+ MarkdownMessage markdown) : base(agentId, "markdown")
+ {
+ Markdown = markdown;
+ }
+ ///
+ /// markdown消息
+ ///
+ [NotNull]
+ [JsonProperty("markdown")]
+ [JsonPropertyName("markdown")]
+ public MarkdownMessage Markdown { get; set; }
+ ///
+ /// 表示是否开启重复消息检查,0表示否,1表示是,默认0
+ ///
+ [JsonProperty("enable_duplicate_check")]
+ [JsonPropertyName("enable_duplicate_check")]
+ public byte EnableDuplicateCheck { get; set; }
+ ///
+ /// 表示是否重复消息检查的时间间隔,默认1800s,最大不超过4小时
+ ///
+ [JsonProperty("duplicate_check_interval")]
+ [JsonPropertyName("duplicate_check_interval")]
+ public int DuplicateCheckInterval { get; set; } = 1800;
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMessage.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMessage.cs
new file mode 100644
index 000000000..a09da91ab
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMessage.cs
@@ -0,0 +1,66 @@
+using JetBrains.Annotations;
+using Newtonsoft.Json;
+using System.Text.Json.Serialization;
+
+namespace LINGYUN.Abp.WeChat.Work.Message;
+///
+/// 企业微信消息
+///
+public abstract class WeChatWorkMessage
+{
+ ///
+ /// 指定接收消息的成员,成员ID列表(多个接收者用‘|’分隔,最多支持1000个)。
+ /// 特殊情况:指定为"@all",则向该企业应用的全部成员发送
+ ///
+ [JsonProperty("touser")]
+ [JsonPropertyName("touser")]
+ public virtual string ToUser { get; set; }
+ ///
+ /// 指定接收消息的部门,部门ID列表,多个接收者用‘|’分隔,最多支持100个。
+ /// 当touser为"@all"时忽略本参数
+ ///
+ [JsonProperty("toparty")]
+ [JsonPropertyName("toparty")]
+ public virtual string ToParty { get; set; }
+ ///
+ /// 指定接收消息的标签,标签ID列表,多个接收者用‘|’分隔,最多支持100个。
+ /// 当touser为"@all"时忽略本参数
+ ///
+ [JsonProperty("totag")]
+ [JsonPropertyName("totag")]
+ public virtual string ToTag { get; set; }
+ ///
+ /// 消息类型
+ ///
+ [NotNull]
+ [JsonProperty("msgtype")]
+ [JsonPropertyName("msgtype")]
+ public virtual string MsgType { get; protected set; }
+ ///
+ /// 企业应用的id,整型。
+ /// 企业内部开发,可在应用的设置页面查看;
+ /// 第三方服务商,可通过接口 获取企业授权信息 获取该参数值
+ ///
+ [NotNull]
+ [JsonProperty("agentid")]
+ [JsonPropertyName("agentid")]
+ public virtual string AgentId { get; protected set; }
+ protected WeChatWorkMessage(
+ string agentId,
+ string msgType,
+ string toUser = "",
+ string toParty = "",
+ string toTag = "")
+ {
+ AgentId = agentId;
+ MsgType = msgType;
+ ToUser = toUser;
+ ToParty = toParty;
+ ToTag = toTag;
+ }
+
+ public virtual string SerializeToJson()
+ {
+ return JsonConvert.SerializeObject(this);
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMessageRequest.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMessageRequest.cs
new file mode 100644
index 000000000..6d1d06a92
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMessageRequest.cs
@@ -0,0 +1,11 @@
+namespace LINGYUN.Abp.WeChat.Work.Message;
+public class WeChatWorkMessageRequest
+{
+ public string AccessToken { get; set; }
+ public WeChatWorkMessage Message { get; set; }
+ public WeChatWorkMessageRequest(string accessToken, WeChatWorkMessage message)
+ {
+ AccessToken = accessToken;
+ Message = message;
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMessageResponse.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMessageResponse.cs
new file mode 100644
index 000000000..0f57f2596
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMessageResponse.cs
@@ -0,0 +1,47 @@
+using Newtonsoft.Json;
+using System.Text.Json.Serialization;
+
+namespace LINGYUN.Abp.WeChat.Work.Message;
+///
+/// 企业微信发送消息响应
+///
+public class WeChatWorkMessageResponse : WeChatWorkResponse
+{
+ ///
+ /// 不合法的userid,不区分大小写,统一转为小写
+ ///
+ [JsonProperty("invaliduser")]
+ [JsonPropertyName("invaliduser")]
+ public string InvalidUser { get; set; }
+ ///
+ /// 不合法的partyid
+ ///
+ [JsonProperty("invalidparty")]
+ [JsonPropertyName("invalidparty")]
+ public string InvalidParty { get; set; }
+ ///
+ /// 不合法的标签id
+ ///
+ [JsonProperty("invalidtag")]
+ [JsonPropertyName("invalidtag")]
+ public string InvalidTag { get; set; }
+ ///
+ /// 没有基础接口许可(包含已过期)的userid
+ ///
+ [JsonProperty("unlicenseduser")]
+ [JsonPropertyName("unlicenseduser")]
+ public string UnLicensedUser { get; set; }
+ ///
+ /// 消息id,用于撤回应用消息
+ ///
+ [JsonProperty("msgid")]
+ [JsonPropertyName("msgid")]
+ public string MsgId { get; set; }
+ ///
+ /// 仅消息类型为“按钮交互型”,“投票选择型”和“多项选择型”的模板卡片消息返回,
+ /// 应用可使用response_code调用更新模版卡片消息接口,72小时内有效,且只能使用一次
+ ///
+ [JsonProperty("response_code")]
+ [JsonPropertyName("response_code")]
+ public string ResponseCode { get; set; }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMessageSender.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMessageSender.cs
new file mode 100644
index 000000000..987944a89
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMessageSender.cs
@@ -0,0 +1,59 @@
+using LINGYUN.Abp.Features.LimitValidation;
+using LINGYUN.Abp.WeChat.Work.Features;
+using LINGYUN.Abp.WeChat.Work.Token;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Newtonsoft.Json;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Features;
+
+namespace LINGYUN.Abp.WeChat.Work.Message;
+
+[RequiresFeature(WeChatWorkFeatureNames.Enable)]
+public class WeChatWorkMessageSender : IWeChatWorkMessageSender, ISingletonDependency
+{
+ public ILogger Logger { get; set; }
+
+ protected IHttpClientFactory HttpClientFactory { get; }
+ protected IWeChatWorkTokenProvider WeChatWorkTokenProvider { get; }
+
+ public WeChatWorkMessageSender(
+ IHttpClientFactory httpClientFactory,
+ IWeChatWorkTokenProvider weChatWorkTokenProvider)
+ {
+ HttpClientFactory = httpClientFactory;
+ WeChatWorkTokenProvider = weChatWorkTokenProvider;
+
+ Logger = NullLogger.Instance;
+ }
+
+ [RequiresFeature(WeChatWorkFeatureNames.Message.Enable)]
+ [RequiresLimitFeature(
+ WeChatWorkFeatureNames.Message.SendLimit,
+ WeChatWorkFeatureNames.Message.SendLimitInterval,
+ LimitPolicy.Days)]
+ public async virtual Task SendAsync(WeChatWorkMessage message, CancellationToken cancellationToken = default)
+ {
+ var token = await WeChatWorkTokenProvider.GetTokenAsync(message.AgentId, cancellationToken);
+ var client = HttpClientFactory.CreateClient(AbpWeChatWorkGlobalConsts.ApiClient);
+
+ var request = new WeChatWorkMessageRequest(
+ token.AccessToken,
+ message);
+
+ using var response = await client.SendMessageAsync(request, cancellationToken);
+ var responseContent = await response.Content.ReadAsStringAsync();
+
+ var messageResponse = JsonConvert.DeserializeObject(responseContent);
+ if (!messageResponse.IsSuccessed)
+ {
+ Logger.LogWarning("Send wechat work message failed");
+ Logger.LogWarning($"Error code: {messageResponse.ErrorCode}, message: {messageResponse.ErrorMessage}");
+ }
+
+ return messageResponse;
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMpNewMessage.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMpNewMessage.cs
new file mode 100644
index 000000000..8973c1d1b
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMpNewMessage.cs
@@ -0,0 +1,55 @@
+using JetBrains.Annotations;
+using LINGYUN.Abp.WeChat.Work.Message.Models;
+using Newtonsoft.Json;
+using System.Text.Json.Serialization;
+
+namespace LINGYUN.Abp.WeChat.Work.Message;
+///
+/// 企业微信文本图文消息
+///
+public class WeChatWorkMpNewMessage : WeChatWorkMessage
+{
+ public WeChatWorkMpNewMessage(
+ string agentId,
+ MpNewMessagePayload mpnews) : base(agentId, "mpnews")
+ {
+ News = mpnews;
+ }
+ ///
+ /// 图文消息(mp)
+ ///
+ [NotNull]
+ [JsonProperty("mpnews")]
+ [JsonPropertyName("mpnews")]
+ public MpNewMessagePayload News { get; set; }
+ ///
+ /// 表示是否是保密消息,
+ /// 0表示可对外分享,
+ /// 1表示不能分享且内容显示水印,
+ /// 2表示仅限在企业内分享,默认为0;
+ /// 注意仅mpnews类型的消息支持safe值为2,其他消息类型不支持
+ ///
+ [JsonProperty("safe")]
+ [JsonPropertyName("safe")]
+ public int Safe { get; set; }
+ ///
+ /// 表示是否开启id转译,0表示否,1表示是,默认0。
+ /// 仅第三方应用需要用到
+ /// 企业自建应用可以忽略。
+ ///
+ [JsonProperty("enable_id_trans")]
+ [JsonPropertyName("enable_id_trans")]
+ public byte EnableIdTrans { get; set; }
+ ///
+ /// 表示是否开启重复消息检查,0表示否,1表示是,默认0
+ ///
+ [JsonProperty("enable_duplicate_check")]
+ [JsonPropertyName("enable_duplicate_check")]
+ public byte EnableDuplicateCheck { get; set; }
+ ///
+ /// 表示是否重复消息检查的时间间隔,默认1800s,最大不超过4小时
+ ///
+ [JsonProperty("duplicate_check_interval")]
+ [JsonPropertyName("duplicate_check_interval")]
+ public int DuplicateCheckInterval { get; set; } = 1800;
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkNewMessage.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkNewMessage.cs
new file mode 100644
index 000000000..0877ae832
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkNewMessage.cs
@@ -0,0 +1,45 @@
+using JetBrains.Annotations;
+using LINGYUN.Abp.WeChat.Work.Message.Models;
+using Newtonsoft.Json;
+using System.Text.Json.Serialization;
+
+namespace LINGYUN.Abp.WeChat.Work.Message;
+///
+/// 企业微信文本图文消息
+///
+public class WeChatWorkNewMessage : WeChatWorkMessage
+{
+ public WeChatWorkNewMessage(
+ string agentId,
+ NewMessagePayload news) : base(agentId, "news")
+ {
+ News = news;
+ }
+ ///
+ /// 图文消息
+ ///
+ [NotNull]
+ [JsonProperty("news")]
+ [JsonPropertyName("news")]
+ public NewMessagePayload News { get; set; }
+ ///
+ /// 表示是否开启id转译,0表示否,1表示是,默认0。
+ /// 仅第三方应用需要用到
+ /// 企业自建应用可以忽略。
+ ///
+ [JsonProperty("enable_id_trans")]
+ [JsonPropertyName("enable_id_trans")]
+ public byte EnableIdTrans { get; set; }
+ ///
+ /// 表示是否开启重复消息检查,0表示否,1表示是,默认0
+ ///
+ [JsonProperty("enable_duplicate_check")]
+ [JsonPropertyName("enable_duplicate_check")]
+ public byte EnableDuplicateCheck { get; set; }
+ ///
+ /// 表示是否重复消息检查的时间间隔,默认1800s,最大不超过4小时
+ ///
+ [JsonProperty("duplicate_check_interval")]
+ [JsonPropertyName("duplicate_check_interval")]
+ public int DuplicateCheckInterval { get; set; } = 1800;
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkTextCardMessage.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkTextCardMessage.cs
new file mode 100644
index 000000000..202f00c4e
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkTextCardMessage.cs
@@ -0,0 +1,45 @@
+using JetBrains.Annotations;
+using LINGYUN.Abp.WeChat.Work.Message.Models;
+using Newtonsoft.Json;
+using System.Text.Json.Serialization;
+
+namespace LINGYUN.Abp.WeChat.Work.Message;
+///
+/// 企业微信文本卡片消息
+///
+public class WeChatWorkTextCardMessage : WeChatWorkMessage
+{
+ public WeChatWorkTextCardMessage(
+ string agentId,
+ TextCardMessage textcard) : base(agentId, "textcard")
+ {
+ TextCard = textcard;
+ }
+ ///
+ /// 卡片消息
+ ///
+ [NotNull]
+ [JsonProperty("textcard")]
+ [JsonPropertyName("textcard")]
+ public TextCardMessage TextCard { get; set; }
+ ///
+ /// 表示是否开启id转译,0表示否,1表示是,默认0。
+ /// 仅第三方应用需要用到
+ /// 企业自建应用可以忽略。
+ ///
+ [JsonProperty("enable_id_trans")]
+ [JsonPropertyName("enable_id_trans")]
+ public byte EnableIdTrans { get; set; }
+ ///
+ /// 表示是否开启重复消息检查,0表示否,1表示是,默认0
+ ///
+ [JsonProperty("enable_duplicate_check")]
+ [JsonPropertyName("enable_duplicate_check")]
+ public byte EnableDuplicateCheck { get; set; }
+ ///
+ /// 表示是否重复消息检查的时间间隔,默认1800s,最大不超过4小时
+ ///
+ [JsonProperty("duplicate_check_interval")]
+ [JsonPropertyName("duplicate_check_interval")]
+ public int DuplicateCheckInterval { get; set; } = 1800;
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkTextMessage.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkTextMessage.cs
new file mode 100644
index 000000000..53bcaaae1
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkTextMessage.cs
@@ -0,0 +1,55 @@
+using JetBrains.Annotations;
+using LINGYUN.Abp.WeChat.Work.Message.Models;
+using Newtonsoft.Json;
+using System.Text.Json.Serialization;
+
+namespace LINGYUN.Abp.WeChat.Work.Message;
+///
+/// 企业微信文本消息
+///
+public class WeChatWorkTextMessage : WeChatWorkMessage
+{
+ public WeChatWorkTextMessage(
+ string agentId,
+ TextMessage text) : base(agentId, "text")
+ {
+ Text = text;
+ }
+
+ ///
+ /// 消息内容,最长不超过2048个字节,超过将截断(支持id转译)
+ ///
+ [NotNull]
+ [JsonProperty("text")]
+ [JsonPropertyName("text")]
+ public TextMessage Text { get; set; }
+ ///
+ /// 表示是否是保密消息,
+ /// 0表示可对外分享,
+ /// 1表示不能分享且内容显示水印,
+ /// 默认为0
+ ///
+ [JsonProperty("safe")]
+ [JsonPropertyName("safe")]
+ public int Safe { get; set; }
+ ///
+ /// 表示是否开启id转译,0表示否,1表示是,默认0。
+ /// 仅第三方应用需要用到
+ /// 企业自建应用可以忽略。
+ ///
+ [JsonProperty("enable_id_trans")]
+ [JsonPropertyName("enable_id_trans")]
+ public byte EnableIdTrans { get; set; }
+ ///
+ /// 表示是否开启重复消息检查,0表示否,1表示是,默认0
+ ///
+ [JsonProperty("enable_duplicate_check")]
+ [JsonPropertyName("enable_duplicate_check")]
+ public byte EnableDuplicateCheck { get; set; }
+ ///
+ /// 表示是否重复消息检查的时间间隔,默认1800s,最大不超过4小时
+ ///
+ [JsonProperty("duplicate_check_interval")]
+ [JsonPropertyName("duplicate_check_interval")]
+ public int DuplicateCheckInterval { get; set; } = 1800;
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkVideoMessage.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkVideoMessage.cs
new file mode 100644
index 000000000..cbda294bb
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkVideoMessage.cs
@@ -0,0 +1,46 @@
+using JetBrains.Annotations;
+using LINGYUN.Abp.WeChat.Work.Message.Models;
+using Newtonsoft.Json;
+using System.Text.Json.Serialization;
+
+namespace LINGYUN.Abp.WeChat.Work.Message;
+///
+/// 企业微信语言消息
+///
+public class WeChatWorkVideoMessage : WeChatWorkMessage
+{
+ public WeChatWorkVideoMessage(
+ string agentId,
+ VideoMessage video) : base(agentId, "video")
+ {
+ Video = video;
+ }
+ ///
+ /// 视频媒体文件
+ ///
+ [NotNull]
+ [JsonProperty("video")]
+ [JsonPropertyName("video")]
+ public VideoMessage Video { get; set; }
+ ///
+ /// 表示是否是保密消息,
+ /// 0表示可对外分享,
+ /// 1表示不能分享且内容显示水印,
+ /// 默认为0
+ ///
+ [JsonProperty("safe")]
+ [JsonPropertyName("safe")]
+ public int Safe { get; set; }
+ ///
+ /// 表示是否开启重复消息检查,0表示否,1表示是,默认0
+ ///
+ [JsonProperty("enable_duplicate_check")]
+ [JsonPropertyName("enable_duplicate_check")]
+ public byte EnableDuplicateCheck { get; set; }
+ ///
+ /// 表示是否重复消息检查的时间间隔,默认1800s,最大不超过4小时
+ ///
+ [JsonProperty("duplicate_check_interval")]
+ [JsonPropertyName("duplicate_check_interval")]
+ public int DuplicateCheckInterval { get; set; } = 1800;
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkVoiceMessage.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkVoiceMessage.cs
new file mode 100644
index 000000000..7d905dbc0
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkVoiceMessage.cs
@@ -0,0 +1,37 @@
+using JetBrains.Annotations;
+using LINGYUN.Abp.WeChat.Work.Message.Models;
+using Newtonsoft.Json;
+using System.Text.Json.Serialization;
+
+namespace LINGYUN.Abp.WeChat.Work.Message;
+///
+/// 企业微信语言消息
+///
+public class WeChatWorkVoiceMessage : WeChatWorkMessage
+{
+ public WeChatWorkVoiceMessage(
+ string agentId,
+ MediaMessage voice) : base(agentId, "voice")
+ {
+ Voice = voice;
+ }
+ ///
+ /// 语音媒体文件
+ ///
+ [NotNull]
+ [JsonProperty("voice")]
+ [JsonPropertyName("voice")]
+ public MediaMessage Voice { get; set; }
+ ///
+ /// 表示是否开启重复消息检查,0表示否,1表示是,默认0
+ ///
+ [JsonProperty("enable_duplicate_check")]
+ [JsonPropertyName("enable_duplicate_check")]
+ public byte EnableDuplicateCheck { get; set; }
+ ///
+ /// 表示是否重复消息检查的时间间隔,默认1800s,最大不超过4小时
+ ///
+ [JsonProperty("duplicate_check_interval")]
+ [JsonPropertyName("duplicate_check_interval")]
+ public int DuplicateCheckInterval { get; set; } = 1800;
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/AbpWeChatWorkCryptoException.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/AbpWeChatWorkCryptoException.cs
new file mode 100644
index 000000000..51e99a5c4
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/AbpWeChatWorkCryptoException.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace LINGYUN.Abp.WeChat.Work.Security;
+public class AbpWeChatWorkCryptoException : AbpWeChatWorkException
+{
+ public AbpWeChatWorkCryptoException()
+ {
+ }
+
+ public AbpWeChatWorkCryptoException(
+ SerializationInfo serializationInfo,
+ StreamingContext context) : base(serializationInfo, context)
+ {
+ }
+
+ public AbpWeChatWorkCryptoException(
+ string agentId,
+ string message = null,
+ string details = null,
+ Exception innerException = null)
+ : this(agentId, "WeChatWork:100400", message, details, innerException)
+ {
+ }
+
+ public AbpWeChatWorkCryptoException(
+ string agentId,
+ string code = null,
+ string message = null,
+ string details = null,
+ Exception innerException = null)
+ : base(code, message, details, innerException)
+ {
+ WithData("AgentId", agentId);
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/Claims/AbpWeChatWorkClaimTypes.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/Claims/AbpWeChatWorkClaimTypes.cs
new file mode 100644
index 000000000..ec550581b
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/Claims/AbpWeChatWorkClaimTypes.cs
@@ -0,0 +1,8 @@
+namespace LINGYUN.Abp.WeChat.Work.Security.Claims;
+public static class AbpWeChatWorkClaimTypes
+{
+ ///
+ /// 用户的唯一标识
+ ///
+ public static string UserId { get; set; } = "wecom-uid"; // 可变更
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/IWeChatWorkCryptoService.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/IWeChatWorkCryptoService.cs
new file mode 100644
index 000000000..1f55f93b4
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/IWeChatWorkCryptoService.cs
@@ -0,0 +1,28 @@
+namespace LINGYUN.Abp.WeChat.Work.Security;
+///
+/// 企业微信加解密接口
+///
+public interface IWeChatWorkCryptoService
+{
+ ///
+ /// 校验
+ ///
+ ///
+ ///
+ ///
+ string Validation(WeChatWorkCryptoEchoData data);
+ ///
+ /// 解密
+ ///
+ ///
+ ///
+ ///
+ string Decrypt(WeChatWorkCryptoDecryptData data);
+ ///
+ /// 加密
+ ///
+ ///
+ ///
+ ///
+ string Encrypt(WeChatWorkCryptoData data);
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/WeChatWorkCryptoConfiguration.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/WeChatWorkCryptoConfiguration.cs
new file mode 100644
index 000000000..8b0f90a4d
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/WeChatWorkCryptoConfiguration.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+
+namespace LINGYUN.Abp.WeChat.Work.Security;
+///
+/// 企业微信加解密配置
+///
+public class WeChatWorkCryptoConfiguration : Dictionary
+{
+ ///
+ /// 用于生成签名的Token
+ ///
+ public string Token {
+ get => this.GetOrDefault(nameof(Token));
+ set => this[nameof(Token)] = value;
+ }
+
+ ///
+ /// 用于消息加密的密钥
+ ///
+ public string EncodingAESKey {
+ get => this.GetOrDefault(nameof(EncodingAESKey));
+ set => this[nameof(EncodingAESKey)] = value;
+ }
+
+ public WeChatWorkCryptoConfiguration()
+ {
+
+ }
+
+ public WeChatWorkCryptoConfiguration(string token, string encodingAESKey)
+ {
+ this[nameof(Token)] = token;
+ this[nameof(EncodingAESKey)] = encodingAESKey;
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/WeChatWorkCryptoConfigurationDictionary.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/WeChatWorkCryptoConfigurationDictionary.cs
new file mode 100644
index 000000000..5bc33ad5b
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/WeChatWorkCryptoConfigurationDictionary.cs
@@ -0,0 +1,12 @@
+using JetBrains.Annotations;
+using System.Collections.Generic;
+
+namespace LINGYUN.Abp.WeChat.Work.Security;
+public class WeChatWorkCryptoConfigurationDictionary : Dictionary
+{
+ [CanBeNull]
+ public WeChatWorkCryptoConfiguration GetCryptoConfigurationOrNull(string feture)
+ {
+ return this.GetOrDefault(feture);
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/WeChatWorkCryptoData.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/WeChatWorkCryptoData.cs
new file mode 100644
index 000000000..c293b0229
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/WeChatWorkCryptoData.cs
@@ -0,0 +1,25 @@
+namespace LINGYUN.Abp.WeChat.Work.Security;
+public class WeChatWorkCryptoData
+{
+ public string ReceiveId { get; }
+ public string Token { get; }
+ public string EncodingAESKey { get; }
+ public string MsgSignature { get; }
+ public string TimeStamp { get; }
+ public string Nonce { get; }
+ public WeChatWorkCryptoData(
+ string receiveId,
+ string token,
+ string encodingAESKey,
+ string msgSignature,
+ string timeStamp,
+ string nonce)
+ {
+ ReceiveId = receiveId;
+ Token = token;
+ EncodingAESKey = encodingAESKey;
+ MsgSignature = msgSignature;
+ TimeStamp = timeStamp;
+ Nonce = nonce;
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/WeChatWorkCryptoDecryptData.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/WeChatWorkCryptoDecryptData.cs
new file mode 100644
index 000000000..87cb6c873
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/WeChatWorkCryptoDecryptData.cs
@@ -0,0 +1,16 @@
+namespace LINGYUN.Abp.WeChat.Work.Security;
+public class WeChatWorkCryptoDecryptData : WeChatWorkCryptoData
+{
+ public string PostData { get; }
+ public WeChatWorkCryptoDecryptData(
+ string postData,
+ string receiveId,
+ string token,
+ string encodingAESKey,
+ string msgSignature,
+ string timeStamp,
+ string nonce) : base(receiveId, token, encodingAESKey, msgSignature, timeStamp, nonce)
+ {
+ PostData = postData;
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/WeChatWorkCryptoEchoData.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/WeChatWorkCryptoEchoData.cs
new file mode 100644
index 000000000..882c956af
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/WeChatWorkCryptoEchoData.cs
@@ -0,0 +1,16 @@
+namespace LINGYUN.Abp.WeChat.Work.Security;
+public class WeChatWorkCryptoEchoData : WeChatWorkCryptoData
+{
+ public string EchoStr { get; }
+ public WeChatWorkCryptoEchoData(
+ string echoStr,
+ string receiveId,
+ string token,
+ string encodingAESKey,
+ string msgSignature,
+ string timeStamp,
+ string nonce) : base(receiveId, token, encodingAESKey, msgSignature, timeStamp, nonce)
+ {
+ EchoStr = echoStr;
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/WeChatWorkCryptoService.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/WeChatWorkCryptoService.cs
new file mode 100644
index 000000000..61eb94a67
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Security/WeChatWorkCryptoService.cs
@@ -0,0 +1,76 @@
+using LINGYUN.Abp.WeChat.Work.Utils;
+using Volo.Abp.DependencyInjection;
+
+namespace LINGYUN.Abp.WeChat.Work.Security;
+public class WeChatWorkCryptoService : IWeChatWorkCryptoService, ISingletonDependency
+{
+ public string Decrypt(WeChatWorkCryptoDecryptData data)
+ {
+ var crypto = new WXBizMsgCrypt(
+ data.Token,
+ data.EncodingAESKey,
+ data.ReceiveId);
+
+ var retMsg = "";
+ var ret = crypto.DecryptMsg(
+ data.MsgSignature,
+ data.TimeStamp,
+ data.Nonce,
+ data.PostData,
+ ref retMsg);
+
+ if (ret != 0)
+ {
+ throw new AbpWeChatWorkCryptoException(data.ReceiveId, code: $"WeChatWork:{ret}");
+ }
+
+ return retMsg;
+ }
+
+ public string Encrypt(WeChatWorkCryptoData data)
+ {
+ var crypto = new WXBizMsgCrypt(
+ data.Token,
+ data.EncodingAESKey,
+ data.ReceiveId);
+
+ var retMsg = "";
+
+ var ret = crypto.EncryptMsg(
+ data.MsgSignature,
+ data.TimeStamp,
+ data.Nonce,
+ ref retMsg);
+
+ if (ret != 0)
+ {
+ throw new AbpWeChatWorkCryptoException(data.ReceiveId, code: $"WeChatWork:{ret}");
+ }
+
+ return retMsg;
+ }
+
+ public string Validation(WeChatWorkCryptoEchoData data)
+ {
+ var crypto = new WXBizMsgCrypt(
+ data.Token,
+ data.EncodingAESKey,
+ data.ReceiveId);
+
+ var retMsg = "";
+
+ var ret = crypto.VerifyURL(
+ data.MsgSignature,
+ data.TimeStamp,
+ data.Nonce,
+ data.EchoStr,
+ ref retMsg);
+
+ if (ret != 0)
+ {
+ throw new AbpWeChatWorkCryptoException(data.ReceiveId, code: $"WeChatWork:{ret}");
+ }
+
+ return retMsg;
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Settings/WeChatWorkSettingDefinitionProvider.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Settings/WeChatWorkSettingDefinitionProvider.cs
new file mode 100644
index 000000000..c2269d1af
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Settings/WeChatWorkSettingDefinitionProvider.cs
@@ -0,0 +1,51 @@
+using LINGYUN.Abp.WeChat.Work.Localization;
+using Volo.Abp.Localization;
+using Volo.Abp.Settings;
+
+namespace LINGYUN.Abp.WeChat.Work.Settings
+{
+ public class WeChatWorkSettingDefinitionProvider : SettingDefinitionProvider
+ {
+ public override void Define(ISettingDefinitionContext context)
+ {
+ context.Add(
+ new SettingDefinition(
+ WeChatWorkSettingNames.EnabledQuickLogin,
+ // 默认启用
+ true.ToString(),
+ L("DisplayName:WeChatWork.EnabledQuickLogin"),
+ L("Description:WeChatWork.EnabledQuickLogin"),
+ isVisibleToClients: true,
+ isEncrypted: false)
+ .WithProviders(
+ DefaultValueSettingValueProvider.ProviderName,
+ ConfigurationSettingValueProvider.ProviderName,
+ GlobalSettingValueProvider.ProviderName,
+ TenantSettingValueProvider.ProviderName)
+ );
+ context.Add(GetConnectionSettings());
+ }
+
+ protected virtual SettingDefinition[] GetConnectionSettings()
+ {
+ return new[]
+ {
+ new SettingDefinition(
+ WeChatWorkSettingNames.Connection.CorpId,
+ displayName: L("DisplayName:WeChatWork.Connection.CorpId"),
+ description: L("Description:WeChatWork.Connection.CorpId"),
+ isEncrypted: false)
+ .WithProviders(
+ DefaultValueSettingValueProvider.ProviderName,
+ ConfigurationSettingValueProvider.ProviderName,
+ GlobalSettingValueProvider.ProviderName,
+ TenantSettingValueProvider.ProviderName),
+ };
+ }
+
+ protected ILocalizableString L(string name)
+ {
+ return LocalizableString.Create(name);
+ }
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Settings/WeChatWorkSettingNames.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Settings/WeChatWorkSettingNames.cs
new file mode 100644
index 000000000..6ca0fe582
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Settings/WeChatWorkSettingNames.cs
@@ -0,0 +1,19 @@
+namespace LINGYUN.Abp.WeChat.Work.Settings
+{
+ public static class WeChatWorkSettingNames
+ {
+ public const string Prefix = "Abp.WeChat.Work";
+
+ ///
+ /// 启用快捷登录
+ ///
+ public const string EnabledQuickLogin = Prefix + ".EnabledQuickLogin";
+
+ public static class Connection
+ {
+ public const string Prefix = WeChatWorkSettingNames.Prefix + ".Connection";
+
+ public static string CorpId = Prefix + ".CorpId";
+ }
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Token/IWeChatWorkTokenProvider.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Token/IWeChatWorkTokenProvider.cs
new file mode 100644
index 000000000..1c96116ef
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Token/IWeChatWorkTokenProvider.cs
@@ -0,0 +1,11 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.WeChat.Work.Token
+{
+ public interface IWeChatWorkTokenProvider
+ {
+ Task GetTokenAsync(string agentId, CancellationToken cancellationToken = default);
+ Task GetTokenAsync(string corpId, string agentId, CancellationToken cancellationToken = default);
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Token/WeChatWorkToken.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Token/WeChatWorkToken.cs
new file mode 100644
index 000000000..b3b29795f
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Token/WeChatWorkToken.cs
@@ -0,0 +1,26 @@
+namespace LINGYUN.Abp.WeChat.Work.Token
+{
+ ///
+ /// 企业微信令牌
+ ///
+ public class WeChatWorkToken
+ {
+ ///
+ /// 访问令牌
+ ///
+ public string AccessToken { get; set; }
+ ///
+ /// 过期时间,单位(s)
+ ///
+ public int ExpiresIn { get; set; }
+ public WeChatWorkToken()
+ {
+
+ }
+ public WeChatWorkToken(string token, int expiresIn)
+ {
+ AccessToken = token;
+ ExpiresIn = expiresIn;
+ }
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Token/WeChatWorkTokenCacheItem.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Token/WeChatWorkTokenCacheItem.cs
new file mode 100644
index 000000000..49b817ca4
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Token/WeChatWorkTokenCacheItem.cs
@@ -0,0 +1,28 @@
+namespace LINGYUN.Abp.WeChat.Work.Token
+{
+ public class WeChatWorkTokenCacheItem
+ {
+ public string CorpId { get; set; }
+
+ public string AgentId { get; set; }
+
+ public WeChatWorkToken Token { get; set; }
+
+ public WeChatWorkTokenCacheItem()
+ {
+
+ }
+
+ public WeChatWorkTokenCacheItem(string corpId, string agentId, WeChatWorkToken token)
+ {
+ CorpId = corpId;
+ AgentId = agentId;
+ Token = token;
+ }
+
+ public static string CalculateCacheKey(string provider, string corpId, string agentId)
+ {
+ return "p:" + provider + ",cp:" + corpId + ",ag:" + agentId;
+ }
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Token/WeChatWorkTokenProvider.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Token/WeChatWorkTokenProvider.cs
new file mode 100644
index 000000000..a85312de1
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Token/WeChatWorkTokenProvider.cs
@@ -0,0 +1,108 @@
+using LINGYUN.Abp.WeChat.Work.Settings;
+using Microsoft.Extensions.Caching.Distributed;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using Newtonsoft.Json;
+using System;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Volo.Abp;
+using Volo.Abp.Caching;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Settings;
+
+namespace LINGYUN.Abp.WeChat.Work.Token
+{
+ public class WeChatWorkTokenProvider : IWeChatWorkTokenProvider, ISingletonDependency
+ {
+ public ILogger Logger { get; set; }
+ protected ISettingProvider SettingProvider { get; }
+ protected IHttpClientFactory HttpClientFactory { get; }
+ protected IDistributedCache Cache { get; }
+ protected WeChatWorkOptions WeChatWorkOptions { get; }
+ public WeChatWorkTokenProvider(
+ ISettingProvider settingProvider,
+ IHttpClientFactory httpClientFactory,
+ IDistributedCache cache,
+ IOptionsMonitor weChatWorkOptions)
+ {
+ HttpClientFactory = httpClientFactory;
+ SettingProvider = settingProvider;
+ Cache = cache;
+ WeChatWorkOptions = weChatWorkOptions.CurrentValue;
+
+ Logger = NullLogger.Instance;
+ }
+
+ public async virtual Task GetTokenAsync(string agentId, CancellationToken cancellationToken = default)
+ {
+ var corpId = await SettingProvider.GetOrNullAsync(WeChatWorkSettingNames.Connection.CorpId);
+
+ return await GetTokenAsync(corpId, agentId, cancellationToken);
+ }
+
+ public async virtual Task GetTokenAsync(
+ string corpId,
+ string agentId,
+ CancellationToken cancellationToken = default)
+ {
+ return (await GetCacheItemAsync("WeChatWorkToken", corpId, agentId, cancellationToken)).Token;
+ }
+
+ protected async virtual Task GetCacheItemAsync(
+ string provider,
+ string corpId,
+ string agentId,
+ CancellationToken cancellationToken = default)
+ {
+ Check.NotNullOrEmpty(corpId, nameof(corpId));
+ Check.NotNullOrEmpty(agentId, nameof(agentId));
+
+ var cacheKey = WeChatWorkTokenCacheItem.CalculateCacheKey(provider, corpId, agentId);
+
+ Logger.LogDebug($"WeChatWorkTokenProvider.GetCacheItemAsync: {cacheKey}");
+
+ var cacheItem = await Cache.GetAsync(cacheKey, token: cancellationToken);
+
+ if (cacheItem != null)
+ {
+ Logger.LogDebug($"Found WeChatWorkToken in the cache: {cacheKey}");
+ return cacheItem;
+ }
+
+ Logger.LogDebug($"Not found WeChatWorkToken in the cache, getting from the httpClient: {cacheKey}");
+
+ var client = HttpClientFactory.CreateClient(AbpWeChatWorkGlobalConsts.ApiClient);
+ var applicationConfiguration = WeChatWorkOptions.Applications.GetConfiguration(agentId);
+
+ var request = new WeChatWorkTokenRequest
+ {
+ CorpId = corpId,
+ CorpSecret = applicationConfiguration.Secret,
+ };
+
+ using var response = await client.GetTokenAsync(request, cancellationToken);
+ var responseContent = await response.Content.ReadAsStringAsync();
+ // 改为直接引用 Newtownsoft.Json
+ var tokenResponse = JsonConvert.DeserializeObject(responseContent);
+ var token = tokenResponse.ToWeChatWorkToken();
+ cacheItem = new WeChatWorkTokenCacheItem(corpId, agentId, token);
+
+ Logger.LogDebug($"Setting the cache item: {cacheKey}");
+
+ var cacheOptions = new DistributedCacheEntryOptions
+ {
+ // 设置绝对过期时间为Token有效期剩余的二分钟
+ AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(token.ExpiresIn - 120),
+ };
+
+ await Cache.SetAsync(cacheKey, cacheItem, cacheOptions, token: cancellationToken);
+
+ Logger.LogDebug($"Finished setting the cache item: {cacheKey}");
+
+ return cacheItem;
+ }
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Token/WeChatWorkTokenRequest.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Token/WeChatWorkTokenRequest.cs
new file mode 100644
index 000000000..09438ff45
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Token/WeChatWorkTokenRequest.cs
@@ -0,0 +1,8 @@
+namespace LINGYUN.Abp.WeChat.Work.Token
+{
+ public class WeChatWorkTokenRequest
+ {
+ public string CorpId { get; set; }
+ public string CorpSecret { get; set; }
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Token/WeChatWorkTokenResponse.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Token/WeChatWorkTokenResponse.cs
new file mode 100644
index 000000000..6ae521ca5
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Token/WeChatWorkTokenResponse.cs
@@ -0,0 +1,27 @@
+using Newtonsoft.Json;
+
+namespace LINGYUN.Abp.WeChat.Work.Token
+{
+ ///
+ /// 微信访问令牌返回对象
+ ///
+ public class WeChatWorkTokenResponse : WeChatWorkResponse
+ {
+ ///
+ /// 访问令牌
+ ///
+ [JsonProperty("access_token")]
+ public string AccessToken { get; set; }
+ ///
+ /// 过期时间,单位(s)
+ ///
+ [JsonProperty("expires_in")]
+ public int ExpiresIn { get; set; }
+
+ public WeChatWorkToken ToWeChatWorkToken()
+ {
+ ThrowIfNotSuccess();
+ return new WeChatWorkToken(AccessToken, ExpiresIn);
+ }
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Utils/Cryptography.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Utils/Cryptography.cs
new file mode 100644
index 000000000..35f014b7f
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Utils/Cryptography.cs
@@ -0,0 +1,233 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Security.Cryptography;
+using System.IO;
+using System.Net;
+
+namespace LINGYUN.Abp.WeChat.Work.Utils
+{
+ internal class Cryptography
+ {
+ public static UInt32 HostToNetworkOrder(UInt32 inval)
+ {
+ UInt32 outval = 0;
+ for (int i = 0; i < 4; i++)
+ outval = (outval << 8) + ((inval >> (i * 8)) & 255);
+ return outval;
+ }
+
+ public static Int32 HostToNetworkOrder(Int32 inval)
+ {
+ Int32 outval = 0;
+ for (int i = 0; i < 4; i++)
+ outval = (outval << 8) + ((inval >> (i * 8)) & 255);
+ return outval;
+ }
+ ///
+ /// 解密方法
+ ///
+ /// 密文
+ ///
+ ///
+ ///
+ public static string AES_decrypt(String Input, string EncodingAESKey, ref string corpid)
+ {
+ byte[] Key;
+ Key = Convert.FromBase64String(EncodingAESKey + "=");
+ byte[] Iv = new byte[16];
+ Array.Copy(Key, Iv, 16);
+ byte[] btmpMsg = AES_decrypt(Input, Iv, Key);
+
+ int len = BitConverter.ToInt32(btmpMsg, 16);
+ len = IPAddress.NetworkToHostOrder(len);
+
+
+ byte[] bMsg = new byte[len];
+ byte[] bCorpid = new byte[btmpMsg.Length - 20 - len];
+ Array.Copy(btmpMsg, 20, bMsg, 0, len);
+ Array.Copy(btmpMsg, 20+len , bCorpid, 0, btmpMsg.Length - 20 - len);
+ string oriMsg = Encoding.UTF8.GetString(bMsg);
+ corpid = Encoding.UTF8.GetString(bCorpid);
+
+
+ return oriMsg;
+ }
+
+ public static String AES_encrypt(String Input, string EncodingAESKey, string corpid)
+ {
+ byte[] Key;
+ Key = Convert.FromBase64String(EncodingAESKey + "=");
+ byte[] Iv = new byte[16];
+ Array.Copy(Key, Iv, 16);
+ string Randcode = CreateRandCode(16);
+ byte[] bRand = Encoding.UTF8.GetBytes(Randcode);
+ byte[] bCorpid = Encoding.UTF8.GetBytes(corpid);
+ byte[] btmpMsg = Encoding.UTF8.GetBytes(Input);
+ byte[] bMsgLen = BitConverter.GetBytes(HostToNetworkOrder(btmpMsg.Length));
+ byte[] bMsg = new byte[bRand.Length + bMsgLen.Length + bCorpid.Length + btmpMsg.Length];
+
+ Array.Copy(bRand, bMsg, bRand.Length);
+ Array.Copy(bMsgLen, 0, bMsg, bRand.Length, bMsgLen.Length);
+ Array.Copy(btmpMsg, 0, bMsg, bRand.Length + bMsgLen.Length, btmpMsg.Length);
+ Array.Copy(bCorpid, 0, bMsg, bRand.Length + bMsgLen.Length + btmpMsg.Length, bCorpid.Length);
+
+ return AES_encrypt(bMsg, Iv, Key);
+
+ }
+ private static string CreateRandCode(int codeLen)
+ {
+ string codeSerial = "2,3,4,5,6,7,a,c,d,e,f,h,i,j,k,m,n,p,r,s,t,A,C,D,E,F,G,H,J,K,M,N,P,Q,R,S,U,V,W,X,Y,Z";
+ if (codeLen == 0)
+ {
+ codeLen = 16;
+ }
+ string[] arr = codeSerial.Split(',');
+ string code = "";
+ int randValue = -1;
+ Random rand = new Random(unchecked((int)DateTime.Now.Ticks));
+ for (int i = 0; i < codeLen; i++)
+ {
+ randValue = rand.Next(0, arr.Length - 1);
+ code += arr[randValue];
+ }
+ return code;
+ }
+
+ private static String AES_encrypt(String Input, byte[] Iv, byte[] Key)
+ {
+ var aes = new RijndaelManaged();
+ //秘钥的大小,以位为单位
+ aes.KeySize = 256;
+ //支持的块大小
+ aes.BlockSize = 128;
+ //填充模式
+ aes.Padding = PaddingMode.PKCS7;
+ aes.Mode = CipherMode.CBC;
+ aes.Key = Key;
+ aes.IV = Iv;
+ var encrypt = aes.CreateEncryptor(aes.Key, aes.IV);
+ byte[] xBuff = null;
+
+ using (var ms = new MemoryStream())
+ {
+ using (var cs = new CryptoStream(ms, encrypt, CryptoStreamMode.Write))
+ {
+ byte[] xXml = Encoding.UTF8.GetBytes(Input);
+ cs.Write(xXml, 0, xXml.Length);
+ }
+ xBuff = ms.ToArray();
+ }
+ String Output = Convert.ToBase64String(xBuff);
+ return Output;
+ }
+
+ private static String AES_encrypt(byte[] Input, byte[] Iv, byte[] Key)
+ {
+ var aes = new RijndaelManaged();
+ //秘钥的大小,以位为单位
+ aes.KeySize = 256;
+ //支持的块大小
+ aes.BlockSize = 128;
+ //填充模式
+ //aes.Padding = PaddingMode.PKCS7;
+ aes.Padding = PaddingMode.None;
+ aes.Mode = CipherMode.CBC;
+ aes.Key = Key;
+ aes.IV = Iv;
+ var encrypt = aes.CreateEncryptor(aes.Key, aes.IV);
+ byte[] xBuff = null;
+
+ #region 自己进行PKCS7补位,用系统自己带的不行
+ byte[] msg = new byte[Input.Length + 32 - Input.Length % 32];
+ Array.Copy(Input, msg, Input.Length);
+ byte[] pad = KCS7Encoder(Input.Length);
+ Array.Copy(pad, 0, msg, Input.Length, pad.Length);
+ #endregion
+
+ #region 注释的也是一种方法,效果一样
+ //ICryptoTransform transform = aes.CreateEncryptor();
+ //byte[] xBuff = transform.TransformFinalBlock(msg, 0, msg.Length);
+ #endregion
+
+ using (var ms = new MemoryStream())
+ {
+ using (var cs = new CryptoStream(ms, encrypt, CryptoStreamMode.Write))
+ {
+ cs.Write(msg, 0, msg.Length);
+ }
+ xBuff = ms.ToArray();
+ }
+
+ String Output = Convert.ToBase64String(xBuff);
+ return Output;
+ }
+
+ private static byte[] KCS7Encoder(int text_length)
+ {
+ int block_size = 32;
+ // 计算需要填充的位数
+ int amount_to_pad = block_size - (text_length % block_size);
+ if (amount_to_pad == 0)
+ {
+ amount_to_pad = block_size;
+ }
+ // 获得补位所用的字符
+ char pad_chr = chr(amount_to_pad);
+ string tmp = "";
+ for (int index = 0; index < amount_to_pad; index++)
+ {
+ tmp += pad_chr;
+ }
+ return Encoding.UTF8.GetBytes(tmp);
+ }
+ /**
+ * 将数字转化成ASCII码对应的字符,用于对明文进行补码
+ *
+ * @param a 需要转化的数字
+ * @return 转化得到的字符
+ */
+ static char chr(int a)
+ {
+
+ byte target = (byte)(a & 0xFF);
+ return (char)target;
+ }
+ private static byte[] AES_decrypt(String Input, byte[] Iv, byte[] Key)
+ {
+ RijndaelManaged aes = new RijndaelManaged();
+ aes.KeySize = 256;
+ aes.BlockSize = 128;
+ aes.Mode = CipherMode.CBC;
+ aes.Padding = PaddingMode.None;
+ aes.Key = Key;
+ aes.IV = Iv;
+ var decrypt = aes.CreateDecryptor(aes.Key, aes.IV);
+ byte[] xBuff = null;
+ using (var ms = new MemoryStream())
+ {
+ using (var cs = new CryptoStream(ms, decrypt, CryptoStreamMode.Write))
+ {
+ byte[] xXml = Convert.FromBase64String(Input);
+ byte[] msg = new byte[xXml.Length + 32 - xXml.Length % 32];
+ Array.Copy(xXml, msg, xXml.Length);
+ cs.Write(xXml, 0, xXml.Length);
+ }
+ xBuff = decode2(ms.ToArray());
+ }
+ return xBuff;
+ }
+ private static byte[] decode2(byte[] decrypted)
+ {
+ int pad = (int)decrypted[decrypted.Length - 1];
+ if (pad < 1 || pad > 32)
+ {
+ pad = 0;
+ }
+ byte[] res = new byte[decrypted.Length - pad];
+ Array.Copy(decrypted, 0, res, 0, decrypted.Length - pad);
+ return res;
+ }
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Utils/HttpContentBuildHelper.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Utils/HttpContentBuildHelper.cs
new file mode 100644
index 000000000..190a1ff89
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Utils/HttpContentBuildHelper.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Text;
+
+namespace LINGYUN.Abp.WeChat.Work.Utils;
+
+internal static class HttpContentBuildHelper
+{
+ public static HttpContent BuildUploadMediaContent(
+ string mediaName,
+ byte[] fileBytes,
+ string fileName,
+ string contentType = "application/octet-stream"
+ )
+ {
+ var bytesFileName = Encoding.UTF8.GetBytes(fileName);
+ var bytesHackedFileName = new char[bytesFileName.Length];
+ Array.Copy(bytesFileName, 0, bytesHackedFileName, 0, bytesFileName.Length);
+ var hackedFileName = new string(bytesHackedFileName);
+
+ var fileContent = new ByteArrayContent(fileBytes);
+ fileContent.Headers.ContentDisposition = ContentDispositionHeaderValue.Parse($"form-data; name=\"{mediaName}\"; filename=\"{hackedFileName}\"");
+ fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType);
+ fileContent.Headers.ContentLength = fileBytes.Length;
+
+ var boundary = "--BOUNDARY--" + DateTimeOffset.Now.Ticks.ToString("x");
+ var httpContent = new MultipartFormDataContent(boundary);
+ httpContent.Headers.ContentType = MediaTypeHeaderValue.Parse($"multipart/form-data; boundary={boundary}");
+ httpContent.Add(fileContent);
+
+ return httpContent;
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Utils/WXBizMsgCrypt.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Utils/WXBizMsgCrypt.cs
new file mode 100644
index 000000000..7d34d817c
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Utils/WXBizMsgCrypt.cs
@@ -0,0 +1,259 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Xml;
+using System.Collections;
+//using System.Web;
+using System.Security.Cryptography;
+//-40001 : 签名验证错误
+//-40002 : xml解析失败
+//-40003 : sha加密生成签名失败
+//-40004 : AESKey 非法
+//-40005 : corpid 校验错误
+//-40006 : AES 加密失败
+//-40007 : AES 解密失败
+//-40008 : 解密后得到的buffer非法
+//-40009 : base64加密异常
+//-40010 : base64解密异常
+namespace LINGYUN.Abp.WeChat.Work.Utils
+{
+ internal class WXBizMsgCrypt
+ {
+ string m_sToken;
+ string m_sEncodingAESKey;
+ string m_sReceiveId;
+ enum WXBizMsgCryptErrorCode
+ {
+ WXBizMsgCrypt_OK = 0,
+ WXBizMsgCrypt_ValidateSignature_Error = -40001,
+ WXBizMsgCrypt_ParseXml_Error = -40002,
+ WXBizMsgCrypt_ComputeSignature_Error = -40003,
+ WXBizMsgCrypt_IllegalAesKey = -40004,
+ WXBizMsgCrypt_ValidateCorpid_Error = -40005,
+ WXBizMsgCrypt_EncryptAES_Error = -40006,
+ WXBizMsgCrypt_DecryptAES_Error = -40007,
+ WXBizMsgCrypt_IllegalBuffer = -40008,
+ WXBizMsgCrypt_EncodeBase64_Error = -40009,
+ WXBizMsgCrypt_DecodeBase64_Error = -40010
+ };
+
+ //构造函数
+ // @param sToken: 企业微信后台,开发者设置的Token
+ // @param sEncodingAESKey: 企业微信后台,开发者设置的EncodingAESKey
+ // @param sReceiveId: 不同场景含义不同,详见文档说明
+ public WXBizMsgCrypt(string sToken, string sEncodingAESKey, string sReceiveId)
+ {
+ m_sToken = sToken;
+ m_sReceiveId = sReceiveId;
+ m_sEncodingAESKey = sEncodingAESKey;
+ }
+
+ //验证URL
+ // @param sMsgSignature: 签名串,对应URL参数的msg_signature
+ // @param sTimeStamp: 时间戳,对应URL参数的timestamp
+ // @param sNonce: 随机串,对应URL参数的nonce
+ // @param sEchoStr: 随机串,对应URL参数的echostr
+ // @param sReplyEchoStr: 解密之后的echostr,当return返回0时有效
+ // @return:成功0,失败返回对应的错误码
+ public int VerifyURL(string sMsgSignature, string sTimeStamp, string sNonce, string sEchoStr, ref string sReplyEchoStr)
+ {
+ int ret = 0;
+ if (m_sEncodingAESKey.Length!=43)
+ {
+ return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_IllegalAesKey;
+ }
+ ret = VerifySignature(m_sToken, sTimeStamp, sNonce, sEchoStr, sMsgSignature);
+ if (0 != ret)
+ {
+ return ret;
+ }
+ sReplyEchoStr = "";
+ string cpid = "";
+ try
+ {
+ sReplyEchoStr = Cryptography.AES_decrypt(sEchoStr, m_sEncodingAESKey, ref cpid); //m_sReceiveId);
+ }
+ catch (Exception)
+ {
+ sReplyEchoStr = "";
+ return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_DecryptAES_Error;
+ }
+ if (cpid != m_sReceiveId)
+ {
+ sReplyEchoStr = "";
+ return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ValidateCorpid_Error;
+ }
+ return 0;
+ }
+
+ // 检验消息的真实性,并且获取解密后的明文
+ // @param sMsgSignature: 签名串,对应URL参数的msg_signature
+ // @param sTimeStamp: 时间戳,对应URL参数的timestamp
+ // @param sNonce: 随机串,对应URL参数的nonce
+ // @param sPostData: 密文,对应POST请求的数据
+ // @param sMsg: 解密后的原文,当return返回0时有效
+ // @return: 成功0,失败返回对应的错误码
+ public int DecryptMsg(string sMsgSignature, string sTimeStamp, string sNonce, string sPostData, ref string sMsg)
+ {
+ if (m_sEncodingAESKey.Length!=43)
+ {
+ return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_IllegalAesKey;
+ }
+ XmlDocument doc = new XmlDocument();
+ XmlNode root;
+ string sEncryptMsg;
+ try
+ {
+ doc.LoadXml(sPostData);
+ root = doc.FirstChild;
+ sEncryptMsg = root["Encrypt"].InnerText;
+ }
+ catch (Exception)
+ {
+ return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ParseXml_Error;
+ }
+ //verify signature
+ int ret = 0;
+ ret = VerifySignature(m_sToken, sTimeStamp, sNonce, sEncryptMsg, sMsgSignature);
+ if (ret != 0)
+ return ret;
+ //decrypt
+ string cpid = "";
+ try
+ {
+ sMsg = Cryptography.AES_decrypt(sEncryptMsg, m_sEncodingAESKey, ref cpid);
+ }
+ catch (FormatException)
+ {
+ sMsg = "";
+ return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_DecodeBase64_Error;
+ }
+ catch (Exception)
+ {
+ sMsg = "";
+ return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_DecryptAES_Error;
+ }
+ if (cpid != m_sReceiveId)
+ return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ValidateCorpid_Error;
+ return 0;
+ }
+
+ //将企业号回复用户的消息加密打包
+ // @param sReplyMsg: 企业号待回复用户的消息,xml格式的字符串
+ // @param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp
+ // @param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce
+ // @param sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串,
+ // 当return返回0时有效
+ // return:成功0,失败返回对应的错误码
+ public int EncryptMsg(string sReplyMsg, string sTimeStamp, string sNonce, ref string sEncryptMsg)
+ {
+ if (m_sEncodingAESKey.Length!=43)
+ {
+ return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_IllegalAesKey;
+ }
+ string raw = "";
+ try
+ {
+ raw = Cryptography.AES_encrypt(sReplyMsg, m_sEncodingAESKey, m_sReceiveId);
+ }
+ catch (Exception)
+ {
+ return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_EncryptAES_Error;
+ }
+ string MsgSigature = "";
+ int ret = 0;
+ ret = GenarateSinature(m_sToken, sTimeStamp, sNonce, raw, ref MsgSigature);
+ if (0 != ret)
+ return ret;
+ sEncryptMsg = "";
+
+ string EncryptLabelHead = "";
+ string MsgSigLabelHead = "";
+ string TimeStampLabelHead = "";
+ string NonceLabelHead = "";
+ sEncryptMsg = sEncryptMsg + "" + EncryptLabelHead + raw + EncryptLabelTail;
+ sEncryptMsg = sEncryptMsg + MsgSigLabelHead + MsgSigature + MsgSigLabelTail;
+ sEncryptMsg = sEncryptMsg + TimeStampLabelHead + sTimeStamp + TimeStampLabelTail;
+ sEncryptMsg = sEncryptMsg + NonceLabelHead + sNonce + NonceLabelTail;
+ sEncryptMsg += "";
+ return 0;
+ }
+
+ public class DictionarySort : System.Collections.IComparer
+ {
+ public int Compare(object oLeft, object oRight)
+ {
+ string sLeft = oLeft as string;
+ string sRight = oRight as string;
+ int iLeftLength = sLeft.Length;
+ int iRightLength = sRight.Length;
+ int index = 0;
+ while (index < iLeftLength && index < iRightLength)
+ {
+ if (sLeft[index] < sRight[index])
+ return -1;
+ else if (sLeft[index] > sRight[index])
+ return 1;
+ else
+ index++;
+ }
+ return iLeftLength - iRightLength;
+
+ }
+ }
+ //Verify Signature
+ private static int VerifySignature(string sToken, string sTimeStamp, string sNonce, string sMsgEncrypt, string sSigture)
+ {
+ string hash = "";
+ int ret = 0;
+ ret = GenarateSinature(sToken, sTimeStamp, sNonce, sMsgEncrypt, ref hash);
+ if (ret != 0)
+ return ret;
+ if (hash == sSigture)
+ return 0;
+ else
+ {
+ return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ValidateSignature_Error;
+ }
+ }
+
+ public static int GenarateSinature(string sToken, string sTimeStamp, string sNonce, string sMsgEncrypt ,ref string sMsgSignature)
+ {
+ ArrayList AL = new ArrayList();
+ AL.Add(sToken);
+ AL.Add(sTimeStamp);
+ AL.Add(sNonce);
+ AL.Add(sMsgEncrypt);
+ AL.Sort(new DictionarySort());
+ string raw = "";
+ for (int i = 0; i < AL.Count; ++i)
+ {
+ raw += AL[i];
+ }
+
+ SHA1 sha;
+ ASCIIEncoding enc;
+ string hash = "";
+ try
+ {
+ sha = new SHA1CryptoServiceProvider();
+ enc = new ASCIIEncoding();
+ byte[] dataToHash = enc.GetBytes(raw);
+ byte[] dataHashed = sha.ComputeHash(dataToHash);
+ hash = BitConverter.ToString(dataHashed).Replace("-", "");
+ hash = hash.ToLower();
+ }
+ catch (Exception)
+ {
+ return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ComputeSignature_Error;
+ }
+ sMsgSignature = hash;
+ return 0;
+ }
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/WeChatWorkApplicationConfiguration.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/WeChatWorkApplicationConfiguration.cs
new file mode 100644
index 000000000..9e2a73106
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/WeChatWorkApplicationConfiguration.cs
@@ -0,0 +1,43 @@
+using JetBrains.Annotations;
+using LINGYUN.Abp.WeChat.Work.Security;
+
+namespace LINGYUN.Abp.WeChat.Work;
+///
+/// 企业微信应用配置
+///
+public class WeChatWorkApplicationConfiguration
+{
+ ///
+ /// 应用的标识
+ ///
+ public string AgentId { get; set; }
+ ///
+ /// 应用的凭证密钥
+ ///
+ public string Secret { get; set; }
+ ///
+ /// 应用加密配置
+ ///
+ public WeChatWorkCryptoConfigurationDictionary CryptoKeys { get; set; }
+
+ public WeChatWorkApplicationConfiguration()
+ {
+ CryptoKeys = new WeChatWorkCryptoConfigurationDictionary();
+ }
+
+ public WeChatWorkApplicationConfiguration(string agentId, string secret)
+ {
+ AgentId = agentId;
+ Secret = secret;
+ CryptoKeys = new WeChatWorkCryptoConfigurationDictionary();
+ }
+
+ [NotNull]
+ public WeChatWorkCryptoConfiguration GetCryptoConfiguration(string feture)
+ {
+ return CryptoKeys.GetCryptoConfigurationOrNull(feture)
+ ?? throw new AbpWeChatWorkException("WeChatWork:101404", $"WeChat Work crypto was not found configuration with feture '{feture}' .")
+ .WithData("AgentId", AgentId)
+ .WithData("Feture", feture);
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/WeChatWorkApplicationConfigurationDictionary.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/WeChatWorkApplicationConfigurationDictionary.cs
new file mode 100644
index 000000000..5847d6b00
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/WeChatWorkApplicationConfigurationDictionary.cs
@@ -0,0 +1,14 @@
+using JetBrains.Annotations;
+using System.Collections.Generic;
+
+namespace LINGYUN.Abp.WeChat.Work;
+public class WeChatWorkApplicationConfigurationDictionary : Dictionary
+{
+ [NotNull]
+ public WeChatWorkApplicationConfiguration GetConfiguration(string agentId)
+ {
+ return this.GetOrDefault(agentId)
+ ?? throw new AbpWeChatWorkException("WeChatWork:100404", $"WeChat Work application was not found configuration with agent '{agentId}' .")
+ .WithData("AgentId", agentId);
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/WeChatWorkErrorCodes.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/WeChatWorkErrorCodes.cs
new file mode 100644
index 000000000..feef7e758
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/WeChatWorkErrorCodes.cs
@@ -0,0 +1,5 @@
+namespace LINGYUN.Abp.WeChat.Work;
+public static class WeChatWorkErrorCodes
+{
+ public const string Namespace = "WeChatWork";
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/WeChatWorkOptions.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/WeChatWorkOptions.cs
new file mode 100644
index 000000000..5355b6201
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/WeChatWorkOptions.cs
@@ -0,0 +1,11 @@
+namespace LINGYUN.Abp.WeChat.Work;
+
+public class WeChatWorkOptions
+{
+ public WeChatWorkApplicationConfigurationDictionary Applications { get; set; }
+
+ public WeChatWorkOptions()
+ {
+ Applications = new WeChatWorkApplicationConfigurationDictionary();
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/WeChatWorkResponse.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/WeChatWorkResponse.cs
new file mode 100644
index 000000000..bf1ec0430
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/WeChatWorkResponse.cs
@@ -0,0 +1,32 @@
+using Newtonsoft.Json;
+using System.Text.Json.Serialization;
+
+namespace LINGYUN.Abp.WeChat.Work;
+///
+/// 企业微信请求响应
+///
+public class WeChatWorkResponse
+{
+ ///
+ /// 错误码
+ ///
+ [JsonProperty("errcode")]
+ [JsonPropertyName("errcode")]
+ public int ErrorCode { get; set; }
+ ///
+ /// 错误消息
+ ///
+ [JsonProperty("errmsg")]
+ [JsonPropertyName("errmsg")]
+ public string ErrorMessage { get; set; }
+
+ public bool IsSuccessed => ErrorCode == 0;
+
+ public void ThrowIfNotSuccess()
+ {
+ if (ErrorCode != 0)
+ {
+ throw new AbpWeChatWorkException($"WeChatWork:{ErrorCode}", $"Wechat work request error:{ErrorMessage}");
+ }
+ }
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/README.md b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/README.md
new file mode 100644
index 000000000..64022ba82
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/README.md
@@ -0,0 +1,14 @@
+# LINGYUN.Abp.WeChat.Work
+
+企业微信集成
+
+
+## 配置使用
+
+
+```csharp
+[DependsOn(typeof(AbpWeChatWorkModule))]
+public class YouProjectModule : AbpModule
+{
+ // other
+}
diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/System/Net/Http/HttpClientWeChatWorkRequestExtensions.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/System/Net/Http/HttpClientWeChatWorkRequestExtensions.cs
new file mode 100644
index 000000000..62f63e6e3
--- /dev/null
+++ b/aspnet-core/modules/wechat/LINGYUN.Abp.WeChat.Work/System/Net/Http/HttpClientWeChatWorkRequestExtensions.cs
@@ -0,0 +1,141 @@
+using LINGYUN.Abp.WeChat.Work.Authorize.Request;
+using LINGYUN.Abp.WeChat.Work.Media;
+using LINGYUN.Abp.WeChat.Work.Message;
+using LINGYUN.Abp.WeChat.Work.Token;
+using LINGYUN.Abp.WeChat.Work.Utils;
+using Newtonsoft.Json;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace System.Net.Http
+{
+ internal static class HttpClientWeChatWorkRequestExtensions
+ {
+ public async static Task GetTokenAsync(this HttpMessageInvoker client, WeChatWorkTokenRequest request, CancellationToken cancellationToken = default)
+ {
+ var urlBuilder = new StringBuilder();
+ urlBuilder.Append("/cgi-bin/gettoken");
+ urlBuilder.AppendFormat("?corpid={0}", request.CorpId);
+ urlBuilder.AppendFormat("&corpsecret={0}", request.CorpSecret);
+
+ var httpRequest = new HttpRequestMessage(HttpMethod.Get, urlBuilder.ToString());
+
+ return await client.SendAsync(httpRequest, cancellationToken);
+ }
+
+ public async static Task GetMediaAsync(
+ this HttpMessageInvoker client,
+ string accessToken,
+ string mediaId,
+ CancellationToken cancellationToken = default)
+ {
+ var urlBuilder = new StringBuilder();
+ urlBuilder.Append("/cgi-bin/media/get");
+ urlBuilder.AppendFormat("?access_token={0}", accessToken);
+ urlBuilder.AppendFormat("&media_id={0}", mediaId);
+
+ var httpRequest = new HttpRequestMessage(HttpMethod.Get, urlBuilder.ToString());
+
+ return await client.SendAsync(httpRequest, cancellationToken);
+ }
+
+ public async static Task GetUserInfoAsync(
+ this HttpMessageInvoker client,
+ string accessToken,
+ string code,
+ CancellationToken cancellationToken = default)
+ {
+ var urlBuilder = new StringBuilder();
+ urlBuilder.Append("/cgi-bin/auth/getuserinfo");
+ urlBuilder.AppendFormat("?access_token={0}", accessToken);
+ urlBuilder.AppendFormat("&code={0}", code);
+
+ var httpRequest = new HttpRequestMessage(HttpMethod.Get, urlBuilder.ToString());
+
+ return await client.SendAsync(httpRequest, cancellationToken);
+ }
+
+ public async static Task GetUserDetailAsync(
+ this HttpMessageInvoker client,
+ string accessToken,
+ WeChatWorkUserDetailRequest request,
+ CancellationToken cancellationToken = default)
+ {
+ var urlBuilder = new StringBuilder();
+ urlBuilder.Append("/cgi-bin/auth/getuserdetail");
+ urlBuilder.AppendFormat("?access_token={0}", accessToken);
+
+ var httpRequest = new HttpRequestMessage(
+ HttpMethod.Post,
+ urlBuilder.ToString())
+ {
+ Content = new StringContent(request.SerializeToJson())
+ };
+
+ return await client.SendAsync(httpRequest, cancellationToken);
+ }
+
+ public async static Task UploadMediaAsync(
+ this HttpMessageInvoker client,
+ string type,
+ WeChatWorkMediaRequest request,
+ CancellationToken cancellationToken = default)
+ {
+ var urlBuilder = new StringBuilder();
+ urlBuilder.Append("/cgi-bin/media/upload");
+ urlBuilder.AppendFormat("?access_token={0}", request.AccessToken);
+ urlBuilder.AppendFormat("&type={0}", type);
+
+ var fileBytes = await request.Content.GetStream().GetAllBytesAsync();
+ var httpRequest = new HttpRequestMessage(
+ HttpMethod.Post,
+ urlBuilder.ToString())
+ {
+ Content = HttpContentBuildHelper.BuildUploadMediaContent("media", fileBytes, request.Content.FileName)
+ };
+
+ return await client.SendAsync(httpRequest, cancellationToken);
+ }
+
+ public async static Task UploadImageAsync(
+ this HttpMessageInvoker client,
+ WeChatWorkMediaRequest request,
+ CancellationToken cancellationToken = default)
+ {
+ var urlBuilder = new StringBuilder();
+ urlBuilder.Append("/cgi-bin/media/uploadimg");
+ urlBuilder.AppendFormat("?access_token={0}", request.AccessToken);
+
+ var fileBytes = await request.Content.GetStream().GetAllBytesAsync();
+ var httpRequest = new HttpRequestMessage(
+ HttpMethod.Post,
+ urlBuilder.ToString())
+ {
+ Content = HttpContentBuildHelper.BuildUploadMediaContent("file", fileBytes, request.Content.FileName)
+ };
+
+ return await client.SendAsync(httpRequest, cancellationToken);
+ }
+
+ public async static Task SendMessageAsync(
+ this HttpMessageInvoker client,
+ WeChatWorkMessageRequest request,
+ CancellationToken cancellationToken = default)
+ {
+ var urlBuilder = new StringBuilder();
+ urlBuilder.Append("/cgi-bin/message/send");
+ urlBuilder.AppendFormat("?access_token={0}", request.AccessToken);
+
+ var httpRequest = new HttpRequestMessage(
+ HttpMethod.Post,
+ urlBuilder.ToString())
+ {
+ Content = new StringContent(request.Message.SerializeToJson())
+ };
+
+ return await client.SendAsync(httpRequest, cancellationToken);
+ }
+ }
+}
diff --git a/aspnet-core/services/LY.MicroService.AuthServer/AuthServerModule.cs b/aspnet-core/services/LY.MicroService.AuthServer/AuthServerModule.cs
index df1abe968..9d862fdb9 100644
--- a/aspnet-core/services/LY.MicroService.AuthServer/AuthServerModule.cs
+++ b/aspnet-core/services/LY.MicroService.AuthServer/AuthServerModule.cs
@@ -14,6 +14,7 @@ using LINGYUN.Abp.OpenIddict.LinkUser;
using LINGYUN.Abp.OpenIddict.Portal;
using LINGYUN.Abp.OpenIddict.Sms;
using LINGYUN.Abp.OpenIddict.WeChat;
+using LINGYUN.Abp.OpenIddict.WeChat.Work;
using LINGYUN.Abp.Saas.EntityFrameworkCore;
using LINGYUN.Abp.Serilog.Enrichers.Application;
using LINGYUN.Abp.Serilog.Enrichers.UniqueId;
@@ -61,6 +62,7 @@ namespace LY.MicroService.AuthServer;
typeof(AbpOpenIddictWeChatModule),
typeof(AbpOpenIddictLinkUserModule),
typeof(AbpOpenIddictPortalModule),
+ typeof(AbpOpenIddictWeChatWorkModule),
typeof(AbpAuthenticationQQModule),
typeof(AbpAuthenticationWeChatModule),
typeof(AbpIdentityOrganizaztionUnitsModule),
diff --git a/aspnet-core/services/LY.MicroService.AuthServer/LY.MicroService.AuthServer.csproj b/aspnet-core/services/LY.MicroService.AuthServer/LY.MicroService.AuthServer.csproj
index 3bd90d2d3..09397bc0b 100644
--- a/aspnet-core/services/LY.MicroService.AuthServer/LY.MicroService.AuthServer.csproj
+++ b/aspnet-core/services/LY.MicroService.AuthServer/LY.MicroService.AuthServer.csproj
@@ -65,6 +65,7 @@
+
diff --git a/aspnet-core/services/LY.MicroService.AuthServer/package.json b/aspnet-core/services/LY.MicroService.AuthServer/package.json
index 6e78a8038..7c55ad172 100644
--- a/aspnet-core/services/LY.MicroService.AuthServer/package.json
+++ b/aspnet-core/services/LY.MicroService.AuthServer/package.json
@@ -3,6 +3,6 @@
"name": "my-app-auth-server",
"private": true,
"dependencies": {
- "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "2.3.2"
+ "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "2.3.3"
}
}
\ No newline at end of file
diff --git a/aspnet-core/services/LY.MicroService.PlatformManagement.HttpApi.Host/LY.MicroService.PlatformManagement.HttpApi.Host.csproj b/aspnet-core/services/LY.MicroService.PlatformManagement.HttpApi.Host/LY.MicroService.PlatformManagement.HttpApi.Host.csproj
index acebca681..fc7b32c14 100644
--- a/aspnet-core/services/LY.MicroService.PlatformManagement.HttpApi.Host/LY.MicroService.PlatformManagement.HttpApi.Host.csproj
+++ b/aspnet-core/services/LY.MicroService.PlatformManagement.HttpApi.Host/LY.MicroService.PlatformManagement.HttpApi.Host.csproj
@@ -70,6 +70,17 @@
+
+
+
+
+
+
+ Always
+
+
+ Always
+
diff --git a/aspnet-core/services/LY.MicroService.PlatformManagement.HttpApi.Host/PlatformManagementHttpApiHostModule.Configure.cs b/aspnet-core/services/LY.MicroService.PlatformManagement.HttpApi.Host/PlatformManagementHttpApiHostModule.Configure.cs
index bc2c3e591..9c3f98bc4 100644
--- a/aspnet-core/services/LY.MicroService.PlatformManagement.HttpApi.Host/PlatformManagementHttpApiHostModule.Configure.cs
+++ b/aspnet-core/services/LY.MicroService.PlatformManagement.HttpApi.Host/PlatformManagementHttpApiHostModule.Configure.cs
@@ -165,11 +165,11 @@ public partial class PlatformManagementHttpApiHostModule
});
Configure(options =>
- {
- // 是否发送错误详情
- options.SendExceptionsDetailsToClients = true;
- options.SendStackTraceToClients = true;
- });
+ {
+ // 是否发送错误详情
+ options.SendExceptionsDetailsToClients = false;
+ options.SendStackTraceToClients = false;
+ });
}
private void ConfigureAuditing(IConfiguration configuration)
diff --git a/aspnet-core/services/LY.MicroService.PlatformManagement.HttpApi.Host/PlatformManagementHttpApiHostModule.cs b/aspnet-core/services/LY.MicroService.PlatformManagement.HttpApi.Host/PlatformManagementHttpApiHostModule.cs
index fc8ab81e0..b4c257da0 100644
--- a/aspnet-core/services/LY.MicroService.PlatformManagement.HttpApi.Host/PlatformManagementHttpApiHostModule.cs
+++ b/aspnet-core/services/LY.MicroService.PlatformManagement.HttpApi.Host/PlatformManagementHttpApiHostModule.cs
@@ -19,6 +19,7 @@ using LINGYUN.Abp.Saas.EntityFrameworkCore;
using LINGYUN.Abp.Serilog.Enrichers.Application;
using LINGYUN.Abp.Serilog.Enrichers.UniqueId;
using LINGYUN.Abp.UI.Navigation.VueVbenAdmin;
+using LINGYUN.Abp.WeChat.Work;
using LINGYUN.Platform;
using LINGYUN.Platform.EntityFrameworkCore;
using LINGYUN.Platform.HttpApi;
@@ -69,6 +70,8 @@ namespace LY.MicroService.PlatformManagement;
typeof(PlatformApplicationModule),
typeof(PlatformHttpApiModule),
typeof(PlatformEntityFrameworkCoreModule),
+ typeof(AbpWeChatWorkApplicationModule),
+ typeof(AbpWeChatWorkHttpApiModule),
typeof(AbpIdentityHttpApiClientModule),
typeof(AbpHttpClientIdentityModelWebModule),
typeof(AbpFeatureManagementEntityFrameworkCoreModule),
diff --git a/aspnet-core/services/LY.MicroService.PlatformManagement.HttpApi.Host/appsettings.Development.json b/aspnet-core/services/LY.MicroService.PlatformManagement.HttpApi.Host/appsettings.Development.json
index fa87331f1..8884c217d 100644
--- a/aspnet-core/services/LY.MicroService.PlatformManagement.HttpApi.Host/appsettings.Development.json
+++ b/aspnet-core/services/LY.MicroService.PlatformManagement.HttpApi.Host/appsettings.Development.json
@@ -47,13 +47,13 @@
}
},
"ConnectionStrings": {
- "Default": "Server=127.0.0.1;Database=Platform-v70;User Id=root;Password=123456",
- "AppPlatform": "Server=127.0.0.1;Database=Platform-v70;User Id=root;Password=123456",
- "AbpFeatureManagement": "Server=127.0.0.1;Database=Platform-v70;User Id=root;Password=123456",
- "AbpSaas": "Server=127.0.0.1;Database=Platform-v70;User Id=root;Password=123456",
- "AbpSettingManagement": "Server=127.0.0.1;Database=Platform-v70;User Id=root;Password=123456",
- "AbpPermissionManagement": "Server=127.0.0.1;Database=Platform-v70;User Id=root;Password=123456",
- "AbpLocalizationManagement": "Server=127.0.0.1;Database=Platform-v70;User Id=root;Password=123456"
+ "Default": "Server=127.0.0.1;Database=Platform-v70;User Id=root;Password=123456;SslMode=None",
+ "AppPlatform": "Server=127.0.0.1;Database=Platform-v70;User Id=root;Password=123456;SslMode=None",
+ "AbpFeatureManagement": "Server=127.0.0.1;Database=Platform-v70;User Id=root;Password=123456;SslMode=None",
+ "AbpSaas": "Server=127.0.0.1;Database=Platform-v70;User Id=root;Password=123456;SslMode=None",
+ "AbpSettingManagement": "Server=127.0.0.1;Database=Platform-v70;User Id=root;Password=123456;SslMode=None",
+ "AbpPermissionManagement": "Server=127.0.0.1;Database=Platform-v70;User Id=root;Password=123456;SslMode=None",
+ "AbpLocalizationManagement": "Server=127.0.0.1;Database=Platform-v70;User Id=root;Password=123456;SslMode=None"
},
"Features": {
"Validation": {
@@ -73,7 +73,7 @@
},
"MySql": {
"TableNamePrefix": "plt",
- "ConnectionString": "Server=127.0.0.1;Database=Platform-v70;User Id=root;Password=123456"
+ "ConnectionString": "Server=127.0.0.1;Database=Platform-v70;User Id=root;Password=123456;SslMode=None"
},
"RabbitMQ": {
"HostName": "127.0.0.1",
diff --git a/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/LY.MicroService.RealtimeMessage.HttpApi.Host.csproj b/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/LY.MicroService.RealtimeMessage.HttpApi.Host.csproj
index 1e89b09da..752c2c125 100644
--- a/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/LY.MicroService.RealtimeMessage.HttpApi.Host.csproj
+++ b/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/LY.MicroService.RealtimeMessage.HttpApi.Host.csproj
@@ -51,6 +51,7 @@
+
@@ -80,8 +81,10 @@
+
+
diff --git a/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/RealtimeMessageHttpApiHostModule.cs b/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/RealtimeMessageHttpApiHostModule.cs
index 910193ebd..ab092ba53 100644
--- a/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/RealtimeMessageHttpApiHostModule.cs
+++ b/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/RealtimeMessageHttpApiHostModule.cs
@@ -9,9 +9,11 @@ using LINGYUN.Abp.BackgroundTasks.Quartz;
using LINGYUN.Abp.Data.DbMigrator;
using LINGYUN.Abp.EventBus.CAP;
using LINGYUN.Abp.ExceptionHandling.Notifications;
+using LINGYUN.Abp.Features.LimitValidation.Redis;
using LINGYUN.Abp.Http.Client.Wrapper;
using LINGYUN.Abp.Identity.EntityFrameworkCore;
using LINGYUN.Abp.Identity.WeChat;
+using LINGYUN.Abp.Identity.WeChat.Work;
using LINGYUN.Abp.IM.SignalR;
using LINGYUN.Abp.Localization.CultureMap;
using LINGYUN.Abp.LocalizationManagement.EntityFrameworkCore;
@@ -26,6 +28,7 @@ using LINGYUN.Abp.Notifications.PushPlus;
using LINGYUN.Abp.Notifications.SignalR;
using LINGYUN.Abp.Notifications.Sms;
using LINGYUN.Abp.Notifications.WeChat.MiniProgram;
+using LINGYUN.Abp.Notifications.WeChat.Work;
using LINGYUN.Abp.Notifications.WxPusher;
using LINGYUN.Abp.Saas.EntityFrameworkCore;
using LINGYUN.Abp.Serilog.Enrichers.Application;
@@ -66,6 +69,7 @@ namespace LY.MicroService.RealtimeMessage;
typeof(AbpNotificationsApplicationModule),
typeof(AbpNotificationsHttpApiModule),
typeof(AbpIdentityWeChatModule),
+ typeof(AbpIdentityWeChatWorkModule),
typeof(AbpBackgroundTasksQuartzModule),
typeof(AbpBackgroundTasksDistributedLockingModule),
typeof(AbpBackgroundTasksExceptionHandlingModule),
@@ -93,9 +97,11 @@ namespace LY.MicroService.RealtimeMessage;
typeof(AbpNotificationsWxPusherModule),
typeof(AbpNotificationsPushPlusModule),
typeof(AbpNotificationsWeChatMiniProgramModule),
+ typeof(AbpNotificationsWeChatWorkModule),
typeof(AbpNotificationsExceptionHandlingModule),
typeof(AbpTextTemplatingScribanModule),
typeof(AbpCAPEventBusModule),
+ typeof(AbpFeaturesValidationRedisModule),
typeof(AbpCachingStackExchangeRedisModule),
typeof(AbpAspNetCoreHttpOverridesModule),
typeof(AbpLocalizationCultureMapModule),
diff --git a/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/appsettings.Development.json b/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/appsettings.Development.json
index 58a343941..bbdc46c20 100644
--- a/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/appsettings.Development.json
+++ b/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/appsettings.Development.json
@@ -54,6 +54,14 @@
"Configuration": "127.0.0.1,defaultDatabase=13"
}
},
+ "Features": {
+ "Validation": {
+ "Redis": {
+ "Configuration": "127.0.0.1,defaultDatabase=8",
+ "InstanceName": "LINGYUN.Abp.Application"
+ }
+ }
+ },
"Redis": {
"IsEnabled": true,
"Configuration": "127.0.0.1,defaultDatabase=8",
diff --git a/aspnet-core/services/LY.MicroService.identityServer/IdentityServerModule.cs b/aspnet-core/services/LY.MicroService.identityServer/IdentityServerModule.cs
index 68d89ea55..d4b908add 100644
--- a/aspnet-core/services/LY.MicroService.identityServer/IdentityServerModule.cs
+++ b/aspnet-core/services/LY.MicroService.identityServer/IdentityServerModule.cs
@@ -12,7 +12,9 @@ using LINGYUN.Abp.Identity.EntityFrameworkCore;
using LINGYUN.Abp.Identity.OrganizaztionUnits;
using LINGYUN.Abp.IdentityServer;
using LINGYUN.Abp.IdentityServer.EntityFrameworkCore;
+using LINGYUN.Abp.IdentityServer.LinkUser;
using LINGYUN.Abp.IdentityServer.Portal;
+using LINGYUN.Abp.IdentityServer.WeChat.Work;
using LINGYUN.Abp.Localization.CultureMap;
using LINGYUN.Abp.LocalizationManagement.EntityFrameworkCore;
using LINGYUN.Abp.Saas.EntityFrameworkCore;
@@ -61,7 +63,9 @@ namespace LY.MicroService.IdentityServer;
// typeof(AbpIdentityHttpApiModule),
typeof(AbpIdentityServerEntityFrameworkCoreModule),
typeof(AbpIdentityServerSmsValidatorModule),
+ typeof(AbpIdentityServerLinkUserModule),
typeof(AbpIdentityServerPortalModule),
+ typeof(AbpIdentityServerWeChatWorkModule),
typeof(AbpAuthenticationWeChatModule),
typeof(AbpAuthenticationQQModule),
typeof(AbpIdentityOrganizaztionUnitsModule),
diff --git a/aspnet-core/services/LY.MicroService.identityServer/LY.MicroService.IdentityServer.csproj b/aspnet-core/services/LY.MicroService.identityServer/LY.MicroService.IdentityServer.csproj
index 1a6643198..6899182c8 100644
--- a/aspnet-core/services/LY.MicroService.identityServer/LY.MicroService.IdentityServer.csproj
+++ b/aspnet-core/services/LY.MicroService.identityServer/LY.MicroService.IdentityServer.csproj
@@ -58,8 +58,10 @@
+
+
diff --git a/aspnet-core/services/LY.MicroService.identityServer/package.json b/aspnet-core/services/LY.MicroService.identityServer/package.json
index 5501ccad5..e0ba78575 100644
--- a/aspnet-core/services/LY.MicroService.identityServer/package.json
+++ b/aspnet-core/services/LY.MicroService.identityServer/package.json
@@ -3,6 +3,6 @@
"name": "my-app-identityserver",
"private": true,
"dependencies": {
- "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "2.3.2"
+ "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "2.3.3"
}
}
\ No newline at end of file
diff --git a/aspnet-core/tests/LINGYUN.Abp.WeChat.Work.Tests/GlobalUsings.cs b/aspnet-core/tests/LINGYUN.Abp.WeChat.Work.Tests/GlobalUsings.cs
new file mode 100644
index 000000000..8c927eb74
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.WeChat.Work.Tests/GlobalUsings.cs
@@ -0,0 +1 @@
+global using Xunit;
\ No newline at end of file
diff --git a/aspnet-core/tests/LINGYUN.Abp.WeChat.Work.Tests/LINGYUN.Abp.WeChat.Work.Tests.csproj b/aspnet-core/tests/LINGYUN.Abp.WeChat.Work.Tests/LINGYUN.Abp.WeChat.Work.Tests.csproj
new file mode 100644
index 000000000..a515dcfaa
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.WeChat.Work.Tests/LINGYUN.Abp.WeChat.Work.Tests.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net7.0
+
+ false
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/tests/LINGYUN.Abp.WeChat.Work.Tests/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkTestBase.cs b/aspnet-core/tests/LINGYUN.Abp.WeChat.Work.Tests/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkTestBase.cs
new file mode 100644
index 000000000..c8a6753a9
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.WeChat.Work.Tests/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkTestBase.cs
@@ -0,0 +1,6 @@
+using LINGYUN.Abp.Tests;
+
+namespace LINGYUN.Abp.WeChat.Work;
+public class AbpWeChatWorkTestBase : AbpTestsBase
+{
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.WeChat.Work.Tests/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkTestModule.cs b/aspnet-core/tests/LINGYUN.Abp.WeChat.Work.Tests/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkTestModule.cs
new file mode 100644
index 000000000..fa293a3a3
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.WeChat.Work.Tests/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkTestModule.cs
@@ -0,0 +1,45 @@
+using Castle.Core.Configuration;
+using LINGYUN.Abp.Tests;
+using Microsoft.Extensions.Caching.StackExchangeRedis;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using StackExchange.Redis;
+using Volo.Abp.Caching;
+using Volo.Abp.Caching.StackExchangeRedis;
+using Volo.Abp.Modularity;
+
+namespace LINGYUN.Abp.WeChat.Work;
+
+[DependsOn(
+ typeof(AbpWeChatWorkModule),
+ typeof(AbpCachingStackExchangeRedisModule),
+ typeof(AbpTestsBaseModule))]
+public class AbpWeChatWorkTestModule : AbpModule
+{
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ var configurationOptions = new AbpConfigurationBuilderOptions
+ {
+ BasePath = @"D:\Projects\Development\Abp\WeChat\Work",
+ EnvironmentName = "Test"
+ };
+
+ context.Services.ReplaceConfiguration(ConfigurationHelper.BuildConfiguration(configurationOptions));
+ }
+
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ var configuration = context.Services.GetConfiguration();
+ Configure(options =>
+ {
+ configuration.GetSection("DistributedCache").Bind(options);
+ });
+
+ Configure(options =>
+ {
+ var redisConfig = ConfigurationOptions.Parse(options.Configuration);
+ options.ConfigurationOptions = redisConfig;
+ options.InstanceName = configuration["Redis:InstanceName"];
+ });
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.WeChat.Work.Tests/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeGenerator_Tests.cs b/aspnet-core/tests/LINGYUN.Abp.WeChat.Work.Tests/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeGenerator_Tests.cs
new file mode 100644
index 000000000..96c0fe13b
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.WeChat.Work.Tests/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeGenerator_Tests.cs
@@ -0,0 +1,46 @@
+using LINGYUN.Abp.WeChat.Work.Settings;
+using Shouldly;
+using System.Threading.Tasks;
+using Volo.Abp.Settings;
+
+namespace LINGYUN.Abp.WeChat.Work.Authorize;
+public class WeChatWorkAuthorizeGenerator_Tests : AbpWeChatWorkTestBase
+{
+ protected ISettingProvider SettingProvider { get; }
+ protected IWeChatWorkAuthorizeGenerator AuthorizeGenerator { get; }
+
+ public WeChatWorkAuthorizeGenerator_Tests()
+ {
+ SettingProvider = GetRequiredService();
+ AuthorizeGenerator = GetRequiredService();
+ }
+
+ [Theory]
+ [InlineData("1000002", "http://api.3dept.com/cgi-bin/query?action=get", "sdjnvh83tg93fojve2g92gyuh29", "code", "snsapi_privateinfo")]
+ public async Task GenerateOAuth2Authorize_Test(
+ string agentid,
+ string redirectUri,
+ string state,
+ string responseType = "code",
+ string scope = "snsapi_base")
+ {
+ var url = await AuthorizeGenerator.GenerateOAuth2AuthorizeAsync(agentid, redirectUri, state, responseType, scope);
+ url.ShouldNotBeNullOrWhiteSpace();
+ }
+
+ [Theory]
+ [InlineData("1000002", "http://api.3dept.com/cgi-bin/query?action=get", "sdjnvh83tg93fojve2g92gyuh29", "CorpApp", "zh")]
+ public async Task GenerateOAuth2Login_Test(
+ string agentid,
+ string redirectUri,
+ string state,
+ string loginType = "CorpApp",
+ string lang = "zh")
+ {
+ var corpId = await SettingProvider.GetOrNullAsync(WeChatWorkSettingNames.Connection.CorpId);
+ corpId.ShouldNotBeNullOrWhiteSpace();
+
+ var url = await AuthorizeGenerator.GenerateOAuth2LoginAsync(corpId, redirectUri, state, loginType, agentid, lang);
+ url.ShouldNotBeNullOrWhiteSpace();
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.WeChat.Work.Tests/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkUserFinder_Tests.cs b/aspnet-core/tests/LINGYUN.Abp.WeChat.Work.Tests/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkUserFinder_Tests.cs
new file mode 100644
index 000000000..27407db49
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.WeChat.Work.Tests/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkUserFinder_Tests.cs
@@ -0,0 +1,29 @@
+using Shouldly;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.WeChat.Work.Authorize;
+public class WeChatWorkUserFinder_Tests : AbpWeChatWorkTestBase
+{
+ protected IWeChatWorkUserFinder WeChatWorkUserFinder { get; }
+ public WeChatWorkUserFinder_Tests()
+ {
+ WeChatWorkUserFinder = GetRequiredService();
+ }
+
+ [Theory]
+ [InlineData("1000002", "nuE7XPAh5AJbQ4SawxH0OmUHO_9PzRD-PSghQafeU3A")]
+ public async Task GetUserInfo_Test(string agentid, string code)
+ {
+ var userInfo = await WeChatWorkUserFinder.GetUserInfoAsync(agentid, code);
+
+ userInfo.ShouldNotBeNull();
+ userInfo.UserId.ShouldNotBeNullOrWhiteSpace();
+ userInfo.UserTicket.ShouldNotBeNullOrWhiteSpace();
+
+ var userDetail = await WeChatWorkUserFinder.GetUserDetailAsync(agentid, userInfo.UserTicket);
+
+ userDetail.ShouldNotBeNull();
+ userDetail.UserId.ShouldBe(userInfo.UserId);
+ userDetail.QrCode.ShouldNotBeNullOrWhiteSpace();
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.WeChat.Work.Tests/LINGYUN/Abp/WeChat/Work/Media/WeChatWorkMediaProvider_Tests.cs b/aspnet-core/tests/LINGYUN.Abp.WeChat.Work.Tests/LINGYUN/Abp/WeChat/Work/Media/WeChatWorkMediaProvider_Tests.cs
new file mode 100644
index 000000000..f53e6f39a
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.WeChat.Work.Tests/LINGYUN/Abp/WeChat/Work/Media/WeChatWorkMediaProvider_Tests.cs
@@ -0,0 +1,61 @@
+using Shouldly;
+using System.IO;
+using System.Threading.Tasks;
+using Volo.Abp.Content;
+
+namespace LINGYUN.Abp.WeChat.Work.Media;
+public class WeChatWorkMediaProvider_Tests : AbpWeChatWorkTestBase
+{
+ protected IWeChatWorkMediaProvider MediaProvider { get; }
+ public WeChatWorkMediaProvider_Tests()
+ {
+ MediaProvider = GetRequiredService();
+ }
+
+ [Theory]
+ [InlineData("1000002", "D:\\Projects\\Development\\Abp\\WeChat\\Work\\image.jpg")]
+ public async Task Get_Media_Test(string agentid, string fileName)
+ {
+ var fileInfo = new FileInfo(fileName);
+ using var fileStream = fileInfo.OpenRead();
+ var uploadMedia = new RemoteStreamContent(fileStream, fileInfo.Name);
+
+ var uploadResponse = await MediaProvider.UploadAsync(agentid, "image", uploadMedia);
+ uploadResponse.IsSuccessed.ShouldBeTrue();
+ uploadResponse.MediaId.ShouldNotBeNullOrEmpty();
+
+ var getResponse = await MediaProvider.GetAsync(agentid, uploadResponse.MediaId);
+ getResponse.ShouldNotBeNull();
+ getResponse.ContentLength.ShouldNotBeNull();
+ getResponse.ContentLength.ShouldBe(uploadMedia.ContentLength);
+ }
+
+ [Theory]
+ [InlineData("1000002", "image", "D:\\Projects\\Development\\Abp\\WeChat\\Work\\image.jpg")]
+ [InlineData("1000002", "voice", "D:\\Projects\\Development\\Abp\\WeChat\\Work\\voice.amr")]
+ [InlineData("1000002", "video", "D:\\Projects\\Development\\Abp\\WeChat\\Work\\video.mp4")]
+ [InlineData("1000002", "file", "D:\\Projects\\Development\\Abp\\WeChat\\Work\\file.txt")]
+ public async Task Upload_Media_Test(string agentid, string type, string fileName)
+ {
+ var fileInfo = new FileInfo(fileName);
+ using var fileStream = fileInfo.OpenRead();
+ var media = new RemoteStreamContent(fileStream, fileInfo.Name);
+
+ var response = await MediaProvider.UploadAsync(agentid, type, media);
+ response.IsSuccessed.ShouldBeTrue();
+ response.MediaId.ShouldNotBeNullOrEmpty();
+ }
+
+ [Theory]
+ [InlineData("1000002", "D:\\Projects\\Development\\Abp\\WeChat\\Work\\image.jpg")]
+ public async Task Upload_Image_Test(string agentid, string fileName)
+ {
+ var fileInfo = new FileInfo(fileName);
+ using var fileStream = fileInfo.OpenRead();
+ var media = new RemoteStreamContent(fileStream, fileInfo.Name);
+
+ var response = await MediaProvider.UploadImageAsync(agentid, media);
+ response.IsSuccessed.ShouldBeTrue();
+ response.Url.ShouldNotBeNullOrEmpty();
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.WeChat.Work.Tests/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMessageSender_Tests.cs b/aspnet-core/tests/LINGYUN.Abp.WeChat.Work.Tests/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMessageSender_Tests.cs
new file mode 100644
index 000000000..7da3151c4
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.WeChat.Work.Tests/LINGYUN/Abp/WeChat/Work/Message/WeChatWorkMessageSender_Tests.cs
@@ -0,0 +1,38 @@
+using LINGYUN.Abp.WeChat.Work.Message.Models;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.WeChat.Work.Message;
+public class WeChatWorkMessageSender_Tests : AbpWeChatWorkTestBase
+{
+ protected IWeChatWorkMessageSender Sender { get; }
+ public WeChatWorkMessageSender_Tests()
+ {
+ Sender = GetRequiredService();
+ }
+
+ [Theory]
+ [InlineData("1000002")]
+ public async Task Send_Text_Message(string agentId)
+ {
+ var text = new TextMessage("你的快递已到,请携带工卡前往邮件中心领取。\n出发前可查看邮件中心视频实况,聪明避开排队。");
+ var message = new WeChatWorkTextMessage(agentId, text)
+ {
+ ToUser = "@all"
+ };
+
+ await Sender.SendAsync(message);
+ }
+
+ [Theory]
+ [InlineData("1000002")]
+ public async Task Send_Markdown_Message(string agentId)
+ {
+ var markdown = new MarkdownMessage("您的会议室已经预定,稍后会同步到`邮箱` \n>**事项详情** \n>事 项:开会 \n>组织者:@miglioguan \n>参与者:@miglioguan、@kunliu、@jamdeezhou、@kanexiong、@kisonwang \n> \n>会议室:广州TIT 1楼 301 \n>日 期:2018年5月18日 \n>时 间:上午9:00-11:00 \n> \n>请准时参加会议。 \n> \n>如需修改会议信息,请点击:[修改会议信息](https://work.weixin.qq.com)");
+ var message = new WeChatWorkMarkdownMessage(agentId, markdown)
+ {
+ ToUser = "@all"
+ };
+
+ await Sender.SendAsync(message);
+ }
+}