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