152 changed files with 5547 additions and 15 deletions
@ -0,0 +1,3 @@ |
|||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
|
<ConfigureAwait ContinueOnCapturedContext="false" /> |
||||
|
</Weavers> |
||||
@ -0,0 +1,30 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
||||
|
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
||||
|
<xs:element name="Weavers"> |
||||
|
<xs:complexType> |
||||
|
<xs:all> |
||||
|
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
||||
|
<xs:complexType> |
||||
|
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:all> |
||||
|
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:schema> |
||||
@ -0,0 +1,24 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<Import Project="..\..\..\configureawait.props" /> |
||||
|
<Import Project="..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>net7.0</TargetFramework> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<None Remove="LINGYUN\Abp\IdentityServer\WeChat\Work\Localization\Resources\*.json" /> |
||||
|
<EmbeddedResource Include="LINGYUN\Abp\IdentityServer\WeChat\Work\Localization\Resources\*.json" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Volo.Abp.IdentityServer.Domain" Version="$(VoloAbpPackageVersion)" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\..\wechat\LINGYUN.Abp.WeChat.Work\LINGYUN.Abp.WeChat.Work.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -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<IIdentityServerBuilder>(builder => |
||||
|
{ |
||||
|
builder.AddExtensionGrantValidator<WeChatWorkGrantValidator>(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
Configure<AbpVirtualFileSystemOptions>(options => |
||||
|
{ |
||||
|
options.FileSets.AddEmbedded<AbpIdentityServerWeChatWorkModule>(); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpLocalizationOptions>(options => |
||||
|
{ |
||||
|
options.Resources |
||||
|
.Get<WeChatWorkResource>() |
||||
|
.AddBaseTypes(typeof(IdentityResource)) |
||||
|
.AddVirtualJson("/LINGYUN/Abp/IdentityServer/WeChat/Work/Localization/Resources"); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -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!" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
{ |
||||
|
"culture": "zh-Hans", |
||||
|
"texts": { |
||||
|
"InvalidGrant:GrantTypeInvalid": "不被允许的授权类型!", |
||||
|
"InvalidGrant:AgentIdOrCodeNotFound": "企业标识未找到或用户取消登录!", |
||||
|
"InvalidGrant:UserIdNotRegister": "企业用户未注册!" |
||||
|
} |
||||
|
} |
||||
@ -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<WeChatWorkGrantValidator> Logger { get; } |
||||
|
protected IEventService EventService { get; } |
||||
|
protected UserManager<IdentityUser> UserManager { get; } |
||||
|
protected IdentitySecurityLogManager IdentitySecurityLogManager { get; } |
||||
|
protected ICurrentTenant CurrentTenant { get; } |
||||
|
protected ISettingProvider SettingProvider { get; } |
||||
|
protected IGuidGenerator GuidGenerator { get; } |
||||
|
protected IStringLocalizer<WeChatWorkResource> WeChatWorkLocalizer { get; } |
||||
|
protected IWeChatWorkUserFinder WeChatWorkUserFinder { get; } |
||||
|
|
||||
|
public WeChatWorkGrantValidator( |
||||
|
IEventService eventService, |
||||
|
ICurrentTenant currentTenant, |
||||
|
IGuidGenerator guidGenerator, |
||||
|
ISettingProvider settingProvider, |
||||
|
UserManager<IdentityUser> userManager, |
||||
|
IdentitySecurityLogManager identitySecurityLogManager, |
||||
|
IStringLocalizer<WeChatWorkResource> weChatWorkLocalizer, |
||||
|
IWeChatWorkUserFinder weChatWorkUserFinder, |
||||
|
ILogger<WeChatWorkGrantValidator> 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<Claim>(); |
||||
|
|
||||
|
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<string> FindClientIdAsync(ExtensionGrantValidationContext context) |
||||
|
{ |
||||
|
return Task.FromResult(context.Request?.Client?.ClientId); |
||||
|
} |
||||
|
|
||||
|
protected virtual Task AddCustomClaimsAsync( |
||||
|
List<Claim> customClaims, |
||||
|
IdentityUser user, |
||||
|
ExtensionGrantValidationContext context) |
||||
|
{ |
||||
|
if (user.TenantId.HasValue) |
||||
|
{ |
||||
|
customClaims.Add( |
||||
|
new Claim( |
||||
|
AbpClaimTypes.TenantId, |
||||
|
user.TenantId?.ToString() |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
} |
||||
@ -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标识, 换取用户信息的关键' \ |
||||
|
``` |
||||
@ -0,0 +1,3 @@ |
|||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
|
<ConfigureAwait ContinueOnCapturedContext="false" /> |
||||
|
</Weavers> |
||||
@ -0,0 +1,30 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
||||
|
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
||||
|
<xs:element name="Weavers"> |
||||
|
<xs:complexType> |
||||
|
<xs:all> |
||||
|
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
||||
|
<xs:complexType> |
||||
|
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:all> |
||||
|
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:schema> |
||||
@ -0,0 +1,24 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<Import Project="..\..\..\common.props" /> |
||||
|
<Import Project="..\..\..\configureawait.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>net7.0</TargetFramework> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<None Remove="LINGYUN\Abp\OpenIddict\WeChat\Work\Localization\Resources\*.json" /> |
||||
|
<EmbeddedResource Include="LINGYUN\Abp\OpenIddict\WeChat\Work\Localization\Resources\*.json" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Volo.Abp.OpenIddict.AspNetCore" Version="$(VoloAbpPackageVersion)" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\..\wechat\LINGYUN.Abp.WeChat.Work\LINGYUN.Abp.WeChat.Work.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -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<OpenIddictServerBuilder>(builder => |
||||
|
{ |
||||
|
builder |
||||
|
.AllowWeChatWorkFlow() |
||||
|
.RegisterWeChatWorkScopes() |
||||
|
.RegisterWeChatWorkClaims(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
Configure<AbpOpenIddictExtensionGrantsOptions>(options => |
||||
|
{ |
||||
|
options.Grants.TryAdd( |
||||
|
AbpWeChatWorkGlobalConsts.GrantType, |
||||
|
new WeChatWorkTokenExtensionGrant()); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpVirtualFileSystemOptions>(options => |
||||
|
{ |
||||
|
options.FileSets.AddEmbedded<AbpOpenIddictWeChatWorkModule>(); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpLocalizationOptions>(options => |
||||
|
{ |
||||
|
options.Resources |
||||
|
.Get<AbpOpenIddictResource>() |
||||
|
.AddVirtualJson("/LINGYUN/Abp/OpenIddict/WeChat/Work/Localization/Resources"); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -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!" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
{ |
||||
|
"culture": "zh-Hans", |
||||
|
"texts": { |
||||
|
"MiniProgramAuthorizationDisabledMessage": "应用程序未开放小程序授权", |
||||
|
"OfficialAuthorizationDisabledMessage": "应用程序未开放公众平台授权", |
||||
|
"SelfRegistrationDisabledMessage": "应用程序未开放注册,请联系管理员添加新用户.", |
||||
|
"InvalidGrant:GrantTypeInvalid": "不被允许的授权类型!", |
||||
|
"InvalidGrant:WeChatTokenInvalid": "微信认证失败!", |
||||
|
"InvalidGrant:WeChatCodeNotFound": "微信登录时获取的 code 为空或不存在!", |
||||
|
"InvalidGrant:WeChatNotRegister": "用户微信账号未绑定!" |
||||
|
} |
||||
|
} |
||||
@ -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<IActionResult> HandleAsync(ExtensionGrantContext context) |
||||
|
{ |
||||
|
await CheckFeatureAsync(context); |
||||
|
|
||||
|
return await HandleWeChatAsync(context); |
||||
|
} |
||||
|
|
||||
|
protected async virtual Task<IActionResult> HandleWeChatAsync(ExtensionGrantContext context) |
||||
|
{ |
||||
|
var logger = GetRequiredService<ILogger<WeChatWorkTokenExtensionGrant>>(context); |
||||
|
var localizer = GetRequiredService<IStringLocalizer<AbpOpenIddictResource>>(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<string, string> |
||||
|
{ |
||||
|
[OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant, |
||||
|
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = localizer["InvalidGrant:AgentIdOrCodeNotFound"] |
||||
|
}); |
||||
|
|
||||
|
return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); |
||||
|
} |
||||
|
|
||||
|
var userFinder = GetRequiredService<IWeChatWorkUserFinder>(context); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
var userInfo = await userFinder.GetUserInfoAsync(agentId, code); |
||||
|
|
||||
|
var userManager = GetRequiredService<IdentityUserManager>(context); |
||||
|
var currentUser = await userManager.FindByLoginAsync(AbpWeChatWorkGlobalConsts.ProviderName, userInfo.UserId); |
||||
|
|
||||
|
if (currentUser == null) |
||||
|
{ |
||||
|
var currentTenant = GetRequiredService<ICurrentTenant>(context); |
||||
|
var settingProvider = GetRequiredService<ISettingProvider>(context); |
||||
|
var guidGenerator = GetRequiredService<IGuidGenerator>(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<string, string> |
||||
|
{ |
||||
|
[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<IStringLocalizer<IdentityResource>>(context); |
||||
|
|
||||
|
logger.LogInformation("Authentication failed for username: {username}, reason: locked out", currentUser.UserName); |
||||
|
|
||||
|
var properties = new AuthenticationProperties(new Dictionary<string, string> |
||||
|
{ |
||||
|
[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<string, string> |
||||
|
{ |
||||
|
[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<T>(ExtensionGrantContext context) |
||||
|
{ |
||||
|
return context.HttpContext.RequestServices.GetRequiredService<T>(); |
||||
|
} |
||||
|
|
||||
|
protected async virtual Task<IActionResult> SetSuccessResultAsync( |
||||
|
ExtensionGrantContext context, |
||||
|
IdentityUser user, |
||||
|
string userId, |
||||
|
ILogger<WeChatWorkTokenExtensionGrant> logger) |
||||
|
{ |
||||
|
logger.LogInformation("Credentials validated for username: {username}", user.UserName); |
||||
|
|
||||
|
var signInManager = GetRequiredService<SignInManager<IdentityUser>>(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<IdentitySecurityLogManager>(context); |
||||
|
|
||||
|
await identitySecurityLogManager.SaveAsync(logContext); |
||||
|
} |
||||
|
|
||||
|
protected virtual Task<string> FindClientIdAsync(ExtensionGrantContext context) |
||||
|
{ |
||||
|
return Task.FromResult(context.Request.ClientId); |
||||
|
} |
||||
|
|
||||
|
protected async virtual Task SetClaimsDestinationsAsync(ExtensionGrantContext context, ClaimsPrincipal principal) |
||||
|
{ |
||||
|
var openIddictClaimsPrincipalManager = GetRequiredService<AbpOpenIddictClaimsPrincipalManager>(context); |
||||
|
|
||||
|
await openIddictClaimsPrincipalManager.HandleAsync(context.Request, principal); |
||||
|
} |
||||
|
|
||||
|
protected async virtual Task<IEnumerable<string>> GetResourcesAsync(ExtensionGrantContext context) |
||||
|
{ |
||||
|
var scopes = context.Request.GetScopes(); |
||||
|
var resources = new List<string>(); |
||||
|
if (!scopes.Any()) |
||||
|
{ |
||||
|
return resources; |
||||
|
} |
||||
|
|
||||
|
var scopeManager = GetRequiredService<IOpenIddictScopeManager>(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); |
||||
|
} |
||||
|
} |
||||
@ -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, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
|
<ConfigureAwait ContinueOnCapturedContext="false" /> |
||||
|
</Weavers> |
||||
@ -0,0 +1,30 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
||||
|
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
||||
|
<xs:element name="Weavers"> |
||||
|
<xs:complexType> |
||||
|
<xs:all> |
||||
|
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
||||
|
<xs:complexType> |
||||
|
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:all> |
||||
|
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:schema> |
||||
@ -0,0 +1,20 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<Import Project="..\..\..\configureawait.props" /> |
||||
|
<Import Project="..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netstandard2.1</TargetFramework> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Volo.Abp.Identity.Domain" Version="$(VoloAbpPackageVersion)" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.WeChat.Work\LINGYUN.Abp.WeChat.Work.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
|
||||
|
</Project> |
||||
@ -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 |
||||
|
{ |
||||
|
|
||||
|
} |
||||
@ -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<string> FindUserIdentifierAsync(string agentId, Guid userId, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
var user = await UserManager.FindByIdAsync(userId.ToString()); |
||||
|
|
||||
|
return GetUserOpenIdOrNull(user, AbpWeChatWorkGlobalConsts.ProviderName); |
||||
|
} |
||||
|
|
||||
|
public async virtual Task<List<string>> FindUserIdentifierListAsync(string agentId, IEnumerable<Guid> userIdList, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
var userIdentifiers = new List<string>(); |
||||
|
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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
|
<ConfigureAwait ContinueOnCapturedContext="false" /> |
||||
|
</Weavers> |
||||
@ -0,0 +1,30 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
||||
|
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
||||
|
<xs:element name="Weavers"> |
||||
|
<xs:complexType> |
||||
|
<xs:all> |
||||
|
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
||||
|
<xs:complexType> |
||||
|
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:all> |
||||
|
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:schema> |
||||
@ -0,0 +1,16 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<Import Project="..\..\..\configureawait.props" /> |
||||
|
<Import Project="..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netstandard2.0</TargetFramework> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\..\common\LINGYUN.Abp.Notifications\LINGYUN.Abp.Notifications.csproj" /> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.WeChat.Work\LINGYUN.Abp.WeChat.Work.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -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"; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 设定发送到所有应用
|
||||
|
/// </summary>
|
||||
|
/// <param name="notificationData"></param>
|
||||
|
/// <returns></returns>
|
||||
|
public static void WithAllAgent( |
||||
|
this NotificationData notificationData) |
||||
|
{ |
||||
|
notificationData.SetAgentId("@all"); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 设定消息应用标识
|
||||
|
/// </summary>
|
||||
|
/// <param name="notificationData"></param>
|
||||
|
/// <param name="agentId"></param>
|
||||
|
/// <returns></returns>
|
||||
|
public static void SetAgentId( |
||||
|
this NotificationData notificationData, |
||||
|
string agentId) |
||||
|
{ |
||||
|
notificationData.TrySetData(AgentIdKey, agentId); |
||||
|
} |
||||
|
/// <summary>
|
||||
|
/// 获取消息应用标识
|
||||
|
/// </summary>
|
||||
|
/// <param name="notificationData"></param>
|
||||
|
public static string GetAgentIdOrNull( |
||||
|
this NotificationData notificationData) |
||||
|
{ |
||||
|
return notificationData.TryGetData(AgentIdKey)?.ToString(); |
||||
|
} |
||||
|
/// <summary>
|
||||
|
/// 指定接收消息的标签,标签ID列表,多个接收者用‘|’分隔,最多支持100个。
|
||||
|
/// </summary>
|
||||
|
/// <param name="notificationData"></param>
|
||||
|
/// <param name="tag"></param>
|
||||
|
/// <returns></returns>
|
||||
|
public static void SetTag( |
||||
|
this NotificationData notificationData, |
||||
|
string tag) |
||||
|
{ |
||||
|
notificationData.TrySetData(ToTagKey, tag); |
||||
|
} |
||||
|
/// <summary>
|
||||
|
/// 获取接收消息的标签
|
||||
|
/// </summary>
|
||||
|
/// <param name="notificationData"></param>
|
||||
|
public static string GetTagOrNull( |
||||
|
this NotificationData notificationData) |
||||
|
{ |
||||
|
return notificationData.TryGetData(ToTagKey)?.ToString(); |
||||
|
} |
||||
|
/// <summary>
|
||||
|
/// 指定接收消息的部门,部门ID列表,多个接收者用‘|’分隔,最多支持100个。
|
||||
|
/// </summary>
|
||||
|
/// <param name="notificationData"></param>
|
||||
|
/// <param name="party"></param>
|
||||
|
/// <returns></returns>
|
||||
|
public static void SetParty( |
||||
|
this NotificationData notificationData, |
||||
|
string party) |
||||
|
{ |
||||
|
notificationData.TrySetData(ToPartyKey, party); |
||||
|
} |
||||
|
/// <summary>
|
||||
|
/// 获取接收消息的部门
|
||||
|
/// </summary>
|
||||
|
/// <param name="notificationData"></param>
|
||||
|
public static string GetPartyOrNull( |
||||
|
this NotificationData notificationData) |
||||
|
{ |
||||
|
return notificationData.TryGetData(ToPartyKey)?.ToString(); |
||||
|
} |
||||
|
} |
||||
@ -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"; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 设定发送到所有应用
|
||||
|
/// </summary>
|
||||
|
/// <param name="notification"></param>
|
||||
|
/// <returns></returns>
|
||||
|
public static NotificationDefinition WithAllAgent( |
||||
|
this NotificationDefinition notification) |
||||
|
{ |
||||
|
return notification.WithAgentId("@all"); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 设定消息应用标识
|
||||
|
/// </summary>
|
||||
|
/// <param name="notification"></param>
|
||||
|
/// <param name="agentId"></param>
|
||||
|
/// <returns></returns>
|
||||
|
public static NotificationDefinition WithAgentId( |
||||
|
this NotificationDefinition notification, |
||||
|
string agentId) |
||||
|
{ |
||||
|
return notification.WithProperty(AgentIdKey, agentId); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 获取消息应用标识
|
||||
|
/// </summary>
|
||||
|
/// <param name="notification"></param>
|
||||
|
public static string GetAgentIdOrNull( |
||||
|
this NotificationDefinition notification) |
||||
|
{ |
||||
|
if (notification.Properties.TryGetValue(AgentIdKey, out var agentIdDefine)) |
||||
|
{ |
||||
|
return agentIdDefine.ToString(); |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 指定接收消息的标签,标签ID列表,多个接收者用‘|’分隔,最多支持100个。
|
||||
|
/// </summary>
|
||||
|
/// <param name="notification"></param>
|
||||
|
/// <param name="tag"></param>
|
||||
|
/// <returns></returns>
|
||||
|
public static NotificationDefinition WithTag( |
||||
|
this NotificationDefinition notification, |
||||
|
string tag) |
||||
|
{ |
||||
|
return notification.WithProperty(ToTagKey, tag); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 获取接收消息的标签
|
||||
|
/// </summary>
|
||||
|
/// <param name="notification"></param>
|
||||
|
public static string GetTagOrNull( |
||||
|
this NotificationDefinition notification) |
||||
|
{ |
||||
|
if (notification.Properties.TryGetValue(ToTagKey, out var tagDefine)) |
||||
|
{ |
||||
|
return tagDefine.ToString(); |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 指定接收消息的部门,部门ID列表,多个接收者用‘|’分隔,最多支持100个。
|
||||
|
/// </summary>
|
||||
|
/// <param name="notification"></param>
|
||||
|
/// <param name="party"></param>
|
||||
|
/// <returns></returns>
|
||||
|
public static NotificationDefinition WithParty( |
||||
|
this NotificationDefinition notification, |
||||
|
string party) |
||||
|
{ |
||||
|
return notification.WithProperty(ToPartyKey, party); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 获取接收消息的部门
|
||||
|
/// </summary>
|
||||
|
/// <param name="notification"></param>
|
||||
|
public static string GetPartyOrNull( |
||||
|
this NotificationDefinition notification) |
||||
|
{ |
||||
|
if (notification.Properties.TryGetValue(ToPartyKey, out var partyDefine)) |
||||
|
{ |
||||
|
return partyDefine.ToString(); |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
@ -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<AbpNotificationsPublishOptions>(options => |
||||
|
{ |
||||
|
options.PublishProviders.Add<WeChatWorkNotificationPublishProvider>(); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -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> weChatWorkOptions) |
||||
|
{ |
||||
|
FeatureChecker = featureChecker; |
||||
|
LocalizerFactory = localizerFactory; |
||||
|
WeChatWorkMessageSender = weChatWorkMessageSender; |
||||
|
WeChatWorkInternalUserFinder = weChatWorkInternalUserFinder; |
||||
|
NotificationDefinitionManager = notificationDefinitionManager; |
||||
|
WeChatWorkOptions = weChatWorkOptions.CurrentValue; |
||||
|
} |
||||
|
|
||||
|
protected async override Task PublishAsync( |
||||
|
NotificationInfo notification, |
||||
|
IEnumerable<UserIdentifier> identifiers, |
||||
|
CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
var sendToAgentIds = new List<string>(); |
||||
|
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<LocalizableStringInfo>(); |
||||
|
var titleLocalizer = await LocalizerFactory.CreateByResourceNameAsync(titleInfo.ResourceName); |
||||
|
title = titleLocalizer[titleInfo.Name, titleInfo.Values].Value; |
||||
|
|
||||
|
var messageInfo = notification.Data.TryGetData("message").As<LocalizableStringInfo>(); |
||||
|
var messageLocalizer = await LocalizerFactory.CreateByResourceNameAsync(messageInfo.ResourceName); |
||||
|
message = messageLocalizer[messageInfo.Name, messageInfo.Values].Value; |
||||
|
|
||||
|
var descriptionInfo = notification.Data.TryGetData("description")?.As<LocalizableStringInfo>(); |
||||
|
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); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
|
<ConfigureAwait ContinueOnCapturedContext="false" /> |
||||
|
</Weavers> |
||||
@ -0,0 +1,30 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
||||
|
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
||||
|
<xs:element name="Weavers"> |
||||
|
<xs:complexType> |
||||
|
<xs:all> |
||||
|
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
||||
|
<xs:complexType> |
||||
|
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:all> |
||||
|
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:schema> |
||||
@ -0,0 +1,15 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<Import Project="..\..\..\configureawait.props" /> |
||||
|
<Import Project="..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netstandard2.0</TargetFramework> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Volo.Abp.Ddd.Application.Contracts" Version="$(VoloAbpPackageVersion)" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,11 @@ |
|||||
|
using Volo.Abp.Application; |
||||
|
using Volo.Abp.Modularity; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Work; |
||||
|
|
||||
|
[DependsOn( |
||||
|
typeof(AbpDddApplicationContractsModule))] |
||||
|
public class AbpWeChatWorkApplicationContractsModule : AbpModule |
||||
|
{ |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
namespace LINGYUN.Abp.WeChat.Work; |
||||
|
|
||||
|
public class AbpWeChatWorkRemoteServiceConsts |
||||
|
{ |
||||
|
public const string RemoteServiceName = "AbpWeChatWork"; |
||||
|
|
||||
|
public const string ModuleName = "wechat-work"; |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Application.Services; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Work.Authorize; |
||||
|
public interface IWeChatWorkAuthorizeAppService : IApplicationService |
||||
|
{ |
||||
|
Task<string> GenerateOAuth2AuthorizeAsync( |
||||
|
string agentid, |
||||
|
string redirectUri, |
||||
|
string responseType = "code", |
||||
|
string scope = "snsapi_base"); |
||||
|
|
||||
|
Task<string> GenerateOAuth2LoginAsync( |
||||
|
string appid, |
||||
|
string redirectUri, |
||||
|
string loginType = "ServiceApp", |
||||
|
string agentid = ""); |
||||
|
} |
||||
@ -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; } |
||||
|
} |
||||
@ -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 |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 加密的字符串。需要解密得到消息内容明文,解密后有random、msg_len、msg、receiveid四个字段,其中msg即为消息内容明文
|
||||
|
/// </summary>
|
||||
|
[JsonPropertyName("echostr")] |
||||
|
public string EchoStr { get; set; } |
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Application.Services; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Work.Message; |
||||
|
/// <summary>
|
||||
|
/// 企业微信消息接口
|
||||
|
/// </summary>
|
||||
|
public interface IWeChatWorkMessageAppService : IApplicationService |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 校验企业微信消息
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// 参考文档:<see cref="https://developer.work.weixin.qq.com/document/path/90238"/>
|
||||
|
/// </remarks>
|
||||
|
/// <param name="agentId"></param>
|
||||
|
/// <param name="input"></param>
|
||||
|
/// <returns></returns>
|
||||
|
Task<string> Handle(string agentId, MessageValidationInput input); |
||||
|
/// <summary>
|
||||
|
/// 处理企业微信消息
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// 参考文档:<see cref="https://developer.work.weixin.qq.com/document/path/90238"/>
|
||||
|
/// </remarks>
|
||||
|
/// <param name="agentId"></param>
|
||||
|
/// <param name="input"></param>
|
||||
|
/// <returns></returns>
|
||||
|
Task<string> Handle(string agentId, MessageHandleInput input); |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
using System.Text.Json.Serialization; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Work.Models; |
||||
|
public class WeChatWorkMessage |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 企业微信加密签名,
|
||||
|
/// msg_signature计算结合了企业填写的token、请求中的timestamp、nonce、加密的消息体
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// 签名计算方法参考: 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
|
||||
|
/// </remarks>
|
||||
|
[JsonPropertyName("msg_signature")] |
||||
|
public string Msg_Signature { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 时间戳。与nonce结合使用,用于防止请求重放攻击。
|
||||
|
/// </summary>
|
||||
|
[JsonPropertyName("timestamp")] |
||||
|
public int TimeStamp { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 随机数。与timestamp结合使用,用于防止请求重放攻击。
|
||||
|
/// </summary>
|
||||
|
[JsonPropertyName("nonce")] |
||||
|
public string Nonce { get; set; } |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
|
<ConfigureAwait ContinueOnCapturedContext="false" /> |
||||
|
</Weavers> |
||||
@ -0,0 +1,30 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
||||
|
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
||||
|
<xs:element name="Weavers"> |
||||
|
<xs:complexType> |
||||
|
<xs:all> |
||||
|
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
||||
|
<xs:complexType> |
||||
|
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:all> |
||||
|
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:schema> |
||||
@ -0,0 +1,20 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<Import Project="..\..\..\configureawait.props" /> |
||||
|
<Import Project="..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netstandard2.0</TargetFramework> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Volo.Abp.Ddd.Application" Version="$(VoloAbpPackageVersion)" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.WeChat.Work\LINGYUN.Abp.WeChat.Work.csproj" /> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.WeChat.Work.Application.Contracts\LINGYUN.Abp.WeChat.Work.Application.Contracts.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -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 |
||||
|
{ |
||||
|
|
||||
|
} |
||||
@ -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<string> 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<string> 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); |
||||
|
} |
||||
|
} |
||||
@ -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<WeChatWorkOptions> options) |
||||
|
{ |
||||
|
_cryptoService = cryptoService; |
||||
|
_options = options.CurrentValue; |
||||
|
} |
||||
|
|
||||
|
public async virtual Task<string> 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<string> 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; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
|
<ConfigureAwait ContinueOnCapturedContext="false" /> |
||||
|
</Weavers> |
||||
@ -0,0 +1,30 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
||||
|
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
||||
|
<xs:element name="Weavers"> |
||||
|
<xs:complexType> |
||||
|
<xs:all> |
||||
|
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
||||
|
<xs:complexType> |
||||
|
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:all> |
||||
|
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:schema> |
||||
@ -0,0 +1,19 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<Import Project="..\..\..\configureawait.props" /> |
||||
|
<Import Project="..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>net7.0</TargetFramework> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Volo.Abp.AspNetCore.Mvc" Version="$(VoloAbpPackageVersion)" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.WeChat.Work.Application.Contracts\LINGYUN.Abp.WeChat.Work.Application.Contracts.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -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<IMvcBuilder>(mvcBuilder => |
||||
|
{ |
||||
|
mvcBuilder.AddApplicationPartIfNotExists(typeof(AbpWeChatWorkHttpApiModule).Assembly); |
||||
|
}); |
||||
|
|
||||
|
//PreConfigure<AbpMvcDataAnnotationsLocalizationOptions>(options =>
|
||||
|
//{
|
||||
|
// options.AddAssemblyResource(
|
||||
|
// typeof(AbpTextTemplatingResource),
|
||||
|
// typeof(AbpWeChatWorkApplicationContractsModule).Assembly);
|
||||
|
//});
|
||||
|
} |
||||
|
} |
||||
@ -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<string> 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<string> 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); |
||||
|
} |
||||
|
} |
||||
@ -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<string> Handle([FromRoute] string agentId, [FromQuery] MessageValidationInput input) |
||||
|
{ |
||||
|
return _service.Handle(agentId, input); |
||||
|
} |
||||
|
|
||||
|
[HttpPost] |
||||
|
[Route("{agentId}")] |
||||
|
public async virtual Task<string> 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); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
|
<ConfigureAwait ContinueOnCapturedContext="false" /> |
||||
|
</Weavers> |
||||
@ -0,0 +1,30 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
||||
|
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
||||
|
<xs:element name="Weavers"> |
||||
|
<xs:complexType> |
||||
|
<xs:all> |
||||
|
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
||||
|
<xs:complexType> |
||||
|
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:all> |
||||
|
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:schema> |
||||
@ -0,0 +1,27 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<Import Project="..\..\..\configureawait.props" /> |
||||
|
<Import Project="..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netstandard2.0</TargetFramework> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<None Remove="LINGYUN\Abp\WeChat\Work\Localization\Resources\*.json" /> |
||||
|
<EmbeddedResource Include="LINGYUN\Abp\WeChat\Work\Localization\Resources\*.json" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Volo.Abp.Features" Version="$(VoloAbpPackageVersion)" /> |
||||
|
<PackageReference Include="Volo.Abp.Caching" Version="$(VoloAbpPackageVersion)" /> |
||||
|
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonPackageVersion)" /> |
||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="$(MicrosoftPackageVersion)" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\..\common\LINGYUN.Abp.Features.LimitValidation\LINGYUN.Abp.Features.LimitValidation.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -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) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,42 @@ |
|||||
|
namespace LINGYUN.Abp.WeChat.Work |
||||
|
{ |
||||
|
public class AbpWeChatWorkGlobalConsts |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 企业微信对应的Provider名称
|
||||
|
/// </summary>
|
||||
|
public static string ProviderName { get; set; } = "WeChat.Work"; |
||||
|
/// <summary>
|
||||
|
/// 企业微信授权类型
|
||||
|
/// </summary>
|
||||
|
public static string GrantType { get; set; } = "wx-work"; |
||||
|
/// <summary>
|
||||
|
/// 企业微信授权名称
|
||||
|
/// </summary>
|
||||
|
public static string AuthenticationScheme { get; set; }= "WeCom"; |
||||
|
/// <summary>
|
||||
|
/// 企业微信个人信息标识
|
||||
|
/// </summary>
|
||||
|
public static string ProfileKey { get; set; } = "wecom.profile"; |
||||
|
/// <summary>
|
||||
|
/// 企业微信授权应用标识参数
|
||||
|
/// </summary>
|
||||
|
public static string AgentId { get; set; } = "agent_id"; |
||||
|
/// <summary>
|
||||
|
/// 企业微信授权Code参数
|
||||
|
/// </summary>
|
||||
|
public static string Code { get; set; }= "code"; |
||||
|
/// <summary>
|
||||
|
/// 企业微信授权显示名称
|
||||
|
/// </summary>
|
||||
|
public static string DisplayName { get; set; } = "企业微信"; |
||||
|
/// <summary>
|
||||
|
///企业微信授权方法名称
|
||||
|
/// </summary>
|
||||
|
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"; |
||||
|
} |
||||
|
} |
||||
@ -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<WeChatWorkOptions>(configuration.GetSection("WeChat:Work")); |
||||
|
|
||||
|
Configure<AbpVirtualFileSystemOptions>(options => |
||||
|
{ |
||||
|
options.FileSets.AddEmbedded<AbpWeChatWorkModule>(); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpLocalizationOptions>(options => |
||||
|
{ |
||||
|
options.Resources |
||||
|
.Add<WeChatWorkResource>("zh-Hans") |
||||
|
.AddVirtualJson("/LINGYUN/Abp/WeChat/Work/Localization/Resources"); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpExceptionLocalizationOptions>(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"); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Work.Authorize; |
||||
|
public interface IWeChatWorkAuthorizeGenerator |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 构造网页授权链接
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// 参考:https://developer.work.weixin.qq.com/document/path/91022
|
||||
|
/// </remarks>
|
||||
|
/// <param name="agentid"></param>
|
||||
|
/// <param name="redirectUri"></param>
|
||||
|
/// <param name="state"></param>
|
||||
|
/// <param name="responseType"></param>
|
||||
|
/// <param name="scope"></param>
|
||||
|
/// <returns></returns>
|
||||
|
Task<string> GenerateOAuth2AuthorizeAsync( |
||||
|
string agentid, |
||||
|
string redirectUri, |
||||
|
string state, |
||||
|
string responseType = "code", |
||||
|
string scope = "snsapi_base"); |
||||
|
/// <summary>
|
||||
|
/// 构建网页登录链接
|
||||
|
/// </summary>
|
||||
|
/// <param name="appid"></param>
|
||||
|
/// <param name="redirectUri"></param>
|
||||
|
/// <param name="state"></param>
|
||||
|
/// <param name="loginType"></param>
|
||||
|
/// <param name="agentid"></param>
|
||||
|
/// <param name="lang"></param>
|
||||
|
/// <returns></returns>
|
||||
|
Task<string> GenerateOAuth2LoginAsync( |
||||
|
string appid, |
||||
|
string redirectUri, |
||||
|
string state, |
||||
|
string loginType = "ServiceApp", |
||||
|
string agentid = "", |
||||
|
string lang = "zh"); |
||||
|
} |
||||
@ -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 |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 通过用户标识查询企业微信用户标识
|
||||
|
/// </summary>
|
||||
|
/// <param name="agentId"></param>
|
||||
|
/// <param name="userId"></param>
|
||||
|
/// <param name="cancellationToken"></param>
|
||||
|
/// <returns></returns>
|
||||
|
Task<string> FindUserIdentifierAsync( |
||||
|
string agentId, |
||||
|
Guid userId, |
||||
|
CancellationToken cancellationToken = default); |
||||
|
/// <summary>
|
||||
|
/// 通过用户标识列表查询企业微信用户标识列表
|
||||
|
/// </summary>
|
||||
|
/// <param name="agentId"></param>
|
||||
|
/// <param name="userIdList"></param>
|
||||
|
/// <param name="cancellationToken"></param>
|
||||
|
/// <returns></returns>
|
||||
|
Task<List<string>> FindUserIdentifierListAsync( |
||||
|
string agentId, |
||||
|
IEnumerable<Guid> userIdList, |
||||
|
CancellationToken cancellationToken = default); |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
using LINGYUN.Abp.WeChat.Work.Authorize.Models; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Work.Authorize; |
||||
|
/// <summary>
|
||||
|
/// 企业微信用户信息查询接口
|
||||
|
/// </summary>
|
||||
|
public interface IWeChatWorkUserFinder |
||||
|
{ |
||||
|
Task<WeChatWorkUserInfo> GetUserInfoAsync( |
||||
|
string agentId, |
||||
|
string code, |
||||
|
CancellationToken cancellationToken = default); |
||||
|
|
||||
|
Task<WeChatWorkUserDetail> GetUserDetailAsync( |
||||
|
string agentId, |
||||
|
string userTicket, |
||||
|
CancellationToken cancellationToken = default); |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
namespace LINGYUN.Abp.WeChat.Work.Authorize.Models; |
||||
|
/// <summary>
|
||||
|
/// 性别
|
||||
|
/// </summary>
|
||||
|
public enum WeChatWorkGender |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 未定义
|
||||
|
/// </summary>
|
||||
|
None = 0, |
||||
|
/// <summary>
|
||||
|
/// 男性
|
||||
|
/// </summary>
|
||||
|
Man = 1, |
||||
|
/// <summary>
|
||||
|
/// 女性
|
||||
|
/// </summary>
|
||||
|
Women = 2 |
||||
|
} |
||||
@ -0,0 +1,77 @@ |
|||||
|
using JetBrains.Annotations; |
||||
|
using Newtonsoft.Json; |
||||
|
using System.Text.Json.Serialization; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Work.Authorize.Models; |
||||
|
/// <summary>
|
||||
|
/// 企业微信用户详情
|
||||
|
/// </summary>
|
||||
|
public class WeChatWorkUserDetail |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 成员UserID
|
||||
|
/// </summary>
|
||||
|
[NotNull] |
||||
|
[JsonProperty("userid")] |
||||
|
[JsonPropertyName("userid")] |
||||
|
public string UserId { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 性别。
|
||||
|
/// 0表示未定义,
|
||||
|
/// 1表示男性,
|
||||
|
/// 2表示女性。
|
||||
|
/// 仅在用户同意snsapi_privateinfo授权时返回真实值,否则返回0
|
||||
|
/// </summary>
|
||||
|
[CanBeNull] |
||||
|
[JsonProperty("gender")] |
||||
|
[JsonPropertyName("gender")] |
||||
|
public WeChatWorkGender Gender { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 头像url。
|
||||
|
/// 仅在用户同意snsapi_privateinfo授权时返回真实头像,否则返回默认头像
|
||||
|
/// </summary>
|
||||
|
[CanBeNull] |
||||
|
[JsonProperty("avatar")] |
||||
|
[JsonPropertyName("avatar")] |
||||
|
public string Avatar { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 员工个人二维码(扫描可添加为外部联系人)
|
||||
|
/// 仅在用户同意snsapi_privateinfo授权时返回
|
||||
|
/// </summary>
|
||||
|
[CanBeNull] |
||||
|
[JsonProperty("qr_code")] |
||||
|
[JsonPropertyName("qr_code")] |
||||
|
public string QrCode { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 手机
|
||||
|
/// 仅在用户同意snsapi_privateinfo授权时返回,第三方应用不可获取
|
||||
|
/// </summary>
|
||||
|
[CanBeNull] |
||||
|
[JsonProperty("mobile")] |
||||
|
[JsonPropertyName("mobile")] |
||||
|
public string Mobile { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 邮箱
|
||||
|
/// 仅在用户同意snsapi_privateinfo授权时返回,第三方应用不可获取
|
||||
|
/// </summary>
|
||||
|
[CanBeNull] |
||||
|
[JsonProperty("email")] |
||||
|
[JsonPropertyName("email")] |
||||
|
public string Email { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 企业邮箱
|
||||
|
/// 仅在用户同意snsapi_privateinfo授权时返回,第三方应用不可获取
|
||||
|
/// </summary>
|
||||
|
[CanBeNull] |
||||
|
[JsonProperty("biz_mail")] |
||||
|
[JsonPropertyName("biz_mail")] |
||||
|
public string WorkEmail { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 地址
|
||||
|
/// 仅在用户同意snsapi_privateinfo授权时返回,第三方应用不可获取
|
||||
|
/// </summary>
|
||||
|
[CanBeNull] |
||||
|
[JsonProperty("address")] |
||||
|
[JsonPropertyName("address")] |
||||
|
public string Address { get; set; } |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
using JetBrains.Annotations; |
||||
|
using Newtonsoft.Json; |
||||
|
using System.Text.Json.Serialization; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Work.Authorize.Models; |
||||
|
/// <summary>
|
||||
|
/// 企业微信用户信息
|
||||
|
/// </summary>
|
||||
|
public class WeChatWorkUserInfo |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 成员UserID
|
||||
|
/// </summary>
|
||||
|
[NotNull] |
||||
|
[JsonProperty("userid")] |
||||
|
[JsonPropertyName("userid")] |
||||
|
public string UserId { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 成员票据,最大为512字节,有效期为1800s
|
||||
|
/// </summary>
|
||||
|
[NotNull] |
||||
|
[JsonProperty("user_ticket")] |
||||
|
[JsonPropertyName("user_ticket")] |
||||
|
public string UserTicket { get; set; } |
||||
|
} |
||||
@ -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<string> FindUserIdentifierAsync( |
||||
|
string agentId, |
||||
|
Guid userId, |
||||
|
CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
string findUserId = null; |
||||
|
return Task.FromResult(findUserId); |
||||
|
} |
||||
|
|
||||
|
public Task<List<string>> FindUserIdentifierListAsync( |
||||
|
string agentId, |
||||
|
IEnumerable<Guid> userIdList, |
||||
|
CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
return Task.FromResult(new List<string>()); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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, |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
/// <summary>
|
||||
|
/// 企业微信用户详情响应
|
||||
|
/// </summary>
|
||||
|
public class WeChatWorkUserDetailResponse : WeChatWorkResponse |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 成员UserID
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("userid")] |
||||
|
[JsonPropertyName("userid")] |
||||
|
public string UserId { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 性别。
|
||||
|
/// 0表示未定义,
|
||||
|
/// 1表示男性,
|
||||
|
/// 2表示女性。
|
||||
|
/// 仅在用户同意snsapi_privateinfo授权时返回真实值,否则返回0
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("gender")] |
||||
|
[JsonPropertyName("gender")] |
||||
|
public WeChatWorkGender Gender { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 头像url。
|
||||
|
/// 仅在用户同意snsapi_privateinfo授权时返回真实头像,否则返回默认头像
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("avatar")] |
||||
|
[JsonPropertyName("avatar")] |
||||
|
public string Avatar { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 员工个人二维码(扫描可添加为外部联系人)
|
||||
|
/// 仅在用户同意snsapi_privateinfo授权时返回
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("qr_code")] |
||||
|
[JsonPropertyName("qr_code")] |
||||
|
public string QrCode { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 手机
|
||||
|
/// 仅在用户同意snsapi_privateinfo授权时返回,第三方应用不可获取
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("mobile")] |
||||
|
[JsonPropertyName("mobile")] |
||||
|
public string Mobile { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 邮箱
|
||||
|
/// 仅在用户同意snsapi_privateinfo授权时返回,第三方应用不可获取
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("email")] |
||||
|
[JsonPropertyName("email")] |
||||
|
public string Email { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 企业邮箱
|
||||
|
/// 仅在用户同意snsapi_privateinfo授权时返回,第三方应用不可获取
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("biz_mail")] |
||||
|
[JsonPropertyName("biz_mail")] |
||||
|
public string WorkEmail { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 地址
|
||||
|
/// 仅在用户同意snsapi_privateinfo授权时返回,第三方应用不可获取
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("address")] |
||||
|
[JsonPropertyName("address")] |
||||
|
public string Address { get; set; } |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
using Newtonsoft.Json; |
||||
|
using System.Text.Json.Serialization; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Work.Authorize.Response; |
||||
|
/// <summary>
|
||||
|
/// 企业微信用户信息响应
|
||||
|
/// </summary>
|
||||
|
public class WeChatWorkUserInfoResponse : WeChatWorkResponse |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 成员UserID
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("userid")] |
||||
|
[JsonPropertyName("userid")] |
||||
|
public string UserId { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 成员票据,最大为512字节,有效期为1800s
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("user_ticket")] |
||||
|
[JsonPropertyName("user_ticket")] |
||||
|
public string UserTicket { get; set; } |
||||
|
} |
||||
@ -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<string> 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<string> 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()); |
||||
|
} |
||||
|
} |
||||
@ -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<WeChatWorkUserInfo> 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<WeChatWorkUserInfoResponse>(responseContent); |
||||
|
|
||||
|
return userInfoResponse.ToUserInfo(); |
||||
|
} |
||||
|
|
||||
|
public async virtual Task<WeChatWorkUserDetail> 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<WeChatWorkUserDetailResponse>(responseContent); |
||||
|
|
||||
|
return userDetailResponse.ToUserDetail(); |
||||
|
} |
||||
|
} |
||||
@ -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<WeChatWorkResource>(name); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
namespace LINGYUN.Abp.WeChat.Work.Features; |
||||
|
|
||||
|
public static class WeChatWorkFeatureNames |
||||
|
{ |
||||
|
public const string GroupName = "WeChat.Work"; |
||||
|
/// <summary>
|
||||
|
/// 启用企业微信
|
||||
|
/// </summary>
|
||||
|
public const string Enable = GroupName + ".Enable"; |
||||
|
|
||||
|
public static class Message |
||||
|
{ |
||||
|
public const string GroupName = WeChatWorkFeatureNames.GroupName + ".Message"; |
||||
|
/// <summary>
|
||||
|
/// 启用消息推送
|
||||
|
/// </summary>
|
||||
|
public const string Enable = GroupName + ".Enable"; |
||||
|
/// <summary>
|
||||
|
/// 发送次数上限
|
||||
|
/// </summary>
|
||||
|
public const string SendLimit = GroupName + ".SendLimit"; |
||||
|
/// <summary>
|
||||
|
/// 发送次数上限时长
|
||||
|
/// </summary>
|
||||
|
public const string SendLimitInterval = GroupName + ".SendLimitInterval"; |
||||
|
} |
||||
|
} |
||||
@ -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" |
||||
|
} |
||||
|
} |
||||
@ -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" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
using Volo.Abp.Localization; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Work.Localization; |
||||
|
|
||||
|
[LocalizationResourceName("AbpWeChatWork")] |
||||
|
public class WeChatWorkResource |
||||
|
{ |
||||
|
} |
||||
@ -0,0 +1,59 @@ |
|||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Content; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Work.Media; |
||||
|
/// <summary>
|
||||
|
/// 素材管理接口
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// API: <see cref="https://developer.work.weixin.qq.com/document/path/91054"/>
|
||||
|
/// </remarks>
|
||||
|
public interface IWeChatWorkMediaProvider |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 上传临时素材
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// API: <see cref="https://developer.work.weixin.qq.com/document/path/90253"/>
|
||||
|
/// </remarks>
|
||||
|
/// <param name="agentId">应用标识</param>
|
||||
|
/// <param name="type">媒体文件类型</param>
|
||||
|
/// <param name="media">待上传文件</param>
|
||||
|
/// <param name="cancellationToken"></param>
|
||||
|
/// <returns></returns>
|
||||
|
Task<WeChatWorkMediaResponse> UploadAsync( |
||||
|
string agentId, |
||||
|
string type, |
||||
|
IRemoteStreamContent media, |
||||
|
CancellationToken cancellationToken = default); |
||||
|
/// <summary>
|
||||
|
/// 获取临时素材
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// API: <see cref="https://developer.work.weixin.qq.com/document/path/90254"/>
|
||||
|
/// </remarks>
|
||||
|
/// <param name="agentId">应用标识</param>
|
||||
|
/// <param name="mediaId">媒体文件id</param>
|
||||
|
/// <param name="cancellationToken"></param>
|
||||
|
/// <returns></returns>
|
||||
|
///
|
||||
|
Task<IRemoteStreamContent> GetAsync( |
||||
|
string agentId, |
||||
|
string mediaId, |
||||
|
CancellationToken cancellationToken = default); |
||||
|
/// <summary>
|
||||
|
/// 上传图片
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// API: <see cref="https://developer.work.weixin.qq.com/document/path/90256"/>
|
||||
|
/// </remarks>
|
||||
|
/// <param name="agentId">应用标识</param>
|
||||
|
/// <param name="image">待上传图片</param>
|
||||
|
/// <param name="cancellationToken"></param>
|
||||
|
/// <returns></returns>
|
||||
|
Task<WeChatWorkImageResponse> UploadImageAsync( |
||||
|
string agentId, |
||||
|
IRemoteStreamContent image, |
||||
|
CancellationToken cancellationToken = default); |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
using Newtonsoft.Json; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Work.Media; |
||||
|
public class WeChatWorkImageResponse : WeChatWorkResponse |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 上传后得到的图片URL。永久有效
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("url")] |
||||
|
public string Url { get; set; } |
||||
|
} |
||||
@ -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<IRemoteStreamContent> 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<WeChatWorkResponse>(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<WeChatWorkMediaResponse> 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<WeChatWorkMediaResponse>(responseContent); |
||||
|
mediaRespose.ThrowIfNotSuccess(); |
||||
|
|
||||
|
return mediaRespose; |
||||
|
} |
||||
|
|
||||
|
public async virtual Task<WeChatWorkImageResponse> 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<WeChatWorkImageResponse>(responseContent); |
||||
|
mediaRespose.ThrowIfNotSuccess(); |
||||
|
|
||||
|
return mediaRespose; |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
using Newtonsoft.Json; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Work.Media; |
||||
|
public class WeChatWorkMediaResponse : WeChatWorkResponse |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 媒体文件类型
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// 图片(image)
|
||||
|
/// 语音(voice)
|
||||
|
/// 视频(video)
|
||||
|
/// 普通文件(file)
|
||||
|
/// </remarks>
|
||||
|
[JsonProperty("type")] |
||||
|
public string Type { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 媒体文件上传后获取的唯一标识,3天内有效
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("media_id")] |
||||
|
public string MediaId { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 媒体文件上传时间戳
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("created_at")] |
||||
|
public string CreatedAt { get; set; } |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Work.Message; |
||||
|
/// <summary>
|
||||
|
/// 消息发送接口
|
||||
|
/// </summary>
|
||||
|
public interface IWeChatWorkMessageSender |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 发送消息
|
||||
|
/// </summary>
|
||||
|
/// <param name="message">继承自 <see cref="WeChatWorkMessage"/> 的企业微信消息载体</param>
|
||||
|
/// <param name="cancellationToken"></param>
|
||||
|
/// <returns></returns>
|
||||
|
Task<WeChatWorkMessageResponse> SendAsync( |
||||
|
WeChatWorkMessage message, |
||||
|
CancellationToken cancellationToken = default); |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
using JetBrains.Annotations; |
||||
|
using Newtonsoft.Json; |
||||
|
using System.Text.Json.Serialization; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Work.Message.Models; |
||||
|
/// <summary>
|
||||
|
/// markdown消息
|
||||
|
/// </summary>
|
||||
|
public class MarkdownMessage |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// markdown内容,最长不超过2048个字节,必须是utf8编码
|
||||
|
/// </summary>
|
||||
|
[NotNull] |
||||
|
[JsonProperty("content")] |
||||
|
[JsonPropertyName("content")] |
||||
|
public string Content { get; set; } |
||||
|
public MarkdownMessage(string content) |
||||
|
{ |
||||
|
Content = content; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
using JetBrains.Annotations; |
||||
|
using Newtonsoft.Json; |
||||
|
using System.Text.Json.Serialization; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Work.Message.Models; |
||||
|
/// <summary>
|
||||
|
/// 媒体文件消息
|
||||
|
/// </summary>
|
||||
|
public class MediaMessage |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
///媒体文件id,可以调用上传临时素材接口获取
|
||||
|
/// </summary>
|
||||
|
[NotNull] |
||||
|
[JsonProperty("media_id")] |
||||
|
[JsonPropertyName("media_id")] |
||||
|
public string MediaId { get; set; } |
||||
|
public MediaMessage(string mediaId) |
||||
|
{ |
||||
|
MediaId = mediaId; |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
/// <summary>
|
||||
|
/// 图文消息(mp)载体
|
||||
|
/// </summary>
|
||||
|
public class MpNewMessagePayload |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 图文消息(mp)列表
|
||||
|
/// </summary>
|
||||
|
[NotNull] |
||||
|
[JsonProperty("articles")] |
||||
|
[JsonPropertyName("articles")] |
||||
|
public List<MpNewMessage> Articles { get; set; } |
||||
|
public MpNewMessagePayload(List<MpNewMessage> articles) |
||||
|
{ |
||||
|
Articles = articles; |
||||
|
} |
||||
|
} |
||||
|
/// <summary>
|
||||
|
/// 图文消息(mp)
|
||||
|
/// </summary>
|
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 标题,不超过128个字节,超过会自动截断(支持id转译)
|
||||
|
/// </summary>
|
||||
|
[NotNull] |
||||
|
[JsonProperty("title")] |
||||
|
[JsonPropertyName("title")] |
||||
|
public string Title { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 图文消息缩略图的media_id, 可以通过素材管理接口获得。此处thumb_media_id即上传接口返回的media_id
|
||||
|
/// </summary>
|
||||
|
[NotNull] |
||||
|
[JsonProperty("thumb_media_id")] |
||||
|
[JsonPropertyName("thumb_media_id")] |
||||
|
public string ThumbMediaId { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 图文消息的作者,不超过64个字节
|
||||
|
/// </summary>
|
||||
|
[CanBeNull] |
||||
|
[JsonProperty("author")] |
||||
|
[JsonPropertyName("author")] |
||||
|
public string Author { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 图文消息点击“阅读原文”之后的页面链接
|
||||
|
/// </summary>
|
||||
|
[CanBeNull] |
||||
|
[JsonProperty("content_source_url")] |
||||
|
[JsonPropertyName("content_source_url")] |
||||
|
public string ContentSourceUrl { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 图文消息的内容,支持html标签,不超过666 K个字节(支持id转译)
|
||||
|
/// </summary>
|
||||
|
[NotNull] |
||||
|
[JsonProperty("content")] |
||||
|
[JsonPropertyName("content")] |
||||
|
public string Content { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 图文消息的描述,不超过512个字节,超过会自动截断(支持id转译)
|
||||
|
/// </summary>
|
||||
|
[CanBeNull] |
||||
|
[JsonProperty("digest")] |
||||
|
[JsonPropertyName("digest")] |
||||
|
public string Description { get; set; } |
||||
|
} |
||||
@ -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; |
||||
|
/// <summary>
|
||||
|
/// 图文消息载体
|
||||
|
/// </summary>
|
||||
|
public class NewMessagePayload |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 图文消息列表
|
||||
|
/// </summary>
|
||||
|
[NotNull] |
||||
|
[JsonProperty("articles")] |
||||
|
[JsonPropertyName("articles")] |
||||
|
public List<NewMessage> Articles { get; set; } |
||||
|
public NewMessagePayload(List<NewMessage> articles) |
||||
|
{ |
||||
|
Articles = articles; |
||||
|
} |
||||
|
} |
||||
|
/// <summary>
|
||||
|
/// 图文消息
|
||||
|
/// </summary>
|
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 标题,不超过128个字节,超过会自动截断(支持id转译)
|
||||
|
/// </summary>
|
||||
|
[NotNull] |
||||
|
[JsonProperty("title")] |
||||
|
[JsonPropertyName("title")] |
||||
|
public string Title { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 描述,不超过512个字节,超过会自动截断(支持id转译)
|
||||
|
/// </summary>
|
||||
|
[CanBeNull] |
||||
|
[JsonProperty("description")] |
||||
|
[JsonPropertyName("description")] |
||||
|
public string Description { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 点击后跳转的链接。
|
||||
|
/// 最长2048字节,请确保包含了协议头(http/https),小程序或者url必须填写一个
|
||||
|
/// </summary>
|
||||
|
[CanBeNull] |
||||
|
[JsonProperty("url")] |
||||
|
[JsonPropertyName("url")] |
||||
|
public string Url { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 图文消息的图片链接,最长2048字节,支持JPG、PNG格式,较好的效果为大图 1068*455,小图150*150。
|
||||
|
/// </summary>
|
||||
|
[CanBeNull] |
||||
|
[JsonProperty("picurl")] |
||||
|
[JsonPropertyName("picurl")] |
||||
|
public string PictureUrl { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 小程序appid,必须是与当前应用关联的小程序,appid和pagepath必须同时填写,填写后会忽略url字段
|
||||
|
/// </summary>
|
||||
|
[CanBeNull] |
||||
|
[JsonProperty("appid")] |
||||
|
[JsonPropertyName("appid")] |
||||
|
public string AppId { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 点击消息卡片后的小程序页面,最长128字节,仅限本小程序内的页面。appid和pagepath必须同时填写,填写后会忽略url字段
|
||||
|
/// </summary>
|
||||
|
[CanBeNull] |
||||
|
[JsonProperty("pagepath")] |
||||
|
[JsonPropertyName("pagepath")] |
||||
|
public string PagePath { get; set; } |
||||
|
} |
||||
@ -0,0 +1,51 @@ |
|||||
|
using JetBrains.Annotations; |
||||
|
using Newtonsoft.Json; |
||||
|
using System.Text.Json.Serialization; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Work.Message.Models; |
||||
|
/// <summary>
|
||||
|
/// 文本卡片消息
|
||||
|
/// </summary>
|
||||
|
public class TextCardMessage |
||||
|
{ |
||||
|
public TextCardMessage( |
||||
|
string title, |
||||
|
string description, |
||||
|
string url, |
||||
|
string buttonText = "") |
||||
|
{ |
||||
|
Title = title; |
||||
|
Description = description; |
||||
|
Url = url; |
||||
|
ButtonText = buttonText; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 标题,不超过128个字节,超过会自动截断(支持id转译)
|
||||
|
/// </summary>
|
||||
|
[NotNull] |
||||
|
[JsonProperty("title")] |
||||
|
[JsonPropertyName("title")] |
||||
|
public string Title { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 描述,不超过512个字节,超过会自动截断(支持id转译)
|
||||
|
/// </summary>
|
||||
|
[NotNull] |
||||
|
[JsonProperty("description")] |
||||
|
[JsonPropertyName("description")] |
||||
|
public string Description { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 点击后跳转的链接。最长2048字节,请确保包含了协议头(http/https)
|
||||
|
/// </summary>
|
||||
|
[NotNull] |
||||
|
[JsonProperty("url")] |
||||
|
[JsonPropertyName("url")] |
||||
|
public string Url { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 按钮文字。 默认为“详情”, 不超过4个文字,超过自动截断。
|
||||
|
/// </summary>
|
||||
|
[CanBeNull] |
||||
|
[JsonProperty("btntxt")] |
||||
|
[JsonPropertyName("btntxt")] |
||||
|
public string ButtonText { get; set; } |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
using JetBrains.Annotations; |
||||
|
using Newtonsoft.Json; |
||||
|
using System.Text.Json.Serialization; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Work.Message.Models; |
||||
|
/// <summary>
|
||||
|
/// 文本消息
|
||||
|
/// </summary>
|
||||
|
public class TextMessage |
||||
|
{ |
||||
|
public TextMessage(string content) |
||||
|
{ |
||||
|
Content = content; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 消息内容,最长不超过2048个字节,超过将截断(支持id转译)
|
||||
|
/// </summary>
|
||||
|
[NotNull] |
||||
|
[JsonProperty("content")] |
||||
|
[JsonPropertyName("content")] |
||||
|
public string Content { get; set; } |
||||
|
} |
||||
@ -0,0 +1,42 @@ |
|||||
|
using JetBrains.Annotations; |
||||
|
using Newtonsoft.Json; |
||||
|
using System.Text.Json.Serialization; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Work.Message.Models; |
||||
|
/// <summary>
|
||||
|
/// 视频消息
|
||||
|
/// </summary>
|
||||
|
public class VideoMessage |
||||
|
{ |
||||
|
public VideoMessage( |
||||
|
string mediaId, |
||||
|
string title = "", |
||||
|
string description = "") |
||||
|
{ |
||||
|
Title = title; |
||||
|
Description = description; |
||||
|
MediaId = mediaId; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 视频消息的标题,不超过128个字节,超过会自动截断
|
||||
|
/// </summary>
|
||||
|
[CanBeNull] |
||||
|
[JsonProperty("title")] |
||||
|
[JsonPropertyName("title")] |
||||
|
public string Title { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 视频消息的描述,不超过512个字节,超过会自动截断
|
||||
|
/// </summary>
|
||||
|
[CanBeNull] |
||||
|
[JsonProperty("description")] |
||||
|
[JsonPropertyName("description")] |
||||
|
public string Description { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 视频媒体文件id,可以调用上传临时素材接口获取
|
||||
|
/// </summary>
|
||||
|
[NotNull] |
||||
|
[JsonProperty("media_id")] |
||||
|
[JsonPropertyName("media_id")] |
||||
|
public string MediaId { get; set; } |
||||
|
} |
||||
@ -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; |
||||
|
/// <summary>
|
||||
|
/// 企业微信文件消息
|
||||
|
/// </summary>
|
||||
|
public class WeChatWorkFileMessage : WeChatWorkMessage |
||||
|
{ |
||||
|
public WeChatWorkFileMessage( |
||||
|
string agentId, |
||||
|
MediaMessage file) : base(agentId, "file") |
||||
|
{ |
||||
|
File = file; |
||||
|
} |
||||
|
/// <summary>
|
||||
|
/// 媒体文件
|
||||
|
/// </summary>
|
||||
|
[NotNull] |
||||
|
[JsonProperty("file")] |
||||
|
[JsonPropertyName("file")] |
||||
|
public MediaMessage File { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 表示是否是保密消息,
|
||||
|
/// 0表示可对外分享,
|
||||
|
/// 1表示不能分享且内容显示水印,
|
||||
|
/// 默认为0
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("safe")] |
||||
|
[JsonPropertyName("safe")] |
||||
|
public int Safe { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 表示是否开启重复消息检查,0表示否,1表示是,默认0
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("enable_duplicate_check")] |
||||
|
[JsonPropertyName("enable_duplicate_check")] |
||||
|
public byte EnableDuplicateCheck { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 表示是否重复消息检查的时间间隔,默认1800s,最大不超过4小时
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("duplicate_check_interval")] |
||||
|
[JsonPropertyName("duplicate_check_interval")] |
||||
|
public int DuplicateCheckInterval { get; set; } = 1800; |
||||
|
} |
||||
@ -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; |
||||
|
/// <summary>
|
||||
|
/// 企业微信图片消息
|
||||
|
/// </summary>
|
||||
|
public class WeChatWorkImageMessage : WeChatWorkMessage |
||||
|
{ |
||||
|
public WeChatWorkImageMessage( |
||||
|
string agentId, |
||||
|
MediaMessage image) : base(agentId, "image") |
||||
|
{ |
||||
|
Image = image; |
||||
|
} |
||||
|
/// <summary>
|
||||
|
/// 图片媒体文件
|
||||
|
/// </summary>
|
||||
|
[NotNull] |
||||
|
[JsonProperty("image")] |
||||
|
[JsonPropertyName("image")] |
||||
|
public MediaMessage Image { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 表示是否是保密消息,
|
||||
|
/// 0表示可对外分享,
|
||||
|
/// 1表示不能分享且内容显示水印,
|
||||
|
/// 默认为0
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("safe")] |
||||
|
[JsonPropertyName("safe")] |
||||
|
public int Safe { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 表示是否开启id转译,0表示否,1表示是,默认0。
|
||||
|
/// 仅第三方应用需要用到
|
||||
|
/// 企业自建应用可以忽略。
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("enable_id_trans")] |
||||
|
[JsonPropertyName("enable_id_trans")] |
||||
|
public byte EnableIdTrans { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 表示是否开启重复消息检查,0表示否,1表示是,默认0
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("enable_duplicate_check")] |
||||
|
[JsonPropertyName("enable_duplicate_check")] |
||||
|
public byte EnableDuplicateCheck { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 表示是否重复消息检查的时间间隔,默认1800s,最大不超过4小时
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("duplicate_check_interval")] |
||||
|
[JsonPropertyName("duplicate_check_interval")] |
||||
|
public int DuplicateCheckInterval { get; set; } = 1800; |
||||
|
} |
||||
@ -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; |
||||
|
/// <summary>
|
||||
|
/// 企业微信markdown消息
|
||||
|
/// </summary>
|
||||
|
public class WeChatWorkMarkdownMessage : WeChatWorkMessage |
||||
|
{ |
||||
|
public WeChatWorkMarkdownMessage( |
||||
|
string agentId, |
||||
|
MarkdownMessage markdown) : base(agentId, "markdown") |
||||
|
{ |
||||
|
Markdown = markdown; |
||||
|
} |
||||
|
/// <summary>
|
||||
|
/// markdown消息
|
||||
|
/// </summary>
|
||||
|
[NotNull] |
||||
|
[JsonProperty("markdown")] |
||||
|
[JsonPropertyName("markdown")] |
||||
|
public MarkdownMessage Markdown { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 表示是否开启重复消息检查,0表示否,1表示是,默认0
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("enable_duplicate_check")] |
||||
|
[JsonPropertyName("enable_duplicate_check")] |
||||
|
public byte EnableDuplicateCheck { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 表示是否重复消息检查的时间间隔,默认1800s,最大不超过4小时
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("duplicate_check_interval")] |
||||
|
[JsonPropertyName("duplicate_check_interval")] |
||||
|
public int DuplicateCheckInterval { get; set; } = 1800; |
||||
|
} |
||||
@ -0,0 +1,66 @@ |
|||||
|
using JetBrains.Annotations; |
||||
|
using Newtonsoft.Json; |
||||
|
using System.Text.Json.Serialization; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Work.Message; |
||||
|
/// <summary>
|
||||
|
/// 企业微信消息
|
||||
|
/// </summary>
|
||||
|
public abstract class WeChatWorkMessage |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 指定接收消息的成员,成员ID列表(多个接收者用‘|’分隔,最多支持1000个)。
|
||||
|
/// 特殊情况:指定为"@all",则向该企业应用的全部成员发送
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("touser")] |
||||
|
[JsonPropertyName("touser")] |
||||
|
public virtual string ToUser { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 指定接收消息的部门,部门ID列表,多个接收者用‘|’分隔,最多支持100个。
|
||||
|
/// 当touser为"@all"时忽略本参数
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("toparty")] |
||||
|
[JsonPropertyName("toparty")] |
||||
|
public virtual string ToParty { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 指定接收消息的标签,标签ID列表,多个接收者用‘|’分隔,最多支持100个。
|
||||
|
/// 当touser为"@all"时忽略本参数
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("totag")] |
||||
|
[JsonPropertyName("totag")] |
||||
|
public virtual string ToTag { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 消息类型
|
||||
|
/// </summary>
|
||||
|
[NotNull] |
||||
|
[JsonProperty("msgtype")] |
||||
|
[JsonPropertyName("msgtype")] |
||||
|
public virtual string MsgType { get; protected set; } |
||||
|
/// <summary>
|
||||
|
/// 企业应用的id,整型。
|
||||
|
/// 企业内部开发,可在应用的设置页面查看;
|
||||
|
/// 第三方服务商,可通过接口 获取企业授权信息 获取该参数值
|
||||
|
/// </summary>
|
||||
|
[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); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
using Newtonsoft.Json; |
||||
|
using System.Text.Json.Serialization; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Work.Message; |
||||
|
/// <summary>
|
||||
|
/// 企业微信发送消息响应
|
||||
|
/// </summary>
|
||||
|
public class WeChatWorkMessageResponse : WeChatWorkResponse |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 不合法的userid,不区分大小写,统一转为小写
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("invaliduser")] |
||||
|
[JsonPropertyName("invaliduser")] |
||||
|
public string InvalidUser { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 不合法的partyid
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("invalidparty")] |
||||
|
[JsonPropertyName("invalidparty")] |
||||
|
public string InvalidParty { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 不合法的标签id
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("invalidtag")] |
||||
|
[JsonPropertyName("invalidtag")] |
||||
|
public string InvalidTag { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 没有基础接口许可(包含已过期)的userid
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("unlicenseduser")] |
||||
|
[JsonPropertyName("unlicenseduser")] |
||||
|
public string UnLicensedUser { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 消息id,用于撤回应用消息
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("msgid")] |
||||
|
[JsonPropertyName("msgid")] |
||||
|
public string MsgId { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 仅消息类型为“按钮交互型”,“投票选择型”和“多项选择型”的模板卡片消息返回,
|
||||
|
/// 应用可使用response_code调用更新模版卡片消息接口,72小时内有效,且只能使用一次
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("response_code")] |
||||
|
[JsonPropertyName("response_code")] |
||||
|
public string ResponseCode { get; set; } |
||||
|
} |
||||
@ -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<WeChatWorkMessageSender> Logger { get; set; } |
||||
|
|
||||
|
protected IHttpClientFactory HttpClientFactory { get; } |
||||
|
protected IWeChatWorkTokenProvider WeChatWorkTokenProvider { get; } |
||||
|
|
||||
|
public WeChatWorkMessageSender( |
||||
|
IHttpClientFactory httpClientFactory, |
||||
|
IWeChatWorkTokenProvider weChatWorkTokenProvider) |
||||
|
{ |
||||
|
HttpClientFactory = httpClientFactory; |
||||
|
WeChatWorkTokenProvider = weChatWorkTokenProvider; |
||||
|
|
||||
|
Logger = NullLogger<WeChatWorkMessageSender>.Instance; |
||||
|
} |
||||
|
|
||||
|
[RequiresFeature(WeChatWorkFeatureNames.Message.Enable)] |
||||
|
[RequiresLimitFeature( |
||||
|
WeChatWorkFeatureNames.Message.SendLimit, |
||||
|
WeChatWorkFeatureNames.Message.SendLimitInterval, |
||||
|
LimitPolicy.Days)] |
||||
|
public async virtual Task<WeChatWorkMessageResponse> 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<WeChatWorkMessageResponse>(responseContent); |
||||
|
if (!messageResponse.IsSuccessed) |
||||
|
{ |
||||
|
Logger.LogWarning("Send wechat work message failed"); |
||||
|
Logger.LogWarning($"Error code: {messageResponse.ErrorCode}, message: {messageResponse.ErrorMessage}"); |
||||
|
} |
||||
|
|
||||
|
return messageResponse; |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
/// <summary>
|
||||
|
/// 企业微信文本图文消息
|
||||
|
/// </summary>
|
||||
|
public class WeChatWorkMpNewMessage : WeChatWorkMessage |
||||
|
{ |
||||
|
public WeChatWorkMpNewMessage( |
||||
|
string agentId, |
||||
|
MpNewMessagePayload mpnews) : base(agentId, "mpnews") |
||||
|
{ |
||||
|
News = mpnews; |
||||
|
} |
||||
|
/// <summary>
|
||||
|
/// 图文消息(mp)
|
||||
|
/// </summary>
|
||||
|
[NotNull] |
||||
|
[JsonProperty("mpnews")] |
||||
|
[JsonPropertyName("mpnews")] |
||||
|
public MpNewMessagePayload News { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 表示是否是保密消息,
|
||||
|
/// 0表示可对外分享,
|
||||
|
/// 1表示不能分享且内容显示水印,
|
||||
|
/// 2表示仅限在企业内分享,默认为0;
|
||||
|
/// 注意仅mpnews类型的消息支持safe值为2,其他消息类型不支持
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("safe")] |
||||
|
[JsonPropertyName("safe")] |
||||
|
public int Safe { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 表示是否开启id转译,0表示否,1表示是,默认0。
|
||||
|
/// 仅第三方应用需要用到
|
||||
|
/// 企业自建应用可以忽略。
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("enable_id_trans")] |
||||
|
[JsonPropertyName("enable_id_trans")] |
||||
|
public byte EnableIdTrans { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 表示是否开启重复消息检查,0表示否,1表示是,默认0
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("enable_duplicate_check")] |
||||
|
[JsonPropertyName("enable_duplicate_check")] |
||||
|
public byte EnableDuplicateCheck { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 表示是否重复消息检查的时间间隔,默认1800s,最大不超过4小时
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("duplicate_check_interval")] |
||||
|
[JsonPropertyName("duplicate_check_interval")] |
||||
|
public int DuplicateCheckInterval { get; set; } = 1800; |
||||
|
} |
||||
@ -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; |
||||
|
/// <summary>
|
||||
|
/// 企业微信文本图文消息
|
||||
|
/// </summary>
|
||||
|
public class WeChatWorkNewMessage : WeChatWorkMessage |
||||
|
{ |
||||
|
public WeChatWorkNewMessage( |
||||
|
string agentId, |
||||
|
NewMessagePayload news) : base(agentId, "news") |
||||
|
{ |
||||
|
News = news; |
||||
|
} |
||||
|
/// <summary>
|
||||
|
/// 图文消息
|
||||
|
/// </summary>
|
||||
|
[NotNull] |
||||
|
[JsonProperty("news")] |
||||
|
[JsonPropertyName("news")] |
||||
|
public NewMessagePayload News { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 表示是否开启id转译,0表示否,1表示是,默认0。
|
||||
|
/// 仅第三方应用需要用到
|
||||
|
/// 企业自建应用可以忽略。
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("enable_id_trans")] |
||||
|
[JsonPropertyName("enable_id_trans")] |
||||
|
public byte EnableIdTrans { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 表示是否开启重复消息检查,0表示否,1表示是,默认0
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("enable_duplicate_check")] |
||||
|
[JsonPropertyName("enable_duplicate_check")] |
||||
|
public byte EnableDuplicateCheck { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 表示是否重复消息检查的时间间隔,默认1800s,最大不超过4小时
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("duplicate_check_interval")] |
||||
|
[JsonPropertyName("duplicate_check_interval")] |
||||
|
public int DuplicateCheckInterval { get; set; } = 1800; |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue