58 changed files with 1768 additions and 7 deletions
@ -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.WxPusher\LINGYUN.Abp.WxPusher.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
|
|||
</Project> |
|||
@ -0,0 +1,12 @@ |
|||
using LINGYUN.Abp.WxPusher; |
|||
using Volo.Abp.Identity; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.Identity.WxPusher; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpWxPusherModule), |
|||
typeof(AbpIdentityDomainModule))] |
|||
public class AbpIdentityWxPusherModule : AbpModule |
|||
{ |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
using LINGYUN.Abp.WxPusher.Security.Claims; |
|||
using LINGYUN.Abp.WxPusher.User; |
|||
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.WxPusher.User; |
|||
|
|||
[Dependency(ServiceLifetime.Transient, ReplaceServices = true)] |
|||
[ExposeServices(typeof(IWxPusherUserStore))] |
|||
public class IdentityWxPusherUserStore : IWxPusherUserStore |
|||
{ |
|||
protected IdentityUserManager UserManager { get; } |
|||
|
|||
public IdentityWxPusherUserStore(IdentityUserManager userManager) |
|||
{ |
|||
UserManager = userManager; |
|||
} |
|||
|
|||
public async virtual Task<List<int>> GetSubscribeTopicsAsync(IEnumerable<Guid> userIds, CancellationToken cancellationToken = default) |
|||
{ |
|||
var topics = new List<int>(); |
|||
|
|||
foreach (var userId in userIds) |
|||
{ |
|||
var user = await UserManager.FindByIdAsync(userId.ToString()); |
|||
|
|||
var userUidClaim = user?.Claims |
|||
.Where(c => c.ClaimType.Equals(AbpWxPusherClaimTypes.Uid)) |
|||
.FirstOrDefault(); |
|||
|
|||
if (userUidClaim != null && |
|||
int.TryParse(userUidClaim.ClaimValue, out var topic)) |
|||
{ |
|||
topics.Add(topic); |
|||
} |
|||
} |
|||
|
|||
return topics.Distinct().ToList(); |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
# LINGYUN.Abp.Identity.WxPusher |
|||
|
|||
IWxPusherUserStore 接口的Identity模块实现, 通过用户Claims来获取关注的topic列表 |
|||
|
|||
## 模块引用 |
|||
|
|||
```csharp |
|||
[DependsOn(typeof(AbpIdentityWxPusherModule))] |
|||
public class YouProjectModule : AbpModule |
|||
{ |
|||
// other |
|||
} |
|||
``` |
|||
@ -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.WxPusher\LINGYUN.Abp.WxPusher.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,74 @@ |
|||
using LINGYUN.Abp.WxPusher.Messages; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace LINGYUN.Abp.Notifications; |
|||
|
|||
public static class NotificationDefinitionExtensions |
|||
{ |
|||
private const string Prefix = "wx-pusher:"; |
|||
private const string ContentTypeKey = Prefix + "contentType"; |
|||
private const string TopicKey = Prefix + "contentType"; |
|||
/// <summary>
|
|||
/// 设定消息内容类型
|
|||
/// </summary>
|
|||
/// <param name="notification"></param>
|
|||
/// <param name="contentType"></param>
|
|||
/// <returns></returns>
|
|||
public static NotificationDefinition WithContentType( |
|||
this NotificationDefinition notification, |
|||
MessageContentType contentType) |
|||
{ |
|||
return notification.WithProperty(ContentTypeKey, contentType); |
|||
} |
|||
/// <summary>
|
|||
/// 获取消息发送通道
|
|||
/// </summary>
|
|||
/// <param name="notification"></param>
|
|||
/// <param name="defaultChannelType"></param>
|
|||
/// <returns></returns>
|
|||
public static MessageContentType GetContentTypeOrDefault( |
|||
this NotificationDefinition notification, |
|||
MessageContentType defaultContentType = MessageContentType.Text) |
|||
{ |
|||
if (notification.Properties.TryGetValue(ContentTypeKey, out var defineContentType) == true && |
|||
defineContentType is MessageContentType contentType) |
|||
{ |
|||
return contentType; |
|||
} |
|||
|
|||
return defaultContentType; |
|||
} |
|||
/// <summary>
|
|||
/// 消息主题(Topic)
|
|||
/// see: https://wxpusher.dingliqc.com/docs/#/?id=%e5%90%8d%e8%af%8d%e8%a7%a3%e9%87%8a
|
|||
/// </summary>
|
|||
/// <param name="notification">群组编码</param>
|
|||
/// <param name="topics"></param>
|
|||
/// <returns>
|
|||
/// <see cref="NotificationDefinition"/>
|
|||
/// </returns>
|
|||
public static NotificationDefinition WithTopics( |
|||
this NotificationDefinition notification, |
|||
List<int> topics) |
|||
{ |
|||
return notification.WithProperty(TopicKey, topics); |
|||
} |
|||
/// <summary>
|
|||
/// 获取消息群发群组编码
|
|||
/// </summary>
|
|||
/// <param name="notification"></param>
|
|||
/// <returns>
|
|||
/// 通知定义的群组编码列表
|
|||
/// </returns>
|
|||
public static List<int> GetTopics( |
|||
this NotificationDefinition notification) |
|||
{ |
|||
if (notification.Properties.TryGetValue(TopicKey, out var topicsDefine) == true && |
|||
topicsDefine is List<int> topics) |
|||
{ |
|||
return topics; |
|||
} |
|||
|
|||
return new List<int>(); |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using LINGYUN.Abp.WxPusher; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.Notifications.WxPusher; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpNotificationModule), |
|||
typeof(AbpWxPusherModule))] |
|||
public class AbpNotificationsPushPlusModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpNotificationOptions>(options => |
|||
{ |
|||
options.PublishProviders.Add<WxPusherNotificationPublishProvider>(); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,100 @@ |
|||
using LINGYUN.Abp.RealTime.Localization; |
|||
using LINGYUN.Abp.WxPusher.Messages; |
|||
using LINGYUN.Abp.WxPusher.User; |
|||
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.Localization; |
|||
|
|||
namespace LINGYUN.Abp.Notifications.WxPusher; |
|||
|
|||
public class WxPusherNotificationPublishProvider : NotificationPublishProvider |
|||
{ |
|||
public const string ProviderName = "WxPusher"; |
|||
|
|||
public override string Name => ProviderName; |
|||
|
|||
protected IWxPusherUserStore WxPusherUserStore { get; } |
|||
|
|||
protected IWxPusherMessageSender WxPusherMessageSender { get; } |
|||
|
|||
protected IStringLocalizerFactory LocalizerFactory { get; } |
|||
|
|||
protected AbpLocalizationOptions LocalizationOptions { get; } |
|||
|
|||
protected INotificationDefinitionManager NotificationDefinitionManager { get; } |
|||
|
|||
public WxPusherNotificationPublishProvider( |
|||
IWxPusherUserStore wxPusherUserStore, |
|||
IWxPusherMessageSender wxPusherMessageSender, |
|||
IStringLocalizerFactory localizerFactory, |
|||
IOptions<AbpLocalizationOptions> localizationOptions, |
|||
INotificationDefinitionManager notificationDefinitionManager) |
|||
{ |
|||
WxPusherUserStore = wxPusherUserStore; |
|||
WxPusherMessageSender = wxPusherMessageSender; |
|||
LocalizerFactory = localizerFactory; |
|||
LocalizationOptions = localizationOptions.Value; |
|||
NotificationDefinitionManager = notificationDefinitionManager; |
|||
} |
|||
|
|||
protected async override Task PublishAsync( |
|||
NotificationInfo notification, |
|||
IEnumerable<UserIdentifier> identifiers, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
var topics = await WxPusherUserStore |
|||
.GetSubscribeTopicsAsync( |
|||
identifiers.Select(x => x.UserId), |
|||
cancellationToken); |
|||
|
|||
var notificationDefine = NotificationDefinitionManager.GetOrNull(notification.Name); |
|||
var topicDefine = notificationDefine?.GetTopics(); |
|||
if (topicDefine.Any()) |
|||
{ |
|||
topics = topicDefine; |
|||
} |
|||
var contentType = notificationDefine?.GetContentTypeOrDefault(MessageContentType.Text) |
|||
?? MessageContentType.Text; |
|||
|
|||
if (!notification.Data.NeedLocalizer()) |
|||
{ |
|||
var title = notification.Data.TryGetData("title").ToString(); |
|||
var message = notification.Data.TryGetData("message").ToString(); |
|||
|
|||
await WxPusherMessageSender.SendAsync( |
|||
content: message, |
|||
summary: title, |
|||
contentType: contentType, |
|||
topicIds: topics, |
|||
cancellationToken: cancellationToken); |
|||
} |
|||
else |
|||
{ |
|||
var titleInfo = notification.Data.TryGetData("title").As<LocalizableStringInfo>(); |
|||
var titleResource = GetResource(titleInfo.ResourceName); |
|||
var title = LocalizerFactory.Create(titleResource.ResourceType)[titleInfo.Name, titleInfo.Values].Value; |
|||
|
|||
var messageInfo = notification.Data.TryGetData("message").As<LocalizableStringInfo>(); |
|||
var messageResource = GetResource(messageInfo.ResourceName); |
|||
var message = LocalizerFactory.Create(messageResource.ResourceType)[messageInfo.Name, messageInfo.Values].Value; |
|||
|
|||
await WxPusherMessageSender.SendAsync( |
|||
content: message, |
|||
summary: title, |
|||
contentType: contentType, |
|||
topicIds: topics, |
|||
cancellationToken: cancellationToken); |
|||
} |
|||
} |
|||
|
|||
private LocalizationResource GetResource(string resourceName) |
|||
{ |
|||
return LocalizationOptions.Resources.Values |
|||
.First(x => x.ResourceName.Equals(resourceName)); |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
# LINGYUN.Abp.Notifications.WxPusher |
|||
|
|||
通知模块的WxPusher实现 |
|||
|
|||
使应用可通过WxPusher发布实时通知 |
|||
|
|||
## 模块引用 |
|||
|
|||
```csharp |
|||
[DependsOn(typeof(AbpNotificationsWxPusherModule))] |
|||
public class YouProjectModule : AbpModule |
|||
{ |
|||
// other |
|||
} |
|||
``` |
|||
@ -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,31 @@ |
|||
<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\WxPusher\Localization\Resources\en.json" /> |
|||
<None Remove="LINGYUN\Abp\WxPusher\Localization\Resources\zh-Hans.json" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<EmbeddedResource Include="LINGYUN\Abp\WxPusher\Localization\Resources\en.json" /> |
|||
<EmbeddedResource Include="LINGYUN\Abp\WxPusher\Localization\Resources\zh-Hans.json" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.Caching" Version="$(VoloAbpPackageVersion)" /> |
|||
<PackageReference Include="Volo.Abp.Json" Version="$(VoloAbpPackageVersion)" /> |
|||
<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,53 @@ |
|||
using LINGYUN.Abp.Features.LimitValidation; |
|||
using LINGYUN.Abp.WxPusher.Localization; |
|||
using LINGYUN.Abp.WxPusher.Messages; |
|||
using LINGYUN.Abp.WxPusher.QrCode; |
|||
using LINGYUN.Abp.WxPusher.User; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.DependencyInjection.Extensions; |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.Caching; |
|||
using Volo.Abp.Json; |
|||
using Volo.Abp.Json.SystemTextJson; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.Settings; |
|||
using Volo.Abp.VirtualFileSystem; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpJsonModule), |
|||
typeof(AbpSettingsModule), |
|||
typeof(AbpCachingModule), |
|||
typeof(AbpFeaturesLimitValidationModule))] |
|||
public class AbpWxPusherModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.AddWxPusherClient(); |
|||
context.Services.TryAddSingleton<IWxPusherUserStore>(NullWxPusherUserStore.Instance); |
|||
|
|||
Configure<AbpSystemTextJsonSerializerOptions>(options => |
|||
{ |
|||
options.UnsupportedTypes.TryAdd<WxPusherResult<int>>(); |
|||
options.UnsupportedTypes.TryAdd<WxPusherResult<string>>(); |
|||
options.UnsupportedTypes.TryAdd<WxPusherResult<CreateQrcodeResult>>(); |
|||
options.UnsupportedTypes.TryAdd<WxPusherResult<GetScanQrCodeResult>>(); |
|||
options.UnsupportedTypes.TryAdd<WxPusherResult<List<SendMessageResult>>>(); |
|||
options.UnsupportedTypes.TryAdd<WxPusherResult<WxPusherPagedResult<UserProfile>>>(); |
|||
}); |
|||
|
|||
Configure<AbpVirtualFileSystemOptions>(options => |
|||
{ |
|||
options.FileSets.AddEmbedded<AbpWxPusherModule>(); |
|||
}); |
|||
|
|||
Configure<AbpLocalizationOptions>(options => |
|||
{ |
|||
options.Resources |
|||
.Add<WxPusherResource>() |
|||
.AddVirtualJson("/LINGYUN/Abp/WxPusher/Localization/Resources"); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
using LINGYUN.Abp.WxPusher.Localization; |
|||
using Volo.Abp.Features; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.Validation.StringValues; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher.Features; |
|||
|
|||
public class WxPusherFeatureDefinitionProvider : FeatureDefinitionProvider |
|||
{ |
|||
public override void Define(IFeatureDefinitionContext context) |
|||
{ |
|||
var group = context.AddGroup( |
|||
name: WxPusherFeatureNames.GroupName, |
|||
displayName: L("Features:WxPusher")); |
|||
group.AddFeature( |
|||
name: WxPusherFeatureNames.Enable, |
|||
defaultValue: "true", |
|||
displayName: L("Features:WxPusherEnable"), |
|||
description: L("Features:WxPusherEnableDesc"), |
|||
valueType: new ToggleStringValueType(new BooleanValueValidator())); |
|||
|
|||
var message = group.AddFeature( |
|||
name: WxPusherFeatureNames.Message.GroupName, |
|||
displayName: L("Features:Message"), |
|||
description: L("Features:Message")); |
|||
|
|||
message.CreateChild( |
|||
name: WxPusherFeatureNames.Message.Enable, |
|||
defaultValue: "true", |
|||
displayName: L("Features:MessageEnable"), |
|||
description: L("Features:MessageEnableDesc"), |
|||
valueType: new ToggleStringValueType(new BooleanValueValidator())); |
|||
message.CreateChild( |
|||
name: WxPusherFeatureNames.Message.SendLimit, |
|||
defaultValue: "500", |
|||
displayName: L("Features:Message.SendLimit"), |
|||
description: L("Features:Message.SendLimitDesc"), |
|||
valueType: new FreeTextStringValueType(new NumericValueValidator(1, 500))); |
|||
message.CreateChild( |
|||
name: WxPusherFeatureNames.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<WxPusherResource>(name); |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
namespace LINGYUN.Abp.WxPusher.Features; |
|||
|
|||
public static class WxPusherFeatureNames |
|||
{ |
|||
public const string GroupName = "WxPusher"; |
|||
|
|||
/// <summary>
|
|||
/// 启用WxPusher
|
|||
/// </summary>
|
|||
public const string Enable = GroupName + ".Enable"; |
|||
|
|||
public static class Message |
|||
{ |
|||
public const string GroupName = WxPusherFeatureNames.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,17 @@ |
|||
{ |
|||
"culture": "en", |
|||
"texts": { |
|||
"Settings:Security.AppToken": "App Token", |
|||
"Settings:Security.AppTokenDesc": "If you have APP_TOKEN, you can send messages to users of the corresponding application. Keep it confidential.", |
|||
"Features:WxPusher": "WxPusher Wechat push service", |
|||
"Features:WxPusherEnable": "Enable WxPusher", |
|||
"Features:WxPusherEnableDesc": "Enable to enable the application to have WxPusher capabilities.", |
|||
"Features:Message": "WxPusher Wechat message push", |
|||
"Features:MessageEnable": "Enable WxPusher Wechat Message Push", |
|||
"Features:MessageEnableDesc": "Enable so that apps will have the ability to be pushed to wechat via WxPusher.", |
|||
"Features:Message.SendLimit": "Amount of wechat message push", |
|||
"Features:Message.SendLimitDesc": "Set to limit the amount of wechat message push.", |
|||
"Features:Message.SendLimitInterval": "Wechat message limit interval", |
|||
"Features:Message.SendLimitIntervalDesc": "Set the wechat message limit period (time scale: days). A single wechat user (UID) can receive a maximum of 500 messages per day. Please arrange the sending frequency reasonably." |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
{ |
|||
"culture": "zh-Hans", |
|||
"texts": { |
|||
"Settings:Security.AppToken": "应用的身份标志", |
|||
"Settings:Security.AppTokenDesc": "拥有APP_TOKEN,就可以给对应的应用的用户发送消息, 请严格保密.", |
|||
"Features:WxPusher": "WxPusher微信推送服务", |
|||
"Features:WxPusherEnable": "启用WxPusher", |
|||
"Features:WxPusherEnableDesc": "启用以使应用拥有WxPusher的能力.", |
|||
"Features:Message": "WxPusher微信消息推送", |
|||
"Features:MessageEnable": "启用WxPusher微信消息推送", |
|||
"Features:MessageEnableDesc": "启用以使应用将拥有通过WxPusher推送到微信的能力.", |
|||
"Features:Message.SendLimit": "微信消息推送量", |
|||
"Features:Message.SendLimitDesc": "设置以限制微信消息推送量.", |
|||
"Features:Message.SendLimitInterval": "微信消息限制周期", |
|||
"Features:Message.SendLimitIntervalDesc": "设置微信消息限制周期(时间刻度: 天).单个微信用户(uid),每天最多接收500条消息,请合理安排发送频率." |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
using Volo.Abp.Localization; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher.Localization; |
|||
|
|||
[LocalizationResourceName("WxPusher")] |
|||
public class WxPusherResource |
|||
{ |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher.Messages; |
|||
|
|||
public interface IWxPusherMessageProvider |
|||
{ |
|||
Task<WxPusherResult<int>> QueryMessageAsync( |
|||
int messageId, |
|||
CancellationToken cancellationToken = default); |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using System.Collections.Generic; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher.Messages; |
|||
|
|||
public interface IWxPusherMessageSender |
|||
{ |
|||
Task<List<SendMessageResult>> SendAsync( |
|||
string content, |
|||
string summary = "", |
|||
MessageContentType contentType = MessageContentType.Text, |
|||
List<int> topicIds = null, |
|||
List<string> uids = null, |
|||
string url = "", |
|||
CancellationToken cancellationToken = default); |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
namespace LINGYUN.Abp.WxPusher.Messages; |
|||
public enum MessageContentType |
|||
{ |
|||
/// <summary>
|
|||
/// 文字
|
|||
/// </summary>
|
|||
Text = 1, |
|||
/// <summary>
|
|||
/// html(只发送body标签内部的数据即可,不包括body标签
|
|||
/// </summary>
|
|||
Html = 2, |
|||
/// <summary>
|
|||
/// markdown
|
|||
/// </summary>
|
|||
Markdown = 3 |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
using Newtonsoft.Json; |
|||
using System.Net.Http; |
|||
using System.Text; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher.Messages; |
|||
|
|||
internal static class MessageHttpClientExtensions |
|||
{ |
|||
public async static Task<string> SendMessageAsync( |
|||
this HttpClient httpClient, |
|||
SendMessage sendMessage, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
var requestMessage = new HttpRequestMessage( |
|||
HttpMethod.Post, |
|||
"/api/send/message"); |
|||
|
|||
var requestBody = JsonConvert.SerializeObject(sendMessage); |
|||
requestMessage.Content = new StringContent(requestBody, Encoding.UTF8, "application/json"); |
|||
|
|||
var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken); |
|||
|
|||
return await httpResponse.Content.ReadAsStringAsync(); |
|||
} |
|||
|
|||
public async static Task<string> QueryMessageAsync( |
|||
this HttpClient httpClient, |
|||
int messageId, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
var requestMessage = new HttpRequestMessage( |
|||
HttpMethod.Get, |
|||
$"/api/send/query/{messageId}"); |
|||
|
|||
var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken); |
|||
|
|||
return await httpResponse.Content.ReadAsStringAsync(); |
|||
} |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
using JetBrains.Annotations; |
|||
using Newtonsoft.Json; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Volo.Abp; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher.Messages; |
|||
|
|||
[Serializable] |
|||
public class SendMessage |
|||
{ |
|||
[JsonProperty("appToken")] |
|||
public string AppToken { get; } |
|||
|
|||
[JsonProperty("content")] |
|||
public string Content { get; } |
|||
|
|||
[JsonProperty("summary")] |
|||
public string Summary { get; set; } |
|||
|
|||
[JsonProperty("contentType")] |
|||
public MessageContentType ContentType { get; } |
|||
|
|||
[JsonProperty("topicIds")] |
|||
public List<int> TopicIds { get; } |
|||
|
|||
[JsonProperty("uids")] |
|||
public List<string> Uids { get; } |
|||
|
|||
[JsonProperty("url")] |
|||
public string Url { get; } |
|||
public SendMessage( |
|||
[NotNull] string appToken, |
|||
[NotNull] string content, |
|||
string summary = "", |
|||
MessageContentType contentType = MessageContentType.Text, |
|||
string url = "") |
|||
{ |
|||
Check.NotNullOrWhiteSpace(appToken, nameof(appToken)); |
|||
Check.NotNullOrWhiteSpace(content, nameof(content)); |
|||
Check.Length(summary, nameof(summary), 100); |
|||
|
|||
AppToken = appToken; |
|||
Content = content; |
|||
Summary = summary; |
|||
ContentType = contentType; |
|||
Url = url; |
|||
|
|||
TopicIds = new List<int>(); |
|||
Uids = new List<string>(); |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
using Newtonsoft.Json; |
|||
using System; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher.Messages; |
|||
|
|||
[Serializable] |
|||
public class SendMessageResult |
|||
{ |
|||
/// <summary>
|
|||
/// 状态码
|
|||
/// </summary>
|
|||
[JsonProperty("code")] |
|||
public int Code { get; set; } |
|||
/// <summary>
|
|||
/// 消息标识
|
|||
/// </summary>
|
|||
[JsonProperty("messageId")] |
|||
public long MessageId { get; set; } |
|||
/// <summary>
|
|||
/// 状态
|
|||
/// </summary>
|
|||
[JsonProperty("status")] |
|||
public string Status { get; set; } |
|||
/// <summary>
|
|||
/// 用户标识
|
|||
/// </summary>
|
|||
[JsonProperty("uid")] |
|||
public string Uid { get; set; } |
|||
/// <summary>
|
|||
/// 群组标识
|
|||
/// </summary>
|
|||
[JsonProperty("topicId")] |
|||
public string TopicId { get; set; } |
|||
/// <summary>
|
|||
/// 是否调用成功
|
|||
/// </summary>
|
|||
public bool IsSuccessed => Code == 1000; |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
using LINGYUN.Abp.WxPusher.Features; |
|||
using System.Net.Http; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Features; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher.Messages; |
|||
|
|||
[RequiresFeature(WxPusherFeatureNames.Enable)] |
|||
public class WxPusherMessageProvider : WxPusherRequestProvider, IWxPusherMessageProvider |
|||
{ |
|||
public async virtual Task<WxPusherResult<int>> QueryMessageAsync( |
|||
int messageId, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
var client = HttpClientFactory.GetPushPlusClient(); |
|||
|
|||
var content = await client.QueryMessageAsync( |
|||
messageId, |
|||
cancellationToken); |
|||
|
|||
return JsonSerializer.Deserialize<WxPusherResult<int>>(content); |
|||
} |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
using LINGYUN.Abp.Features.LimitValidation; |
|||
using LINGYUN.Abp.WxPusher.Features; |
|||
using LINGYUN.Abp.WxPusher.Token; |
|||
using System.Collections.Generic; |
|||
using System.Net.Http; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Features; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher.Messages; |
|||
|
|||
[RequiresFeature(WxPusherFeatureNames.Enable)] |
|||
public class WxPusherMessageSender : WxPusherRequestProvider, IWxPusherMessageSender |
|||
{ |
|||
protected IWxPusherTokenProvider WxPusherTokenProvider { get; } |
|||
|
|||
public WxPusherMessageSender(IWxPusherTokenProvider wxPusherTokenProvider) |
|||
{ |
|||
WxPusherTokenProvider = wxPusherTokenProvider; |
|||
} |
|||
|
|||
[RequiresFeature(WxPusherFeatureNames.Message.Enable)] |
|||
[RequiresLimitFeature( |
|||
WxPusherFeatureNames.Message.SendLimit, |
|||
WxPusherFeatureNames.Message.SendLimitInterval, |
|||
LimitPolicy.Days)] |
|||
public async virtual Task<List<SendMessageResult>> SendAsync( |
|||
string content, |
|||
string summary = "", |
|||
MessageContentType contentType = MessageContentType.Text, |
|||
List<int> topicIds = null, |
|||
List<string> uids = null, |
|||
string url = "", |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
var token = await WxPusherTokenProvider.GetTokenAsync(cancellationToken); |
|||
var client = HttpClientFactory.GetPushPlusClient(); |
|||
var sendMessage = new SendMessage( |
|||
token, |
|||
content, |
|||
summary, |
|||
contentType, |
|||
url); |
|||
if (topicIds != null) |
|||
{ |
|||
sendMessage.TopicIds.AddIfNotContains(topicIds); |
|||
} |
|||
if (uids != null) |
|||
{ |
|||
sendMessage.Uids.AddIfNotContains(uids); |
|||
} |
|||
|
|||
var resultContent = await client.SendMessageAsync( |
|||
sendMessage, |
|||
cancellationToken); |
|||
|
|||
var response = JsonSerializer |
|||
.Deserialize<WxPusherResult<List<SendMessageResult>>>(resultContent); |
|||
|
|||
return response.GetData(); |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
using JetBrains.Annotations; |
|||
using Newtonsoft.Json; |
|||
using System; |
|||
using Volo.Abp; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher.QrCode; |
|||
|
|||
[Serializable] |
|||
public class CreateQrcodeRequest |
|||
{ |
|||
/// <summary>
|
|||
/// 应用的标志
|
|||
/// </summary>
|
|||
[JsonProperty("appToken")] |
|||
public string AppToken { get; } |
|||
/// <summary>
|
|||
/// 二维码携带的参数,最长64位
|
|||
/// </summary>
|
|||
[JsonProperty("extra")] |
|||
public string Extra { get; } |
|||
/// <summary>
|
|||
/// 二维码有效时间,s为单位,最大30天。
|
|||
/// </summary>
|
|||
[JsonProperty("validTime")] |
|||
public int ValidTime { get; } |
|||
|
|||
public CreateQrcodeRequest( |
|||
[NotNull] string appToken, |
|||
[NotNull] string extra, |
|||
int validTime = 1800) |
|||
{ |
|||
Check.NotNullOrWhiteSpace(appToken, nameof(appToken)); |
|||
Check.NotNullOrWhiteSpace(extra, nameof(extra), 64); |
|||
|
|||
AppToken = appToken; |
|||
Extra = extra; |
|||
ValidTime = validTime; |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
using Newtonsoft.Json; |
|||
using System; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher.QrCode; |
|||
|
|||
[Serializable] |
|||
public class CreateQrcodeResult |
|||
{ |
|||
[JsonProperty("expires")] |
|||
public long Expires { get; set; } |
|||
|
|||
[JsonProperty("code")] |
|||
public string Code { get; set; } |
|||
|
|||
[JsonProperty("shortUrl")] |
|||
public string ShortUrl { get; set; } |
|||
|
|||
[JsonProperty("url")] |
|||
public string Url { get; set; } |
|||
|
|||
[JsonProperty("extra")] |
|||
public string Extra { get; set; } |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
using Newtonsoft.Json; |
|||
using System; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher.QrCode; |
|||
|
|||
[Serializable] |
|||
public class GetScanQrCodeResult |
|||
{ |
|||
[JsonProperty("appId")] |
|||
public int AppId { get; set; } |
|||
|
|||
[JsonProperty("appKey")] |
|||
public string AppKey { get; set; } |
|||
|
|||
[JsonProperty("appName")] |
|||
public string AppName { get; set; } |
|||
|
|||
[JsonProperty("extra")] |
|||
public string Extra { get; set; } |
|||
|
|||
[JsonProperty("source")] |
|||
public string Source { get; set; } |
|||
|
|||
[JsonProperty("time")] |
|||
public long Time { get; set; } |
|||
|
|||
[JsonProperty("uid")] |
|||
public string Uid { get; set; } |
|||
|
|||
[JsonProperty("userHeadImg")] |
|||
public string UserHeadImg { get; set; } |
|||
|
|||
[JsonProperty("userName")] |
|||
public string UserName { get; set; } |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using JetBrains.Annotations; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher.QrCode; |
|||
|
|||
public interface IWxPusherQrCodeProvider |
|||
{ |
|||
Task<CreateQrcodeResult> CreateQrcodeAsync( |
|||
[NotNull] string extra, |
|||
int validTime = 1800, |
|||
CancellationToken cancellationToken = default); |
|||
|
|||
Task<GetScanQrCodeResult> GetScanQrCodeAsync( |
|||
[NotNull] string code, |
|||
CancellationToken cancellationToken = default); |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
using Newtonsoft.Json; |
|||
using System.Net.Http; |
|||
using System.Text; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher.QrCode; |
|||
|
|||
internal static class QrCodeHttpClientExtensions |
|||
{ |
|||
public async static Task<string> CreateQrcodeAsync( |
|||
this HttpClient httpClient, |
|||
CreateQrcodeRequest qrcodeRequest, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
var requestMessage = new HttpRequestMessage( |
|||
HttpMethod.Post, |
|||
"/api/fun/create/qrcode"); |
|||
|
|||
var requestBody = JsonConvert.SerializeObject(qrcodeRequest); |
|||
requestMessage.Content = new StringContent(requestBody, Encoding.UTF8, "application/json"); |
|||
|
|||
var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken); |
|||
|
|||
return await httpResponse.Content.ReadAsStringAsync(); |
|||
} |
|||
|
|||
public async static Task<string> GetScanQrCodeUidAsync( |
|||
this HttpClient httpClient, |
|||
string code, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
var requestMessage = new HttpRequestMessage( |
|||
HttpMethod.Get, |
|||
$"/api/fun/scan-qrcode-uid?code={code}"); |
|||
|
|||
var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken); |
|||
|
|||
return await httpResponse.Content.ReadAsStringAsync(); |
|||
} |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
using JetBrains.Annotations; |
|||
using LINGYUN.Abp.WxPusher.Features; |
|||
using LINGYUN.Abp.WxPusher.Token; |
|||
using System.Net.Http; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp; |
|||
using Volo.Abp.Features; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher.QrCode; |
|||
|
|||
[RequiresFeature(WxPusherFeatureNames.Enable)] |
|||
public class WxPusherQrCodeProvider : WxPusherRequestProvider, IWxPusherQrCodeProvider |
|||
{ |
|||
protected IWxPusherTokenProvider WxPusherTokenProvider { get; } |
|||
|
|||
public WxPusherQrCodeProvider(IWxPusherTokenProvider wxPusherTokenProvider) |
|||
{ |
|||
WxPusherTokenProvider = wxPusherTokenProvider; |
|||
} |
|||
|
|||
public async virtual Task<CreateQrcodeResult> CreateQrcodeAsync( |
|||
[NotNull] string extra, |
|||
int validTime = 1800, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
var token = await WxPusherTokenProvider.GetTokenAsync(cancellationToken); |
|||
var client = HttpClientFactory.GetPushPlusClient(); |
|||
var request = new CreateQrcodeRequest(token, extra, validTime); |
|||
|
|||
var content = await client.CreateQrcodeAsync( |
|||
request, |
|||
cancellationToken); |
|||
|
|||
var response = JsonSerializer.Deserialize<WxPusherResult<CreateQrcodeResult>>(content); |
|||
|
|||
return response.GetData(); |
|||
} |
|||
|
|||
public async virtual Task<GetScanQrCodeResult> GetScanQrCodeAsync( |
|||
[NotNull] string code, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
Check.NotNullOrWhiteSpace(code, nameof(code)); |
|||
|
|||
var client = HttpClientFactory.GetPushPlusClient(); |
|||
|
|||
var content = await client.GetScanQrCodeUidAsync( |
|||
code, |
|||
cancellationToken); |
|||
|
|||
var response = JsonSerializer.Deserialize<WxPusherResult<GetScanQrCodeResult>>(content); |
|||
|
|||
return response.GetData(); |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
namespace LINGYUN.Abp.WxPusher.Security.Claims; |
|||
|
|||
public static class AbpWxPusherClaimTypes |
|||
{ |
|||
/// <summary>
|
|||
/// 用户的唯一标识
|
|||
/// </summary>
|
|||
public static string Uid { get; set; } = "wx-pusher-uid"; |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
using LINGYUN.Abp.WxPusher.Localization; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.Settings; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher.Settings; |
|||
|
|||
public class WxPusherSettingDefinitionProvider : SettingDefinitionProvider |
|||
{ |
|||
public override void Define(ISettingDefinitionContext context) |
|||
{ |
|||
context.Add(new[] |
|||
{ |
|||
new SettingDefinition( |
|||
name: WxPusherSettingNames.Security.AppToken, |
|||
displayName: L("Settings:Security.AppToken"), |
|||
description: L("Settings:Security.AppTokenDesc"), |
|||
isEncrypted: true) |
|||
.WithProviders( |
|||
DefaultValueSettingValueProvider.ProviderName, |
|||
ConfigurationSettingValueProvider.ProviderName, |
|||
GlobalSettingValueProvider.ProviderName, |
|||
TenantSettingValueProvider.ProviderName), |
|||
}); |
|||
} |
|||
|
|||
public static ILocalizableString L(string name) |
|||
{ |
|||
return LocalizableString.Create<WxPusherResource>(name); |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
namespace LINGYUN.Abp.WxPusher.Settings; |
|||
|
|||
public static class WxPusherSettingNames |
|||
{ |
|||
public const string Prefix = "WxPusher"; |
|||
|
|||
public static class Security |
|||
{ |
|||
public const string Prefix = WxPusherSettingNames.Prefix + ".Security"; |
|||
|
|||
public const string AppToken = Prefix + ".AppToken"; |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher.Token; |
|||
|
|||
public interface IWxPusherTokenProvider |
|||
{ |
|||
Task<string> GetTokenAsync(CancellationToken cancellationToken = default); |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
using LINGYUN.Abp.WxPusher.Settings; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Settings; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher.Token; |
|||
|
|||
public class WxPusherTokenProvider : IWxPusherTokenProvider, ITransientDependency |
|||
{ |
|||
protected ISettingProvider SettingProvider { get; } |
|||
|
|||
public WxPusherTokenProvider(ISettingProvider settingProvider) |
|||
{ |
|||
SettingProvider = settingProvider; |
|||
} |
|||
|
|||
public async virtual Task<string> GetTokenAsync(CancellationToken cancellationToken = default) |
|||
{ |
|||
return await SettingProvider.GetOrNullAsync( |
|||
WxPusherSettingNames.Security.AppToken); |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
namespace LINGYUN.Abp.WxPusher.User; |
|||
public enum FlowType |
|||
{ |
|||
/// <summary>
|
|||
/// 关注应用
|
|||
/// </summary>
|
|||
App = 0, |
|||
/// <summary>
|
|||
/// 关注topic
|
|||
/// </summary>
|
|||
Topic = 1, |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher.User; |
|||
/// <summary>
|
|||
/// 用户接口
|
|||
/// </summary>
|
|||
public interface IWxPusherUserProvider |
|||
{ |
|||
/// <summary>
|
|||
/// 查询App的关注用户V2
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// 获取到所有关注应用/主题的微信用户用户信息。需要注意,一个微信用户,如果同时关注应用,主题,甚至关注多个主题,会返回多条记录。
|
|||
/// </remarks>
|
|||
/// <param name="page">请求数据的页码</param>
|
|||
/// <param name="pageSize">分页大小,不能超过100</param>
|
|||
/// <param name="uid">用户的uid,可选,如果不传就是查询所有用户,传uid就是查某个用户的信息。</param>
|
|||
/// <param name="isBlock">查询拉黑用户,可选,不传查询所有用户,true查询拉黑用户,false查询没有拉黑的用户</param>
|
|||
/// <param name="type">关注的类型,可选,不传查询所有用户,0是应用,1是主题</param>
|
|||
/// <param name="cancellationToken"></param>
|
|||
/// <returns></returns>
|
|||
Task<WxPusherPagedResult<UserProfile>> GetUserListAsync( |
|||
int page = 1, |
|||
int pageSize = 10, |
|||
string uid = null, |
|||
bool? isBlock = null, |
|||
FlowType? type = null, |
|||
CancellationToken cancellationToken = default); |
|||
/// <summary>
|
|||
/// 删除用户
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// 你可以删除用户对应用、主题的关注,删除以后,用户可以重新关注,如想让用户再次关注,可以调用拉黑接口,对用户拉黑。
|
|||
/// </remarks>
|
|||
/// <param name="id">用户id,通过用户查询接口可以获取</param>
|
|||
/// <param name="cancellationToken"></param>
|
|||
/// <returns></returns>
|
|||
Task<bool> DeleteUserAsync( |
|||
int id, |
|||
CancellationToken cancellationToken = default); |
|||
/// <summary>
|
|||
/// 拉黑用户
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// 拉黑以后不能再发送消息,用户也不能再次关注,除非你取消对他的拉黑。调用拉黑接口,不用再调用删除接口。
|
|||
/// </remarks>
|
|||
/// <param name="id">用户id,通过用户查询接口可以获取</param>
|
|||
/// <param name="reject">是否拉黑,true表示拉黑,false表示取消拉黑</param>
|
|||
/// <param name="cancellationToken"></param>
|
|||
/// <returns></returns>
|
|||
Task<bool> RejectUserAsync( |
|||
int id, |
|||
bool reject, |
|||
CancellationToken cancellationToken = default); |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher.User; |
|||
|
|||
public interface IWxPusherUserStore |
|||
{ |
|||
/// <summary>
|
|||
/// 获取用户订阅的topic列表
|
|||
/// </summary>
|
|||
/// <param name="userIds">用户标识列表</param>
|
|||
/// <param name="cancellationToken"></param>
|
|||
/// <returns></returns>
|
|||
Task<List<int>> GetSubscribeTopicsAsync( |
|||
IEnumerable<Guid> userIds, |
|||
CancellationToken cancellationToken = default); |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher.User; |
|||
|
|||
public sealed class NullWxPusherUserStore : IWxPusherUserStore |
|||
{ |
|||
public readonly static IWxPusherUserStore Instance = new NullWxPusherUserStore(); |
|||
|
|||
private NullWxPusherUserStore() |
|||
{ |
|||
} |
|||
|
|||
public Task<List<int>> GetSubscribeTopicsAsync( |
|||
IEnumerable<Guid> userIds, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
return Task.FromResult(new List<int>()); |
|||
} |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
using System.Net.Http; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher.User; |
|||
|
|||
internal static class UserHttpClientExtensions |
|||
{ |
|||
public async static Task<string> GetUserListAsync( |
|||
this HttpClient httpClient, |
|||
string appToken, |
|||
int page = 1, |
|||
int pageSize = 10, |
|||
string uid = null, |
|||
bool? isBlock = null, |
|||
FlowType? type = null, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
var requestUrl = "/api/fun/wxuser/v2?appToken=$appToken&page=$page&pageSize=$pageSize&uid=$uid&isBlock=$isBlock&type=$type"; |
|||
|
|||
requestUrl = requestUrl |
|||
.Replace("$appToken", appToken) |
|||
.Replace("$page", page.ToString()) |
|||
.Replace("$pageSize", pageSize.ToString()) |
|||
.Replace("$uid", uid ?? "") |
|||
.Replace("$isBlock", isBlock?.ToString() ?? "") |
|||
.Replace("$type", type.HasValue ? ((int)type).ToString() : ""); |
|||
|
|||
var requestMessage = new HttpRequestMessage( |
|||
HttpMethod.Get, |
|||
requestUrl); |
|||
|
|||
var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken); |
|||
|
|||
return await httpResponse.Content.ReadAsStringAsync(); |
|||
} |
|||
|
|||
public async static Task<string> DeleteUserAsync( |
|||
this HttpClient httpClient, |
|||
string appToken, |
|||
int id, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
var requestMessage = new HttpRequestMessage( |
|||
HttpMethod.Delete, |
|||
$"/api/fun/remove?appToken={appToken}&id={id}"); |
|||
|
|||
var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken); |
|||
|
|||
return await httpResponse.Content.ReadAsStringAsync(); |
|||
} |
|||
|
|||
public async static Task<string> RejectUserAsync( |
|||
this HttpClient httpClient, |
|||
string appToken, |
|||
int id, |
|||
bool reject, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
var requestMessage = new HttpRequestMessage( |
|||
HttpMethod.Put, |
|||
$"/api/fun/reject?appToken={appToken}&id={id}&reject={reject}"); |
|||
|
|||
var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken); |
|||
|
|||
return await httpResponse.Content.ReadAsStringAsync(); |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
using Newtonsoft.Json; |
|||
using System; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher.User; |
|||
|
|||
[Serializable] |
|||
public class UserProfile |
|||
{ |
|||
/// <summary>
|
|||
/// 用户uid
|
|||
/// </summary>
|
|||
[JsonProperty("uid")] |
|||
public string Uid { get; set; } |
|||
/// <summary>
|
|||
/// 新用户微信不再返回 ,强制返回空
|
|||
/// </summary>
|
|||
[JsonProperty("headImg")] |
|||
public string HeadImg { get; set; } |
|||
/// <summary>
|
|||
/// 创建时间
|
|||
/// </summary>
|
|||
[JsonProperty("createTime")] |
|||
public long CreateTime { get; set; } |
|||
/// <summary>
|
|||
/// 新用户微信不再返回 ,强制返回空
|
|||
/// </summary>
|
|||
[JsonProperty("nickName")] |
|||
public string NickName { get; set; } |
|||
/// <summary>
|
|||
/// 是否拉黑
|
|||
/// </summary>
|
|||
[JsonProperty("reject")] |
|||
public bool Reject { get; set; } |
|||
/// <summary>
|
|||
/// 如果调用删除或者拉黑接口,需要这个id
|
|||
/// </summary>
|
|||
[JsonProperty("id")] |
|||
public int Id { get; set; } |
|||
/// <summary>
|
|||
/// 关注类型,
|
|||
/// 0:关注应用,
|
|||
/// 1:关注topic
|
|||
/// </summary>
|
|||
[JsonProperty("type")] |
|||
public FlowType Type { get; set; } |
|||
/// <summary>
|
|||
/// 关注的应用或者主题名字
|
|||
/// </summary>
|
|||
[JsonProperty("target")] |
|||
public string Target { get; set; } |
|||
} |
|||
@ -0,0 +1,87 @@ |
|||
using LINGYUN.Abp.WxPusher.Features; |
|||
using LINGYUN.Abp.WxPusher.Token; |
|||
using System; |
|||
using System.Net.Http; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Features; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher.User; |
|||
|
|||
[RequiresFeature(WxPusherFeatureNames.Enable)] |
|||
public class WxPusherUserProvider : WxPusherRequestProvider, IWxPusherUserProvider |
|||
{ |
|||
protected IWxPusherTokenProvider WxPusherTokenProvider { get; } |
|||
|
|||
public WxPusherUserProvider(IWxPusherTokenProvider wxPusherTokenProvider) |
|||
{ |
|||
WxPusherTokenProvider = wxPusherTokenProvider; |
|||
} |
|||
|
|||
public async virtual Task<bool> DeleteUserAsync( |
|||
int id, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
var token = await WxPusherTokenProvider.GetTokenAsync(cancellationToken); |
|||
var client = HttpClientFactory.GetPushPlusClient(); |
|||
|
|||
var content = await client.DeleteUserAsync( |
|||
token, |
|||
id, |
|||
cancellationToken); |
|||
|
|||
var response = JsonSerializer.Deserialize<WxPusherResult<string>>(content); |
|||
|
|||
return response.Success; |
|||
} |
|||
|
|||
public async virtual Task<WxPusherPagedResult<UserProfile>> GetUserListAsync( |
|||
int page = 1, |
|||
int pageSize = 10, |
|||
string uid = null, |
|||
bool? isBlock = null, |
|||
FlowType? type = null, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
if (pageSize > 100) |
|||
{ |
|||
throw new ArgumentException("pageSize must be equal to or lower than 100!", nameof(pageSize)); |
|||
} |
|||
|
|||
var token = await WxPusherTokenProvider.GetTokenAsync(cancellationToken); |
|||
var client = HttpClientFactory.GetPushPlusClient(); |
|||
|
|||
var content = await client.GetUserListAsync( |
|||
token, |
|||
page, |
|||
pageSize, |
|||
uid, |
|||
isBlock, |
|||
type, |
|||
cancellationToken); |
|||
|
|||
var response = JsonSerializer |
|||
.Deserialize<WxPusherResult<WxPusherPagedResult<UserProfile>>>(content); |
|||
|
|||
return response.GetData(); |
|||
} |
|||
|
|||
public async virtual Task<bool> RejectUserAsync( |
|||
int id, |
|||
bool reject, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
var token = await WxPusherTokenProvider.GetTokenAsync(cancellationToken); |
|||
var client = HttpClientFactory.GetPushPlusClient(); |
|||
|
|||
var content = await client.RejectUserAsync( |
|||
token, |
|||
id, |
|||
reject, |
|||
cancellationToken); |
|||
|
|||
var response = JsonSerializer.Deserialize<WxPusherResult<string>>(content); |
|||
|
|||
return response.Success; |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
using Newtonsoft.Json; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher; |
|||
|
|||
[Serializable] |
|||
public class WxPusherPagedResult<T> |
|||
{ |
|||
/// <summary>
|
|||
/// 总数
|
|||
/// </summary>
|
|||
[JsonProperty("total")] |
|||
public int Total { get; set; } |
|||
/// <summary>
|
|||
/// 当前页码
|
|||
/// </summary>
|
|||
[JsonProperty("page")] |
|||
public int Page { get; set; } |
|||
/// <summary>
|
|||
/// 页码大小
|
|||
/// </summary>
|
|||
[JsonProperty("pageSize")] |
|||
public int PageSize { get; set; } |
|||
/// <summary>
|
|||
/// 记录列表
|
|||
/// </summary>
|
|||
[JsonProperty("records")] |
|||
public List<T> Records { get; set; } = new List<T>(); |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
using Volo.Abp; |
|||
using Volo.Abp.ExceptionHandling; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher; |
|||
|
|||
public class WxPusherRemoteCallException : AbpException, IHasErrorCode |
|||
{ |
|||
public string Code { get; } |
|||
|
|||
public WxPusherRemoteCallException(string code, string message) |
|||
: base($"The WxPusher api returns an error: {code} - {message}") |
|||
{ |
|||
Code = code; |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Logging.Abstractions; |
|||
using System.Net.Http; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Json; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher; |
|||
|
|||
public abstract class WxPusherRequestProvider : ITransientDependency |
|||
{ |
|||
public IAbpLazyServiceProvider LazyServiceProvider { get; set; } |
|||
|
|||
protected ILoggerFactory LoggerFactory => LazyServiceProvider.LazyGetRequiredService<ILoggerFactory>(); |
|||
protected ILogger Logger => LazyServiceProvider.LazyGetService<ILogger>(provider => LoggerFactory?.CreateLogger(GetType().FullName) ?? NullLogger.Instance); |
|||
protected IJsonSerializer JsonSerializer => LazyServiceProvider.LazyGetRequiredService<IJsonSerializer>(); |
|||
protected IHttpClientFactory HttpClientFactory => LazyServiceProvider.LazyGetRequiredService<IHttpClientFactory>(); |
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
using Newtonsoft.Json; |
|||
using System; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher; |
|||
|
|||
[Serializable] |
|||
public class WxPusherResult<T> |
|||
{ |
|||
/// <summary>
|
|||
/// 状态码
|
|||
/// </summary>
|
|||
[JsonProperty("code")] |
|||
public int Code { get; set; } |
|||
/// <summary>
|
|||
/// 错误消息
|
|||
/// </summary>
|
|||
[JsonProperty("msg")] |
|||
public string Message { get; set; } |
|||
/// <summary>
|
|||
/// 返回数据
|
|||
/// </summary>
|
|||
[JsonProperty("data")] |
|||
public T Data { get; set; } |
|||
/// <summary>
|
|||
/// 是否调用成功
|
|||
/// </summary>
|
|||
[JsonProperty("success")] |
|||
public bool Success { get; set; } |
|||
|
|||
public WxPusherResult() |
|||
{ |
|||
} |
|||
|
|||
public WxPusherResult(int code, string message) |
|||
{ |
|||
Code = code; |
|||
Message = message; |
|||
} |
|||
|
|||
public WxPusherResult(int code, string message, T data) |
|||
{ |
|||
Code = code; |
|||
Message = message; |
|||
Data = data; |
|||
} |
|||
|
|||
public T GetData() |
|||
{ |
|||
ThrowOfFailed(); |
|||
|
|||
return Data; |
|||
} |
|||
|
|||
public void ThrowOfFailed() |
|||
{ |
|||
if (!Success) |
|||
{ |
|||
throw new WxPusherRemoteCallException(Code.ToString(), Message); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
using System; |
|||
|
|||
namespace Microsoft.Extensions.DependencyInjection; |
|||
|
|||
internal static class IServiceCollectionExtensions |
|||
{ |
|||
public static IServiceCollection AddWxPusherClient( |
|||
this IServiceCollection services) |
|||
{ |
|||
services.AddHttpClient( |
|||
"_Abp_WxPusher_Client", |
|||
(httpClient) => |
|||
{ |
|||
httpClient.BaseAddress = new Uri("https://wxpusher.zjiecode.com"); |
|||
}); |
|||
|
|||
return services; |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
# LINGYUN.Abp.WxPusher |
|||
|
|||
集成WxPusher |
|||
|
|||
实现WxPusher相关Api文档,拥有WxPusher开放能力 |
|||
|
|||
详情见WxPusher文档: https://wxpusher.dingliqc.com/docs/#/ |
|||
|
|||
## 模块引用 |
|||
|
|||
```csharp |
|||
[DependsOn(typeof(AbpWxPusherModule))] |
|||
public class YouProjectModule : AbpModule |
|||
{ |
|||
// other |
|||
} |
|||
``` |
|||
|
|||
## 用户订阅 |
|||
|
|||
实现 [IWxPusherUserStore](./LINGYUN/Abp/WxPusher/User/IWxPusherUserStore) 接口获取用户订阅列表 |
|||
|
|||
## Features |
|||
|
|||
* WxPusher WxPusher特性分组 |
|||
* WxPusher.Enable 全局启用WxPusher |
|||
* WxPusher.Message.Enable 全局启用WxPusher消息通道 |
|||
* WxPusher.Message WxPusher消息推送 |
|||
* WxPusher.Message.Enable 启用WxPusher消息推送 |
|||
* WxPusher.Message.SendLimit WxPusher消息推送限制次数 |
|||
* WxPusher.Message.SendLimitInterval WxPusher消息推送限制周期(天) |
|||
|
|||
## Settings |
|||
|
|||
* WxPusher.Security.AppToken 应用的身份标志,拥有APP_TOKEN,就可以给对应的应用的用户发送消息, 请严格保密. |
|||
@ -0,0 +1,10 @@ |
|||
namespace System.Net.Http; |
|||
|
|||
internal static class IHttpClientFactoryExtensions |
|||
{ |
|||
public static HttpClient GetPushPlusClient( |
|||
this IHttpClientFactory httpClientFactory) |
|||
{ |
|||
return httpClientFactory.CreateClient("_Abp_WxPusher_Client"); |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net6.0</TargetFramework> |
|||
<RootNamespace /> |
|||
<IsPackable>false</IsPackable> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\modules\wx-pusher\LINGYUN.Abp.WxPusher\LINGYUN.Abp.WxPusher.csproj" /> |
|||
<ProjectReference Include="..\LINGYUN.Abp.TestBase\LINGYUN.Abp.TestsBase.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,7 @@ |
|||
using LINGYUN.Abp.Tests; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher; |
|||
|
|||
public class AbpWxPusherTestBase : AbpTestsBase<AbpWxPusherTestModule> |
|||
{ |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
using LINGYUN.Abp.Tests; |
|||
using Microsoft.Extensions.Configuration; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpWxPusherModule), |
|||
typeof(AbpTestsBaseModule))] |
|||
public class AbpWxPusherTestModule : AbpModule |
|||
{ |
|||
public override void PreConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
var configurationOptions = new AbpConfigurationBuilderOptions |
|||
{ |
|||
BasePath = @"D:\Projects\Development\Abp\WxPusher", |
|||
EnvironmentName = "Test" |
|||
}; |
|||
var configuration = ConfigurationHelper.BuildConfiguration(configurationOptions); |
|||
|
|||
context.Services.ReplaceConfiguration(configuration); |
|||
} |
|||
} |
|||
@ -0,0 +1,60 @@ |
|||
using Shouldly; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Xunit; |
|||
|
|||
namespace LINGYUN.Abp.WxPusher.Messages; |
|||
|
|||
public class WxPusherMessageSenderTests : AbpWxPusherTestBase |
|||
{ |
|||
protected IWxPusherMessageSender WxPusherMessageSender { get; } |
|||
public WxPusherMessageSenderTests() |
|||
{ |
|||
WxPusherMessageSender = GetRequiredService<IWxPusherMessageSender>(); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("Content from the Xunit unit test. \r\n Click the link at the top to redirect baidu site.")] |
|||
public async virtual Task Send_Text_Test(string content) |
|||
{ |
|||
var result = await WxPusherMessageSender |
|||
.SendAsync( |
|||
content, |
|||
contentType: MessageContentType.Text, |
|||
topicIds: new List<int> { 7182 }, |
|||
url: "https://www.baidu.com/"); |
|||
|
|||
result.ShouldNotBeNull(); |
|||
result.Count.ShouldBeGreaterThanOrEqualTo(1); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("<h>Content from the Xunit unit test.</h> <br /> <a href=\"https://www.baidu.com/\">Click to redirect baidu site.</a>")] |
|||
public async virtual Task Send_Html_Test(string content) |
|||
{ |
|||
var result = await WxPusherMessageSender |
|||
.SendAsync( |
|||
content, |
|||
contentType: MessageContentType.Html, |
|||
topicIds: new List<int> { 7182 }, |
|||
url: "https://www.baidu.com/"); |
|||
|
|||
result.ShouldNotBeNull(); |
|||
result.Count.ShouldBeGreaterThanOrEqualTo(1); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("**Content from the Xunit unit test.** <br /> <a href=\"https://www.baidu.com/\">Click to redirect baidu site.</a>")] |
|||
public async virtual Task Send_Markdown_Test(string content) |
|||
{ |
|||
var result = await WxPusherMessageSender |
|||
.SendAsync( |
|||
content, |
|||
contentType: MessageContentType.Markdown, |
|||
topicIds: new List<int> { 7182 }, |
|||
url: "https://www.baidu.com/"); |
|||
|
|||
result.ShouldNotBeNull(); |
|||
result.Count.ShouldBeGreaterThanOrEqualTo(1); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue