refactor(architecture): harden module boundaries
Some checks failed
ci / release-gate (push) Has been cancelled

This commit is contained in:
2026-08-04 12:10:36 +08:00
parent b38f12e60b
commit 33375a38d7
218 changed files with 5124 additions and 3828 deletions

View File

@@ -15,7 +15,7 @@ internal sealed class PointAdministrationService(CommerceAdministrationDependenc
CancellationToken cancellationToken = default)
{
await AssertAdminAsync(actor, cancellationToken);
var tasks = dbContext.PointActivityTasks.AsNoTracking()
var tasks = pointsPersistence.PointActivityTasks.AsNoTracking()
.Where(item => item.TenantId == actor.TenantId);
if (!string.IsNullOrWhiteSpace(query.Status))
tasks = tasks.Where(item => item.Status == ParsePointTaskStatus(query.Status));
@@ -38,16 +38,16 @@ internal sealed class PointAdministrationService(CommerceAdministrationDependenc
throw new CommerceException("Point task points and claim limit must be positive.", "invalid_point_task");
var task = command.Id.HasValue
? await dbContext.PointActivityTasks.SingleOrDefaultAsync(
? await pointsPersistence.PointActivityTasks.SingleOrDefaultAsync(
item => item.TenantId == actor.TenantId && item.Id == command.Id.Value,
cancellationToken)
: await dbContext.PointActivityTasks.SingleOrDefaultAsync(
: await pointsPersistence.PointActivityTasks.SingleOrDefaultAsync(
item => item.TenantId == actor.TenantId && item.TaskKey == command.TaskKey.Trim(),
cancellationToken);
if (task is null)
{
task = new PointActivityTask { TenantId = actor.TenantId };
dbContext.PointActivityTasks.Add(task);
pointsPersistence.PointActivityTasks.Add(task);
}
task.TaskKey = command.TaskKey.Trim();
@@ -63,7 +63,7 @@ internal sealed class PointAdministrationService(CommerceAdministrationDependenc
task.Rules = JsonObjectOrDefault(command.Rules);
task.Metadata = JsonObjectOrDefault(command.Metadata);
await dbContext.SaveChangesAsync(cancellationToken);
await unitOfWork.SaveChangesAsync(cancellationToken);
return task;
}
@@ -73,7 +73,7 @@ internal sealed class PointAdministrationService(CommerceAdministrationDependenc
CancellationToken cancellationToken = default)
{
await AssertAdminAsync(actor, cancellationToken);
var claims = dbContext.PointActivityClaims.AsNoTracking()
var claims = pointsPersistence.PointActivityClaims.AsNoTracking()
.Where(item => item.TenantId == actor.TenantId);
if (query.UserId.HasValue) claims = claims.Where(item => item.UserId == query.UserId.Value);
@@ -90,7 +90,7 @@ internal sealed class PointAdministrationService(CommerceAdministrationDependenc
CancellationToken cancellationToken = default)
{
await AssertAdminAsync(actor, cancellationToken);
var items = dbContext.PointExchangeItems.AsNoTracking()
var items = pointsPersistence.PointExchangeItems.AsNoTracking()
.Where(item => item.TenantId == actor.TenantId);
if (!string.IsNullOrWhiteSpace(query.Status))
items = items.Where(item => item.Status == ParsePointExchangeItemStatus(query.Status));
@@ -116,16 +116,16 @@ internal sealed class PointAdministrationService(CommerceAdministrationDependenc
throw new CommerceException("Point exchange item cost must be positive.", "invalid_point_exchange_item");
var item = command.Id.HasValue
? await dbContext.PointExchangeItems.SingleOrDefaultAsync(
? await pointsPersistence.PointExchangeItems.SingleOrDefaultAsync(
entry => entry.TenantId == actor.TenantId && entry.Id == command.Id.Value,
cancellationToken)
: await dbContext.PointExchangeItems.SingleOrDefaultAsync(
: await pointsPersistence.PointExchangeItems.SingleOrDefaultAsync(
entry => entry.TenantId == actor.TenantId && entry.ItemKey == command.ItemKey.Trim(),
cancellationToken);
if (item is null)
{
item = new PointExchangeItem { TenantId = actor.TenantId };
dbContext.PointExchangeItems.Add(item);
pointsPersistence.PointExchangeItems.Add(item);
}
item.RegionId = command.RegionId;
@@ -143,7 +143,7 @@ internal sealed class PointAdministrationService(CommerceAdministrationDependenc
item.FulfillmentPayload = JsonObjectOrDefault(command.FulfillmentPayload);
item.Metadata = JsonObjectOrDefault(command.Metadata);
await dbContext.SaveChangesAsync(cancellationToken);
await unitOfWork.SaveChangesAsync(cancellationToken);
return item;
}
@@ -153,7 +153,7 @@ internal sealed class PointAdministrationService(CommerceAdministrationDependenc
CancellationToken cancellationToken = default)
{
await AssertAdminAsync(actor, cancellationToken);
var orders = dbContext.PointExchangeOrders.AsNoTracking()
var orders = pointsPersistence.PointExchangeOrders.AsNoTracking()
.Where(item => item.TenantId == actor.TenantId);
if (query.UserId.HasValue) orders = orders.Where(item => item.UserId == query.UserId.Value);
@@ -173,7 +173,7 @@ internal sealed class PointAdministrationService(CommerceAdministrationDependenc
CancellationToken cancellationToken = default)
{
await AssertAdminAsync(actor, cancellationToken);
var order = await dbContext.PointExchangeOrders
var order = await pointsPersistence.PointExchangeOrders
.SingleOrDefaultAsync(item => item.TenantId == actor.TenantId && item.Id == command.OrderId,
cancellationToken)
?? throw new CommerceException("Point exchange order was not found.",
@@ -189,7 +189,7 @@ internal sealed class PointAdministrationService(CommerceAdministrationDependenc
order.CancelledAt ??= DateTimeOffset.UtcNow;
}
await dbContext.SaveChangesAsync(cancellationToken);
await unitOfWork.SaveChangesAsync(cancellationToken);
return order;
}
}

View File

@@ -10,7 +10,10 @@ using Tiku.Infrastructure.Persistence;
namespace Tiku.Infrastructure.Points;
public sealed class PointService(TikuDbContext dbContext) : IPointService
public sealed class PointService(IPointsPersistence pointsPersistence,
ILearningPersistence learningPersistence,
ICommercePersistence commercePersistence,
IIdentityPersistence identityPersistence) : IPointService
{
public async Task<PointSummaryItem> GetSummaryAsync(
PointActor actor,
@@ -28,7 +31,7 @@ public sealed class PointService(TikuDbContext dbContext) : IPointService
await AssertActiveMemberAsync(actor, cancellationToken);
var now = DateTimeOffset.UtcNow;
var limit = Math.Clamp(query.Limit ?? 50, 1, 200);
var tasks = await dbContext.PointActivityTasks
var tasks = await pointsPersistence.PointActivityTasks
.AsNoTracking()
.Where(item =>
item.TenantId == actor.TenantId &&
@@ -40,7 +43,7 @@ public sealed class PointService(TikuDbContext dbContext) : IPointService
.Take(limit)
.ToArrayAsync(cancellationToken);
var taskIds = tasks.Select(item => item.Id).ToArray();
var claimCounts = await dbContext.PointActivityClaims
var claimCounts = await pointsPersistence.PointActivityClaims
.AsNoTracking()
.Where(item =>
item.TenantId == actor.TenantId &&
@@ -67,7 +70,7 @@ public sealed class PointService(TikuDbContext dbContext) : IPointService
await AssertActiveMemberAsync(actor, cancellationToken);
var taskKey = NormalizeKey(command.TaskKey, "task_key_required");
var now = DateTimeOffset.UtcNow;
var task = await dbContext.PointActivityTasks
var task = await pointsPersistence.PointActivityTasks
.SingleOrDefaultAsync(item =>
item.TenantId == actor.TenantId &&
item.TaskKey == taskKey,
@@ -81,7 +84,7 @@ public sealed class PointService(TikuDbContext dbContext) : IPointService
if (!string.IsNullOrWhiteSpace(command.SourceType) && command.SourceId.HasValue)
{
var sourceType = NormalizeOptional(command.SourceType);
var existing = await dbContext.PointActivityClaims
var existing = await pointsPersistence.PointActivityClaims
.AsNoTracking()
.Where(item =>
item.TenantId == actor.TenantId &&
@@ -95,7 +98,7 @@ public sealed class PointService(TikuDbContext dbContext) : IPointService
if (existing is not null) return ToClaimItem(existing);
}
var claimedCount = await dbContext.PointActivityClaims.CountAsync(
var claimedCount = await pointsPersistence.PointActivityClaims.CountAsync(
item =>
item.TenantId == actor.TenantId &&
item.UserId == actor.UserId &&
@@ -119,9 +122,9 @@ public sealed class PointService(TikuDbContext dbContext) : IPointService
ClaimedAt = now,
Metadata = JsonSerializer.SerializeToElement(new { source = "student_points" })
};
dbContext.PointActivityClaims.Add(claim);
pointsPersistence.PointActivityClaims.Add(claim);
var balanceAfter = (await GetSummaryCoreAsync(actor, cancellationToken)).BalancePoints + task.Points;
dbContext.UserScoreEvents.Add(new UserScoreEvent
learningPersistence.UserScoreEvents.Add(new UserScoreEvent
{
TenantId = actor.TenantId,
UserId = actor.UserId,
@@ -139,7 +142,7 @@ public sealed class PointService(TikuDbContext dbContext) : IPointService
task.TaskKey
})
});
await dbContext.SaveChangesAsync(cancellationToken);
await pointsPersistence.SaveChangesAsync(cancellationToken);
return ToClaimItem(claim);
}
@@ -151,7 +154,7 @@ public sealed class PointService(TikuDbContext dbContext) : IPointService
await AssertActiveMemberAsync(actor, cancellationToken);
var now = DateTimeOffset.UtcNow;
var balance = (await GetSummaryCoreAsync(actor, cancellationToken)).BalancePoints;
var items = dbContext.PointExchangeItems
var items = pointsPersistence.PointExchangeItems
.AsNoTracking()
.Where(item =>
item.TenantId == actor.TenantId &&
@@ -175,11 +178,11 @@ public sealed class PointService(TikuDbContext dbContext) : IPointService
CancellationToken cancellationToken = default)
{
await AssertActiveMemberAsync(actor, cancellationToken);
await using var transaction = dbContext.Database.IsRelational()
? await dbContext.Database.BeginTransactionAsync(cancellationToken)
await using var transaction = pointsPersistence.Database.IsRelational()
? await pointsPersistence.Database.BeginTransactionAsync(cancellationToken)
: null;
var now = DateTimeOffset.UtcNow;
var item = await dbContext.PointExchangeItems
var item = await pointsPersistence.PointExchangeItems
.SingleOrDefaultAsync(entry =>
entry.TenantId == actor.TenantId &&
entry.Id == command.ItemId,
@@ -214,8 +217,8 @@ public sealed class PointService(TikuDbContext dbContext) : IPointService
FulfillmentSnapshot = item.FulfillmentPayload,
Metadata = JsonSerializer.SerializeToElement(new { source = "student_points_exchange" })
};
dbContext.PointExchangeOrders.Add(order);
dbContext.UserScoreEvents.Add(new UserScoreEvent
pointsPersistence.PointExchangeOrders.Add(order);
learningPersistence.UserScoreEvents.Add(new UserScoreEvent
{
TenantId = actor.TenantId,
UserId = actor.UserId,
@@ -235,7 +238,7 @@ public sealed class PointService(TikuDbContext dbContext) : IPointService
if (item.ItemType == PointExchangeItemType.Entitlement)
await GrantEntitlementAsync(actor, item, order, now, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
await pointsPersistence.SaveChangesAsync(cancellationToken);
if (transaction is not null) await transaction.CommitAsync(cancellationToken);
return ToExchangeOrderItem(order);
}
@@ -246,7 +249,7 @@ public sealed class PointService(TikuDbContext dbContext) : IPointService
CancellationToken cancellationToken = default)
{
await AssertActiveMemberAsync(actor, cancellationToken);
var orders = dbContext.PointExchangeOrders
var orders = pointsPersistence.PointExchangeOrders
.AsNoTracking()
.Where(item => item.TenantId == actor.TenantId && item.UserId == actor.UserId);
if (!string.IsNullOrWhiteSpace(query.Status))
@@ -263,14 +266,14 @@ public sealed class PointService(TikuDbContext dbContext) : IPointService
PointActor actor,
CancellationToken cancellationToken)
{
var earned = await dbContext.PointActivityClaims
var earned = await pointsPersistence.PointActivityClaims
.AsNoTracking()
.Where(item =>
item.TenantId == actor.TenantId &&
item.UserId == actor.UserId &&
item.Status == PointActivityClaimStatus.Claimed)
.SumAsync(item => (int?)item.Points, cancellationToken) ?? 0;
var spent = await dbContext.PointExchangeOrders
var spent = await pointsPersistence.PointExchangeOrders
.AsNoTracking()
.Where(item =>
item.TenantId == actor.TenantId &&
@@ -288,7 +291,7 @@ public sealed class PointService(TikuDbContext dbContext) : IPointService
CancellationToken cancellationToken)
{
var days = item.Days ?? ReadInt(item.FulfillmentPayload, "days") ?? 0;
var current = await dbContext.Entitlements
var current = await commercePersistence.Entitlements
.Where(entry =>
entry.TenantId == actor.TenantId &&
entry.UserId == actor.UserId &&
@@ -298,7 +301,7 @@ public sealed class PointService(TikuDbContext dbContext) : IPointService
.FirstOrDefaultAsync(cancellationToken);
if (current is null)
{
dbContext.Entitlements.Add(new Entitlement
commercePersistence.Entitlements.Add(new Entitlement
{
TenantId = actor.TenantId,
UserId = actor.UserId,
@@ -324,7 +327,7 @@ public sealed class PointService(TikuDbContext dbContext) : IPointService
private async Task AssertActiveMemberAsync(PointActor actor, CancellationToken cancellationToken)
{
var exists = await dbContext.TenantMemberships.AnyAsync(item =>
var exists = await identityPersistence.TenantMemberships.AnyAsync(item =>
item.TenantId == actor.TenantId &&
item.UserId == actor.UserId &&
item.Status == MembershipStatus.Active,