committed by
GitHub
66 changed files with 22991 additions and 176 deletions
File diff suppressed because it is too large
@ -0,0 +1,48 @@ |
|||||
|
using System; |
||||
|
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; |
||||
|
|
||||
|
#nullable disable |
||||
|
|
||||
|
namespace LINGYUN.Abp.MicroService.AuthServer.Migrations |
||||
|
{ |
||||
|
/// <inheritdoc />
|
||||
|
public partial class AddIdentityUserInactive : Migration |
||||
|
{ |
||||
|
/// <inheritdoc />
|
||||
|
protected override void Up(MigrationBuilder migrationBuilder) |
||||
|
{ |
||||
|
migrationBuilder.CreateTable( |
||||
|
name: "AbpUserInactives", |
||||
|
columns: table => new |
||||
|
{ |
||||
|
Id = table.Column<long>(type: "bigint", nullable: false) |
||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), |
||||
|
UserId = table.Column<Guid>(type: "uuid", nullable: false), |
||||
|
TenantId = table.Column<Guid>(type: "uuid", nullable: true), |
||||
|
LastSignInTime = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false), |
||||
|
LastNotificationTime = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), |
||||
|
Status = table.Column<int>(type: "integer", nullable: false), |
||||
|
DeactivatedTime = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), |
||||
|
CreationTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false), |
||||
|
CreatorId = table.Column<Guid>(type: "uuid", nullable: true) |
||||
|
}, |
||||
|
constraints: table => |
||||
|
{ |
||||
|
table.PrimaryKey("PK_AbpUserInactives", x => x.Id); |
||||
|
}); |
||||
|
|
||||
|
migrationBuilder.CreateIndex( |
||||
|
name: "IX_AbpUserInactives_TenantId_UserId", |
||||
|
table: "AbpUserInactives", |
||||
|
columns: new[] { "TenantId", "UserId" }); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override void Down(MigrationBuilder migrationBuilder) |
||||
|
{ |
||||
|
migrationBuilder.DropTable( |
||||
|
name: "AbpUserInactives"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,49 @@ |
|||||
|
using System; |
||||
|
using Microsoft.EntityFrameworkCore.Metadata; |
||||
|
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
|
||||
|
#nullable disable |
||||
|
|
||||
|
namespace LY.MicroService.Applications.Single.EntityFrameworkCore.MySql.Migrations |
||||
|
{ |
||||
|
/// <inheritdoc />
|
||||
|
public partial class AddIdentityUserInactive : Migration |
||||
|
{ |
||||
|
/// <inheritdoc />
|
||||
|
protected override void Up(MigrationBuilder migrationBuilder) |
||||
|
{ |
||||
|
migrationBuilder.CreateTable( |
||||
|
name: "AbpUserInactives", |
||||
|
columns: table => new |
||||
|
{ |
||||
|
Id = table.Column<long>(type: "bigint", nullable: false) |
||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), |
||||
|
UserId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"), |
||||
|
TenantId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"), |
||||
|
LastSignInTime = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: false), |
||||
|
LastNotificationTime = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: true), |
||||
|
Status = table.Column<int>(type: "int", nullable: false), |
||||
|
DeactivatedTime = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: true), |
||||
|
CreationTime = table.Column<DateTime>(type: "datetime(6)", nullable: false), |
||||
|
CreatorId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci") |
||||
|
}, |
||||
|
constraints: table => |
||||
|
{ |
||||
|
table.PrimaryKey("PK_AbpUserInactives", x => x.Id); |
||||
|
}) |
||||
|
.Annotation("MySql:CharSet", "utf8mb4"); |
||||
|
|
||||
|
migrationBuilder.CreateIndex( |
||||
|
name: "IX_AbpUserInactives_TenantId_UserId", |
||||
|
table: "AbpUserInactives", |
||||
|
columns: new[] { "TenantId", "UserId" }); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override void Down(MigrationBuilder migrationBuilder) |
||||
|
{ |
||||
|
migrationBuilder.DropTable( |
||||
|
name: "AbpUserInactives"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,48 @@ |
|||||
|
using System; |
||||
|
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; |
||||
|
|
||||
|
#nullable disable |
||||
|
|
||||
|
namespace LY.MicroService.Applications.Single.EntityFrameworkCore.PostgreSql.Migrations |
||||
|
{ |
||||
|
/// <inheritdoc />
|
||||
|
public partial class AddIdentityUserInactive : Migration |
||||
|
{ |
||||
|
/// <inheritdoc />
|
||||
|
protected override void Up(MigrationBuilder migrationBuilder) |
||||
|
{ |
||||
|
migrationBuilder.CreateTable( |
||||
|
name: "AbpUserInactives", |
||||
|
columns: table => new |
||||
|
{ |
||||
|
Id = table.Column<long>(type: "bigint", nullable: false) |
||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), |
||||
|
UserId = table.Column<Guid>(type: "uuid", nullable: false), |
||||
|
TenantId = table.Column<Guid>(type: "uuid", nullable: true), |
||||
|
LastSignInTime = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false), |
||||
|
LastNotificationTime = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), |
||||
|
Status = table.Column<int>(type: "integer", nullable: false), |
||||
|
DeactivatedTime = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), |
||||
|
CreationTime = table.Column<DateTime>(type: "timestamp without time zone", nullable: false), |
||||
|
CreatorId = table.Column<Guid>(type: "uuid", nullable: true) |
||||
|
}, |
||||
|
constraints: table => |
||||
|
{ |
||||
|
table.PrimaryKey("PK_AbpUserInactives", x => x.Id); |
||||
|
}); |
||||
|
|
||||
|
migrationBuilder.CreateIndex( |
||||
|
name: "IX_AbpUserInactives_TenantId_UserId", |
||||
|
table: "AbpUserInactives", |
||||
|
columns: new[] { "TenantId", "UserId" }); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override void Down(MigrationBuilder migrationBuilder) |
||||
|
{ |
||||
|
migrationBuilder.DropTable( |
||||
|
name: "AbpUserInactives"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,47 @@ |
|||||
|
using System; |
||||
|
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
|
||||
|
#nullable disable |
||||
|
|
||||
|
namespace LY.MicroService.Applications.Single.EntityFrameworkCore.SqlServer.Migrations |
||||
|
{ |
||||
|
/// <inheritdoc />
|
||||
|
public partial class AddIdentityUserInactive : Migration |
||||
|
{ |
||||
|
/// <inheritdoc />
|
||||
|
protected override void Up(MigrationBuilder migrationBuilder) |
||||
|
{ |
||||
|
migrationBuilder.CreateTable( |
||||
|
name: "AbpUserInactives", |
||||
|
columns: table => new |
||||
|
{ |
||||
|
Id = table.Column<long>(type: "bigint", nullable: false) |
||||
|
.Annotation("SqlServer:Identity", "1, 1"), |
||||
|
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false), |
||||
|
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true), |
||||
|
LastSignInTime = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false), |
||||
|
LastNotificationTime = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true), |
||||
|
Status = table.Column<int>(type: "int", nullable: false), |
||||
|
DeactivatedTime = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true), |
||||
|
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false), |
||||
|
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true) |
||||
|
}, |
||||
|
constraints: table => |
||||
|
{ |
||||
|
table.PrimaryKey("PK_AbpUserInactives", x => x.Id); |
||||
|
}); |
||||
|
|
||||
|
migrationBuilder.CreateIndex( |
||||
|
name: "IX_AbpUserInactives_TenantId_UserId", |
||||
|
table: "AbpUserInactives", |
||||
|
columns: new[] { "TenantId", "UserId" }); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override void Down(MigrationBuilder migrationBuilder) |
||||
|
{ |
||||
|
migrationBuilder.DropTable( |
||||
|
name: "AbpUserInactives"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,49 @@ |
|||||
|
using System; |
||||
|
using Microsoft.EntityFrameworkCore.Metadata; |
||||
|
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
|
||||
|
#nullable disable |
||||
|
|
||||
|
namespace LY.MicroService.AuthServer.EntityFrameworkCore.Migrations |
||||
|
{ |
||||
|
/// <inheritdoc />
|
||||
|
public partial class AddIdentityUserInactive : Migration |
||||
|
{ |
||||
|
/// <inheritdoc />
|
||||
|
protected override void Up(MigrationBuilder migrationBuilder) |
||||
|
{ |
||||
|
migrationBuilder.CreateTable( |
||||
|
name: "AbpUserInactives", |
||||
|
columns: table => new |
||||
|
{ |
||||
|
Id = table.Column<long>(type: "bigint", nullable: false) |
||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), |
||||
|
UserId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"), |
||||
|
TenantId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"), |
||||
|
LastSignInTime = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: false), |
||||
|
LastNotificationTime = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: true), |
||||
|
Status = table.Column<int>(type: "int", nullable: false), |
||||
|
DeactivatedTime = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: true), |
||||
|
CreationTime = table.Column<DateTime>(type: "datetime(6)", nullable: false), |
||||
|
CreatorId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci") |
||||
|
}, |
||||
|
constraints: table => |
||||
|
{ |
||||
|
table.PrimaryKey("PK_AbpUserInactives", x => x.Id); |
||||
|
}) |
||||
|
.Annotation("MySql:CharSet", "utf8mb4"); |
||||
|
|
||||
|
migrationBuilder.CreateIndex( |
||||
|
name: "IX_AbpUserInactives_TenantId_UserId", |
||||
|
table: "AbpUserInactives", |
||||
|
columns: new[] { "TenantId", "UserId" }); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override void Down(MigrationBuilder migrationBuilder) |
||||
|
{ |
||||
|
migrationBuilder.DropTable( |
||||
|
name: "AbpUserInactives"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
namespace LINGYUN.Abp.Identity; |
||||
|
|
||||
|
public enum InactiveUserStatus |
||||
|
{ |
||||
|
Notified = 1, |
||||
|
Deactivated = 2, |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Domain.Repositories; |
||||
|
using Volo.Abp.Identity; |
||||
|
using Volo.Abp.Specifications; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Identity; |
||||
|
|
||||
|
public interface IIdentityUserInactiveRepository : IBasicRepository<IdentityUserInactive, long> |
||||
|
{ |
||||
|
Task<IdentityUserInactive> FindByUserIdAsync( |
||||
|
Guid userId, |
||||
|
CancellationToken cancellationToken = default); |
||||
|
|
||||
|
Task<int> GetInactiveUserCountAsync( |
||||
|
DateTime threshold, |
||||
|
IEnumerable<Guid> exceptUserIds = null, |
||||
|
CancellationToken cancellationToken = default); |
||||
|
|
||||
|
Task<List<IdentityUser>> GetInactiveUserListAsync( |
||||
|
DateTime threshold, |
||||
|
IEnumerable<Guid> exceptUserIds = null, |
||||
|
string sorting = nameof(IdentityUser.LastSignInTime), |
||||
|
int maxResultCount = 10, |
||||
|
int skipCount = 0, |
||||
|
CancellationToken cancellationToken = default); |
||||
|
|
||||
|
Task<int> GetCountAsync( |
||||
|
ISpecification<IdentityUserInactive> specification, |
||||
|
CancellationToken cancellationToken = default); |
||||
|
|
||||
|
Task<List<IdentityUserInactive>> GetListAsync( |
||||
|
ISpecification<IdentityUserInactive> specification, |
||||
|
string sorting = nameof(IdentityUserInactive.CreationTime), |
||||
|
int maxResultCount = 10, |
||||
|
int skipCount = 0, |
||||
|
CancellationToken cancellationToken = default); |
||||
|
} |
||||
@ -0,0 +1,48 @@ |
|||||
|
using System; |
||||
|
using Volo.Abp.Domain.Entities.Auditing; |
||||
|
using Volo.Abp.MultiTenancy; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Identity; |
||||
|
|
||||
|
public class IdentityUserInactive : CreationAuditedEntity<long>, IMultiTenant |
||||
|
{ |
||||
|
public virtual Guid UserId { get; protected set; } |
||||
|
|
||||
|
public virtual Guid? TenantId { get; protected set; } |
||||
|
|
||||
|
public virtual DateTimeOffset LastSignInTime { get; protected set; } |
||||
|
|
||||
|
public virtual DateTimeOffset? LastNotificationTime { get; protected set; } |
||||
|
|
||||
|
public virtual InactiveUserStatus Status { get; protected set; } |
||||
|
|
||||
|
public virtual DateTimeOffset? DeactivatedTime { get; protected set; } |
||||
|
|
||||
|
protected IdentityUserInactive() |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public IdentityUserInactive( |
||||
|
Guid userId, |
||||
|
DateTimeOffset lastSignInTime, |
||||
|
Guid? tenantId = null) |
||||
|
{ |
||||
|
UserId = userId; |
||||
|
TenantId = tenantId; |
||||
|
LastSignInTime = lastSignInTime; |
||||
|
Status = InactiveUserStatus.Notified; |
||||
|
} |
||||
|
|
||||
|
public void MarkNotified(DateTimeOffset now) |
||||
|
{ |
||||
|
LastNotificationTime = now; |
||||
|
Status = InactiveUserStatus.Notified; |
||||
|
} |
||||
|
|
||||
|
public void MarkDeactivated(DateTimeOffset now) |
||||
|
{ |
||||
|
DeactivatedTime = now; |
||||
|
Status = InactiveUserStatus.Deactivated; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Domain.Entities.Events.Distributed; |
||||
|
using Volo.Abp.EventBus.Distributed; |
||||
|
using Volo.Abp.Uow; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Identity.Session; |
||||
|
|
||||
|
public class IdentitySessionUserInactiveSynchronizer : |
||||
|
IDistributedEventHandler<EntityCreatedEto<IdentitySessionEto>>, |
||||
|
ITransientDependency |
||||
|
{ |
||||
|
private readonly IIdentityUserInactiveRepository _identityUserInactiveRepository; |
||||
|
|
||||
|
public IdentitySessionUserInactiveSynchronizer(IIdentityUserInactiveRepository identityUserInactiveRepository) |
||||
|
{ |
||||
|
_identityUserInactiveRepository = identityUserInactiveRepository; |
||||
|
} |
||||
|
|
||||
|
[UnitOfWork] |
||||
|
public async virtual Task HandleEventAsync(EntityCreatedEto<IdentitySessionEto> eventData) |
||||
|
{ |
||||
|
var userInactive = await _identityUserInactiveRepository.FindByUserIdAsync(eventData.Entity.UserId); |
||||
|
if (userInactive == null) |
||||
|
{ |
||||
|
await _identityUserInactiveRepository.DeleteAsync(userInactive); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,100 @@ |
|||||
|
using Microsoft.EntityFrameworkCore; |
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Linq.Dynamic.Core; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Domain.Repositories.EntityFrameworkCore; |
||||
|
using Volo.Abp.EntityFrameworkCore; |
||||
|
using Volo.Abp.Identity; |
||||
|
using Volo.Abp.Identity.EntityFrameworkCore; |
||||
|
using Volo.Abp.Specifications; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Identity.EntityFrameworkCore; |
||||
|
|
||||
|
public class EfCoreIdentityUserInactiveRepository : EfCoreRepository<IIdentityDbContext, IdentityUserInactive, long>, IIdentityUserInactiveRepository |
||||
|
{ |
||||
|
public EfCoreIdentityUserInactiveRepository( |
||||
|
IDbContextProvider<IIdentityDbContext> dbContextProvider) |
||||
|
: base(dbContextProvider) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public async virtual Task<IdentityUserInactive> FindByUserIdAsync( |
||||
|
Guid userId, |
||||
|
CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
return await (await GetQueryableAsync()) |
||||
|
.Where(x => x.UserId == userId) |
||||
|
.FirstOrDefaultAsync(GetCancellationToken(cancellationToken)); |
||||
|
} |
||||
|
|
||||
|
public async virtual Task<int> GetInactiveUserCountAsync( |
||||
|
DateTime threshold, |
||||
|
IEnumerable<Guid> exceptUserIds = null, |
||||
|
CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
var dbContext = await GetDbContextAsync(); |
||||
|
var ignoreUserIds = dbContext.Set<IdentityUserInactive>() |
||||
|
.Select(x => x.UserId); |
||||
|
|
||||
|
return await dbContext.Set<IdentityUser>() |
||||
|
.WhereIf(exceptUserIds?.Count() > 0, x => !exceptUserIds.Contains(x.Id)) |
||||
|
.Where(x => !ignoreUserIds.Contains(x.Id)) |
||||
|
.Where(x => x.IsActive && |
||||
|
((x.LastSignInTime.HasValue && x.LastSignInTime < threshold) || |
||||
|
// 更新到abp新版后用户未登陆过使用上次修改时间作为判断依据?
|
||||
|
// (x.LastModificationTime.HasValue && x.LastModificationTime < threshold) ||
|
||||
|
x.CreationTime < threshold)) |
||||
|
.CountAsync(GetCancellationToken(cancellationToken)); |
||||
|
} |
||||
|
|
||||
|
public async virtual Task<List<IdentityUser>> GetInactiveUserListAsync( |
||||
|
DateTime threshold, |
||||
|
IEnumerable<Guid> exceptUserIds = null, |
||||
|
string sorting = nameof(IdentityUser.LastSignInTime), |
||||
|
int maxResultCount = 10, |
||||
|
int skipCount = 0, |
||||
|
CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
var dbContext = await GetDbContextAsync(); |
||||
|
var ignoreUserIds = dbContext.Set<IdentityUserInactive>() |
||||
|
.Select(x => x.UserId); |
||||
|
|
||||
|
return await dbContext.Set<IdentityUser>() |
||||
|
.WhereIf(exceptUserIds?.Count() > 0, x => !exceptUserIds.Contains(x.Id)) |
||||
|
.Where(x => !ignoreUserIds.Contains(x.Id)) |
||||
|
.Where(x => x.IsActive && |
||||
|
((x.LastSignInTime.HasValue && x.LastSignInTime < threshold) || |
||||
|
// 更新到abp新版后用户未登陆过使用上次修改时间作为判断依据?
|
||||
|
// (x.LastModificationTime.HasValue && x.LastModificationTime < threshold) ||
|
||||
|
x.CreationTime < threshold)) |
||||
|
.OrderBy(!sorting.IsNullOrWhiteSpace() ? sorting : nameof(IdentityUser.LastSignInTime)) |
||||
|
.PageBy(skipCount, maxResultCount) |
||||
|
.ToListAsync(GetCancellationToken(cancellationToken)); |
||||
|
} |
||||
|
|
||||
|
public async virtual Task<int> GetCountAsync( |
||||
|
ISpecification<IdentityUserInactive> specification, |
||||
|
CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
return await (await GetQueryableAsync()) |
||||
|
.Where(specification.ToExpression()) |
||||
|
.CountAsync(GetCancellationToken(cancellationToken)); |
||||
|
} |
||||
|
|
||||
|
public async virtual Task<List<IdentityUserInactive>> GetListAsync( |
||||
|
ISpecification<IdentityUserInactive> specification, |
||||
|
string sorting = nameof(IdentityUserInactive.CreationTime), |
||||
|
int maxResultCount = 10, |
||||
|
int skipCount = 0, |
||||
|
CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
return await (await GetQueryableAsync()) |
||||
|
.Where(specification.ToExpression()) |
||||
|
.OrderBy(!sorting.IsNullOrWhiteSpace() ? sorting : nameof(IdentityUserInactive.CreationTime)) |
||||
|
.PageBy(skipCount, maxResultCount) |
||||
|
.ToListAsync(GetCancellationToken(cancellationToken)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
using JetBrains.Annotations; |
||||
|
using Microsoft.EntityFrameworkCore; |
||||
|
using Volo.Abp.EntityFrameworkCore.Modeling; |
||||
|
using Volo.Abp.Identity; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Identity.EntityFrameworkCore; |
||||
|
|
||||
|
public static class IdentityUserInactiveDbContextModelBuilderExtensions |
||||
|
{ |
||||
|
public static void ConfigureIdentityUserInactive([NotNull] this ModelBuilder builder) |
||||
|
{ |
||||
|
builder.Entity<IdentityUserInactive>(b => |
||||
|
{ |
||||
|
b.ToTable(AbpIdentityDbProperties.DbTablePrefix + "UserInactives", AbpIdentityDbProperties.DbSchema); |
||||
|
|
||||
|
b.ConfigureByConvention(); |
||||
|
|
||||
|
b.HasIndex(x => new { x.TenantId, x.UserId }); |
||||
|
|
||||
|
b.ApplyObjectExtensionMappings(); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
{ |
||||
|
"culture": "en", |
||||
|
"texts": { |
||||
|
"InactiveUserReminderNotifier": "Account Activity Reminder", |
||||
|
"InactiveUserDeactivationNotifier": "Account Deactivation Notice", |
||||
|
"InactiveUserDeletionNotifier": "Account Deletion Notice", |
||||
|
"InactiveUserReminderHeader": "Account Activity Reminder", |
||||
|
"InactiveUserReminderWarning": "We have noticed that your account has been inactive for an extended period. To keep your account active and secure, we recommend logging in.", |
||||
|
"InactiveUserReminderAction": "If you wish to keep your account, please log in before", |
||||
|
"InactiveUserReminderReactivate": ".", |
||||
|
"InactiveUserReminderClosure": "If you no longer need this account, no action is required. However, please note that if you do not log in before", |
||||
|
"InactiveUserReminderInactive": ", your account will be deactivated, and you will not be able to log in.", |
||||
|
"InactiveUserDeactivationNotice": "This notice is to inform you that, due to", |
||||
|
"InactiveUserDeactivationWarning": "consecutive days of inactivity, your account has been deactivated. Your data is still stored but inaccessible. If needed, please contact us before", |
||||
|
"InactiveUserDeactivationAction": "to restore your account. If not addressed in time, your account will be permanently deleted.", |
||||
|
"NoReactionClosure": "If you do not wish to reactivate your account, no further action is required. After the reactivation period ends, your account will be automatically queued for deletion.", |
||||
|
"AutomatedNotification": "This is an automated notification. Please do not reply directly to this email.", |
||||
|
"InactiveUserDeactivationHeader": "Account Deactivation Notice", |
||||
|
"InactiveUserDeletionHeader": "Account Deletion Notice", |
||||
|
"InactiveUserDeletionNotice": "Due to prolonged inactivity and failure to respond to previous notices, your account has been permanently deleted. All data associated with you has been removed from the platform.", |
||||
|
"WhatYouCanDo": "What you can do:", |
||||
|
"CreateNewAccountInstructions": "To continue using the service, please", |
||||
|
"CreateNewAccountStep1": "Create a new account", |
||||
|
"CreateNewAccountStep2": "Set up your profile", |
||||
|
"CreateNewAccountStep3": "Resubscribe to services", |
||||
|
"LastSignInTime": "Last sign-in time", |
||||
|
"SignInToAccount": "Sign in to account" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
{ |
||||
|
"culture": "zh-Hans", |
||||
|
"texts": { |
||||
|
"InactiveUserReminderNotifier": "账户保持活跃通知", |
||||
|
"InactiveUserDeactivationNotifier": "账户停用通知", |
||||
|
"InactiveUserDeletionNotifier": "账户删除通知", |
||||
|
"InactiveUserReminderHeader": "账户保持活跃提醒", |
||||
|
"InactiveUserReminderWarning": "我们注意到您的账户已经长时间没有活动,为保持您的账户活跃和安全,建议您登录账户.", |
||||
|
"InactiveUserReminderAction": "如果您想保留您的账号,请在", |
||||
|
"InactiveUserReminderReactivate": "之前登录.", |
||||
|
"InactiveUserReminderClosure": "如果您不再需要此账户,则无需采取任何操作.但请注意,如果截止", |
||||
|
"InactiveUserReminderInactive": "之前您仍未登录账号,系统将禁用您的账号,此时您将无法登录您的账号.", |
||||
|
"InactiveUserDeactivationNotice": "此通知旨在告知您,由于您已连续", |
||||
|
"InactiveUserDeactivationWarning": "天未登录,您的账号已被禁用,您的数据仍被存储但无法访问,如果有需要可在", |
||||
|
"InactiveUserDeactivationAction": "之前联系我们恢复账号,如未及时处理账号将被永久删除.", |
||||
|
"NoReactionClosure": "如果您不希望重新激活账户,则无需采取进一步操作.重新激活期限过后,您的账户将自动进入删除队列.", |
||||
|
"AutomatedNotification": "这是自动发送的通知, 请勿直接回复此邮件.", |
||||
|
"InactiveUserDeactivationHeader": "账户停用通知", |
||||
|
"InactiveUserDeletionHeader": "账户删除通知", |
||||
|
"InactiveUserDeletionNotice": "由于长期不活跃且未响应之前的通知,您的账户已被永久删除,与您相关的数据已从平台中移除.", |
||||
|
"WhatYouCanDo": "您可以:", |
||||
|
"CreateNewAccountInstructions": "如需继续使用服务,请", |
||||
|
"CreateNewAccountStep1": "创建新账户", |
||||
|
"CreateNewAccountStep2": "设置个人资料", |
||||
|
"CreateNewAccountStep3": "重新订阅服务", |
||||
|
"LastSignInTime": "上次登录时间", |
||||
|
"SignInToAccount": "登录账号" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
using Volo.Abp.Identity.Localization; |
||||
|
using Volo.Abp.Localization; |
||||
|
using Volo.Abp.TextTemplating; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Identity.Notifications.Templates; |
||||
|
|
||||
|
public class IdentityNotificationTemplateDefinitionProvider : TemplateDefinitionProvider |
||||
|
{ |
||||
|
public override void Define(ITemplateDefinitionContext context) |
||||
|
{ |
||||
|
context.Add( |
||||
|
new TemplateDefinition( |
||||
|
IdentityNotificationNames.IdentityUser.InactiveUserReminderNotifier, |
||||
|
displayName: L("InactiveUserReminderNotifier"), |
||||
|
localizationResource: typeof(IdentityResource) |
||||
|
).WithVirtualFilePath("/LINGYUN/Abp/Identity/Notifications/Templates/InactiveUserReminderNotifier.tpl", true), |
||||
|
new TemplateDefinition( |
||||
|
IdentityNotificationNames.IdentityUser.InactiveUserDeactivationNotifier, |
||||
|
displayName: L("InactiveUserDeactivationNotifier"), |
||||
|
localizationResource: typeof(IdentityResource) |
||||
|
).WithVirtualFilePath("/LINGYUN/Abp/Identity/Notifications/Templates/InactiveUserDeactivationNotifier.tpl", true), |
||||
|
new TemplateDefinition( |
||||
|
IdentityNotificationNames.IdentityUser.InactiveUserDeletionNotifier, |
||||
|
displayName: L("InactiveUserDeletionNotifier"), |
||||
|
localizationResource: typeof(IdentityResource) |
||||
|
).WithVirtualFilePath("/LINGYUN/Abp/Identity/Notifications/Templates/InactiveUserDeletionNotifier.tpl", true) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private static ILocalizableString L(string name) |
||||
|
{ |
||||
|
return LocalizableString.Create<IdentityResource>(name); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="zh-CN"> |
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<title>{{L "InactiveUserDeactivationNotifier"}}</title> |
||||
|
</head> |
||||
|
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background-color: #f5f5f5; margin: 0; padding: 20px;"> |
||||
|
<table width="100%" cellpadding="0" cellspacing="0" border="0"> |
||||
|
<tr> |
||||
|
<td align="center"> |
||||
|
<table width="500" cellpadding="0" cellspacing="0" border="0" style="background-color: #ffffff; border-radius: 4px;"> |
||||
|
<tr> |
||||
|
<td style="padding: 30px 30px 20px 30px;"> |
||||
|
<h2 style="margin: 0 0 8px 0; font-size: 20px; color: #333;">{{L "InactiveUserDeactivationHeader"}}</h2> |
||||
|
</td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<td style="padding: 0 30px 20px 30px;"> |
||||
|
<p style="margin: 0 0 16px 0; font-size: 14px; color: #555; line-height: 1.5;"><strong>{{model.email}}</strong></p> |
||||
|
<p style="margin: 0 0 16px 0; font-size: 14px; color: #555; line-height: 1.5;"> |
||||
|
{{L "InactiveUserDeactivationNotice"}}<strong>{{model.inactivityDays}}</strong>{{L "InactiveUserDeactivationWarning"}} |
||||
|
<strong>{{model.reactivateDate | date.to_string '%F'}}</strong>{{L "InactiveUserDeactivationAction"}} |
||||
|
</p> |
||||
|
<p style="margin: 20px 0 16px 0; font-size: 14px; color: #555; line-height: 1.5;">{{L "NoReactionClosure"}}</p> |
||||
|
<p style="margin: 20px 0 0 0; font-size: 12px; color: #999;">{{L "AutomatedNotification"}}</p> |
||||
|
</td> |
||||
|
</tr> |
||||
|
</table> |
||||
|
</td> |
||||
|
</tr> |
||||
|
</table> |
||||
|
</body> |
||||
|
</html> |
||||
@ -0,0 +1,30 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="zh-CN"> |
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<title>{{L "InactiveUserDeletionNotifier"}}</title> |
||||
|
</head> |
||||
|
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background-color: #f5f5f5; margin: 0; padding: 20px;"> |
||||
|
<table width="100%" cellpadding="0" cellspacing="0" border="0"> |
||||
|
<tr> |
||||
|
<td align="center"> |
||||
|
<table width="500" cellpadding="0" cellspacing="0" border="0" style="background-color: #ffffff; border-radius: 4px;"> |
||||
|
<tr> |
||||
|
<td style="padding: 30px 30px 20px 30px;"> |
||||
|
<h2 style="margin: 0 0 8px 0; font-size: 20px; color: #333;">{{L "InactiveUserDeletionHeader"}}</h2> |
||||
|
</td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<td style="padding: 0 30px 20px 30px;"> |
||||
|
<p style="margin: 0 0 16px 0; font-size: 14px; color: #555; line-height: 1.5;">{{model.email}}</p> |
||||
|
<p style="margin: 20px 0 16px 0; font-size: 14px; color: #555; line-height: 1.5;">{{L "InactiveUserDeletionNotice"}}</p> |
||||
|
<p style="margin: 0 0 16px 0; font-size: 14px; color: #555; line-height: 1.5;"><strong>{{L "WhatYouCanDo"}}</strong> {{L "CreateNewAccountInstructions"}}{{L "CreateNewAccountStep1"}},{{L "CreateNewAccountStep2"}},{{L "CreateNewAccountStep3"}}.</p> |
||||
|
<p style="margin: 20px 0 0 0; font-size: 12px; color: #999;">{{L "AutomatedNotification"}}</p> |
||||
|
</td> |
||||
|
</tr> |
||||
|
</table> |
||||
|
</td> |
||||
|
</tr> |
||||
|
</table> |
||||
|
</body> |
||||
|
</html> |
||||
@ -0,0 +1,36 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="zh-CN"> |
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<title>{{L "InactiveUserReminderNotifier"}}</title> |
||||
|
</head> |
||||
|
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background-color: #f5f5f5; margin: 0; padding: 20px;"> |
||||
|
<table width="100%" cellpadding="0" cellspacing="0" border="0"> |
||||
|
<tr> |
||||
|
<td align="center"> |
||||
|
<table width="500" cellpadding="0" cellspacing="0" border="0" style="background-color: #ffffff; border-radius: 4px;"> |
||||
|
<tr> |
||||
|
<td style="padding: 30px 30px 20px 30px;"> |
||||
|
<h2 style="margin: 0 0 8px 0; font-size: 20px; color: #333;">{{L "InactiveUserReminderHeader"}}</h2> |
||||
|
</td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<td style="padding: 0 30px 20px 30px;"> |
||||
|
<p style="margin: 0 0 16px 0; font-size: 14px; color: #555; line-height: 1.5;"><strong>{{model.email}}</strong></p> |
||||
|
<p style="margin: 0 0 16px 0; font-size: 14px; color: #555; line-height: 1.5;">{{L "InactiveUserReminderWarning"}}</p> |
||||
|
<p style="margin: 0 0 8px 0; font-size: 14px; color: #555;">{{L "LastSignInTime"}}: <strong>{{if model.lastSignInTime}}{{model.lastSignInTime | date.to_string '%F'}}{{else}}{{L "Never"}}{{end}}</strong>, |
||||
|
{{L "InactiveUserReminderAction"}}<strong>{{model.reactivateDate | date.to_string '%F'}}</strong>{{L "InactiveUserReminderReactivate"}} |
||||
|
</p> |
||||
|
<p style="margin: 20px 0 16px 0; font-size: 14px; color: #555; line-height: 1.5;"> |
||||
|
{{L "InactiveUserReminderClosure"}}<strong>{{model.reactivateDate | date.to_string '%F'}}</strong>{{L "InactiveUserReminderInactive"}} |
||||
|
</p> |
||||
|
<p style="margin: 20px 0 0 0;"><a href="{{model.loginUrl}}" style="color: #1a73e8;">{{L "SignInToAccount"}}</a></p> |
||||
|
<p style="margin: 20px 0 0 0; font-size: 12px; color: #999;">{{L "AutomatedNotification"}}</p> |
||||
|
</td> |
||||
|
</tr> |
||||
|
</table> |
||||
|
</td> |
||||
|
</tr> |
||||
|
</table> |
||||
|
</body> |
||||
|
</html> |
||||
@ -1,125 +1,293 @@ |
|||||
//using LINGYUN.Abp.BackgroundTasks;
|
using LINGYUN.Abp.BackgroundTasks; |
||||
//using LINGYUN.Abp.Identity.Notifications;
|
using LINGYUN.Abp.Identity.Notifications; |
||||
//using LINGYUN.Abp.Notifications;
|
using LINGYUN.Abp.Notifications; |
||||
//using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Localization; |
||||
//using System.Collections.Generic;
|
using Microsoft.Extensions.Logging; |
||||
//using System.Linq;
|
using System; |
||||
//using System.Threading.Tasks;
|
using System.Collections.Generic; |
||||
//using Volo.Abp.Domain.Repositories;
|
using System.Linq; |
||||
//using Volo.Abp.Identity;
|
using System.Threading.Tasks; |
||||
//using Volo.Abp.Specifications;
|
using Volo.Abp.Emailing; |
||||
//using Volo.Abp.Timing;
|
using Volo.Abp.Identity; |
||||
|
using Volo.Abp.Identity.Localization; |
||||
//namespace LINGYUN.Abp.Identity.Jobs;
|
using Volo.Abp.Specifications; |
||||
|
using Volo.Abp.TextTemplating; |
||||
///// <summary>
|
using Volo.Abp.Timing; |
||||
///// 用户清理作业
|
|
||||
///// </summary>
|
namespace LINGYUN.Abp.Identity.Jobs; |
||||
///// <remarks>
|
|
||||
///// 清理长期未登录用户
|
/// <summary>
|
||||
///// </remarks>
|
/// 不活跃用户清理作业
|
||||
//public class InactiveIdentityUserCleanupJob : IJobRunnable
|
/// </summary>
|
||||
//{
|
/// <remarks>
|
||||
// public const string Name = "InactiveIdentityUserCleanupJob";
|
/// 清理长期不活跃用户
|
||||
|
/// </remarks>
|
||||
// #region Definition Paramters
|
public class InactiveIdentityUserCleanupJob : IJobRunnable |
||||
|
{ |
||||
// public readonly static IReadOnlyList<JobDefinitionParamter> Paramters =
|
public const string Name = "InactiveIdentityUserCleanupJob"; |
||||
// new List<JobDefinitionParamter>
|
|
||||
// {
|
#region Definition Paramters
|
||||
// new JobDefinitionParamter(
|
|
||||
// PropertyUserInactiveCleanupDays,
|
public readonly static IReadOnlyList<JobDefinitionParamter> Paramters = |
||||
// LocalizableStatic.Create("DisplayName:UserInactiveCleanupDays"),
|
new List<JobDefinitionParamter> |
||||
// LocalizableStatic.Create("Description:UserInactiveCleanupDays")),
|
{ |
||||
// new JobDefinitionParamter(
|
new JobDefinitionParamter( |
||||
// PropertyUserInactiveNotifierDays,
|
PropertyInactivityPeriodDays, |
||||
// LocalizableStatic.Create("DisplayName:UserInactiveNotifierDays"),
|
LocalizableStatic.Create("DisplayName:InactivityPeriodDays"), |
||||
// LocalizableStatic.Create("Description:UserInactiveNotifierDays")),
|
LocalizableStatic.Create("Description:InactivityPeriodDays")), |
||||
// };
|
new JobDefinitionParamter( |
||||
|
PropertyGracePeriodDays, |
||||
// #endregion
|
LocalizableStatic.Create("DisplayName:GracePeriodDays"), |
||||
// /// <summary>
|
LocalizableStatic.Create("Description:GracePeriodDays")), |
||||
// /// 不活跃用户清理时间, 单位: 天
|
}; |
||||
// /// </summary>
|
|
||||
// public const string PropertyUserInactiveCleanupDays = "UserInactiveCleanupDays";
|
#endregion
|
||||
// /// <summary>
|
/// <summary>
|
||||
// /// 不活跃用户通知时间, 单位: 天
|
/// 停用不活跃用户天数
|
||||
// /// </summary>
|
/// </summary>
|
||||
// public const string PropertyUserInactiveNotifierDays = "UserInactiveNotifierDays";
|
public const string PropertyInactivityPeriodDays = "InactivityPeriodDays"; |
||||
|
/// <summary>
|
||||
// public async virtual Task ExecuteAsync(JobRunnableContext context)
|
/// 已停用用户宽限天数
|
||||
// {
|
/// </summary>
|
||||
// var logger = context.GetRequiredService<ILogger<InactiveIdentityUserCleanupJob>>();
|
public const string PropertyGracePeriodDays = "GracePeriodDays"; |
||||
|
|
||||
// // 不活跃用户清理时间
|
public async virtual Task ExecuteAsync(JobRunnableContext context) |
||||
// var inactiveCleanupDays = context.GetOrDefaultJobData(PropertyUserInactiveCleanupDays, 30);
|
{ |
||||
// // 不活跃用户通知时间
|
await Task.WhenAll([ |
||||
// var inactiveNotifierDays = context.GetOrDefaultJobData(PropertyUserInactiveNotifierDays, 180);
|
SetInactiveIdentityUsers(context), |
||||
|
DeleteInactiveIdentityUsers(context) |
||||
// var clock = context.GetRequiredService<IClock>();
|
]); |
||||
// var identityUserRepo = context.GetRequiredService<IIdentityUserRepository>();
|
} |
||||
// var identityUserSessionRepo = context.GetRequiredService<IIdentitySessionRepository>();
|
|
||||
|
protected async virtual Task SetInactiveIdentityUsers(JobRunnableContext context) |
||||
// // 获取需要清理的用户集合
|
{ |
||||
// var specification = new ExpressionSpecification<IdentitySession>(
|
// 不活跃用户停用时间: 用户距离上次登录天数 + 宽限天数
|
||||
// x => x.LastAccessed <= clock.Now.AddDays(-inactiveNotifierDays));
|
var inactivityPeriodDays = context.GetOrDefaultJobData(PropertyInactivityPeriodDays, 180); |
||||
|
var gracePeriodDays = context.GetOrDefaultJobData(PropertyGracePeriodDays, 60); |
||||
// using (identityUserSessionRepo.DisableTracking())
|
|
||||
// {
|
var clock = context.GetRequiredService<IClock>(); |
||||
// var inactiveUserCount = await identityUserSessionRepo.GetCountAsync(specification);
|
var logger = context.GetRequiredService<ILogger<InactiveIdentityUserCleanupJob>>(); |
||||
// if (inactiveUserCount == 0)
|
|
||||
// {
|
var identityUserRepo = context.GetRequiredService<IIdentityUserRepository>(); |
||||
// logger.LogInformation("There are no inactive users to be notified.");
|
var identityUserInactiveRepo = context.GetRequiredService<IIdentityUserInactiveRepository>(); |
||||
// return;
|
|
||||
// }
|
var specification = new ExpressionSpecification<IdentityUserInactive>( |
||||
// // 不活跃用户会话集合
|
x => x.Status == InactiveUserStatus.Notified && x.LastSignInTime < clock.Now.AddDays(-(inactivityPeriodDays + gracePeriodDays))); |
||||
// var inactiveUsers = await identityUserSessionRepo.GetListAsync(specification, maxResultCount: inactiveUserCount);
|
|
||||
|
var inactiveUserCount = await identityUserInactiveRepo.GetCountAsync(specification); |
||||
// // 直接清理的不活跃用户集合
|
if (inactiveUserCount == 0) |
||||
// var inactiveCleanupUsers = inactiveUsers.Where(x => x.LastAccessed <= clock.Now.AddDays(-inactiveCleanupDays));
|
{ |
||||
|
logger.LogInformation("There are no inactive users that can be deactivated."); |
||||
// // 需要通知的不活跃用户
|
return; |
||||
// var inactiveNotifierUsers = inactiveUsers.ExceptBy(inactiveCleanupUsers.Select(x => x.Id), x => x.Id);
|
} |
||||
// if (inactiveNotifierUsers.Count() != 0)
|
|
||||
// {
|
var inactiveUserBatch = 1000; |
||||
// await SendInactiveUserNotifier(context, inactiveNotifierUsers);
|
|
||||
// }
|
while (true) |
||||
|
{ |
||||
// if (inactiveCleanupUsers.Count() != 0)
|
var identityUserInactives = await identityUserInactiveRepo.GetListAsync( |
||||
// {
|
specification, |
||||
// logger.LogInformation(
|
maxResultCount: inactiveUserBatch); |
||||
// "Prepare to clean up {count} users who have been inactive for more than {inactiveCleanupDays} days.",
|
if (identityUserInactives.Count == 0) |
||||
// inactiveCleanupUsers.Count(),
|
{ |
||||
// inactiveCleanupDays);
|
logger.LogInformation("There are no inactive users that can be deactivated."); |
||||
|
break; |
||||
// // 清理不活跃用户
|
} |
||||
// await identityUserRepo.DeleteManyAsync(inactiveCleanupUsers.Select(x => x.UserId));
|
|
||||
// }
|
var inactiveUsers = await identityUserRepo.GetListByIdsAsync( |
||||
// }
|
identityUserInactives.Select(x => x.UserId)); |
||||
// logger.LogInformation($"Cleaned inactive users.");
|
if (inactiveUsers.Count > 0) |
||||
// }
|
{ |
||||
|
inactiveUsers.ForEach((inactiveUser) => |
||||
// /// <summary>
|
{ |
||||
// /// 发送不活跃用户清理通知
|
inactiveUser.SetIsActive(false); |
||||
// /// </summary>
|
}); |
||||
// /// <param name="context"></param>
|
|
||||
// /// <param name="userSessions"></param>
|
await identityUserRepo.UpdateManyAsync(inactiveUsers); |
||||
// /// <returns></returns>
|
|
||||
// private async Task SendInactiveUserNotifier(JobRunnableContext context, IEnumerable<IdentitySession> userSessions)
|
await SendInactiveUserDeactivationNotifier(context, inactiveUsers); |
||||
// {
|
} |
||||
// // TODO: 完成不活跃用户清理通知
|
|
||||
// var notificationSender = context.GetRequiredService<INotificationSender>();
|
identityUserInactives.ForEach((identityUserInactive) => |
||||
|
{ |
||||
// var notificationTemplate = new NotificationTemplate(
|
identityUserInactive.MarkDeactivated(clock.Now); |
||||
// IdentityNotificationNames.IdentityUser.CleaningUpInactiveUsers,
|
}); |
||||
// data: new Dictionary<string, object>
|
|
||||
// {
|
await identityUserInactiveRepo.UpdateManyAsync(identityUserInactives); |
||||
|
|
||||
// });
|
if (identityUserInactives.Count < inactiveUserBatch) |
||||
|
{ |
||||
// await notificationSender.SendNofiterAsync(
|
break; |
||||
// IdentityNotificationNames.IdentityUser.CleaningUpInactiveUsers,
|
} |
||||
// notificationTemplate
|
} |
||||
// );
|
|
||||
// }
|
logger.LogInformation("The operation to deactivate inactive users has been successfully completed."); |
||||
//}
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 发送不活跃用户停用通知
|
||||
|
/// </summary>
|
||||
|
/// <param name="context"></param>
|
||||
|
/// <param name="inactiveUsers"></param>
|
||||
|
/// <returns></returns>
|
||||
|
protected async virtual Task SendInactiveUserDeactivationNotifier(JobRunnableContext context, IEnumerable<IdentityUser> inactiveUsers) |
||||
|
{ |
||||
|
var clock = context.GetRequiredService<IClock>(); |
||||
|
var notificationSender = context.GetRequiredService<INotificationSender>(); |
||||
|
|
||||
|
var inactivityPeriodDays = context.GetOrDefaultJobData(PropertyInactivityPeriodDays, 180); |
||||
|
var gracePeriodDays = context.GetOrDefaultJobData(PropertyGracePeriodDays, 60); |
||||
|
|
||||
|
var reactivateDate = clock.Now.AddDays(gracePeriodDays); |
||||
|
await Parallel.ForEachAsync( |
||||
|
inactiveUsers, |
||||
|
context.CancellationToken, |
||||
|
async (inactiveUser, ctx) => |
||||
|
{ |
||||
|
var inactivityDays = (int)(clock.Now - (inactiveUser.LastSignInTime ?? inactiveUser.CreationTime)).TotalDays; |
||||
|
var notificationTemplateData = new Dictionary<string, object> |
||||
|
{ |
||||
|
{ "now", clock.Now }, |
||||
|
{ "name", inactiveUser.Name }, |
||||
|
{ "userName", inactiveUser.UserName }, |
||||
|
{ "surname", inactiveUser.Surname }, |
||||
|
{ "email", inactiveUser.Email }, |
||||
|
{ "isActive", inactiveUser.IsActive }, |
||||
|
{ "inactivityDays", inactivityDays }, |
||||
|
{ "reactivateDate", reactivateDate }, |
||||
|
{ "creationTime", clock.Normalize(inactiveUser.CreationTime) }, |
||||
|
}; |
||||
|
if (inactiveUser.LastSignInTime.HasValue) |
||||
|
{ |
||||
|
notificationTemplateData["lastSignInTime"] = clock.Normalize(inactiveUser.LastSignInTime.Value.DateTime); |
||||
|
} |
||||
|
var notificationTemplate = new NotificationTemplate( |
||||
|
IdentityNotificationNames.IdentityUser.InactiveUserDeactivationNotifier, |
||||
|
data: notificationTemplateData); |
||||
|
|
||||
|
await notificationSender.SendNofiterAsync( |
||||
|
IdentityNotificationNames.IdentityUser.InactiveUserDeactivationNotifier, |
||||
|
template: notificationTemplate, |
||||
|
user: new UserIdentifier(inactiveUser.Id, inactiveUser.UserName), |
||||
|
tenantId: inactiveUser.TenantId, |
||||
|
severity: NotificationSeverity.Warn); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
protected async virtual Task DeleteInactiveIdentityUsers(JobRunnableContext context) |
||||
|
{ |
||||
|
// 不活跃用户删除时间: 距离上次发送不活跃通知天数 + 宽限天数
|
||||
|
var gracePeriodDays = context.GetOrDefaultJobData(PropertyGracePeriodDays, 60); |
||||
|
|
||||
|
var clock = context.GetRequiredService<IClock>(); |
||||
|
var logger = context.GetRequiredService<ILogger<InactiveIdentityUserCleanupJob>>(); |
||||
|
|
||||
|
var identityUserRepo = context.GetRequiredService<IIdentityUserRepository>(); |
||||
|
var identityUserInactiveRepo = context.GetRequiredService<IIdentityUserInactiveRepository>(); |
||||
|
|
||||
|
var specification = new ExpressionSpecification<IdentityUserInactive>( |
||||
|
x => x.Status == InactiveUserStatus.Deactivated && x.DeactivatedTime < clock.Now.AddDays(-gracePeriodDays)); |
||||
|
|
||||
|
var inactiveUserCount = await identityUserInactiveRepo.GetCountAsync(specification); if (inactiveUserCount == 0) |
||||
|
{ |
||||
|
logger.LogInformation("There are no inactive users to delete."); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var inactiveUserBatch = 1000; |
||||
|
|
||||
|
while (true) |
||||
|
{ |
||||
|
var identityUserInactives = await identityUserInactiveRepo.GetListAsync( |
||||
|
specification, |
||||
|
maxResultCount: inactiveUserBatch); |
||||
|
if (identityUserInactives.Count == 0) |
||||
|
{ |
||||
|
logger.LogInformation("There are no inactive users to delete."); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
await identityUserInactiveRepo.DeleteManyAsync(identityUserInactives); |
||||
|
|
||||
|
var inactiveUsers = await identityUserRepo.GetListByIdsAsync( |
||||
|
identityUserInactives.Select(x => x.UserId)); |
||||
|
if (inactiveUsers.Count > 0) |
||||
|
{ |
||||
|
await identityUserRepo.DeleteManyAsync(inactiveUsers); |
||||
|
|
||||
|
await SendInactiveUserDeletionNotifier(context, inactiveUsers); |
||||
|
} |
||||
|
|
||||
|
if (identityUserInactives.Count < inactiveUserBatch) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
logger.LogInformation("The operation to deactivate inactive users has been successfully completed."); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 发送不活跃用户删除通知
|
||||
|
/// </summary>
|
||||
|
/// <param name="context"></param>
|
||||
|
/// <param name="inactiveUsers"></param>
|
||||
|
/// <returns></returns>
|
||||
|
protected async virtual Task SendInactiveUserDeletionNotifier(JobRunnableContext context, IEnumerable<IdentityUser> inactiveUsers) |
||||
|
{ |
||||
|
var clock = context.GetRequiredService<IClock>(); |
||||
|
var emailSender = context.GetRequiredService<IEmailSender>(); |
||||
|
var templateRenderer = context.GetRequiredService<ITemplateRenderer>(); |
||||
|
var logger = context.GetRequiredService<ILogger<InactiveIdentityUserCleanupJob>>(); |
||||
|
var stringLocalizer = context.GetRequiredService<IStringLocalizer<IdentityResource>>(); |
||||
|
|
||||
|
var emailTitle = stringLocalizer["InactiveUserDeletionNotifier"]; |
||||
|
var gracePeriodDays = context.GetOrDefaultJobData(PropertyGracePeriodDays, 60); |
||||
|
var inactivityPeriodDays = context.GetOrDefaultJobData(PropertyInactivityPeriodDays, 180); |
||||
|
|
||||
|
await Parallel.ForEachAsync( |
||||
|
inactiveUsers, |
||||
|
context.CancellationToken, |
||||
|
async (inactiveUser, ctx) => |
||||
|
{ |
||||
|
if (!inactiveUser.EmailConfirmed) |
||||
|
{ |
||||
|
logger.LogWarning("{userName} email has not been confirmed, skipping the account deletion notification sending.", inactiveUser.UserName); |
||||
|
return; |
||||
|
} |
||||
|
try |
||||
|
{ |
||||
|
var notificationTemplateData = new Dictionary<string, object> |
||||
|
{ |
||||
|
{ "now", clock.Now }, |
||||
|
{ "name", inactiveUser.Name }, |
||||
|
{ "userName", inactiveUser.UserName }, |
||||
|
{ "surname", inactiveUser.Surname }, |
||||
|
{ "email", inactiveUser.Email }, |
||||
|
{ "isActive", inactiveUser.IsActive }, |
||||
|
{ "inactivityPeriodDays", inactivityPeriodDays }, |
||||
|
{ "gracePeriodDays", gracePeriodDays }, |
||||
|
{ "creationTime", clock.Normalize(inactiveUser.CreationTime) }, |
||||
|
}; |
||||
|
if (inactiveUser.LastSignInTime.HasValue) |
||||
|
{ |
||||
|
notificationTemplateData["lastSignInTime"] = clock.Normalize(inactiveUser.LastSignInTime.Value.DateTime); |
||||
|
} |
||||
|
|
||||
|
var emailBody = await templateRenderer.RenderAsync( |
||||
|
IdentityNotificationNames.IdentityUser.InactiveUserDeletionNotifier, |
||||
|
notificationTemplateData); |
||||
|
|
||||
|
await emailSender.SendAsync( |
||||
|
inactiveUser.Email, |
||||
|
emailTitle, |
||||
|
emailBody, |
||||
|
isBodyHtml: true); |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
logger.LogWarning("Failure in sending the email for notifying the deletion of the user {userName}, error: {message}", inactiveUser.UserName, ex.Message); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|||||
@ -0,0 +1,179 @@ |
|||||
|
using LINGYUN.Abp.BackgroundTasks; |
||||
|
using LINGYUN.Abp.Identity.Notifications; |
||||
|
using LINGYUN.Abp.Identity.Settings; |
||||
|
using LINGYUN.Abp.Notifications; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.DistributedLocking; |
||||
|
using Volo.Abp.Identity; |
||||
|
using Volo.Abp.MultiTenancy; |
||||
|
using Volo.Abp.Roles; |
||||
|
using Volo.Abp.Settings; |
||||
|
using Volo.Abp.Timing; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Identity.Jobs; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 不活跃用户通知作业
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// 通知长期未登录用户保持账号活跃
|
||||
|
/// </remarks>
|
||||
|
public class InactiveIdentityUserNotifierJob : IJobRunnable |
||||
|
{ |
||||
|
public const string Name = "InactiveIdentityUserNotifierJob"; |
||||
|
|
||||
|
#region Definition Paramters
|
||||
|
|
||||
|
public readonly static IReadOnlyList<JobDefinitionParamter> Paramters = |
||||
|
new List<JobDefinitionParamter> |
||||
|
{ |
||||
|
new JobDefinitionParamter( |
||||
|
PropertyInactivityPeriodDays, |
||||
|
LocalizableStatic.Create("DisplayName:InactivityPeriodDays"), |
||||
|
LocalizableStatic.Create("Description:InactivityPeriodDays")), |
||||
|
}; |
||||
|
|
||||
|
#endregion
|
||||
|
/// <summary>
|
||||
|
/// 不活跃用户通知时间, 单位: 天
|
||||
|
/// </summary>
|
||||
|
public const string PropertyInactivityPeriodDays = "InactivityPeriodDays"; |
||||
|
|
||||
|
public async virtual Task ExecuteAsync(JobRunnableContext context) |
||||
|
{ |
||||
|
var logger = context.GetRequiredService<ILogger<InactiveIdentityUserNotifierJob>>(); |
||||
|
|
||||
|
var currentTenant = context.GetRequiredService<ICurrentTenant>(); |
||||
|
var distributedLock = context.GetRequiredService<IAbpDistributedLock>(); |
||||
|
var distributedLockKey = $"{nameof(InactiveIdentityUserNotifierJob)}_{currentTenant.Id}"; |
||||
|
await using var jobLockHandle = await distributedLock.TryAcquireAsync(distributedLockKey); |
||||
|
if (jobLockHandle == null) |
||||
|
{ |
||||
|
logger.LogInformation("Handle is null because of the locking for : {distributedLockKey}", distributedLockKey); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
logger.LogInformation("Lock is acquired for {distributedLockKey}.", distributedLockKey); |
||||
|
|
||||
|
// 不活跃用户通知时间
|
||||
|
var inactiveNotifierDays = context.GetOrDefaultJobData(PropertyInactivityPeriodDays, 180); |
||||
|
// 排除系统管理员用户
|
||||
|
var exceptUserIds = new List<Guid>(); |
||||
|
|
||||
|
var clock = context.GetRequiredService<IClock>(); |
||||
|
|
||||
|
var identityRoleManager = context.GetRequiredService<IdentityRoleManager>(); |
||||
|
var identityUserManager = context.GetRequiredService<IdentityUserManager>(); |
||||
|
var identityUserRepo = context.GetRequiredService<IIdentityUserRepository>(); |
||||
|
var identityUserInactiveRepo = context.GetRequiredService<IIdentityUserInactiveRepository>(); |
||||
|
|
||||
|
// 管理员角色用户不清理
|
||||
|
var adminRoleUsers = await identityUserRepo.GetListByNormalizedRoleNameAsync( |
||||
|
identityRoleManager.NormalizeKey(AbpRoleConsts.AdminRoleName)); |
||||
|
if (adminRoleUsers.Count > 0) |
||||
|
{ |
||||
|
exceptUserIds.AddIfNotContains(adminRoleUsers.Select(x => x.Id)); |
||||
|
} |
||||
|
// 管理员用户不清理
|
||||
|
var adminUsers = await identityUserRepo.GetUsersByNormalizedUserNameAsync( |
||||
|
identityUserManager.NormalizeName("admin")); |
||||
|
if (adminUsers.Count > 0) |
||||
|
{ |
||||
|
exceptUserIds.AddIfNotContains(adminUsers.Select(x => x.Id)); |
||||
|
} |
||||
|
|
||||
|
var inactiveUserNotifierTime = clock.Now.AddDays(-inactiveNotifierDays); |
||||
|
|
||||
|
var inactiveUserCount = await identityUserInactiveRepo.GetInactiveUserCountAsync( |
||||
|
inactiveUserNotifierTime, |
||||
|
exceptUserIds); |
||||
|
if (inactiveUserCount == 0) |
||||
|
{ |
||||
|
logger.LogInformation("There are no inactive users to be notified."); |
||||
|
return; |
||||
|
} |
||||
|
var inactiveUserBatch = 1000; |
||||
|
|
||||
|
while (true) |
||||
|
{ |
||||
|
var inactiveUsers = await identityUserInactiveRepo.GetInactiveUserListAsync( |
||||
|
inactiveUserNotifierTime, |
||||
|
exceptUserIds, |
||||
|
maxResultCount: inactiveUserBatch); |
||||
|
if (inactiveUsers.Count == 0) |
||||
|
{ |
||||
|
logger.LogInformation("There are no inactive users to be notified."); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
var identityUserInactives = inactiveUsers.Select(user => |
||||
|
new IdentityUserInactive( |
||||
|
user.Id, |
||||
|
user.LastSignInTime ?? user.LastModificationTime ?? user.CreationTime, |
||||
|
user.TenantId)); |
||||
|
|
||||
|
await identityUserInactiveRepo.InsertManyAsync(identityUserInactives); |
||||
|
|
||||
|
await SendInactiveUserReminderNotifier(context, inactiveUsers); |
||||
|
|
||||
|
if (inactiveUsers.Count < inactiveUserBatch) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
logger.LogInformation($"The job of sending notifications to inactive users has been successfully completed.."); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 发送不活跃用户提醒通知
|
||||
|
/// </summary>
|
||||
|
/// <param name="context"></param>
|
||||
|
/// <param name="inactiveUsers"></param>
|
||||
|
/// <returns></returns>
|
||||
|
private async Task SendInactiveUserReminderNotifier(JobRunnableContext context, IEnumerable<IdentityUser> inactiveUsers) |
||||
|
{ |
||||
|
var clock = context.GetRequiredService<IClock>(); |
||||
|
var settingProvider = context.GetRequiredService<ISettingProvider>(); |
||||
|
var notificationSender = context.GetRequiredService<INotificationSender>(); |
||||
|
|
||||
|
var userLoginUri = await settingProvider.GetOrNullAsync(IdentitySettingNames.Link.UserLoginUri); |
||||
|
var inactiveNotifierDays = context.GetOrDefaultJobData(PropertyInactivityPeriodDays, 180); |
||||
|
var reactivateDate = clock.Now.AddDays(inactiveNotifierDays); |
||||
|
await Parallel.ForEachAsync( |
||||
|
inactiveUsers, |
||||
|
context.CancellationToken, |
||||
|
async (inactiveUser, ctx) => |
||||
|
{ |
||||
|
var notificationTemplateData = new Dictionary<string, object> |
||||
|
{ |
||||
|
{ "now", clock.Now }, |
||||
|
{ "loginUrl", userLoginUri }, |
||||
|
{ "name", inactiveUser.Name }, |
||||
|
{ "userName", inactiveUser.UserName }, |
||||
|
{ "surname", inactiveUser.Surname }, |
||||
|
{ "email", inactiveUser.Email }, |
||||
|
{ "isActive", inactiveUser.IsActive }, |
||||
|
{ "reactivateDate", reactivateDate }, |
||||
|
{ "creationTime", clock.Normalize(inactiveUser.CreationTime) }, |
||||
|
}; |
||||
|
if (inactiveUser.LastSignInTime.HasValue) |
||||
|
{ |
||||
|
notificationTemplateData["lastSignInTime"] = clock.Normalize(inactiveUser.LastSignInTime.Value.DateTime); |
||||
|
} |
||||
|
var notificationTemplate = new NotificationTemplate( |
||||
|
IdentityNotificationNames.IdentityUser.InactiveUserReminderNotifier, |
||||
|
data: notificationTemplateData); |
||||
|
|
||||
|
await notificationSender.SendNofiterAsync( |
||||
|
IdentityNotificationNames.IdentityUser.InactiveUserReminderNotifier, |
||||
|
template: notificationTemplate, |
||||
|
user: new UserIdentifier(inactiveUser.Id, inactiveUser.UserName), |
||||
|
tenantId: inactiveUser.TenantId); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Domain.Entities.Events; |
||||
|
using Volo.Abp.Users; |
||||
|
|
||||
|
namespace LY.MicroService.Applications.Single.EventBus.Local; |
||||
|
|
||||
|
public class UserCreateSubscribeIdentityNotificationsEventHandler : ILocalEventHandler<EntityCreatedEventData<UserEto>>, ITransientDependency |
||||
|
{ |
||||
|
private readonly INotificationSubscriptionManager _notificationSubscriptionManager; |
||||
|
public UserCreateSubscribeIdentityNotificationsEventHandler( |
||||
|
INotificationSubscriptionManager notificationSubscriptionManager |
||||
|
) |
||||
|
{ |
||||
|
_notificationSubscriptionManager = notificationSubscriptionManager; |
||||
|
} |
||||
|
|
||||
|
public async virtual Task HandleEventAsync(EntityCreatedEventData<UserEto> eventData) |
||||
|
{ |
||||
|
var userIdentifer = new UserIdentifier(eventData.Entity.Id, eventData.Entity.UserName); |
||||
|
// 订阅用户会话过期通知
|
||||
|
await _notificationSubscriptionManager |
||||
|
.SubscribeAsync( |
||||
|
eventData.Entity.TenantId, |
||||
|
userIdentifer, |
||||
|
IdentityNotificationNames.Session.ExpirationSession); |
||||
|
// 订阅用户账号不活跃通知
|
||||
|
await _notificationSubscriptionManager |
||||
|
.SubscribeAsync( |
||||
|
eventData.Entity.TenantId, |
||||
|
userIdentifer, |
||||
|
IdentityNotificationNames.IdentityUser.InactiveUserReminderNotifier); |
||||
|
// 订阅用户账号停用通知
|
||||
|
await _notificationSubscriptionManager |
||||
|
.SubscribeAsync( |
||||
|
eventData.Entity.TenantId, |
||||
|
userIdentifer, |
||||
|
IdentityNotificationNames.IdentityUser.InactiveUserDeactivationNotifier); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue