feat: complete phase six backoffice operations

This commit is contained in:
2026-07-28 14:31:43 +08:00
parent 747ff59d76
commit 99e4e43122
31 changed files with 23504 additions and 26 deletions

View File

@@ -1,4 +1,5 @@
using System.Text.Json;
using Tiku.Application.Jobs;
using Tiku.Domain.Commerce;
using Tiku.Domain.Tenancy;
@@ -180,6 +181,81 @@ public sealed record CreateReconciliationBatchCommand(
public sealed record TenantReconciliationBatchList(IReadOnlyCollection<CommerceReconciliationBatch> Items);
public sealed record TenantReconciliationIssueList(IReadOnlyCollection<CommerceReconciliationIssue> Items);
public sealed record TenantReconciliationItemList(IReadOnlyCollection<CommerceReconciliationItem> Items);
public sealed record TenantReconciliationIssueEventList(IReadOnlyCollection<CommerceReconciliationIssueEvent> Items);
public sealed record TenantCommerceAnomalySummary(
int OpenRefundCount,
int ProcessingRefundCount,
int OpenReconciliationIssueCount,
int FailedReconciliationBatchCount,
int PendingPaymentCount,
int PaymentMismatchCount);
public sealed record CreateAdjustmentVoucherCommand(
Guid? IssueId,
Guid? BatchId,
Guid? ItemId,
Guid? OrderId,
Guid? PaymentId,
Guid? RefundRequestId,
CommerceAdjustmentDirection Direction,
int AmountCents,
string? Currency,
string Reason,
string? ProofAssetKey,
JsonElement Metadata);
public sealed record UpdateAdjustmentVoucherStatusCommand(
Guid VoucherId,
CommerceAdjustmentVoucherStatus Status,
string? Note);
public sealed record TenantAdjustmentVoucherList(IReadOnlyCollection<CommerceAdjustmentVoucher> Items);
public sealed record TenantAdjustmentVoucherEventList(IReadOnlyCollection<CommerceAdjustmentVoucherEvent> Items);
public sealed record TenantAdjustmentReport(
int DraftCount,
int PendingReviewCount,
int ApprovedCount,
int ClosedCount,
int IncreaseRevenueCents,
int DecreaseRevenueCents);
public sealed record PreviewReconciliationImportCommand(
string Provider,
DateOnly BillDate,
ReconciliationBillType BillType,
string SourceName,
JsonElement Rows);
public sealed record ReconciliationImportPreview(
int TotalCount,
int PaymentCount,
int RefundCount,
int InvalidCount,
int AmountCents,
int RefundAmountCents,
string SourceHash);
public sealed record ImportReconciliationCommand(
string Provider,
DateOnly BillDate,
ReconciliationBillType BillType,
string SourceName,
JsonElement Rows);
public sealed record RequestProviderBillJobCommand(
string Provider,
DateOnly BillDate,
ReconciliationBillType BillType,
DateTimeOffset? RunAfter = null);
public sealed record RefundNotificationCommand(
string Provider,
string RefundNo,
string? ProviderRefundNo,
CommerceRefundStatus Status,
string? EventId,
JsonElement Payload);
public sealed record UpdateReconciliationIssueCommand(
Guid IssueId,
@@ -324,4 +400,72 @@ public interface ICommerceAdminService
CommerceAdminActor actor,
UpdateReconciliationIssueCommand command,
CancellationToken cancellationToken = default);
Task<TenantReconciliationItemList> GetReconciliationItemsAsync(
CommerceAdminActor actor,
Guid batchId,
CancellationToken cancellationToken = default);
Task<TenantReconciliationIssueEventList> GetReconciliationIssueEventsAsync(
CommerceAdminActor actor,
Guid issueId,
CancellationToken cancellationToken = default);
Task<TenantCommerceAnomalySummary> GetAnomalySummaryAsync(
CommerceAdminActor actor,
CancellationToken cancellationToken = default);
Task<TenantAdjustmentVoucherList> GetAdjustmentVouchersAsync(
CommerceAdminActor actor,
CommerceAdminQuery query,
CancellationToken cancellationToken = default);
Task<CommerceAdjustmentVoucher> GetAdjustmentVoucherAsync(
CommerceAdminActor actor,
Guid voucherId,
CancellationToken cancellationToken = default);
Task<CommerceAdjustmentVoucher> CreateAdjustmentVoucherAsync(
CommerceAdminActor actor,
CreateAdjustmentVoucherCommand command,
CancellationToken cancellationToken = default);
Task<CommerceAdjustmentVoucher> UpdateAdjustmentVoucherStatusAsync(
CommerceAdminActor actor,
UpdateAdjustmentVoucherStatusCommand command,
CancellationToken cancellationToken = default);
Task<TenantAdjustmentVoucherEventList> GetAdjustmentVoucherEventsAsync(
CommerceAdminActor actor,
Guid voucherId,
CancellationToken cancellationToken = default);
Task<TenantAdjustmentReport> GetAdjustmentReportAsync(
CommerceAdminActor actor,
CancellationToken cancellationToken = default);
Task<ReconciliationImportPreview> PreviewReconciliationImportAsync(
CommerceAdminActor actor,
PreviewReconciliationImportCommand command,
CancellationToken cancellationToken = default);
Task<CommerceReconciliationBatch> ImportReconciliationAsync(
CommerceAdminActor actor,
ImportReconciliationCommand command,
CancellationToken cancellationToken = default);
Task<BackgroundJobItem> RequestProviderBillJobAsync(
CommerceAdminActor actor,
RequestProviderBillJobCommand command,
CancellationToken cancellationToken = default);
Task<IReadOnlyCollection<BackgroundJobItem>> GetProviderBillJobsAsync(
CommerceAdminActor actor,
CommerceAdminQuery query,
CancellationToken cancellationToken = default);
Task<CommerceRefundRequest> ProcessRefundNotificationAsync(
Guid tenantId,
RefundNotificationCommand command,
CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,262 @@
using System.Text.Json;
using Tiku.Domain.Commerce;
using Tiku.Domain.Platform;
using Tiku.Domain.Tenancy;
using Tiku.Domain.Identity;
namespace Tiku.Application.PlatformAdmin;
public sealed record PlatformAdminActor(Guid UserId);
public sealed record PlatformAdminQuery(string? Status = null, string? Search = null, int? Limit = null);
public sealed record PlatformOverview(
int TenantCount,
int ActiveTenantCount,
int SuspendedTenantCount,
int OrderCount,
int PaidOrderCount,
int RevenueCents,
int QuestionBankCount,
int QuestionCount,
int LearningActiveUserCount);
public sealed record PlatformTenantList(IReadOnlyCollection<PlatformTenantItem> Items);
public sealed record PlatformTenantItem(
Guid Id,
string Slug,
string Name,
string? LegalName,
TenantStatus Status,
TenantMode Mode,
BillingStatus BillingStatus,
DateTimeOffset? SubscriptionExpiresAt,
int DomainCount,
DateTimeOffset CreatedAt,
DateTimeOffset UpdatedAt);
public sealed record PlatformTenantDetail(
PlatformTenantItem Tenant,
IReadOnlyCollection<PlatformTenantDomainItem> Domains,
IReadOnlyCollection<PlatformTenantSubscriptionItem> Subscriptions,
TenantBillingProfileItem? BillingProfile);
public sealed record PlatformTenantDomainItem(
Guid Id,
Guid TenantId,
string Host,
TenantDomainType DomainType,
TenantDomainStatus Status,
bool IsPrimary,
DateTimeOffset? VerifiedAt,
DateTimeOffset? DnsVerifiedAt,
DateTimeOffset? TlsReadyAt,
DateTimeOffset? LastCheckedAt,
string? LastFailureReason);
public sealed record PlatformTenantSubscriptionItem(
Guid Id,
Guid TenantId,
string PlanCode,
TenantSubscriptionStatus Status,
DateTimeOffset? StartsAt,
DateTimeOffset? ExpiresAt,
string? BillingCycle,
int AmountCents,
JsonElement Metadata);
public sealed record TenantBillingProfileItem(
Guid TenantId,
string? BillingName,
string? TaxId,
string? ContactName,
string? ContactPhoneMasked,
string? ContactEmail,
string? BillingAddress,
string? InvoiceTitle,
TenantInvoiceTitleType? InvoiceType,
string? BankName,
string? BankAccountMasked,
JsonElement Metadata);
public sealed record CreatePlatformTenantCommand(
string Slug,
string Name,
string? LegalName,
TenantStatus Status,
BillingStatus BillingStatus,
JsonElement Metadata);
public sealed record UpdatePlatformTenantStatusCommand(
Guid TenantId,
TenantStatus Status,
BillingStatus BillingStatus,
string? Reason);
public sealed record UpsertPlatformTenantBillingProfileCommand(
Guid TenantId,
string? BillingName,
string? TaxId,
string? ContactName,
string? ContactPhone,
string? ContactEmail,
string? BillingAddress,
string? InvoiceTitle,
TenantInvoiceTitleType? InvoiceType,
string? BankName,
string? BankAccountMasked,
JsonElement Metadata);
public sealed record PlatformPlanList(IReadOnlyCollection<PlatformSaasPlan> Items);
public sealed record UpsertPlatformSubscriptionCommand(
Guid TenantId,
string PlanCode,
TenantSubscriptionStatus Status,
DateTimeOffset? StartsAt,
DateTimeOffset? ExpiresAt,
string? BillingCycle,
int AmountCents,
JsonElement Metadata);
public sealed record PlatformDomainList(IReadOnlyCollection<PlatformTenantDomainItem> Items);
public sealed record PlatformDomainRecheckResult(Guid DomainId, TenantDomainStatus Status, DateTimeOffset LastCheckedAt);
public sealed record PlatformStaffList(IReadOnlyCollection<PlatformStaffItem> Items);
public sealed record PlatformStaffItem(
Guid UserId,
string? Name,
string? PhoneMasked,
string? Email,
UserStatus Status,
IReadOnlyCollection<string> RoleCodes,
DateTimeOffset CreatedAt,
DateTimeOffset UpdatedAt);
public sealed record UpsertPlatformStaffCommand(
Guid? UserId,
string? Email,
string? Phone,
string? Name,
UserStatus Status,
IReadOnlyCollection<Guid> RoleIds);
public sealed record UpdatePlatformStaffStatusCommand(Guid UserId, UserStatus Status, string? Reason);
public sealed record PlatformAuditLogList(IReadOnlyCollection<PlatformAuditLogItem> Items);
public sealed record PlatformAuditLogItem(
Guid Id,
Guid? TenantId,
Guid? ActorUserId,
string Action,
string? TargetType,
string? TargetId,
JsonElement Details,
string? IpAddress,
string? UserAgent,
DateTimeOffset CreatedAt);
public sealed record PlatformAuditAlertList(IReadOnlyCollection<PlatformAuditAlert> Items);
public sealed record UpdatePlatformAuditAlertStatusCommand(
Guid AlertId,
PlatformAuditAlertStatus Status,
string? ResolutionNote);
public sealed record PlatformDunningChannelList(IReadOnlyCollection<PlatformDunningChannelItem> Items);
public sealed record PlatformDunningChannelItem(
Guid Id,
string ChannelCode,
string Name,
string? Description,
bool Enabled,
PlatformDunningProvider Provider,
string WebhookUrlMasked,
string? SecretRef,
IReadOnlyCollection<string> ReminderTypes,
IReadOnlyCollection<string> ReminderChannels,
int MinReminderLevel,
IReadOnlyCollection<Guid> TenantIds,
int TimeoutSeconds,
JsonElement Metadata,
DateTimeOffset CreatedAt,
DateTimeOffset UpdatedAt);
public sealed record UpsertPlatformDunningChannelCommand(
Guid? ChannelId,
string ChannelCode,
string Name,
string? Description,
bool Enabled,
PlatformDunningProvider Provider,
string WebhookUrl,
string? SecretRef,
IReadOnlyCollection<string> ReminderTypes,
IReadOnlyCollection<string> ReminderChannels,
int MinReminderLevel,
IReadOnlyCollection<Guid> TenantIds,
int TimeoutSeconds,
JsonElement Metadata);
public sealed record DisablePlatformDunningChannelCommand(Guid ChannelId, string? Reason);
public sealed record PlatformDunningEventList(IReadOnlyCollection<PlatformDunningEventItem> Items);
public sealed record PlatformDunningEventItem(
Guid Id,
Guid TenantId,
Guid ChannelId,
Guid ReminderId,
Guid InvoiceId,
PlatformDunningProvider Provider,
PlatformDunningNotificationStatus Status,
int Attempts,
DateTimeOffset ScheduledAt,
DateTimeOffset? NextAttemptAt,
DateTimeOffset? LastAttemptAt,
DateTimeOffset? SentAt,
string? LastError,
int? LastHttpCode,
string? LastResponseSummary,
JsonElement RequestPayload,
JsonElement Metadata,
DateTimeOffset CreatedAt,
DateTimeOffset UpdatedAt);
public sealed record RetryPlatformDunningEventCommand(Guid EventId, string? Reason);
public interface IPlatformAdminService
{
Task<PlatformOverview> GetOverviewAsync(PlatformAdminActor actor, CancellationToken cancellationToken = default);
Task<PlatformTenantList> GetTenantsAsync(PlatformAdminActor actor, PlatformAdminQuery query, CancellationToken cancellationToken = default);
Task<PlatformTenantDetail> GetTenantDetailAsync(PlatformAdminActor actor, Guid tenantId, CancellationToken cancellationToken = default);
Task<PlatformTenantItem> CreateTenantAsync(PlatformAdminActor actor, CreatePlatformTenantCommand command, CancellationToken cancellationToken = default);
Task<PlatformTenantItem> UpdateTenantStatusAsync(PlatformAdminActor actor, UpdatePlatformTenantStatusCommand command, CancellationToken cancellationToken = default);
Task<TenantBillingProfileItem> UpsertTenantBillingProfileAsync(PlatformAdminActor actor, UpsertPlatformTenantBillingProfileCommand command, CancellationToken cancellationToken = default);
Task<PlatformPlanList> GetPlansAsync(PlatformAdminActor actor, PlatformAdminQuery query, CancellationToken cancellationToken = default);
Task<PlatformTenantSubscriptionItem> UpsertSubscriptionAsync(PlatformAdminActor actor, UpsertPlatformSubscriptionCommand command, CancellationToken cancellationToken = default);
Task<PlatformDomainList> GetDomainsAsync(PlatformAdminActor actor, PlatformAdminQuery query, CancellationToken cancellationToken = default);
Task<PlatformDomainRecheckResult> RecheckDomainAsync(PlatformAdminActor actor, Guid domainId, CancellationToken cancellationToken = default);
Task<PlatformStaffList> GetStaffAsync(PlatformAdminActor actor, PlatformAdminQuery query, CancellationToken cancellationToken = default);
Task<PlatformStaffItem> UpsertStaffAsync(PlatformAdminActor actor, UpsertPlatformStaffCommand command, CancellationToken cancellationToken = default);
Task<PlatformStaffItem> UpdateStaffStatusAsync(PlatformAdminActor actor, UpdatePlatformStaffStatusCommand command, CancellationToken cancellationToken = default);
Task<PlatformAuditLogList> GetAuditLogsAsync(PlatformAdminActor actor, PlatformAdminQuery query, CancellationToken cancellationToken = default);
Task<PlatformAuditAlertList> GetAuditAlertsAsync(PlatformAdminActor actor, PlatformAdminQuery query, CancellationToken cancellationToken = default);
Task<PlatformAuditAlert> UpdateAuditAlertStatusAsync(PlatformAdminActor actor, UpdatePlatformAuditAlertStatusCommand command, CancellationToken cancellationToken = default);
Task<PlatformDunningChannelList> GetDunningChannelsAsync(PlatformAdminActor actor, PlatformAdminQuery query, CancellationToken cancellationToken = default);
Task<PlatformDunningChannelItem> UpsertDunningChannelAsync(PlatformAdminActor actor, UpsertPlatformDunningChannelCommand command, CancellationToken cancellationToken = default);
Task<PlatformDunningChannelItem> DisableDunningChannelAsync(PlatformAdminActor actor, DisablePlatformDunningChannelCommand command, CancellationToken cancellationToken = default);
Task<PlatformDunningEventList> GetDunningEventsAsync(PlatformAdminActor actor, PlatformAdminQuery query, CancellationToken cancellationToken = default);
Task<PlatformDunningEventItem> GetDunningEventDetailAsync(PlatformAdminActor actor, Guid eventId, CancellationToken cancellationToken = default);
Task<PlatformDunningEventItem> RetryDunningEventAsync(PlatformAdminActor actor, RetryPlatformDunningEventCommand command, CancellationToken cancellationToken = default);
}
public sealed class PlatformAdminException(string message, string code) : Exception(message)
{
public string Code { get; } = code;
}

View File

@@ -20,6 +20,7 @@ public static class BackendPermissions
public const string PlatformRoleManage = "platform:role:manage";
public const string PlatformQuestionBankManage = "platform:question-bank:manage";
public const string PlatformAuditView = "platform:audit:view";
public const string PlatformBillingNotification = "platform:billing:notification";
public static readonly IReadOnlySet<string> Tenant = new HashSet<string>(StringComparer.Ordinal)
{
@@ -43,7 +44,8 @@ public static class BackendPermissions
PlatformStaffManage,
PlatformRoleManage,
PlatformQuestionBankManage,
PlatformAuditView
PlatformAuditView,
PlatformBillingNotification
};
public static void EnsureTenant(string permissionCode)

View File

@@ -5,6 +5,10 @@ namespace Tiku.Application.TenantAdmin;
public interface ITenantAdminDirectService
{
Task<TenantAdminOverviewItem> GetOverviewAsync(
TenantAdminActor actor,
CancellationToken cancellationToken = default);
Task<TenantAdminClassList> GetClassesAsync(
TenantAdminActor actor,
TenantAdminClassFilter filter,
@@ -50,6 +54,56 @@ public interface ITenantAdminDirectService
UpdateTenantAdminStudentStatusCommand command,
CancellationToken cancellationToken = default);
Task<TenantAdminStudentImportPreview> PreviewStudentImportAsync(
TenantAdminActor actor,
TenantAdminStudentImportCommand command,
CancellationToken cancellationToken = default);
Task<TenantAdminStudentImportResult> ImportStudentsAsync(
TenantAdminActor actor,
TenantAdminStudentImportCommand command,
CancellationToken cancellationToken = default);
Task<TenantAdminBulkOperationResult> BulkAssignClassAsync(
TenantAdminActor actor,
TenantAdminBulkAssignClassCommand command,
CancellationToken cancellationToken = default);
Task<TenantAdminBulkOperationResult> BulkUpdateStudentStatusAsync(
TenantAdminActor actor,
TenantAdminBulkStatusCommand command,
CancellationToken cancellationToken = default);
Task<CatalogList<TenantSupervisionRuleItem>> GetSupervisionRulesAsync(
TenantAdminActor actor,
CancellationToken cancellationToken = default);
Task<ContentManagementResult<TenantSupervisionRuleItem>> UpsertSupervisionRuleAsync(
TenantAdminActor actor,
UpsertTenantSupervisionRuleCommand command,
CancellationToken cancellationToken = default);
Task<TenantSupervisionPreview> PreviewSupervisionAsync(
TenantAdminActor actor,
CancellationToken cancellationToken = default);
Task<TenantSupervisionGenerateResult> GenerateSupervisionFollowupsAsync(
TenantAdminActor actor,
TenantSupervisionGenerateCommand command,
CancellationToken cancellationToken = default);
Task<TenantFollowupReport> GetFollowupReportAsync(
TenantAdminActor actor,
CancellationToken cancellationToken = default);
Task<TenantFeedbackReport> GetFeedbackReportAsync(
TenantAdminActor actor,
CancellationToken cancellationToken = default);
Task<TenantPointRiskReport> GetPointRiskReportAsync(
TenantAdminActor actor,
CancellationToken cancellationToken = default);
Task<CatalogList<TenantAdminStudentNoteItem>> GetStudentNotesAsync(
TenantAdminActor actor,
TenantAdminStudentActivityFilter filter,

View File

@@ -78,6 +78,20 @@ public sealed record TenantAdminFeedbackFilter(
string? Keyword = null,
int? Limit = null);
public sealed record TenantAdminOverviewItem(
int StudentCount,
int ClassCount,
int StaffCount,
int ActivePracticeCount,
int TodayPracticeCount,
int PendingFollowupCount,
int UnreadNotificationCount,
int PaidOrderCount,
int RevenueCents,
int PendingRefundCount,
int OpenReconciliationIssueCount,
DateTimeOffset GeneratedAt);
public sealed record UpsertTenantAdminClassCommand(
Guid? Id,
Guid? RegionId,
@@ -120,6 +134,109 @@ public sealed record UpdateTenantAdminStudentStatusCommand(
string? Status,
string? Reason);
public sealed record TenantAdminStudentImportRowCommand(
UserLookupCommand User,
Guid? RegionId,
Guid? ClassId,
string? AvatarPreset,
JsonElement RawProfile);
public sealed record TenantAdminStudentImportCommand(
IReadOnlyCollection<TenantAdminStudentImportRowCommand> Rows);
public sealed record TenantAdminStudentImportPreview(
int TotalCount,
int ValidCount,
int InvalidCount,
IReadOnlyCollection<TenantAdminStudentImportPreviewItem> Items);
public sealed record TenantAdminStudentImportPreviewItem(
int RowNo,
bool Valid,
string? Reason,
string? Phone,
string? Email,
string? Name,
Guid? RegionId,
Guid? ClassId);
public sealed record TenantAdminStudentImportResult(
int TotalCount,
int CreatedOrUpdatedCount,
int ClassAssignedCount,
IReadOnlyCollection<TenantAdminStudentImportPreviewItem> InvalidItems);
public sealed record TenantAdminBulkAssignClassCommand(
Guid ClassId,
IReadOnlyCollection<Guid> UserIds);
public sealed record TenantAdminBulkStatusCommand(
IReadOnlyCollection<Guid> UserIds,
string? Status,
string? Reason);
public sealed record TenantAdminBulkOperationResult(
int RequestedCount,
int SucceededCount,
int FailedCount,
IReadOnlyCollection<Guid> FailedUserIds);
public sealed record TenantSupervisionRuleItem(
string Code,
string Title,
bool Enabled,
int? DaysWithoutCheckIn,
int? MaxQuestionsAnsweredToday,
string FollowupType,
string Priority,
JsonElement Metadata);
public sealed record UpsertTenantSupervisionRuleCommand(
string Code,
string Title,
bool Enabled,
int? DaysWithoutCheckIn,
int? MaxQuestionsAnsweredToday,
string? FollowupType,
string? Priority,
JsonElement Metadata);
public sealed record TenantSupervisionPreview(
IReadOnlyCollection<TenantSupervisionRiskStudentItem> Items);
public sealed record TenantSupervisionRiskStudentItem(
Guid UserId,
string? Name,
string? PhoneMasked,
Guid? RegionId,
IReadOnlyCollection<string> RuleCodes,
IReadOnlyCollection<string> Reasons);
public sealed record TenantSupervisionGenerateCommand(
IReadOnlyCollection<Guid>? UserIds,
Guid? AssignedToUserId,
DateTimeOffset? DueAt);
public sealed record TenantSupervisionGenerateResult(int CreatedCount);
public sealed record TenantFollowupReport(
int OpenCount,
int InProgressCount,
int DoneCount,
int OverdueCount);
public sealed record TenantFeedbackReport(
int PendingCount,
int AcceptedCount,
int RejectedCount,
int ResolvedCount,
int ClosedCount);
public sealed record TenantPointRiskReport(
int NegativeScoreUserCount,
int HighPointClaimUserCount,
int CancelledExchangeOrderCount);
public sealed record UpsertTenantAdminStudentNoteCommand(
Guid? Id,
Guid StudentUserId,