feat(platform): add CRM SMS and payment settings

This commit is contained in:
2026-07-29 15:41:27 +08:00
parent 6db200a2fc
commit ead7bdc2e2
25 changed files with 23250 additions and 140 deletions

View File

@@ -8,6 +8,8 @@ using Tiku.Application.Security;
using Tiku.Domain.Identity;
using Tiku.Domain.Operations;
using Tiku.Domain.Platform;
using Tiku.Domain.Growth;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;
namespace Tiku.Infrastructure.Bootstrap;
@@ -31,6 +33,9 @@ public static class DevelopmentPlatformAdminSeeder
if (dbContext.PlatformBackendUserRoles.Any())
{
EnsurePlatformPermissionCatalog(dbContext);
SeedDemoTenantCapabilities(dbContext);
dbContext.SaveChanges();
return false;
}
@@ -44,6 +49,7 @@ public static class DevelopmentPlatformAdminSeeder
.Select(permission => permission.Code)
.ToHashSet(StringComparer.Ordinal);
AddSeedGraph(dbContext, temporaryPassword, permissionCodes, existingPermissionCodes);
SeedDemoTenantCapabilities(dbContext);
dbContext.SaveChanges();
WriteFirstLoginInstructions(temporaryPassword);
return true;
@@ -57,6 +63,9 @@ public static class DevelopmentPlatformAdminSeeder
if (await dbContext.PlatformBackendUserRoles.AnyAsync(cancellationToken))
{
EnsurePlatformPermissionCatalog(dbContext);
SeedDemoTenantCapabilities(dbContext);
await dbContext.SaveChangesAsync(cancellationToken);
return false;
}
@@ -72,6 +81,7 @@ public static class DevelopmentPlatformAdminSeeder
.ToArrayAsync(cancellationToken))
.ToHashSet(StringComparer.Ordinal);
AddSeedGraph(dbContext, temporaryPassword, permissionCodes, existingPermissionCodes);
SeedDemoTenantCapabilities(dbContext);
await dbContext.SaveChangesAsync(cancellationToken);
WriteFirstLoginInstructions(temporaryPassword);
return true;
@@ -166,6 +176,299 @@ public static class DevelopmentPlatformAdminSeeder
});
}
private static void EnsurePlatformPermissionCatalog(TikuDbContext dbContext)
{
var permissionCodes = BackendPermissions.Platform.ToArray();
var moduleCodes = permissionCodes
.Select(PermissionModuleCatalog.ResolvePermissionModuleCode)
.Distinct(StringComparer.Ordinal)
.ToArray();
var existingModuleCodes = dbContext.PermissionModules
.Where(module => moduleCodes.Contains(module.Code))
.Select(module => module.Code)
.ToHashSet(StringComparer.Ordinal);
foreach (var code in moduleCodes.Where(code => !existingModuleCodes.Contains(code)))
{
dbContext.PermissionModules.Add(new PermissionModule
{
Code = code,
Name = code,
Area = BackendPermissionArea.Platform,
RequiredFeatureCode = PermissionModuleCatalog.RequiredFeatures[code]
});
}
var existingPermissionCodes = dbContext.BackendPermissions
.Where(permission => permissionCodes.Contains(permission.Code))
.Select(permission => permission.Code)
.ToHashSet(StringComparer.Ordinal);
foreach (var code in permissionCodes.Where(code => !existingPermissionCodes.Contains(code)))
{
dbContext.BackendPermissions.Add(new BackendPermission
{
Code = code,
Name = code,
Area = BackendPermissionArea.Platform,
PermissionModuleCode = PermissionModuleCatalog.ResolvePermissionModuleCode(code),
Description = "Built-in platform permission.",
IsSystem = true
});
}
var superAdminRoleId = dbContext.PlatformBackendRoles
.Where(role => role.Code == RoleCode)
.Select(role => (Guid?)role.Id)
.FirstOrDefault();
if (superAdminRoleId is null)
{
return;
}
var boundPermissionCodes = dbContext.PlatformBackendRolePermissions
.Where(binding => binding.RoleId == superAdminRoleId.Value)
.Select(binding => binding.PermissionCode)
.ToHashSet(StringComparer.Ordinal);
dbContext.PlatformBackendRolePermissions.AddRange(permissionCodes
.Where(code => !boundPermissionCodes.Contains(code))
.Select(code => new PlatformBackendRolePermission
{
RoleId = superAdminRoleId.Value,
PermissionCode = code
}));
}
private static void SeedDemoTenantCapabilities(TikuDbContext dbContext)
{
var tenantA = EnsureTenant(dbContext, "demo-crm-school", "演示题库机构 A");
var tenantB = EnsureTenant(dbContext, "demo-sms-campus", "演示题库机构 B");
EnsureCrmDemo(dbContext, tenantA);
EnsureSmsDemo(dbContext, tenantA, "aliyun", "登录验证码");
EnsureSmsDemo(dbContext, tenantB, "tencent", "营销通知");
EnsurePlatformPaymentDemo(dbContext, tenantA);
EnsureTenantPaymentDemo(dbContext, tenantA);
}
private static Tenant EnsureTenant(TikuDbContext dbContext, string slug, string name)
{
var tenant = dbContext.Tenants.SingleOrDefault(value => value.Slug == slug);
if (tenant is not null)
{
return tenant;
}
tenant = new Tenant
{
Slug = slug,
Name = name,
LegalName = $"{name}有限公司",
Status = TenantStatus.Active,
BillingStatus = BillingStatus.Active,
Metadata = JsonSerializer.SerializeToElement(new { demo = true })
};
dbContext.Tenants.Add(tenant);
return tenant;
}
private static void EnsureCrmDemo(TikuDbContext dbContext, Tenant tenant)
{
if (!dbContext.CrmConfigs.Any(value => value.TenantId == tenant.Id))
{
dbContext.CrmConfigs.Add(new CrmConfig
{
TenantId = tenant.Id,
Enabled = true,
Url = "https://crm.example.test/webhook/tiku",
SecretRef = "tenant_secret:crm:demo:redacted",
FormName = "学生留资表单",
ExamType = "ielts",
TimeoutSeconds = 10,
DelaySeconds = 0,
AssignmentMode = ReferralAssignmentMode.RoundRobin,
AssignmentPool = JsonSerializer.SerializeToElement(new[] { "顾问一", "顾问二" }),
AssignmentConfig = JsonSerializer.SerializeToElement(new { retry = 3, source = "development_seed" })
});
}
if (!dbContext.CrmWebhookQueue.Any(value => value.TenantId == tenant.Id && value.IdempotencyKey == "demo-crm-failed-lead"))
{
dbContext.CrmWebhookQueue.Add(new CrmWebhookQueueItem
{
TenantId = tenant.Id,
RecordId = "CRM-DEMO-LEAD-001",
LeadId = "LEAD-001",
Source = "tenant.student.crm_push",
Provider = "generic",
Status = CrmWebhookQueueStatus.Failed,
Attempts = 2,
LastError = "demo timeout",
LastHttpCode = 504,
TargetUrl = "https://crm.example.test/webhook/tiku",
IdempotencyKey = "demo-crm-failed-lead",
Payload = JsonSerializer.SerializeToElement(new { student = "王同学", phoneMasked = "138****0001" })
});
}
}
private static void EnsureSmsDemo(TikuDbContext dbContext, Tenant tenant, string provider, string templateName)
{
var scene = templateName.Contains("登录", StringComparison.Ordinal) ? "login" : "marketing";
var channel = dbContext.SmsChannels.SingleOrDefault(value => value.TenantId == tenant.Id && value.Provider == provider && value.Scene == scene);
if (channel is null)
{
channel = new SmsChannel
{
TenantId = tenant.Id,
Provider = provider,
Name = $"{tenant.Name} {provider} 短信渠道",
Signature = "题库演示",
Scene = scene,
Status = TenantExternalProviderStatus.Active,
SecretRef = $"tenant_secret:sms:{provider}:redacted",
MonthlyQuota = 10000,
ConfigPublic = JsonSerializer.SerializeToElement(new { region = "cn", redacted = true }),
Metadata = JsonSerializer.SerializeToElement(new { demo = true })
};
dbContext.SmsChannels.Add(channel);
}
var templateCode = $"demo_{provider}_{scene}";
var template = dbContext.SmsTemplates.SingleOrDefault(value => value.TenantId == tenant.Id && value.Code == templateCode);
if (template is null)
{
template = new SmsTemplate
{
TenantId = tenant.Id,
ChannelId = channel.Id,
Code = templateCode,
Name = templateName,
Type = scene == "login" ? SmsTemplateType.VerificationCode : SmsTemplateType.Marketing,
AuditStatus = SmsTemplateAuditStatus.Approved,
Status = SmsTemplateStatus.Active,
ProviderTemplateCode = $"TPL_{provider.ToUpperInvariant()}_DEMO",
Content = "您的验证码为 ${code},请勿泄露。",
ApprovedAt = DateTimeOffset.UtcNow,
Metadata = JsonSerializer.SerializeToElement(new { demo = true })
};
dbContext.SmsTemplates.Add(template);
}
if (!dbContext.SmsSendLogs.Any(value => value.TenantId == tenant.Id && value.ProviderMessageId == $"demo-{provider}-{scene}-001"))
{
dbContext.SmsSendLogs.Add(new SmsSendLog
{
TenantId = tenant.Id,
ChannelId = channel.Id,
TemplateId = template.Id,
Provider = provider,
Scene = scene,
PhoneMasked = "138****0001",
Status = SmsSendLogStatus.Sent,
ProviderMessageId = $"demo-{provider}-{scene}-001",
SentAt = DateTimeOffset.UtcNow,
Metadata = JsonSerializer.SerializeToElement(new { demo = true })
});
}
}
private static void EnsurePlatformPaymentDemo(TikuDbContext dbContext, Tenant tenant)
{
var app = dbContext.PlatformPaymentApps.SingleOrDefault(value => value.AppCode == "platform_saas_collect");
if (app is null)
{
app = new PlatformPaymentApp
{
AppCode = "platform_saas_collect",
AppName = "平台 SaaS 收款",
Status = PlatformPaymentAppStatus.Active,
SettlementMode = "PlatformCollect",
Description = "Development demo platform payment app.",
Metadata = JsonSerializer.SerializeToElement(new { demo = true })
};
dbContext.PlatformPaymentApps.Add(app);
}
if (!dbContext.PlatformPaymentChannels.Any(value => value.AppId == app.Id && value.Provider == "manual"))
{
dbContext.PlatformPaymentChannels.Add(new PlatformPaymentChannel
{
AppId = app.Id,
Provider = "manual",
Mode = "PlatformCollect",
Status = PlatformPaymentChannelStatus.Active,
DisplayName = "线下人工确认",
SecretRef = "platform_payment:manual:redacted",
CallbackPath = "/api/platform-billing/payments/notify/manual",
ConfigPublic = JsonSerializer.SerializeToElement(new { manual = true })
});
}
if (!dbContext.PlatformBillingPayments.Any(value => value.PaymentNo == "PB-DEMO-MANUAL-001"))
{
var quote = new PlatformBillingQuote
{
TenantId = tenant.Id,
QuoteNo = "BQ-DEMO-001",
IdempotencyKey = "demo-platform-payment-quote",
Status = PlatformBillingQuoteStatus.Converted,
Purpose = PlatformBillingOrderPurpose.NewSubscription,
TotalAmountCents = 19900,
ExpiresAt = DateTimeOffset.UtcNow.AddDays(7)
};
var order = new PlatformBillingOrder
{
TenantId = tenant.Id,
QuoteId = quote.Id,
OrderNo = "BO-DEMO-001",
IdempotencyKey = "demo-platform-payment-order",
Purpose = PlatformBillingOrderPurpose.NewSubscription,
Status = PlatformBillingOrderStatus.PendingPayment,
TotalAmountCents = 19900,
ExpiresAt = DateTimeOffset.UtcNow.AddDays(7)
};
var payment = new PlatformBillingPayment
{
TenantId = tenant.Id,
OrderId = order.Id,
PaymentNo = "PB-DEMO-MANUAL-001",
IdempotencyKey = "demo-platform-payment-payment",
Provider = "manual",
Method = "bank_transfer",
Status = PlatformBillingPaymentStatus.Pending,
AmountCents = 19900
};
dbContext.PlatformBillingQuotes.Add(quote);
dbContext.PlatformBillingOrders.Add(order);
dbContext.PlatformBillingPayments.Add(payment);
dbContext.PlatformBillingPaymentEvents.Add(new PlatformBillingPaymentEvent
{
TenantId = tenant.Id,
PaymentId = payment.Id,
Provider = "manual",
ProviderEventId = "demo-platform-payment-created",
EventType = "payment_created",
Payload = JsonSerializer.SerializeToElement(new { demo = true, amountCents = 19900 })
});
}
}
private static void EnsureTenantPaymentDemo(TikuDbContext dbContext, Tenant tenant)
{
if (!dbContext.TenantExternalProviders.Any(value => value.TenantId == tenant.Id && value.Capability == TenantExternalProviderCapability.Payment && value.Provider == "manual"))
{
dbContext.TenantExternalProviders.Add(new TenantExternalProvider
{
TenantId = tenant.Id,
Capability = TenantExternalProviderCapability.Payment,
Provider = "manual",
Status = TenantExternalProviderStatus.Active,
DisplayName = "租户线下收款",
SecretRef = "tenant_payment:manual:redacted",
ConfigPublic = JsonSerializer.SerializeToElement(new { manual = true }),
Metadata = JsonSerializer.SerializeToElement(new { demo = true })
});
}
}
private static string GenerateTemporaryPassword() =>
$"Tiku!{Convert.ToHexString(RandomNumberGenerator.GetBytes(16))}9a";