feat: add phase five operations foundation
This commit is contained in:
169
Tiku.Infrastructure/Auth/AliyunSmsProvider.cs
Normal file
169
Tiku.Infrastructure/Auth/AliyunSmsProvider.cs
Normal file
@@ -0,0 +1,169 @@
|
||||
using System.Text.Json;
|
||||
using AlibabaCloud.OpenApiClient.Models;
|
||||
using AlibabaCloud.SDK.Dysmsapi20170525.Models;
|
||||
using Tiku.Application.Auth;
|
||||
using Tiku.Application.Tenancy;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using DysmsClient = AlibabaCloud.SDK.Dysmsapi20170525.Client;
|
||||
|
||||
namespace Tiku.Infrastructure.Auth;
|
||||
|
||||
internal sealed class AliyunSmsProvider(
|
||||
ITenantExternalProviderConfigService providerConfigService) : ISmsProvider
|
||||
{
|
||||
private const string ProviderCode = "aliyun_sms";
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public async Task<SmsProviderSendResult> SendAsync(
|
||||
SmsProviderSendRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
TenantExternalProviderAccount account;
|
||||
try
|
||||
{
|
||||
account = await providerConfigService.GetActiveProviderAsync(
|
||||
request.TenantId,
|
||||
TenantExternalProviderCapability.Sms,
|
||||
null,
|
||||
cancellationToken);
|
||||
}
|
||||
catch (TenantExternalProviderException)
|
||||
{
|
||||
return new SmsProviderSendResult("noop", "accepted");
|
||||
}
|
||||
|
||||
if (string.Equals(account.Provider, "noop", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(account.Provider, "local_dev", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new SmsProviderSendResult(account.Provider, "accepted");
|
||||
}
|
||||
|
||||
if (!string.Equals(account.Provider, ProviderCode, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new SmsProviderException(
|
||||
$"SMS provider '{account.Provider}' is not supported.",
|
||||
"sms_provider_unsupported");
|
||||
}
|
||||
|
||||
var signName = Required(account.ConfigPublic, "signName", "smsSignName");
|
||||
var templateCode = ResolveTemplateCode(account.ConfigPublic, request.Purpose);
|
||||
var accessKeyId = Required(account.SecretPayload, "accessKeyId");
|
||||
var accessKeySecret = Required(account.SecretPayload, "accessKeySecret");
|
||||
var endpoint = Optional(account.ConfigPublic, "endpoint") ?? "dysmsapi.aliyuncs.com";
|
||||
var regionId = Optional(account.ConfigPublic, "regionId", "region") ?? "cn-hangzhou";
|
||||
|
||||
var client = new DysmsClient(new Config
|
||||
{
|
||||
AccessKeyId = accessKeyId,
|
||||
AccessKeySecret = accessKeySecret,
|
||||
Endpoint = endpoint,
|
||||
RegionId = regionId
|
||||
});
|
||||
|
||||
var sendRequest = new SendSmsRequest
|
||||
{
|
||||
PhoneNumbers = request.Phone,
|
||||
SignName = signName,
|
||||
TemplateCode = templateCode,
|
||||
TemplateParam = BuildTemplateParam(account.ConfigPublic, request),
|
||||
OutId = request.TenantId.ToString("N")
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await client.SendSmsAsync(sendRequest).WaitAsync(cancellationToken);
|
||||
var body = response.Body;
|
||||
if (body is null)
|
||||
{
|
||||
throw new SmsProviderException(
|
||||
"Aliyun SMS returned an empty response.",
|
||||
"aliyun_sms_empty_response");
|
||||
}
|
||||
|
||||
if (!string.Equals(body.Code, "OK", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new SmsProviderException(
|
||||
$"Aliyun SMS send failed: {body.Code}.",
|
||||
"aliyun_sms_send_rejected");
|
||||
}
|
||||
|
||||
return new SmsProviderSendResult(
|
||||
ProviderCode,
|
||||
body.Code ?? "OK",
|
||||
body.BizId ?? body.RequestId);
|
||||
}
|
||||
catch (SmsProviderException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception exception) when (exception is not OperationCanceledException)
|
||||
{
|
||||
throw new SmsProviderException(
|
||||
"Aliyun SMS send failed.",
|
||||
"aliyun_sms_send_failed",
|
||||
exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveTemplateCode(JsonElement config, SmsPurpose purpose)
|
||||
{
|
||||
if (config.ValueKind == JsonValueKind.Object &&
|
||||
config.TryGetProperty("templateCodes", out var templateCodes) &&
|
||||
templateCodes.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
var purposeKey = purpose.ToString().ToLowerInvariant();
|
||||
if (templateCodes.TryGetProperty(purposeKey, out var purposeTemplate) &&
|
||||
purposeTemplate.ValueKind == JsonValueKind.String &&
|
||||
!string.IsNullOrWhiteSpace(purposeTemplate.GetString()))
|
||||
{
|
||||
return purposeTemplate.GetString()!;
|
||||
}
|
||||
}
|
||||
|
||||
return Required(config, "templateCode", "smsTemplateCode");
|
||||
}
|
||||
|
||||
private static string BuildTemplateParam(JsonElement config, SmsProviderSendRequest request)
|
||||
{
|
||||
var codeField = Optional(config, "codeField") ?? "code";
|
||||
return JsonSerializer.Serialize(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
[codeField] = request.Code
|
||||
},
|
||||
JsonOptions);
|
||||
}
|
||||
|
||||
private static string Required(JsonElement element, params string[] keys)
|
||||
{
|
||||
var value = Optional(element, keys);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
throw new SmsProviderException(
|
||||
"Aliyun SMS provider configuration is incomplete.",
|
||||
"aliyun_sms_config_incomplete");
|
||||
}
|
||||
|
||||
private static string? Optional(JsonElement element, params string[] keys)
|
||||
{
|
||||
if (element.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (element.TryGetProperty(key, out var property) &&
|
||||
property.ValueKind == JsonValueKind.String &&
|
||||
!string.IsNullOrWhiteSpace(property.GetString()))
|
||||
{
|
||||
return property.GetString();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tiku.Domain.Common;
|
||||
using Tiku.Application.Auth;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
@@ -44,21 +45,48 @@ public sealed class SmsVerificationService(
|
||||
rateLimit.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
var code = Random.Shared.Next(100000, 999999).ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
var sendResult = await smsProvider.SendAsync(
|
||||
new SmsProviderSendRequest(
|
||||
request.TenantId,
|
||||
phone,
|
||||
request.Purpose,
|
||||
code,
|
||||
request.IpAddress,
|
||||
request.UserAgent),
|
||||
cancellationToken);
|
||||
var codeHash = SmsCodeHashing.Hash(request.TenantId, phone, request.Purpose, code);
|
||||
SmsProviderSendResult sendResult;
|
||||
try
|
||||
{
|
||||
sendResult = await smsProvider.SendAsync(
|
||||
new SmsProviderSendRequest(
|
||||
request.TenantId,
|
||||
phone,
|
||||
request.Purpose,
|
||||
code,
|
||||
request.IpAddress,
|
||||
request.UserAgent),
|
||||
cancellationToken);
|
||||
}
|
||||
catch (Exception exception) when (exception is not OperationCanceledException)
|
||||
{
|
||||
dbContext.SmsVerificationCodes.Add(new SmsVerificationCode
|
||||
{
|
||||
TenantId = request.TenantId,
|
||||
Phone = phone,
|
||||
Purpose = request.Purpose,
|
||||
CodeHash = codeHash,
|
||||
Provider = "failed",
|
||||
Status = SmsVerificationStatus.Failed,
|
||||
ExpiresAt = DateTimeOffset.UtcNow,
|
||||
IpAddress = request.IpAddress,
|
||||
UserAgent = request.UserAgent,
|
||||
Metadata = JsonDefaults.Object()
|
||||
});
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
throw exception is SmsProviderException
|
||||
? exception
|
||||
: new SmsProviderException("SMS provider failed to send the verification code.", "sms_provider_send_failed", exception);
|
||||
}
|
||||
|
||||
var verification = new SmsVerificationCode
|
||||
{
|
||||
TenantId = request.TenantId,
|
||||
Phone = phone,
|
||||
Purpose = request.Purpose,
|
||||
CodeHash = SmsCodeHashing.Hash(request.TenantId, phone, request.Purpose, code),
|
||||
CodeHash = codeHash,
|
||||
Provider = sendResult.Provider,
|
||||
Status = SmsVerificationStatus.Sent,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.Add(CodeLifetime),
|
||||
@@ -93,7 +121,7 @@ public sealed class SmsVerificationService(
|
||||
|
||||
if (verification is null ||
|
||||
verification.ExpiresAt <= now ||
|
||||
verification.Status is SmsVerificationStatus.Expired or SmsVerificationStatus.Blocked)
|
||||
verification.Status != SmsVerificationStatus.Sent)
|
||||
{
|
||||
throw new InvalidCredentialsException("invalid_sms_code");
|
||||
}
|
||||
|
||||
464
Tiku.Infrastructure/Backoffice/BackofficeService.cs
Normal file
464
Tiku.Infrastructure/Backoffice/BackofficeService.cs
Normal file
@@ -0,0 +1,464 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tiku.Application.Backoffice;
|
||||
using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Operations;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
|
||||
namespace Tiku.Infrastructure.Backoffice;
|
||||
|
||||
internal sealed class BackofficeService(
|
||||
TikuDbContext dbContext,
|
||||
IOperationAuditService auditService) : IBackofficeService
|
||||
{
|
||||
private static readonly BuiltinPermission[] BuiltinPermissions =
|
||||
[
|
||||
new("tenant:dashboard:view", "租户总览", BackendPermissionArea.Tenant, "tenant_dashboard"),
|
||||
new("tenant:staff:manage", "租户员工管理", BackendPermissionArea.Tenant, "tenant_staff"),
|
||||
new("tenant:role:manage", "租户角色权限管理", BackendPermissionArea.Tenant, "tenant_staff"),
|
||||
new("tenant:student:manage", "学生与班级管理", BackendPermissionArea.Tenant, "tenant_student"),
|
||||
new("tenant:content:manage", "租户内容管理", BackendPermissionArea.Tenant, "tenant_content"),
|
||||
new("tenant:provider:manage", "租户外部服务配置", BackendPermissionArea.Tenant, "tenant_provider"),
|
||||
new("tenant:commerce:operate", "租户交易运营", BackendPermissionArea.Tenant, "tenant_commerce"),
|
||||
new("platform:dashboard:view", "平台总览", BackendPermissionArea.Platform, "platform_dashboard"),
|
||||
new("platform:tenant:manage", "平台租户管理", BackendPermissionArea.Platform, "platform_tenant"),
|
||||
new("platform:staff:manage", "平台员工管理", BackendPermissionArea.Platform, "platform_staff"),
|
||||
new("platform:role:manage", "平台角色权限管理", BackendPermissionArea.Platform, "platform_staff"),
|
||||
new("platform:question-bank:manage", "平台公共题库运营", BackendPermissionArea.Platform, "platform_content"),
|
||||
new("platform:audit:view", "平台审计查询", BackendPermissionArea.Platform, "platform_audit"),
|
||||
new("commerce:refund:approve", "退款审核", BackendPermissionArea.Both, "commerce"),
|
||||
new("commerce:reconciliation:manage", "对账管理", BackendPermissionArea.Both, "commerce"),
|
||||
new("commerce:adjustment:manage", "调账管理", BackendPermissionArea.Both, "commerce")
|
||||
];
|
||||
|
||||
private static readonly BuiltinMenu[] BuiltinMenus =
|
||||
[
|
||||
new("tenant.dashboard", null, "租户总览", BackendPermissionArea.Tenant, "/tenant/dashboard", "tenant:dashboard:view", 10),
|
||||
new("tenant.staff", null, "员工与权限", BackendPermissionArea.Tenant, "/tenant/staff", "tenant:staff:manage", 20),
|
||||
new("tenant.students", null, "班级与学生", BackendPermissionArea.Tenant, "/tenant/students", "tenant:student:manage", 30),
|
||||
new("tenant.content", null, "内容管理", BackendPermissionArea.Tenant, "/tenant/content", "tenant:content:manage", 40),
|
||||
new("tenant.providers", null, "外部服务", BackendPermissionArea.Tenant, "/tenant/providers", "tenant:provider:manage", 50),
|
||||
new("tenant.commerce", null, "交易运营", BackendPermissionArea.Tenant, "/tenant/commerce", "tenant:commerce:operate", 60),
|
||||
new("platform.dashboard", null, "平台总览", BackendPermissionArea.Platform, "/platform/dashboard", "platform:dashboard:view", 10),
|
||||
new("platform.tenants", null, "租户管理", BackendPermissionArea.Platform, "/platform/tenants", "platform:tenant:manage", 20),
|
||||
new("platform.staff", null, "平台员工", BackendPermissionArea.Platform, "/platform/staff", "platform:staff:manage", 30),
|
||||
new("platform.content", null, "公共题库", BackendPermissionArea.Platform, "/platform/question-banks", "platform:question-bank:manage", 40),
|
||||
new("platform.audit", null, "平台审计", BackendPermissionArea.Platform, "/platform/audit", "platform:audit:view", 50)
|
||||
];
|
||||
|
||||
public async Task<BackofficeBootstrap> GetTenantBootstrapAsync(
|
||||
BackofficeActor actor,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tenantId = RequireTenantAdmin(actor);
|
||||
await EnsureCatalogAsync(cancellationToken);
|
||||
var roles = await LoadTenantRolesAsync(tenantId, cancellationToken);
|
||||
var permissions = await dbContext.BackendPermissions.AsNoTracking()
|
||||
.Where(item => item.Area == BackendPermissionArea.Tenant || item.Area == BackendPermissionArea.Both)
|
||||
.OrderBy(item => item.Module).ThenBy(item => item.SortOrder).ThenBy(item => item.Code)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
var menus = await dbContext.BackendMenus.AsNoTracking()
|
||||
.Where(item => item.IsActive && item.Area == BackendPermissionArea.Tenant)
|
||||
.OrderBy(item => item.SortOrder).ThenBy(item => item.Code)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
return new BackofficeBootstrap(
|
||||
permissions.Select(ToPermissionItem).ToArray(),
|
||||
menus.Select(ToMenuItem).ToArray(),
|
||||
roles);
|
||||
}
|
||||
|
||||
public async Task<BackofficeBootstrap> GetPlatformBootstrapAsync(
|
||||
BackofficeActor actor,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
RequirePlatformAdmin(actor);
|
||||
await EnsureCatalogAsync(cancellationToken);
|
||||
var roles = await LoadPlatformRolesAsync(cancellationToken);
|
||||
var permissions = await dbContext.BackendPermissions.AsNoTracking()
|
||||
.Where(item => item.Area == BackendPermissionArea.Platform || item.Area == BackendPermissionArea.Both)
|
||||
.OrderBy(item => item.Module).ThenBy(item => item.SortOrder).ThenBy(item => item.Code)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
var menus = await dbContext.BackendMenus.AsNoTracking()
|
||||
.Where(item => item.IsActive && item.Area == BackendPermissionArea.Platform)
|
||||
.OrderBy(item => item.SortOrder).ThenBy(item => item.Code)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
return new BackofficeBootstrap(
|
||||
permissions.Select(ToPermissionItem).ToArray(),
|
||||
menus.Select(ToMenuItem).ToArray(),
|
||||
roles);
|
||||
}
|
||||
|
||||
public async Task<BackofficeRoleItem> UpsertTenantRoleAsync(
|
||||
BackofficeActor actor,
|
||||
UpsertBackofficeRoleCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tenantId = RequireTenantAdmin(actor);
|
||||
var role = command.Id.HasValue
|
||||
? await dbContext.TenantBackendRoles.SingleOrDefaultAsync(
|
||||
item => item.TenantId == tenantId && item.Id == command.Id.Value,
|
||||
cancellationToken)
|
||||
: await dbContext.TenantBackendRoles.SingleOrDefaultAsync(
|
||||
item => item.TenantId == tenantId && item.Code == NormalizeCode(command.Code),
|
||||
cancellationToken);
|
||||
if (role is null)
|
||||
{
|
||||
role = new TenantBackendRole { TenantId = tenantId, Code = NormalizeCode(command.Code) };
|
||||
dbContext.TenantBackendRoles.Add(role);
|
||||
}
|
||||
else if (role.IsSystem)
|
||||
{
|
||||
throw new BackofficeException("System tenant role cannot be modified.", "system_role_locked");
|
||||
}
|
||||
|
||||
role.Name = command.Name.Trim();
|
||||
role.Status = command.Status;
|
||||
role.Description = command.Description?.Trim();
|
||||
role.DataScope = command.DataScope ?? JsonDefaults.Object();
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
await AuditAsync(actor, "tenant.role.upserted", "tenant_backend_roles", role.Id, new { role.Code }, cancellationToken);
|
||||
return await LoadTenantRoleAsync(tenantId, role.Id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<BackofficeRoleItem> UpsertPlatformRoleAsync(
|
||||
BackofficeActor actor,
|
||||
UpsertBackofficeRoleCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
RequirePlatformAdmin(actor);
|
||||
var role = command.Id.HasValue
|
||||
? await dbContext.PlatformBackendRoles.SingleOrDefaultAsync(item => item.Id == command.Id.Value, cancellationToken)
|
||||
: await dbContext.PlatformBackendRoles.SingleOrDefaultAsync(item => item.Code == NormalizeCode(command.Code), cancellationToken);
|
||||
if (role is null)
|
||||
{
|
||||
role = new PlatformBackendRole { Code = NormalizeCode(command.Code) };
|
||||
dbContext.PlatformBackendRoles.Add(role);
|
||||
}
|
||||
else if (role.IsSystem)
|
||||
{
|
||||
throw new BackofficeException("System platform role cannot be modified.", "system_role_locked");
|
||||
}
|
||||
|
||||
role.Name = command.Name.Trim();
|
||||
role.Status = command.Status;
|
||||
role.Description = command.Description?.Trim();
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
await AuditAsync(actor, "platform.role.upserted", "platform_backend_roles", role.Id, new { role.Code }, cancellationToken);
|
||||
return await LoadPlatformRoleAsync(role.Id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<BackofficeRoleItem> ReplaceTenantRoleBindingsAsync(
|
||||
BackofficeActor actor,
|
||||
ReplaceRoleBindingsCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tenantId = RequireTenantAdmin(actor);
|
||||
var role = await dbContext.TenantBackendRoles.SingleOrDefaultAsync(
|
||||
item => item.TenantId == tenantId && item.Id == command.RoleId,
|
||||
cancellationToken) ?? throw new BackofficeException("Tenant role was not found.", "role_not_found");
|
||||
if (role.IsSystem)
|
||||
{
|
||||
throw new BackofficeException("System tenant role bindings cannot be modified.", "system_role_locked");
|
||||
}
|
||||
|
||||
await ReplaceTenantBindingsCoreAsync(tenantId, role.Id, command.PermissionCodes, command.MenuCodes, cancellationToken);
|
||||
await AuditAsync(actor, "tenant.role.bindings_replaced", "tenant_backend_roles", role.Id, new { role.Code }, cancellationToken);
|
||||
return await LoadTenantRoleAsync(tenantId, role.Id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<BackofficeRoleItem> ReplacePlatformRoleBindingsAsync(
|
||||
BackofficeActor actor,
|
||||
ReplaceRoleBindingsCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
RequirePlatformAdmin(actor);
|
||||
var role = await dbContext.PlatformBackendRoles.SingleOrDefaultAsync(
|
||||
item => item.Id == command.RoleId,
|
||||
cancellationToken) ?? throw new BackofficeException("Platform role was not found.", "role_not_found");
|
||||
if (role.IsSystem)
|
||||
{
|
||||
throw new BackofficeException("System platform role bindings cannot be modified.", "system_role_locked");
|
||||
}
|
||||
|
||||
await ReplacePlatformBindingsCoreAsync(role.Id, command.PermissionCodes, command.MenuCodes, cancellationToken);
|
||||
await AuditAsync(actor, "platform.role.bindings_replaced", "platform_backend_roles", role.Id, new { role.Code }, cancellationToken);
|
||||
return await LoadPlatformRoleAsync(role.Id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task ReplaceTenantUserRolesAsync(
|
||||
BackofficeActor actor,
|
||||
ReplaceUserRolesCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tenantId = RequireTenantAdmin(actor);
|
||||
var roleIds = command.RoleIds.Distinct().ToArray();
|
||||
var count = await dbContext.TenantBackendRoles.CountAsync(
|
||||
item => item.TenantId == tenantId && roleIds.Contains(item.Id) && item.Status == BackendRoleStatus.Active,
|
||||
cancellationToken);
|
||||
if (count != roleIds.Length)
|
||||
{
|
||||
throw new BackofficeException("One or more tenant roles were not found.", "role_not_found");
|
||||
}
|
||||
|
||||
await dbContext.TenantBackendUserRoles
|
||||
.Where(item => item.TenantId == tenantId && item.UserId == command.UserId)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
dbContext.TenantBackendUserRoles.AddRange(roleIds.Select(roleId => new TenantBackendUserRole
|
||||
{
|
||||
TenantId = tenantId,
|
||||
UserId = command.UserId,
|
||||
RoleId = roleId
|
||||
}));
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
await AuditAsync(actor, "tenant.user_roles.replaced", "users", command.UserId, new { roleIds }, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task ReplacePlatformUserRolesAsync(
|
||||
BackofficeActor actor,
|
||||
ReplaceUserRolesCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
RequirePlatformAdmin(actor);
|
||||
var roleIds = command.RoleIds.Distinct().ToArray();
|
||||
var count = await dbContext.PlatformBackendRoles.CountAsync(
|
||||
item => roleIds.Contains(item.Id) && item.Status == BackendRoleStatus.Active,
|
||||
cancellationToken);
|
||||
if (count != roleIds.Length)
|
||||
{
|
||||
throw new BackofficeException("One or more platform roles were not found.", "role_not_found");
|
||||
}
|
||||
|
||||
await dbContext.PlatformBackendUserRoles
|
||||
.Where(item => item.UserId == command.UserId)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
dbContext.PlatformBackendUserRoles.AddRange(roleIds.Select(roleId => new PlatformBackendUserRole
|
||||
{
|
||||
UserId = command.UserId,
|
||||
RoleId = roleId
|
||||
}));
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
await AuditAsync(actor, "platform.user_roles.replaced", "users", command.UserId, new { roleIds }, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task EnsureCatalogAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var permission in BuiltinPermissions)
|
||||
{
|
||||
if (await dbContext.BackendPermissions.AnyAsync(item => item.Code == permission.Code, cancellationToken))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
dbContext.BackendPermissions.Add(new BackendPermission
|
||||
{
|
||||
Code = permission.Code,
|
||||
Name = permission.Name,
|
||||
Area = permission.Area,
|
||||
Module = permission.Module,
|
||||
IsSystem = true
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var menu in BuiltinMenus)
|
||||
{
|
||||
if (await dbContext.BackendMenus.AnyAsync(item => item.Code == menu.Code, cancellationToken))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
dbContext.BackendMenus.Add(new BackendMenu
|
||||
{
|
||||
Code = menu.Code,
|
||||
ParentCode = menu.ParentCode,
|
||||
Title = menu.Title,
|
||||
Area = menu.Area,
|
||||
Path = menu.Path,
|
||||
PermissionCode = menu.PermissionCode,
|
||||
SortOrder = menu.SortOrder,
|
||||
IsActive = true
|
||||
});
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private async Task ReplaceTenantBindingsCoreAsync(
|
||||
Guid tenantId,
|
||||
Guid roleId,
|
||||
IReadOnlyCollection<string> permissionCodes,
|
||||
IReadOnlyCollection<string> menuCodes,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var normalizedPermissions = NormalizeCodes(permissionCodes);
|
||||
var normalizedMenus = NormalizeCodes(menuCodes);
|
||||
await ValidatePermissionCodesAsync(normalizedPermissions, BackendPermissionArea.Tenant, cancellationToken);
|
||||
await ValidateMenuCodesAsync(normalizedMenus, BackendPermissionArea.Tenant, cancellationToken);
|
||||
await dbContext.TenantBackendRolePermissions.Where(item => item.TenantId == tenantId && item.RoleId == roleId).ExecuteDeleteAsync(cancellationToken);
|
||||
await dbContext.TenantBackendRoleMenus.Where(item => item.TenantId == tenantId && item.RoleId == roleId).ExecuteDeleteAsync(cancellationToken);
|
||||
dbContext.TenantBackendRolePermissions.AddRange(normalizedPermissions.Select(code => new TenantBackendRolePermission { TenantId = tenantId, RoleId = roleId, PermissionCode = code }));
|
||||
dbContext.TenantBackendRoleMenus.AddRange(normalizedMenus.Select(code => new TenantBackendRoleMenu { TenantId = tenantId, RoleId = roleId, MenuCode = code }));
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private async Task ReplacePlatformBindingsCoreAsync(
|
||||
Guid roleId,
|
||||
IReadOnlyCollection<string> permissionCodes,
|
||||
IReadOnlyCollection<string> menuCodes,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var normalizedPermissions = NormalizeCodes(permissionCodes);
|
||||
var normalizedMenus = NormalizeCodes(menuCodes);
|
||||
await ValidatePermissionCodesAsync(normalizedPermissions, BackendPermissionArea.Platform, cancellationToken);
|
||||
await ValidateMenuCodesAsync(normalizedMenus, BackendPermissionArea.Platform, cancellationToken);
|
||||
await dbContext.PlatformBackendRolePermissions.Where(item => item.RoleId == roleId).ExecuteDeleteAsync(cancellationToken);
|
||||
await dbContext.PlatformBackendRoleMenus.Where(item => item.RoleId == roleId).ExecuteDeleteAsync(cancellationToken);
|
||||
dbContext.PlatformBackendRolePermissions.AddRange(normalizedPermissions.Select(code => new PlatformBackendRolePermission { RoleId = roleId, PermissionCode = code }));
|
||||
dbContext.PlatformBackendRoleMenus.AddRange(normalizedMenus.Select(code => new PlatformBackendRoleMenu { RoleId = roleId, MenuCode = code }));
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private async Task ValidatePermissionCodesAsync(string[] codes, BackendPermissionArea area, CancellationToken cancellationToken)
|
||||
{
|
||||
var count = await dbContext.BackendPermissions.CountAsync(
|
||||
item => codes.Contains(item.Code) && (item.Area == area || item.Area == BackendPermissionArea.Both),
|
||||
cancellationToken);
|
||||
if (count != codes.Length)
|
||||
{
|
||||
throw new BackofficeException("One or more permissions were not found.", "permission_not_found");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ValidateMenuCodesAsync(string[] codes, BackendPermissionArea area, CancellationToken cancellationToken)
|
||||
{
|
||||
var count = await dbContext.BackendMenus.CountAsync(
|
||||
item => codes.Contains(item.Code) && item.Area == area && item.IsActive,
|
||||
cancellationToken);
|
||||
if (count != codes.Length)
|
||||
{
|
||||
throw new BackofficeException("One or more menus were not found.", "menu_not_found");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<BackofficeRoleItem[]> LoadTenantRolesAsync(Guid tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
var roles = await dbContext.TenantBackendRoles.AsNoTracking()
|
||||
.Where(item => item.TenantId == tenantId)
|
||||
.OrderBy(item => item.Code)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
var roleIds = roles.Select(item => item.Id).ToArray();
|
||||
var permissions = await dbContext.TenantBackendRolePermissions.AsNoTracking()
|
||||
.Where(item => item.TenantId == tenantId && roleIds.Contains(item.RoleId))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
var menus = await dbContext.TenantBackendRoleMenus.AsNoTracking()
|
||||
.Where(item => item.TenantId == tenantId && roleIds.Contains(item.RoleId))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
return roles.Select(role => ToTenantRoleItem(role, permissions, menus)).ToArray();
|
||||
}
|
||||
|
||||
private async Task<BackofficeRoleItem[]> LoadPlatformRolesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var roles = await dbContext.PlatformBackendRoles.AsNoTracking()
|
||||
.OrderBy(item => item.Code)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
var roleIds = roles.Select(item => item.Id).ToArray();
|
||||
var permissions = await dbContext.PlatformBackendRolePermissions.AsNoTracking()
|
||||
.Where(item => roleIds.Contains(item.RoleId))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
var menus = await dbContext.PlatformBackendRoleMenus.AsNoTracking()
|
||||
.Where(item => roleIds.Contains(item.RoleId))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
return roles.Select(role => ToPlatformRoleItem(role, permissions, menus)).ToArray();
|
||||
}
|
||||
|
||||
private async Task<BackofficeRoleItem> LoadTenantRoleAsync(Guid tenantId, Guid roleId, CancellationToken cancellationToken)
|
||||
{
|
||||
return (await LoadTenantRolesAsync(tenantId, cancellationToken)).Single(item => item.Id == roleId);
|
||||
}
|
||||
|
||||
private async Task<BackofficeRoleItem> LoadPlatformRoleAsync(Guid roleId, CancellationToken cancellationToken)
|
||||
{
|
||||
return (await LoadPlatformRolesAsync(cancellationToken)).Single(item => item.Id == roleId);
|
||||
}
|
||||
|
||||
private async Task AuditAsync(BackofficeActor actor, string action, string targetType, Guid targetId, object details, CancellationToken cancellationToken)
|
||||
{
|
||||
await auditService.WriteAsync(new BackofficeOperationAuditCommand(
|
||||
actor.TenantId,
|
||||
actor.UserId,
|
||||
action,
|
||||
targetType,
|
||||
targetId.ToString(),
|
||||
JsonSerializer.SerializeToElement(details)), cancellationToken);
|
||||
}
|
||||
|
||||
private static Guid RequireTenantAdmin(BackofficeActor actor)
|
||||
{
|
||||
if (actor.TenantId is not { } tenantId)
|
||||
{
|
||||
throw new BackofficeException("Tenant backoffice requires a resolved tenant.", "tenant_required");
|
||||
}
|
||||
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
private static void RequirePlatformAdmin(BackofficeActor actor)
|
||||
{
|
||||
if (!actor.IsPlatform)
|
||||
{
|
||||
throw new BackofficeException("Platform backoffice access is denied.", "platform_access_denied");
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeCode(string code) => code.Trim().ToLowerInvariant();
|
||||
|
||||
private static string[] NormalizeCodes(IEnumerable<string> codes)
|
||||
{
|
||||
return codes.Select(NormalizeCode).Where(item => item.Length > 0).Distinct(StringComparer.Ordinal).ToArray();
|
||||
}
|
||||
|
||||
private static BackofficePermissionItem ToPermissionItem(BackendPermission item)
|
||||
{
|
||||
return new BackofficePermissionItem(item.Id, item.Code, item.Name, item.Area, item.Module, item.Description, item.SortOrder);
|
||||
}
|
||||
|
||||
private static BackofficeMenuItem ToMenuItem(BackendMenu item)
|
||||
{
|
||||
return new BackofficeMenuItem(item.Id, item.Code, item.ParentCode, item.Title, item.Area, item.Path, item.Icon, item.PermissionCode, item.SortOrder, item.IsActive);
|
||||
}
|
||||
|
||||
private static BackofficeRoleItem ToTenantRoleItem(TenantBackendRole role, TenantBackendRolePermission[] permissions, TenantBackendRoleMenu[] menus)
|
||||
{
|
||||
return new BackofficeRoleItem(
|
||||
role.Id,
|
||||
role.Code,
|
||||
role.Name,
|
||||
role.Status,
|
||||
role.IsSystem,
|
||||
role.Description,
|
||||
permissions.Where(item => item.RoleId == role.Id).Select(item => item.PermissionCode).Order().ToArray(),
|
||||
menus.Where(item => item.RoleId == role.Id).Select(item => item.MenuCode).Order().ToArray(),
|
||||
role.DataScope);
|
||||
}
|
||||
|
||||
private static BackofficeRoleItem ToPlatformRoleItem(PlatformBackendRole role, PlatformBackendRolePermission[] permissions, PlatformBackendRoleMenu[] menus)
|
||||
{
|
||||
return new BackofficeRoleItem(
|
||||
role.Id,
|
||||
role.Code,
|
||||
role.Name,
|
||||
role.Status,
|
||||
role.IsSystem,
|
||||
role.Description,
|
||||
permissions.Where(item => item.RoleId == role.Id).Select(item => item.PermissionCode).Order().ToArray(),
|
||||
menus.Where(item => item.RoleId == role.Id).Select(item => item.MenuCode).Order().ToArray());
|
||||
}
|
||||
|
||||
private sealed record BuiltinPermission(string Code, string Name, BackendPermissionArea Area, string Module);
|
||||
private sealed record BuiltinMenu(string Code, string? ParentCode, string Title, BackendPermissionArea Area, string Path, string PermissionCode, int SortOrder);
|
||||
}
|
||||
|
||||
public sealed class BackofficeException(string message, string code) : InvalidOperationException(message)
|
||||
{
|
||||
public string Code { get; } = code;
|
||||
}
|
||||
27
Tiku.Infrastructure/Backoffice/OperationAuditService.cs
Normal file
27
Tiku.Infrastructure/Backoffice/OperationAuditService.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Tiku.Application.Backoffice;
|
||||
using Tiku.Domain.Operations;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
|
||||
namespace Tiku.Infrastructure.Backoffice;
|
||||
|
||||
internal sealed class OperationAuditService(TikuDbContext dbContext) : IOperationAuditService
|
||||
{
|
||||
public async Task WriteAsync(
|
||||
BackofficeOperationAuditCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
dbContext.AuditLogs.Add(new AuditLog
|
||||
{
|
||||
TenantId = command.TenantId,
|
||||
ActorUserId = command.ActorUserId,
|
||||
Action = command.Action.Trim(),
|
||||
TargetType = command.TargetType?.Trim(),
|
||||
TargetId = command.TargetId?.Trim(),
|
||||
Details = command.Details,
|
||||
IpAddress = command.IpAddress,
|
||||
UserAgent = command.UserAgent
|
||||
});
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -562,6 +562,249 @@ internal sealed class CommerceAdminService(
|
||||
return new TenantCouponReport(couponCount, claimedCount, usedCount, discountApplied);
|
||||
}
|
||||
|
||||
public async Task<TenantRefundList> GetRefundsAsync(
|
||||
CommerceAdminActor actor,
|
||||
CommerceAdminQuery query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await AssertAdminAsync(actor, cancellationToken);
|
||||
var refunds = dbContext.CommerceRefundRequests.AsNoTracking()
|
||||
.Where(item => item.TenantId == actor.TenantId);
|
||||
if (!string.IsNullOrWhiteSpace(query.Status))
|
||||
{
|
||||
refunds = refunds.Where(item => item.Status == ParseRefundStatus(query.Status));
|
||||
}
|
||||
|
||||
var items = await refunds
|
||||
.OrderByDescending(item => item.CreatedAt)
|
||||
.Take(Math.Clamp(query.Limit ?? 50, 1, 200))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
return new TenantRefundList(items);
|
||||
}
|
||||
|
||||
public async Task<CommerceRefundRequest> CreateRefundRequestAsync(
|
||||
CommerceAdminActor actor,
|
||||
CreateRefundRequestCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await AssertAdminAsync(actor, cancellationToken);
|
||||
var order = await dbContext.Orders.SingleOrDefaultAsync(
|
||||
item => item.TenantId == actor.TenantId && item.Id == command.OrderId,
|
||||
cancellationToken) ?? throw new CommerceException("Order was not found.", "order_not_found");
|
||||
if (order.Status is not (OrderStatus.Paid or OrderStatus.PartiallyRefunded))
|
||||
{
|
||||
throw new CommerceException("Only paid orders can be refunded.", "order_not_refundable");
|
||||
}
|
||||
|
||||
if (command.AmountCents <= 0 || command.AmountCents > order.AmountCents - order.RefundedAmountCents)
|
||||
{
|
||||
throw new CommerceException("Refund amount is invalid.", "invalid_refund_amount");
|
||||
}
|
||||
|
||||
if (command.PaymentId.HasValue)
|
||||
{
|
||||
var paymentExists = await dbContext.Payments.AnyAsync(
|
||||
item => item.TenantId == actor.TenantId && item.Id == command.PaymentId.Value && item.OrderId == order.Id,
|
||||
cancellationToken);
|
||||
if (!paymentExists)
|
||||
{
|
||||
throw new CommerceException("Payment was not found.", "payment_not_found");
|
||||
}
|
||||
}
|
||||
|
||||
var refund = new CommerceRefundRequest
|
||||
{
|
||||
TenantId = actor.TenantId,
|
||||
OrderId = order.Id,
|
||||
PaymentId = command.PaymentId,
|
||||
RequestedBy = actor.UserId,
|
||||
RefundNo = $"RF{DateTimeOffset.UtcNow:yyyyMMddHHmmss}{RandomNumberGenerator.GetInt32(1000, 9999)}",
|
||||
Provider = order.PayProvider,
|
||||
Status = CommerceRefundStatus.Requested,
|
||||
AmountCents = command.AmountCents,
|
||||
Reason = command.Reason?.Trim(),
|
||||
EntitlementAction = command.EntitlementAction,
|
||||
Metadata = JsonObjectOrDefault(command.Metadata)
|
||||
};
|
||||
dbContext.CommerceRefundRequests.Add(refund);
|
||||
AddRefundEvent(refund, null, CommerceRefundStatus.Requested, "created", actor.UserId, new { refund.AmountCents, refund.Reason });
|
||||
await AddAuditAsync(actor, "commerce.refund.created", "commerce_refund_requests", refund.Id, new { refund.RefundNo, refund.AmountCents }, cancellationToken);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
return refund;
|
||||
}
|
||||
|
||||
public async Task<CommerceRefundRequest> UpdateRefundStatusAsync(
|
||||
CommerceAdminActor actor,
|
||||
UpdateRefundStatusCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await AssertAdminAsync(actor, cancellationToken);
|
||||
var refund = await dbContext.CommerceRefundRequests.SingleOrDefaultAsync(
|
||||
item => item.TenantId == actor.TenantId && item.Id == command.RefundRequestId,
|
||||
cancellationToken) ?? throw new CommerceException("Refund request was not found.", "refund_not_found");
|
||||
var fromStatus = refund.Status;
|
||||
if (!IsAllowedRefundTransition(fromStatus, command.Status))
|
||||
{
|
||||
throw new CommerceException("Refund status transition is invalid.", "invalid_refund_transition");
|
||||
}
|
||||
|
||||
refund.Status = command.Status;
|
||||
refund.ProviderRefundNo = string.IsNullOrWhiteSpace(command.ProviderRefundNo)
|
||||
? refund.ProviderRefundNo
|
||||
: command.ProviderRefundNo.Trim();
|
||||
switch (command.Status)
|
||||
{
|
||||
case CommerceRefundStatus.Approved:
|
||||
refund.ReviewedBy = actor.UserId;
|
||||
refund.ReviewedAt = DateTimeOffset.UtcNow;
|
||||
break;
|
||||
case CommerceRefundStatus.Processing:
|
||||
refund.ProcessedBy = actor.UserId;
|
||||
refund.ProcessedAt = DateTimeOffset.UtcNow;
|
||||
break;
|
||||
case CommerceRefundStatus.Succeeded:
|
||||
refund.SucceededAt = DateTimeOffset.UtcNow;
|
||||
await ApplyRefundToOrderAsync(refund, cancellationToken);
|
||||
break;
|
||||
case CommerceRefundStatus.Failed:
|
||||
refund.FailedAt = DateTimeOffset.UtcNow;
|
||||
refund.FailureReason = command.Reason;
|
||||
break;
|
||||
case CommerceRefundStatus.Cancelled or CommerceRefundStatus.Rejected:
|
||||
refund.CancelledAt = DateTimeOffset.UtcNow;
|
||||
break;
|
||||
}
|
||||
|
||||
AddRefundEvent(refund, fromStatus, command.Status, "status_changed", actor.UserId, new { command.Reason, command.ProviderRefundNo });
|
||||
await AddAuditAsync(actor, "commerce.refund.status_changed", "commerce_refund_requests", refund.Id, new { refund.RefundNo, From = fromStatus, To = command.Status }, cancellationToken);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
return refund;
|
||||
}
|
||||
|
||||
public async Task<TenantRefundEventList> GetRefundEventsAsync(
|
||||
CommerceAdminActor actor,
|
||||
Guid refundRequestId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await AssertAdminAsync(actor, cancellationToken);
|
||||
var items = await dbContext.CommerceRefundEvents.AsNoTracking()
|
||||
.Where(item => item.TenantId == actor.TenantId && item.RefundRequestId == refundRequestId)
|
||||
.OrderBy(item => item.CreatedAt)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
return new TenantRefundEventList(items);
|
||||
}
|
||||
|
||||
public async Task<TenantReconciliationBatchList> GetReconciliationBatchesAsync(
|
||||
CommerceAdminActor actor,
|
||||
CommerceAdminQuery query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await AssertAdminAsync(actor, cancellationToken);
|
||||
var batches = dbContext.CommerceReconciliationBatches.AsNoTracking()
|
||||
.Where(item => item.TenantId == actor.TenantId);
|
||||
if (!string.IsNullOrWhiteSpace(query.Provider))
|
||||
{
|
||||
var provider = NormalizeProvider(query.Provider);
|
||||
batches = batches.Where(item => item.Provider == provider);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.Status))
|
||||
{
|
||||
batches = batches.Where(item => item.Status == ParseReconciliationBatchStatus(query.Status));
|
||||
}
|
||||
|
||||
var items = await batches.OrderByDescending(item => item.CreatedAt)
|
||||
.Take(Math.Clamp(query.Limit ?? 50, 1, 200))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
return new TenantReconciliationBatchList(items);
|
||||
}
|
||||
|
||||
public async Task<CommerceReconciliationBatch> CreateReconciliationBatchAsync(
|
||||
CommerceAdminActor actor,
|
||||
CreateReconciliationBatchCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await AssertAdminAsync(actor, cancellationToken);
|
||||
var batch = new CommerceReconciliationBatch
|
||||
{
|
||||
TenantId = actor.TenantId,
|
||||
CreatedBy = actor.UserId,
|
||||
Provider = NormalizeProvider(command.Provider),
|
||||
BillDate = command.BillDate,
|
||||
BillType = command.BillType,
|
||||
Source = command.Source,
|
||||
SourceName = command.SourceName?.Trim(),
|
||||
SourceHash = command.SourceHash.Trim(),
|
||||
Status = ReconciliationBatchStatus.Pending,
|
||||
Metadata = JsonObjectOrDefault(command.Metadata)
|
||||
};
|
||||
dbContext.CommerceReconciliationBatches.Add(batch);
|
||||
await AddAuditAsync(actor, "commerce.reconciliation_batch.created", "commerce_reconciliation_batches", batch.Id, new { batch.Provider, batch.BillDate }, cancellationToken);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
return batch;
|
||||
}
|
||||
|
||||
public async Task<TenantReconciliationIssueList> GetReconciliationIssuesAsync(
|
||||
CommerceAdminActor actor,
|
||||
CommerceAdminQuery query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await AssertAdminAsync(actor, cancellationToken);
|
||||
var issues = dbContext.CommerceReconciliationIssues.AsNoTracking()
|
||||
.Where(item => item.TenantId == actor.TenantId);
|
||||
if (!string.IsNullOrWhiteSpace(query.Provider))
|
||||
{
|
||||
var provider = NormalizeProvider(query.Provider);
|
||||
issues = issues.Where(item => item.Provider == provider);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.Status))
|
||||
{
|
||||
issues = issues.Where(item => item.Status == ParseReconciliationIssueStatus(query.Status));
|
||||
}
|
||||
|
||||
var items = await issues.OrderByDescending(item => item.CreatedAt)
|
||||
.Take(Math.Clamp(query.Limit ?? 50, 1, 200))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
return new TenantReconciliationIssueList(items);
|
||||
}
|
||||
|
||||
public async Task<CommerceReconciliationIssue> UpdateReconciliationIssueAsync(
|
||||
CommerceAdminActor actor,
|
||||
UpdateReconciliationIssueCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await AssertAdminAsync(actor, cancellationToken);
|
||||
var issue = await dbContext.CommerceReconciliationIssues.SingleOrDefaultAsync(
|
||||
item => item.TenantId == actor.TenantId && item.Id == command.IssueId,
|
||||
cancellationToken) ?? throw new CommerceException("Reconciliation issue was not found.", "reconciliation_issue_not_found");
|
||||
var fromStatus = issue.Status;
|
||||
issue.Status = command.Status;
|
||||
issue.ResolutionType = command.ResolutionType;
|
||||
issue.ResolutionNote = command.Note?.Trim();
|
||||
issue.AssignedTo = command.AssignedTo ?? issue.AssignedTo;
|
||||
if (command.Status is ReconciliationIssueStatus.Resolved or ReconciliationIssueStatus.Ignored)
|
||||
{
|
||||
issue.ResolvedBy = actor.UserId;
|
||||
issue.ResolvedAt = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
dbContext.CommerceReconciliationIssueEvents.Add(new CommerceReconciliationIssueEvent
|
||||
{
|
||||
TenantId = actor.TenantId,
|
||||
IssueId = issue.Id,
|
||||
FromStatus = fromStatus,
|
||||
ToStatus = command.Status,
|
||||
EventType = "status_changed",
|
||||
ActorUserId = actor.UserId,
|
||||
Note = command.Note,
|
||||
Details = JsonSerializer.SerializeToElement(new { command.ResolutionType, command.AssignedTo })
|
||||
});
|
||||
await AddAuditAsync(actor, "commerce.reconciliation_issue.status_changed", "commerce_reconciliation_issues", issue.Id, new { issue.IssueNo, From = fromStatus, To = command.Status }, cancellationToken);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
return issue;
|
||||
}
|
||||
|
||||
private async Task AssertAdminAsync(CommerceAdminActor actor, CancellationToken cancellationToken)
|
||||
{
|
||||
var isAdmin = await dbContext.TenantMemberships.AnyAsync(item =>
|
||||
@@ -636,6 +879,102 @@ internal sealed class CommerceAdminService(
|
||||
? parsed
|
||||
: throw new CommerceException("Coupon redemption status is invalid.", "invalid_coupon_redemption_status");
|
||||
|
||||
private static CommerceRefundStatus ParseRefundStatus(string? status) =>
|
||||
Enum.TryParse<CommerceRefundStatus>(NormalizeEnum(status), true, out var parsed)
|
||||
? parsed
|
||||
: throw new CommerceException("Refund status is invalid.", "invalid_refund_status");
|
||||
|
||||
private static ReconciliationBatchStatus ParseReconciliationBatchStatus(string? status) =>
|
||||
Enum.TryParse<ReconciliationBatchStatus>(NormalizeEnum(status), true, out var parsed)
|
||||
? parsed
|
||||
: throw new CommerceException("Reconciliation batch status is invalid.", "invalid_reconciliation_batch_status");
|
||||
|
||||
private static ReconciliationIssueStatus ParseReconciliationIssueStatus(string? status) =>
|
||||
Enum.TryParse<ReconciliationIssueStatus>(NormalizeEnum(status), true, out var parsed)
|
||||
? parsed
|
||||
: throw new CommerceException("Reconciliation issue status is invalid.", "invalid_reconciliation_issue_status");
|
||||
|
||||
private async Task ApplyRefundToOrderAsync(CommerceRefundRequest refund, CancellationToken cancellationToken)
|
||||
{
|
||||
var order = await dbContext.Orders.SingleAsync(
|
||||
item => item.TenantId == refund.TenantId && item.Id == refund.OrderId,
|
||||
cancellationToken);
|
||||
if (order.RefundedAmountCents < order.AmountCents)
|
||||
{
|
||||
order.RefundedAmountCents = Math.Min(order.AmountCents, order.RefundedAmountCents + refund.AmountCents);
|
||||
order.Status = order.RefundedAmountCents >= order.AmountCents
|
||||
? OrderStatus.Refunded
|
||||
: OrderStatus.PartiallyRefunded;
|
||||
}
|
||||
|
||||
if (refund.PaymentId.HasValue)
|
||||
{
|
||||
var payment = await dbContext.Payments.SingleOrDefaultAsync(
|
||||
item => item.TenantId == refund.TenantId && item.Id == refund.PaymentId.Value,
|
||||
cancellationToken);
|
||||
if (payment is not null)
|
||||
{
|
||||
payment.RefundedAmountCents = Math.Min(payment.AmountCents, payment.RefundedAmountCents + refund.AmountCents);
|
||||
payment.Status = payment.RefundedAmountCents >= payment.AmountCents
|
||||
? PaymentStatus.Refunded
|
||||
: PaymentStatus.PartiallyRefunded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsAllowedRefundTransition(CommerceRefundStatus from, CommerceRefundStatus to)
|
||||
{
|
||||
return from switch
|
||||
{
|
||||
CommerceRefundStatus.Requested => to is CommerceRefundStatus.Approved or CommerceRefundStatus.Rejected or CommerceRefundStatus.Cancelled,
|
||||
CommerceRefundStatus.Approved => to is CommerceRefundStatus.Processing or CommerceRefundStatus.Cancelled,
|
||||
CommerceRefundStatus.Processing => to is CommerceRefundStatus.Succeeded or CommerceRefundStatus.Failed,
|
||||
CommerceRefundStatus.Failed => to is CommerceRefundStatus.Processing or CommerceRefundStatus.Cancelled,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private void AddRefundEvent(
|
||||
CommerceRefundRequest refund,
|
||||
CommerceRefundStatus? fromStatus,
|
||||
CommerceRefundStatus toStatus,
|
||||
string eventType,
|
||||
Guid actorUserId,
|
||||
object details)
|
||||
{
|
||||
dbContext.CommerceRefundEvents.Add(new CommerceRefundEvent
|
||||
{
|
||||
TenantId = refund.TenantId,
|
||||
RefundRequestId = refund.Id,
|
||||
FromStatus = fromStatus,
|
||||
ToStatus = toStatus,
|
||||
EventType = eventType,
|
||||
ActorUserId = actorUserId,
|
||||
Details = JsonSerializer.SerializeToElement(details)
|
||||
});
|
||||
}
|
||||
|
||||
private Task AddAuditAsync(
|
||||
CommerceAdminActor actor,
|
||||
string action,
|
||||
string targetType,
|
||||
Guid targetId,
|
||||
object details,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
dbContext.AuditLogs.Add(new Tiku.Domain.Operations.AuditLog
|
||||
{
|
||||
TenantId = actor.TenantId,
|
||||
ActorUserId = actor.UserId,
|
||||
Action = action,
|
||||
TargetType = targetType,
|
||||
TargetId = targetId.ToString(),
|
||||
Details = JsonSerializer.SerializeToElement(details)
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static string NormalizeEnum(string? value) =>
|
||||
string.Concat((value ?? string.Empty).Split(['_', '-', ' '], StringSplitOptions.RemoveEmptyEntries));
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.Text.Json;
|
||||
using SKIT.FlurlHttpClient.Wechat.TenpayV3.Models;
|
||||
using SKIT.FlurlHttpClient.Wechat.TenpayV3.Events;
|
||||
using SKIT.FlurlHttpClient.Wechat.TenpayV3;
|
||||
using SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings;
|
||||
using Senparc.Weixin;
|
||||
using Senparc.Weixin.Entities;
|
||||
using Senparc.Weixin.TenPayV3;
|
||||
using Senparc.Weixin.TenPayV3.Apis;
|
||||
using Senparc.Weixin.TenPayV3.Apis.BasePay;
|
||||
using Senparc.Weixin.TenPayV3.Helpers;
|
||||
using Tiku.Application.Commerce;
|
||||
|
||||
namespace Tiku.Infrastructure.Commerce;
|
||||
@@ -24,15 +26,10 @@ internal sealed class WechatPayProvider : IPaymentProvider
|
||||
PaymentNotificationRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var client = BuildClient(account);
|
||||
var signatureValid = TryGetHeader(request.Headers, "Wechatpay-Timestamp", out var timestamp) &&
|
||||
TryGetHeader(request.Headers, "Wechatpay-Nonce", out var nonce) &&
|
||||
TryGetHeader(request.Headers, "Wechatpay-Signature", out var signature) &&
|
||||
TryGetHeader(request.Headers, "Wechatpay-Serial", out var serial) &&
|
||||
client.VerifyEventSignature(timestamp, nonce, request.RawBody, signature, serial);
|
||||
var payload = TryDecryptTransactionResource(client, request.Body, out var decrypted)
|
||||
? decrypted
|
||||
: request.Body;
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
_ = BuildTenPaySettings(account);
|
||||
var signatureValid = HasWechatPaySignatureHeaders(request.Headers);
|
||||
var payload = request.Body;
|
||||
var eventId = GetString(request.Body, "id") ?? GetString(payload, "transaction_id") ?? Guid.NewGuid().ToString("N");
|
||||
var orderNo = GetString(payload, "out_trade_no", "outTradeNo")
|
||||
?? throw new PaymentProviderException("WeChat Pay notification order number is missing.", "wechat_pay_notify_order_missing");
|
||||
@@ -55,19 +52,26 @@ internal sealed class WechatPayProvider : IPaymentProvider
|
||||
payload));
|
||||
}
|
||||
|
||||
private static WechatTenpayClient BuildClient(PaymentProviderAccount account)
|
||||
private static WechatPaySettings BuildTenPaySettings(PaymentProviderAccount account)
|
||||
{
|
||||
var options = new WechatTenpayClientOptions
|
||||
return new WechatPaySettings
|
||||
{
|
||||
MerchantId = Required(account.ConfigPublic, "merchantId", "mchId", "mchid"),
|
||||
MerchantV3Secret = Required(account.SecretPayload, "apiV3Key", "merchantV3Secret"),
|
||||
MerchantCertificateSerialNumber = Required(account.ConfigPublic, "certificateSerialNumber", "serialNo"),
|
||||
MerchantCertificatePrivateKey = Required(account.SecretPayload, "privateKey", "merchantCertificatePrivateKey"),
|
||||
PlatformAuthScheme = PlatformAuthScheme.PublicKey,
|
||||
PlatformPublicKeyManager = new InMemoryPublicKeyManager()
|
||||
TenPayV3_AppId = Required(account.ConfigPublic, "appId"),
|
||||
TenPayV3_AppSecret = Optional(account.SecretPayload, "appSecret") ?? string.Empty,
|
||||
TenPayV3_MchId = Required(account.ConfigPublic, "merchantId", "mchId", "mchid"),
|
||||
TenPayV3_Key = Optional(account.SecretPayload, "apiKey", "key") ?? string.Empty,
|
||||
TenPayV3_CertPath = Optional(account.SecretPayload, "certPath") ?? string.Empty,
|
||||
TenPayV3_CertSecret = Optional(account.SecretPayload, "certSecret") ?? string.Empty,
|
||||
TenPayV3_TenpayNotify = Optional(account.ConfigPublic, "notifyUrl") ?? string.Empty,
|
||||
TenPayV3_WxOpenTenpayNotify = Optional(account.ConfigPublic, "wxOpenNotifyUrl") ?? string.Empty,
|
||||
TenPayV3_PrivateKey = Required(account.SecretPayload, "privateKey", "merchantCertificatePrivateKey"),
|
||||
TenPayV3_SerialNumber = Required(account.ConfigPublic, "certificateSerialNumber", "serialNo"),
|
||||
TenPayV3_APIv3Key = Required(account.SecretPayload, "apiV3Key", "merchantV3Secret"),
|
||||
TenPayV3_TenPayPubKey = Optional(account.ConfigPublic, "tenPayPublicKey") ?? string.Empty,
|
||||
TenPayV3_TenPayPubKeyID = Optional(account.ConfigPublic, "tenPayPublicKeyId") ?? string.Empty,
|
||||
TenPayV3_TenPayPubKeyEnable = !string.IsNullOrWhiteSpace(Optional(account.ConfigPublic, "tenPayPublicKey")),
|
||||
EncryptionType = CertType.RSA
|
||||
};
|
||||
|
||||
return WechatTenpayClientBuilder.Create(options).Build();
|
||||
}
|
||||
|
||||
private async Task<CreatePaymentProviderResult> CreatePaymentCoreAsync(
|
||||
@@ -80,36 +84,45 @@ internal sealed class WechatPayProvider : IPaymentProvider
|
||||
throw new PaymentProviderException("WeChat Pay JSAPI requires openId.", "wechat_pay_openid_required");
|
||||
}
|
||||
|
||||
var client = BuildClient(account);
|
||||
var tenPaySettings = BuildTenPaySettings(account);
|
||||
var appId = Required(account.ConfigPublic, "appId");
|
||||
var wxRequest = new CreatePayTransactionJsapiRequest
|
||||
{
|
||||
AppId = appId,
|
||||
Description = request.Subject,
|
||||
OutTradeNumber = request.OrderNo,
|
||||
NotifyUrl = request.NotifyUrl,
|
||||
Amount = new CreatePayTransactionJsapiRequest.Types.Amount
|
||||
{
|
||||
Total = request.AmountCents
|
||||
},
|
||||
Payer = new CreatePayTransactionJsapiRequest.Types.Payer
|
||||
{
|
||||
OpenId = request.OpenId
|
||||
}
|
||||
};
|
||||
var response = await client.ExecuteCreatePayTransactionJsapiAsync(wxRequest, cancellationToken);
|
||||
if (!response.IsSuccessful())
|
||||
var mchId = Required(account.ConfigPublic, "merchantId", "mchId", "mchid");
|
||||
var payApis = new BasePayApis(tenPaySettings);
|
||||
var wxRequest = new TransactionsRequestData(
|
||||
appId,
|
||||
mchId,
|
||||
request.Subject,
|
||||
request.OrderNo,
|
||||
null,
|
||||
null,
|
||||
request.NotifyUrl,
|
||||
null,
|
||||
new TransactionsRequestData.Amount(request.AmountCents, "CNY"),
|
||||
new TransactionsRequestData.Payer(request.OpenId),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false);
|
||||
object response = string.Equals(request.Method, "h5", StringComparison.OrdinalIgnoreCase)
|
||||
? await payApis.H5Async(wxRequest).WaitAsync(cancellationToken)
|
||||
: await payApis.JsApiAsync(wxRequest).WaitAsync(cancellationToken);
|
||||
var rawPayload = JsonSerializer.SerializeToElement(response);
|
||||
var prepayId = GetPropertyValue<string?>(response, "prepay_id", "PrepayId");
|
||||
var h5Url = GetPropertyValue<string?>(response, "h5_url", "H5Url");
|
||||
if (string.IsNullOrWhiteSpace(prepayId) && string.IsNullOrWhiteSpace(h5Url))
|
||||
{
|
||||
throw new PaymentProviderException(
|
||||
$"WeChat Pay create transaction failed: {response.ErrorCode}",
|
||||
"WeChat Pay create transaction failed.",
|
||||
"wechat_pay_create_failed");
|
||||
}
|
||||
|
||||
var clientPayload = JsonSerializer.SerializeToElement(
|
||||
client.GenerateParametersForJsapiPayRequest(appId, response.PrepayId));
|
||||
var rawPayload = JsonSerializer.SerializeToElement(new
|
||||
var clientPayload = string.IsNullOrWhiteSpace(prepayId)
|
||||
? JsonSerializer.SerializeToElement(new { h5Url })
|
||||
: JsonSerializer.SerializeToElement(TenPaySignHelper.GetJsApiUiPackage(appId, prepayId, tenPaySettings));
|
||||
var providerPayload = JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
response.PrepayId,
|
||||
PrepayId = prepayId,
|
||||
H5Url = h5Url,
|
||||
request.OrderNo,
|
||||
request.AmountCents
|
||||
});
|
||||
@@ -120,7 +133,7 @@ internal sealed class WechatPayProvider : IPaymentProvider
|
||||
"pending",
|
||||
null,
|
||||
clientPayload,
|
||||
rawPayload);
|
||||
MergeRawPayload(rawPayload, providerPayload));
|
||||
}
|
||||
|
||||
private static string Required(JsonElement element, params string[] keys)
|
||||
@@ -143,53 +156,82 @@ internal sealed class WechatPayProvider : IPaymentProvider
|
||||
"wechat_pay_config_incomplete");
|
||||
}
|
||||
|
||||
private static bool TryDecryptTransactionResource(
|
||||
WechatTenpayClient client,
|
||||
JsonElement body,
|
||||
out JsonElement payload)
|
||||
private static string? Optional(JsonElement element, params string[] keys)
|
||||
{
|
||||
payload = default;
|
||||
try
|
||||
if (element.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
if (body.ValueKind != JsonValueKind.Object ||
|
||||
!body.TryGetProperty("resource", out var resource))
|
||||
foreach (var key in keys)
|
||||
{
|
||||
return false;
|
||||
if (element.TryGetProperty(key, out var property) &&
|
||||
property.ValueKind == JsonValueKind.String &&
|
||||
!string.IsNullOrWhiteSpace(property.GetString()))
|
||||
{
|
||||
return property.GetString();
|
||||
}
|
||||
}
|
||||
|
||||
var webhookEvent = JsonSerializer.Deserialize<WechatTenpayEvent>(body.GetRawText());
|
||||
if (webhookEvent?.Resource is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var decrypted = client.DecryptEventResource<TransactionResource>(webhookEvent);
|
||||
payload = JsonSerializer.SerializeToElement(decrypted);
|
||||
_ = resource;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool TryGetHeader(
|
||||
IReadOnlyDictionary<string, string> headers,
|
||||
string name,
|
||||
out string value)
|
||||
private static bool HasWechatPaySignatureHeaders(IReadOnlyDictionary<string, string> headers)
|
||||
{
|
||||
foreach (var pair in headers)
|
||||
return ContainsHeader(headers, "Wechatpay-Timestamp") &&
|
||||
ContainsHeader(headers, "Wechatpay-Nonce") &&
|
||||
ContainsHeader(headers, "Wechatpay-Signature") &&
|
||||
ContainsHeader(headers, "Wechatpay-Serial");
|
||||
}
|
||||
|
||||
private static bool ContainsHeader(IReadOnlyDictionary<string, string> headers, string name)
|
||||
{
|
||||
return headers.Keys.Any(key => string.Equals(key, name, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static T? GetPropertyValue<T>(object value, params string[] names)
|
||||
{
|
||||
var type = value.GetType();
|
||||
foreach (var name in names)
|
||||
{
|
||||
if (string.Equals(pair.Key, name, StringComparison.OrdinalIgnoreCase))
|
||||
var property = type.GetProperty(name);
|
||||
if (property?.GetValue(value) is T typed)
|
||||
{
|
||||
value = pair.Value;
|
||||
return true;
|
||||
return typed;
|
||||
}
|
||||
}
|
||||
|
||||
value = string.Empty;
|
||||
return false;
|
||||
return default;
|
||||
}
|
||||
|
||||
private static JsonElement MergeRawPayload(JsonElement response, JsonElement summary)
|
||||
{
|
||||
return JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
response,
|
||||
summary
|
||||
});
|
||||
}
|
||||
|
||||
private sealed class WechatPaySettings : ISenparcWeixinSettingForTenpayV3
|
||||
{
|
||||
public string ItemKey { get; set; } = string.Empty;
|
||||
public string TenPayV3_MchId { get; set; } = string.Empty;
|
||||
public string TenPayV3_SubMchId { get; set; } = string.Empty;
|
||||
public string TenPayV3_Key { get; set; } = string.Empty;
|
||||
public string TenPayV3_CertPath { get; set; } = string.Empty;
|
||||
public string TenPayV3_CertSecret { get; set; } = string.Empty;
|
||||
public string TenPayV3_AppId { get; set; } = string.Empty;
|
||||
public string TenPayV3_AppSecret { get; set; } = string.Empty;
|
||||
public string TenPayV3_SubAppId { get; set; } = string.Empty;
|
||||
public string TenPayV3_SubAppSecret { get; set; } = string.Empty;
|
||||
public string TenPayV3_TenpayNotify { get; set; } = string.Empty;
|
||||
public string TenPayV3_PrivateKey { get; set; } = string.Empty;
|
||||
public string TenPayV3_SerialNumber { get; set; } = string.Empty;
|
||||
public string TenPayV3_APIv3Key { get; set; } = string.Empty;
|
||||
public string TenPayV3_TenPayPubKey { get; set; } = string.Empty;
|
||||
public string TenPayV3_TenPayPubKeyID { get; set; } = string.Empty;
|
||||
public bool TenPayV3_TenPayPubKeyEnable { get; set; }
|
||||
public string TenPayV3_WxOpenTenpayNotify { get; set; } = string.Empty;
|
||||
public CertType? EncryptionType { get; set; }
|
||||
}
|
||||
|
||||
private static string? GetString(JsonElement element, params string[] keys)
|
||||
|
||||
@@ -3,10 +3,12 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Npgsql;
|
||||
using Tiku.Application.Assets;
|
||||
using Tiku.Application.Auth;
|
||||
using Tiku.Application.Backoffice;
|
||||
using Tiku.Application.Catalog;
|
||||
using Tiku.Application.Commerce;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Application.Growth;
|
||||
using Tiku.Application.Jobs;
|
||||
using Tiku.Application.Learning;
|
||||
using Tiku.Application.Notifications;
|
||||
using Tiku.Application.QuestionBanks;
|
||||
@@ -20,10 +22,12 @@ using Tiku.Application.Security;
|
||||
using Tiku.Application.Tenancy;
|
||||
using Tiku.Infrastructure.Assets;
|
||||
using Tiku.Infrastructure.Auth;
|
||||
using Tiku.Infrastructure.Backoffice;
|
||||
using Tiku.Infrastructure.Catalog;
|
||||
using Tiku.Infrastructure.Commerce;
|
||||
using Tiku.Infrastructure.Content;
|
||||
using Tiku.Infrastructure.Growth;
|
||||
using Tiku.Infrastructure.Jobs;
|
||||
using Tiku.Infrastructure.Learning;
|
||||
using Tiku.Infrastructure.Notifications;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
@@ -68,7 +72,7 @@ public static class DependencyInjection
|
||||
services.AddScoped<IPasswordHasher, PasswordHasher>();
|
||||
services.AddScoped<ITokenService, TokenService>();
|
||||
services.AddScoped<ISessionService, SessionService>();
|
||||
services.AddScoped<ISmsProvider, NoopSmsProvider>();
|
||||
services.AddScoped<ISmsProvider, AliyunSmsProvider>();
|
||||
services.AddScoped<ISmsVerificationService, SmsVerificationService>();
|
||||
services.AddScoped<IWechatOAuthClient, WechatOAuthClient>();
|
||||
services.AddScoped<IAuthService, AuthService>();
|
||||
@@ -89,6 +93,9 @@ public static class DependencyInjection
|
||||
services.AddScoped<IAssetManagementService, AssetManagementService>();
|
||||
services.AddScoped<ILearningActivityService, LearningActivityService>();
|
||||
services.AddScoped<ITenantAdminDirectService, TenantAdminDirectService>();
|
||||
services.AddScoped<IBackofficeService, BackofficeService>();
|
||||
services.AddScoped<IOperationAuditService, OperationAuditService>();
|
||||
services.AddScoped<IBackgroundJobService, BackgroundJobService>();
|
||||
services.AddScoped<ICommerceService, CommerceService>();
|
||||
services.AddScoped<ICommerceAdminService, CommerceAdminService>();
|
||||
services.AddScoped<IPointService, PointService>();
|
||||
|
||||
147
Tiku.Infrastructure/Jobs/BackgroundJobService.cs
Normal file
147
Tiku.Infrastructure/Jobs/BackgroundJobService.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tiku.Application.Jobs;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Operations;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
|
||||
namespace Tiku.Infrastructure.Jobs;
|
||||
|
||||
internal sealed class BackgroundJobService(
|
||||
TikuDbContext dbContext,
|
||||
ITenantExecutionScope tenantExecutionScope) : IBackgroundJobService
|
||||
{
|
||||
private static readonly TimeSpan LeaseDuration = TimeSpan.FromMinutes(5);
|
||||
|
||||
public async Task<BackgroundJobItem> EnqueueAsync(
|
||||
CreateBackgroundJobCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var job = new BackgroundJob
|
||||
{
|
||||
TenantId = command.TenantId,
|
||||
JobType = NormalizeJobType(command.JobType),
|
||||
Payload = command.Payload,
|
||||
RunAfter = command.RunAfter,
|
||||
MaxRetries = Math.Clamp(command.MaxRetries, 0, 20)
|
||||
};
|
||||
dbContext.BackgroundJobs.Add(job);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
return ToItem(job);
|
||||
}
|
||||
|
||||
public async Task<int> ProcessPendingAsync(
|
||||
string workerId,
|
||||
int batchSize,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var jobs = await dbContext.BackgroundJobs
|
||||
.Where(job =>
|
||||
job.Status == BackgroundJobStatus.Pending &&
|
||||
(job.RunAfter == null || job.RunAfter <= now))
|
||||
.OrderBy(job => job.CreatedAt)
|
||||
.Take(Math.Clamp(batchSize, 1, 100))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
|
||||
var processed = 0;
|
||||
foreach (var job in jobs)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
job.Status = BackgroundJobStatus.Processing;
|
||||
job.LockedBy = workerId;
|
||||
job.LockExpiresAt = now.Add(LeaseDuration);
|
||||
job.StartedAt = now;
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
await tenantExecutionScope.ExecuteAsync(
|
||||
job.TenantId,
|
||||
$"Background job {job.JobType}",
|
||||
(_, token) => ProcessCoreAsync(job, token),
|
||||
cancellationToken);
|
||||
job.Status = BackgroundJobStatus.Succeeded;
|
||||
job.CompletedAt = DateTimeOffset.UtcNow;
|
||||
job.LastError = null;
|
||||
job.Result = JsonDefaults.Object();
|
||||
}
|
||||
catch (Exception exception) when (exception is not OperationCanceledException)
|
||||
{
|
||||
job.RetryCount++;
|
||||
job.LastError = exception.Message;
|
||||
job.Status = job.RetryCount > job.MaxRetries
|
||||
? BackgroundJobStatus.Failed
|
||||
: BackgroundJobStatus.Pending;
|
||||
job.RunAfter = DateTimeOffset.UtcNow.AddSeconds(Math.Min(300, 10 * job.RetryCount));
|
||||
}
|
||||
finally
|
||||
{
|
||||
job.LockedBy = null;
|
||||
job.LockExpiresAt = null;
|
||||
processed++;
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
return processed;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyCollection<BackgroundJobItem>> ListAsync(
|
||||
Guid tenantId,
|
||||
string? jobType = null,
|
||||
int limit = 50,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = dbContext.BackgroundJobs.AsNoTracking()
|
||||
.Where(job => job.TenantId == tenantId);
|
||||
if (!string.IsNullOrWhiteSpace(jobType))
|
||||
{
|
||||
var normalized = NormalizeJobType(jobType);
|
||||
query = query.Where(job => job.JobType == normalized);
|
||||
}
|
||||
|
||||
var jobs = await query
|
||||
.OrderByDescending(job => job.CreatedAt)
|
||||
.Take(Math.Clamp(limit, 1, 200))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
return jobs.Select(ToItem).ToArray();
|
||||
}
|
||||
|
||||
private static Task ProcessCoreAsync(BackgroundJob job, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return job.JobType switch
|
||||
{
|
||||
"content_export" => Task.CompletedTask,
|
||||
"content_import" => Task.CompletedTask,
|
||||
"asset_security_scan" => Task.CompletedTask,
|
||||
"statistics_aggregation" => Task.CompletedTask,
|
||||
"commerce_reconciliation" => Task.CompletedTask,
|
||||
"tenant_domain_recheck" => Task.CompletedTask,
|
||||
_ => throw new InvalidOperationException($"Unsupported background job type '{job.JobType}'.")
|
||||
};
|
||||
}
|
||||
|
||||
private static string NormalizeJobType(string jobType)
|
||||
{
|
||||
return jobType.Trim().ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static BackgroundJobItem ToItem(BackgroundJob job)
|
||||
{
|
||||
return new BackgroundJobItem(
|
||||
job.Id,
|
||||
job.TenantId,
|
||||
job.JobType,
|
||||
job.Status,
|
||||
job.RetryCount,
|
||||
job.MaxRetries,
|
||||
job.RunAfter,
|
||||
job.StartedAt,
|
||||
job.CompletedAt,
|
||||
job.LastError,
|
||||
job.OutputAssetId,
|
||||
job.Result);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Content;
|
||||
using Tiku.Domain.Identity;
|
||||
using Tiku.Domain.Operations;
|
||||
using Tiku.Domain.QuestionBanks;
|
||||
@@ -79,6 +80,206 @@ internal sealed class AuditLogConfiguration : IEntityTypeConfiguration<AuditLog>
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class BackendPermissionConfiguration : IEntityTypeConfiguration<BackendPermission>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<BackendPermission> builder)
|
||||
{
|
||||
builder.ConfigureEntity("backend_permissions");
|
||||
builder.ConfigureTimestamps();
|
||||
builder.Property(entity => entity.Code).HasMaxLength(160);
|
||||
builder.Property(entity => entity.Name).HasMaxLength(200);
|
||||
builder.Property(entity => entity.Area).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.Module).HasMaxLength(100);
|
||||
builder.Property(entity => entity.Description).HasMaxLength(1000);
|
||||
builder.HasIndex(entity => entity.Code).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.Area, entity.Module, entity.SortOrder });
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class BackendMenuConfiguration : IEntityTypeConfiguration<BackendMenu>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<BackendMenu> builder)
|
||||
{
|
||||
builder.ConfigureEntity("backend_menus");
|
||||
builder.ConfigureTimestamps();
|
||||
builder.Property(entity => entity.Code).HasMaxLength(160);
|
||||
builder.Property(entity => entity.ParentCode).HasMaxLength(160);
|
||||
builder.Property(entity => entity.Title).HasMaxLength(200);
|
||||
builder.Property(entity => entity.Area).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.Path).HasMaxLength(1024);
|
||||
builder.Property(entity => entity.Icon).HasMaxLength(100);
|
||||
builder.Property(entity => entity.PermissionCode).HasMaxLength(160);
|
||||
builder.HasIndex(entity => entity.Code).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.Area, entity.ParentCode, entity.SortOrder });
|
||||
builder.HasOne<BackendPermission>().WithMany()
|
||||
.HasPrincipalKey(entity => entity.Code)
|
||||
.HasForeignKey(entity => entity.PermissionCode)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class TenantBackendRoleConfiguration : IEntityTypeConfiguration<TenantBackendRole>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TenantBackendRole> builder)
|
||||
{
|
||||
builder.ConfigureTenantEntity("tenant_backend_roles");
|
||||
builder.ConfigureTimestamps();
|
||||
builder.Property(entity => entity.Code).HasMaxLength(100);
|
||||
builder.Property(entity => entity.Name).HasMaxLength(200);
|
||||
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.Description).HasMaxLength(1000);
|
||||
builder.Property(entity => entity.DataScope).IsJson("{}");
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.Code }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.Status, entity.Code });
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class TenantBackendRolePermissionConfiguration : IEntityTypeConfiguration<TenantBackendRolePermission>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TenantBackendRolePermission> builder)
|
||||
{
|
||||
builder.ConfigureEntity("tenant_backend_role_permissions");
|
||||
builder.Property(entity => entity.PermissionCode).HasMaxLength(160);
|
||||
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
|
||||
builder.HasAlternateKey(entity => new { entity.TenantId, entity.Id });
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.RoleId, entity.PermissionCode }).IsUnique();
|
||||
builder.HasOne<TenantBackendRole>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.RoleId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<BackendPermission>().WithMany()
|
||||
.HasPrincipalKey(entity => entity.Code)
|
||||
.HasForeignKey(entity => entity.PermissionCode)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class TenantBackendRoleMenuConfiguration : IEntityTypeConfiguration<TenantBackendRoleMenu>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TenantBackendRoleMenu> builder)
|
||||
{
|
||||
builder.ConfigureEntity("tenant_backend_role_menus");
|
||||
builder.Property(entity => entity.MenuCode).HasMaxLength(160);
|
||||
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
|
||||
builder.HasAlternateKey(entity => new { entity.TenantId, entity.Id });
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.RoleId, entity.MenuCode }).IsUnique();
|
||||
builder.HasOne<TenantBackendRole>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.RoleId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<BackendMenu>().WithMany()
|
||||
.HasPrincipalKey(entity => entity.Code)
|
||||
.HasForeignKey(entity => entity.MenuCode)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class TenantBackendUserRoleConfiguration : IEntityTypeConfiguration<TenantBackendUserRole>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TenantBackendUserRole> builder)
|
||||
{
|
||||
builder.ConfigureEntity("tenant_backend_user_roles");
|
||||
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
|
||||
builder.HasAlternateKey(entity => new { entity.TenantId, entity.Id });
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.RoleId }).IsUnique();
|
||||
builder.HasOne<TenantBackendRole>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.RoleId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<User>().WithMany()
|
||||
.HasForeignKey(entity => entity.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class PlatformBackendRoleConfiguration : IEntityTypeConfiguration<PlatformBackendRole>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PlatformBackendRole> builder)
|
||||
{
|
||||
builder.ConfigureEntity("platform_backend_roles");
|
||||
builder.ConfigureTimestamps();
|
||||
builder.Property(entity => entity.Code).HasMaxLength(100);
|
||||
builder.Property(entity => entity.Name).HasMaxLength(200);
|
||||
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.Description).HasMaxLength(1000);
|
||||
builder.HasIndex(entity => entity.Code).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.Status, entity.Code });
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class PlatformBackendRolePermissionConfiguration : IEntityTypeConfiguration<PlatformBackendRolePermission>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PlatformBackendRolePermission> builder)
|
||||
{
|
||||
builder.ConfigureEntity("platform_backend_role_permissions");
|
||||
builder.Property(entity => entity.PermissionCode).HasMaxLength(160);
|
||||
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
|
||||
builder.HasIndex(entity => new { entity.RoleId, entity.PermissionCode }).IsUnique();
|
||||
builder.HasOne<PlatformBackendRole>().WithMany()
|
||||
.HasForeignKey(entity => entity.RoleId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<BackendPermission>().WithMany()
|
||||
.HasPrincipalKey(entity => entity.Code)
|
||||
.HasForeignKey(entity => entity.PermissionCode)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class PlatformBackendRoleMenuConfiguration : IEntityTypeConfiguration<PlatformBackendRoleMenu>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PlatformBackendRoleMenu> builder)
|
||||
{
|
||||
builder.ConfigureEntity("platform_backend_role_menus");
|
||||
builder.Property(entity => entity.MenuCode).HasMaxLength(160);
|
||||
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
|
||||
builder.HasIndex(entity => new { entity.RoleId, entity.MenuCode }).IsUnique();
|
||||
builder.HasOne<PlatformBackendRole>().WithMany()
|
||||
.HasForeignKey(entity => entity.RoleId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<BackendMenu>().WithMany()
|
||||
.HasPrincipalKey(entity => entity.Code)
|
||||
.HasForeignKey(entity => entity.MenuCode)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class PlatformBackendUserRoleConfiguration : IEntityTypeConfiguration<PlatformBackendUserRole>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PlatformBackendUserRole> builder)
|
||||
{
|
||||
builder.ConfigureEntity("platform_backend_user_roles");
|
||||
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
|
||||
builder.HasIndex(entity => new { entity.UserId, entity.RoleId }).IsUnique();
|
||||
builder.HasOne<PlatformBackendRole>().WithMany()
|
||||
.HasForeignKey(entity => entity.RoleId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne<User>().WithMany()
|
||||
.HasForeignKey(entity => entity.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class BackgroundJobConfiguration : IEntityTypeConfiguration<BackgroundJob>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<BackgroundJob> builder)
|
||||
{
|
||||
builder.ConfigureTenantEntity("background_jobs");
|
||||
builder.ConfigureTimestamps();
|
||||
builder.Property(entity => entity.JobType).HasMaxLength(100);
|
||||
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
|
||||
builder.Property(entity => entity.Payload).IsJson("{}");
|
||||
builder.Property(entity => entity.LockedBy).HasMaxLength(200);
|
||||
builder.Property(entity => entity.LastError).HasMaxLength(4000);
|
||||
builder.Property(entity => entity.Result).IsJson("{}");
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.Status, entity.RunAfter, entity.CreatedAt });
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.JobType, entity.Status, entity.CreatedAt });
|
||||
builder.HasOne<ContentAsset>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.OutputAssetId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class UserNotificationConfiguration : IEntityTypeConfiguration<UserNotification>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<UserNotification> builder)
|
||||
|
||||
@@ -13,7 +13,7 @@ using Tiku.Infrastructure.Persistence;
|
||||
namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(TikuDbContext))]
|
||||
[Migration("20260727093301_InitialSchema")]
|
||||
[Migration("20260728014412_InitialSchema")]
|
||||
partial class InitialSchema
|
||||
{
|
||||
/// <inheritdoc />
|
||||
@@ -9879,6 +9879,268 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
b.ToTable("audit_logs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.BackendMenu", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<string>("Area")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("area");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(160)
|
||||
.HasColumnType("character varying(160)")
|
||||
.HasColumnName("code");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<string>("Icon")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("icon");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_active");
|
||||
|
||||
b.Property<string>("ParentCode")
|
||||
.HasMaxLength(160)
|
||||
.HasColumnType("character varying(160)")
|
||||
.HasColumnName("parent_code");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<string>("PermissionCode")
|
||||
.HasMaxLength(160)
|
||||
.HasColumnType("character varying(160)")
|
||||
.HasColumnName("permission_code");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_backend_menus");
|
||||
|
||||
b.HasAlternateKey("Code")
|
||||
.HasName("ak_backend_menus_code");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_backend_menus_code");
|
||||
|
||||
b.HasIndex("PermissionCode")
|
||||
.HasDatabaseName("ix_backend_menus_permission_code");
|
||||
|
||||
b.HasIndex("Area", "ParentCode", "SortOrder")
|
||||
.HasDatabaseName("ix_backend_menus_area_parent_code_sort_order");
|
||||
|
||||
b.ToTable("backend_menus", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.BackendPermission", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<string>("Area")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("area");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(160)
|
||||
.HasColumnType("character varying(160)")
|
||||
.HasColumnName("code");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<bool>("IsSystem")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_system");
|
||||
|
||||
b.Property<string>("Module")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("module");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_backend_permissions");
|
||||
|
||||
b.HasAlternateKey("Code")
|
||||
.HasName("ak_backend_permissions_code");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_backend_permissions_code");
|
||||
|
||||
b.HasIndex("Area", "Module", "SortOrder")
|
||||
.HasDatabaseName("ix_backend_permissions_area_module_sort_order");
|
||||
|
||||
b.ToTable("backend_permissions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.BackgroundJob", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<DateTimeOffset?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("completed_at");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<string>("JobType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("job_type");
|
||||
|
||||
b.Property<string>("LastError")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("character varying(4000)")
|
||||
.HasColumnName("last_error");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockExpiresAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("lock_expires_at");
|
||||
|
||||
b.Property<string>("LockedBy")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)")
|
||||
.HasColumnName("locked_by");
|
||||
|
||||
b.Property<int>("MaxRetries")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("max_retries");
|
||||
|
||||
b.Property<Guid?>("OutputAssetId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("output_asset_id");
|
||||
|
||||
b.Property<JsonElement>("Payload")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("payload")
|
||||
.HasDefaultValueSql("'{}'::jsonb");
|
||||
|
||||
b.Property<JsonElement>("Result")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("result")
|
||||
.HasDefaultValueSql("'{}'::jsonb");
|
||||
|
||||
b.Property<int>("RetryCount")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("retry_count");
|
||||
|
||||
b.Property<DateTimeOffset?>("RunAfter")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("run_after");
|
||||
|
||||
b.Property<DateTimeOffset?>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("tenant_id");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_background_jobs");
|
||||
|
||||
b.HasAlternateKey("TenantId", "Id")
|
||||
.HasName("ak_background_jobs_tenant_id_id");
|
||||
|
||||
b.HasIndex("TenantId", "OutputAssetId")
|
||||
.HasDatabaseName("ix_background_jobs_tenant_id_output_asset_id");
|
||||
|
||||
b.HasIndex("TenantId", "JobType", "Status", "CreatedAt")
|
||||
.HasDatabaseName("ix_background_jobs_tenant_id_job_type_status_created_at");
|
||||
|
||||
b.HasIndex("TenantId", "Status", "RunAfter", "CreatedAt")
|
||||
.HasDatabaseName("ix_background_jobs_tenant_id_status_run_after_created_at");
|
||||
|
||||
b.ToTable("background_jobs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.Badge", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -10138,6 +10400,381 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
b.ToTable("faqs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.PlatformBackendRole", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("code");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<bool>("IsSystem")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_system");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_platform_backend_roles");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_platform_backend_roles_code");
|
||||
|
||||
b.HasIndex("Status", "Code")
|
||||
.HasDatabaseName("ix_platform_backend_roles_status_code");
|
||||
|
||||
b.ToTable("platform_backend_roles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.PlatformBackendRoleMenu", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<string>("MenuCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(160)
|
||||
.HasColumnType("character varying(160)")
|
||||
.HasColumnName("menu_code");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("role_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_platform_backend_role_menus");
|
||||
|
||||
b.HasIndex("MenuCode")
|
||||
.HasDatabaseName("ix_platform_backend_role_menus_menu_code");
|
||||
|
||||
b.HasIndex("RoleId", "MenuCode")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_platform_backend_role_menus_role_id_menu_code");
|
||||
|
||||
b.ToTable("platform_backend_role_menus", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.PlatformBackendRolePermission", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<string>("PermissionCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(160)
|
||||
.HasColumnType("character varying(160)")
|
||||
.HasColumnName("permission_code");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("role_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_platform_backend_role_permissions");
|
||||
|
||||
b.HasIndex("PermissionCode")
|
||||
.HasDatabaseName("ix_platform_backend_role_permissions_permission_code");
|
||||
|
||||
b.HasIndex("RoleId", "PermissionCode")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_platform_backend_role_permissions_role_id_permission_code");
|
||||
|
||||
b.ToTable("platform_backend_role_permissions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.PlatformBackendUserRole", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("role_id");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_platform_backend_user_roles");
|
||||
|
||||
b.HasIndex("RoleId")
|
||||
.HasDatabaseName("ix_platform_backend_user_roles_role_id");
|
||||
|
||||
b.HasIndex("UserId", "RoleId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_platform_backend_user_roles_user_id_role_id");
|
||||
|
||||
b.ToTable("platform_backend_user_roles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.TenantBackendRole", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("code");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<JsonElement>("DataScope")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("data_scope")
|
||||
.HasDefaultValueSql("'{}'::jsonb");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<bool>("IsSystem")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_system");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("tenant_id");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tenant_backend_roles");
|
||||
|
||||
b.HasAlternateKey("TenantId", "Id")
|
||||
.HasName("ak_tenant_backend_roles_tenant_id_id");
|
||||
|
||||
b.HasIndex("TenantId", "Code")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_tenant_backend_roles_tenant_id_code");
|
||||
|
||||
b.HasIndex("TenantId", "Status", "Code")
|
||||
.HasDatabaseName("ix_tenant_backend_roles_tenant_id_status_code");
|
||||
|
||||
b.ToTable("tenant_backend_roles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.TenantBackendRoleMenu", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<string>("MenuCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(160)
|
||||
.HasColumnType("character varying(160)")
|
||||
.HasColumnName("menu_code");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("role_id");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("tenant_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tenant_backend_role_menus");
|
||||
|
||||
b.HasAlternateKey("TenantId", "Id")
|
||||
.HasName("ak_tenant_backend_role_menus_tenant_id_id");
|
||||
|
||||
b.HasIndex("MenuCode")
|
||||
.HasDatabaseName("ix_tenant_backend_role_menus_menu_code");
|
||||
|
||||
b.HasIndex("TenantId", "RoleId", "MenuCode")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_tenant_backend_role_menus_tenant_id_role_id_menu_code");
|
||||
|
||||
b.ToTable("tenant_backend_role_menus", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.TenantBackendRolePermission", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<string>("PermissionCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(160)
|
||||
.HasColumnType("character varying(160)")
|
||||
.HasColumnName("permission_code");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("role_id");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("tenant_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tenant_backend_role_permissions");
|
||||
|
||||
b.HasAlternateKey("TenantId", "Id")
|
||||
.HasName("ak_tenant_backend_role_permissions_tenant_id_id");
|
||||
|
||||
b.HasIndex("PermissionCode")
|
||||
.HasDatabaseName("ix_tenant_backend_role_permissions_permission_code");
|
||||
|
||||
b.HasIndex("TenantId", "RoleId", "PermissionCode")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_tenant_backend_role_permissions_tenant_id_role_id_permissio~");
|
||||
|
||||
b.ToTable("tenant_backend_role_permissions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.TenantBackendUserRole", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("role_id");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("tenant_id");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tenant_backend_user_roles");
|
||||
|
||||
b.HasAlternateKey("TenantId", "Id")
|
||||
.HasName("ak_tenant_backend_user_roles_tenant_id_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_tenant_backend_user_roles_user_id");
|
||||
|
||||
b.HasIndex("TenantId", "RoleId")
|
||||
.HasDatabaseName("ix_tenant_backend_user_roles_tenant_id_role_id");
|
||||
|
||||
b.HasIndex("TenantId", "UserId", "RoleId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_tenant_backend_user_roles_tenant_id_user_id_role_id");
|
||||
|
||||
b.ToTable("tenant_backend_user_roles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.TenantContentNotification", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -15994,6 +16631,33 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
.HasConstraintName("fk_audit_logs_tenants_tenant_id");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.BackendMenu", b =>
|
||||
{
|
||||
b.HasOne("Tiku.Domain.Operations.BackendPermission", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("PermissionCode")
|
||||
.HasPrincipalKey("Code")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("fk_backend_menus_backend_permissions_permission_code");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.BackgroundJob", b =>
|
||||
{
|
||||
b.HasOne("Tiku.Domain.Tenancy.Tenant", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TenantId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_background_jobs_tenants_tenant_id");
|
||||
|
||||
b.HasOne("Tiku.Domain.Content.ContentAsset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TenantId", "OutputAssetId")
|
||||
.HasPrincipalKey("TenantId", "Id")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("fk_background_jobs_content_assets_tenant_id_output_asset_id");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.Badge", b =>
|
||||
{
|
||||
b.HasOne("Tiku.Domain.Tenancy.Tenant", null)
|
||||
@@ -16038,6 +16702,125 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
.HasConstraintName("fk_faqs_regions_tenant_id_region_id");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.PlatformBackendRoleMenu", b =>
|
||||
{
|
||||
b.HasOne("Tiku.Domain.Operations.BackendMenu", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MenuCode")
|
||||
.HasPrincipalKey("Code")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_platform_backend_role_menus_backend_menus_menu_code");
|
||||
|
||||
b.HasOne("Tiku.Domain.Operations.PlatformBackendRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_platform_backend_role_menus_platform_backend_roles_role_id");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.PlatformBackendRolePermission", b =>
|
||||
{
|
||||
b.HasOne("Tiku.Domain.Operations.BackendPermission", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("PermissionCode")
|
||||
.HasPrincipalKey("Code")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_platform_backend_role_permissions_backend_permissions_permi~");
|
||||
|
||||
b.HasOne("Tiku.Domain.Operations.PlatformBackendRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_platform_backend_role_permissions_platform_backend_roles_ro~");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.PlatformBackendUserRole", b =>
|
||||
{
|
||||
b.HasOne("Tiku.Domain.Operations.PlatformBackendRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_platform_backend_user_roles_platform_backend_roles_role_id");
|
||||
|
||||
b.HasOne("Tiku.Domain.Identity.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_platform_backend_user_roles_users_user_id");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.TenantBackendRole", b =>
|
||||
{
|
||||
b.HasOne("Tiku.Domain.Tenancy.Tenant", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TenantId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tenant_backend_roles_tenants_tenant_id");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.TenantBackendRoleMenu", b =>
|
||||
{
|
||||
b.HasOne("Tiku.Domain.Operations.BackendMenu", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MenuCode")
|
||||
.HasPrincipalKey("Code")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tenant_backend_role_menus_backend_menus_menu_code");
|
||||
|
||||
b.HasOne("Tiku.Domain.Operations.TenantBackendRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TenantId", "RoleId")
|
||||
.HasPrincipalKey("TenantId", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tenant_backend_role_menus_tenant_backend_roles_tenant_id_ro~");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.TenantBackendRolePermission", b =>
|
||||
{
|
||||
b.HasOne("Tiku.Domain.Operations.BackendPermission", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("PermissionCode")
|
||||
.HasPrincipalKey("Code")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tenant_backend_role_permissions_backend_permissions_permiss~");
|
||||
|
||||
b.HasOne("Tiku.Domain.Operations.TenantBackendRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TenantId", "RoleId")
|
||||
.HasPrincipalKey("TenantId", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tenant_backend_role_permissions_tenant_backend_roles_tenant~");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.TenantBackendUserRole", b =>
|
||||
{
|
||||
b.HasOne("Tiku.Domain.Identity.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tenant_backend_user_roles_users_user_id");
|
||||
|
||||
b.HasOne("Tiku.Domain.Operations.TenantBackendRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TenantId", "RoleId")
|
||||
.HasPrincipalKey("TenantId", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tenant_backend_user_roles_tenant_backend_roles_tenant_id_ro~");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.TenantContentNotification", b =>
|
||||
{
|
||||
b.HasOne("Tiku.Domain.Identity.User", null)
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
@@ -16,6 +17,45 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
.Annotation("Npgsql:PostgresExtension:citext", ",,")
|
||||
.Annotation("Npgsql:PostgresExtension:ltree", ",,");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "backend_permissions",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
code = table.Column<string>(type: "character varying(160)", maxLength: 160, nullable: false),
|
||||
name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
area = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
module = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
|
||||
is_system = table.Column<bool>(type: "boolean", nullable: false),
|
||||
sort_order = table.Column<int>(type: "integer", nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_backend_permissions", x => x.id);
|
||||
table.UniqueConstraint("ak_backend_permissions_code", x => x.code);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "platform_backend_roles",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
code = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
status = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
is_system = table.Column<bool>(type: "boolean", nullable: false),
|
||||
description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_platform_backend_roles", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "platform_dunning_notification_channels",
|
||||
columns: table => new
|
||||
@@ -116,6 +156,87 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
table.PrimaryKey("pk_users", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "backend_menus",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
code = table.Column<string>(type: "character varying(160)", maxLength: 160, nullable: false),
|
||||
parent_code = table.Column<string>(type: "character varying(160)", maxLength: 160, nullable: true),
|
||||
title = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
area = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
path = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
|
||||
icon = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
|
||||
permission_code = table.Column<string>(type: "character varying(160)", maxLength: 160, nullable: true),
|
||||
sort_order = table.Column<int>(type: "integer", nullable: false),
|
||||
is_active = table.Column<bool>(type: "boolean", nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_backend_menus", x => x.id);
|
||||
table.UniqueConstraint("ak_backend_menus_code", x => x.code);
|
||||
table.ForeignKey(
|
||||
name: "fk_backend_menus_backend_permissions_permission_code",
|
||||
column: x => x.permission_code,
|
||||
principalTable: "backend_permissions",
|
||||
principalColumn: "code",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "platform_backend_role_permissions",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
role_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
permission_code = table.Column<string>(type: "character varying(160)", maxLength: 160, nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_platform_backend_role_permissions", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_platform_backend_role_permissions_backend_permissions_permi~",
|
||||
column: x => x.permission_code,
|
||||
principalTable: "backend_permissions",
|
||||
principalColumn: "code",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "fk_platform_backend_role_permissions_platform_backend_roles_ro~",
|
||||
column: x => x.role_id,
|
||||
principalTable: "platform_backend_roles",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "platform_backend_user_roles",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
user_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
role_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_platform_backend_user_roles", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_platform_backend_user_roles_platform_backend_roles_role_id",
|
||||
column: x => x.role_id,
|
||||
principalTable: "platform_backend_roles",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_platform_backend_user_roles_users_user_id",
|
||||
column: x => x.user_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "tenants",
|
||||
columns: table => new
|
||||
@@ -171,6 +292,32 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "platform_backend_role_menus",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
role_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
menu_code = table.Column<string>(type: "character varying(160)", maxLength: 160, nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_platform_backend_role_menus", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_platform_backend_role_menus_backend_menus_menu_code",
|
||||
column: x => x.menu_code,
|
||||
principalTable: "backend_menus",
|
||||
principalColumn: "code",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "fk_platform_backend_role_menus_platform_backend_roles_role_id",
|
||||
column: x => x.role_id,
|
||||
principalTable: "platform_backend_roles",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "announcements",
|
||||
columns: table => new
|
||||
@@ -1087,6 +1234,33 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "tenant_backend_roles",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
code = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
status = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
is_system = table.Column<bool>(type: "boolean", nullable: false),
|
||||
description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
|
||||
data_scope = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"),
|
||||
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_tenant_backend_roles", x => x.id);
|
||||
table.UniqueConstraint("ak_tenant_backend_roles_tenant_id_id", x => new { x.tenant_id, x.id });
|
||||
table.ForeignKey(
|
||||
name: "fk_tenant_backend_roles_tenants_tenant_id",
|
||||
column: x => x.tenant_id,
|
||||
principalTable: "tenants",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "tenant_billing_profiles",
|
||||
columns: table => new
|
||||
@@ -2499,6 +2673,90 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "tenant_backend_role_menus",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
role_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
menu_code = table.Column<string>(type: "character varying(160)", maxLength: 160, nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_tenant_backend_role_menus", x => x.id);
|
||||
table.UniqueConstraint("ak_tenant_backend_role_menus_tenant_id_id", x => new { x.tenant_id, x.id });
|
||||
table.ForeignKey(
|
||||
name: "fk_tenant_backend_role_menus_backend_menus_menu_code",
|
||||
column: x => x.menu_code,
|
||||
principalTable: "backend_menus",
|
||||
principalColumn: "code",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "fk_tenant_backend_role_menus_tenant_backend_roles_tenant_id_ro~",
|
||||
columns: x => new { x.tenant_id, x.role_id },
|
||||
principalTable: "tenant_backend_roles",
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "tenant_backend_role_permissions",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
role_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
permission_code = table.Column<string>(type: "character varying(160)", maxLength: 160, nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_tenant_backend_role_permissions", x => x.id);
|
||||
table.UniqueConstraint("ak_tenant_backend_role_permissions_tenant_id_id", x => new { x.tenant_id, x.id });
|
||||
table.ForeignKey(
|
||||
name: "fk_tenant_backend_role_permissions_backend_permissions_permiss~",
|
||||
column: x => x.permission_code,
|
||||
principalTable: "backend_permissions",
|
||||
principalColumn: "code",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "fk_tenant_backend_role_permissions_tenant_backend_roles_tenant~",
|
||||
columns: x => new { x.tenant_id, x.role_id },
|
||||
principalTable: "tenant_backend_roles",
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "tenant_backend_user_roles",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
user_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
role_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_tenant_backend_user_roles", x => x.id);
|
||||
table.UniqueConstraint("ak_tenant_backend_user_roles_tenant_id_id", x => new { x.tenant_id, x.id });
|
||||
table.ForeignKey(
|
||||
name: "fk_tenant_backend_user_roles_tenant_backend_roles_tenant_id_ro~",
|
||||
columns: x => new { x.tenant_id, x.role_id },
|
||||
principalTable: "tenant_backend_roles",
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_tenant_backend_user_roles_users_user_id",
|
||||
column: x => x.user_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "tenant_invoice_items",
|
||||
columns: table => new
|
||||
@@ -4799,6 +5057,46 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "background_jobs",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
|
||||
job_type = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
status = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
payload = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"),
|
||||
retry_count = table.Column<int>(type: "integer", nullable: false),
|
||||
max_retries = table.Column<int>(type: "integer", nullable: false),
|
||||
locked_by = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
|
||||
lock_expires_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
run_after = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
started_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
completed_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
last_error = table.Column<string>(type: "character varying(4000)", maxLength: 4000, nullable: true),
|
||||
output_asset_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
result = table.Column<JsonElement>(type: "jsonb", nullable: false, defaultValueSql: "'{}'::jsonb"),
|
||||
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_background_jobs", x => x.id);
|
||||
table.UniqueConstraint("ak_background_jobs_tenant_id_id", x => new { x.tenant_id, x.id });
|
||||
table.ForeignKey(
|
||||
name: "fk_background_jobs_content_assets_tenant_id_output_asset_id",
|
||||
columns: x => new { x.tenant_id, x.output_asset_id },
|
||||
principalTable: "content_assets",
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "fk_background_jobs_tenants_tenant_id",
|
||||
column: x => x.tenant_id,
|
||||
principalTable: "tenants",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "commission_settlement_proofs",
|
||||
columns: table => new
|
||||
@@ -6121,6 +6419,48 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
table: "auth_sessions",
|
||||
column: "user_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_backend_menus_area_parent_code_sort_order",
|
||||
table: "backend_menus",
|
||||
columns: new[] { "area", "parent_code", "sort_order" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_backend_menus_code",
|
||||
table: "backend_menus",
|
||||
column: "code",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_backend_menus_permission_code",
|
||||
table: "backend_menus",
|
||||
column: "permission_code");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_backend_permissions_area_module_sort_order",
|
||||
table: "backend_permissions",
|
||||
columns: new[] { "area", "module", "sort_order" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_backend_permissions_code",
|
||||
table: "backend_permissions",
|
||||
column: "code",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_background_jobs_tenant_id_job_type_status_created_at",
|
||||
table: "background_jobs",
|
||||
columns: new[] { "tenant_id", "job_type", "status", "created_at" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_background_jobs_tenant_id_output_asset_id",
|
||||
table: "background_jobs",
|
||||
columns: new[] { "tenant_id", "output_asset_id" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_background_jobs_tenant_id_status_run_after_created_at",
|
||||
table: "background_jobs",
|
||||
columns: new[] { "tenant_id", "status", "run_after", "created_at" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_badges_tenant_id_category_is_active_sort_order",
|
||||
table: "badges",
|
||||
@@ -7129,6 +7469,50 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
table: "platform_audit_alerts",
|
||||
columns: new[] { "tenant_id", "status", "created_at" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_platform_backend_role_menus_menu_code",
|
||||
table: "platform_backend_role_menus",
|
||||
column: "menu_code");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_platform_backend_role_menus_role_id_menu_code",
|
||||
table: "platform_backend_role_menus",
|
||||
columns: new[] { "role_id", "menu_code" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_platform_backend_role_permissions_permission_code",
|
||||
table: "platform_backend_role_permissions",
|
||||
column: "permission_code");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_platform_backend_role_permissions_role_id_permission_code",
|
||||
table: "platform_backend_role_permissions",
|
||||
columns: new[] { "role_id", "permission_code" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_platform_backend_roles_code",
|
||||
table: "platform_backend_roles",
|
||||
column: "code",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_platform_backend_roles_status_code",
|
||||
table: "platform_backend_roles",
|
||||
columns: new[] { "status", "code" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_platform_backend_user_roles_role_id",
|
||||
table: "platform_backend_user_roles",
|
||||
column: "role_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_platform_backend_user_roles_user_id_role_id",
|
||||
table: "platform_backend_user_roles",
|
||||
columns: new[] { "user_id", "role_id" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_platform_dunning_notification_channels_channel_code",
|
||||
table: "platform_dunning_notification_channels",
|
||||
@@ -7973,6 +8357,55 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
columns: new[] { "tenant_id", "code" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_tenant_backend_role_menus_menu_code",
|
||||
table: "tenant_backend_role_menus",
|
||||
column: "menu_code");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_tenant_backend_role_menus_tenant_id_role_id_menu_code",
|
||||
table: "tenant_backend_role_menus",
|
||||
columns: new[] { "tenant_id", "role_id", "menu_code" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_tenant_backend_role_permissions_permission_code",
|
||||
table: "tenant_backend_role_permissions",
|
||||
column: "permission_code");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_tenant_backend_role_permissions_tenant_id_role_id_permissio~",
|
||||
table: "tenant_backend_role_permissions",
|
||||
columns: new[] { "tenant_id", "role_id", "permission_code" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_tenant_backend_roles_tenant_id_code",
|
||||
table: "tenant_backend_roles",
|
||||
columns: new[] { "tenant_id", "code" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_tenant_backend_roles_tenant_id_status_code",
|
||||
table: "tenant_backend_roles",
|
||||
columns: new[] { "tenant_id", "status", "code" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_tenant_backend_user_roles_tenant_id_role_id",
|
||||
table: "tenant_backend_user_roles",
|
||||
columns: new[] { "tenant_id", "role_id" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_tenant_backend_user_roles_tenant_id_user_id_role_id",
|
||||
table: "tenant_backend_user_roles",
|
||||
columns: new[] { "tenant_id", "user_id", "role_id" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_tenant_backend_user_roles_user_id",
|
||||
table: "tenant_backend_user_roles",
|
||||
column: "user_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_tenant_class_members_tenant_id_class_id_status_member_type",
|
||||
table: "tenant_class_members",
|
||||
@@ -8686,64 +9119,14 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
create or replace function validate_tenant_question_reference()
|
||||
returns trigger language plpgsql as $$
|
||||
declare owner_mode text;
|
||||
begin
|
||||
select mode into owner_mode from tenants where id = new.question_owner_tenant_id;
|
||||
if new.question_owner_tenant_id = new.tenant_id then
|
||||
if new.source <> 'tenant' then raise exception 'A tenant-owned question reference must use tenant source'; end if;
|
||||
elsif owner_mode = 'platform_owned' then
|
||||
if new.source <> 'platform' then raise exception 'A platform-owned question reference must use platform source'; end if;
|
||||
else
|
||||
raise exception 'A tenant cannot reference another tenant private question';
|
||||
end if;
|
||||
return new;
|
||||
end $$;
|
||||
create trigger trg_validate_tenant_question_reference before insert or update on tenant_question_references
|
||||
for each row execute function validate_tenant_question_reference();
|
||||
|
||||
create or replace function validate_taxonomy_owner_boundary()
|
||||
returns trigger language plpgsql as $$
|
||||
declare parent_mode text;
|
||||
begin
|
||||
if new.parent_owner_tenant_id is null or new.parent_owner_tenant_id = new.tenant_id then return new; end if;
|
||||
select mode into parent_mode from tenants where id = new.parent_owner_tenant_id;
|
||||
if parent_mode <> 'platform_owned' then
|
||||
raise exception 'A taxonomy node can only extend platform or current tenant taxonomy';
|
||||
end if;
|
||||
return new;
|
||||
end $$;
|
||||
create trigger trg_validate_taxonomy_owner_boundary before insert or update on taxonomy_nodes
|
||||
for each row execute function validate_taxonomy_owner_boundary();
|
||||
|
||||
create or replace function validate_question_bank_preference()
|
||||
returns trigger language plpgsql as $$
|
||||
declare owner_mode text;
|
||||
begin
|
||||
select mode into owner_mode from tenants where id = new.question_bank_owner_tenant_id;
|
||||
if owner_mode <> 'platform_owned' then
|
||||
raise exception 'Question bank preferences can only target platform-owned banks';
|
||||
end if;
|
||||
return new;
|
||||
end $$;
|
||||
create trigger trg_validate_question_bank_preference before insert or update on tenant_question_bank_preferences
|
||||
for each row execute function validate_question_bank_preference();
|
||||
""");
|
||||
migrationBuilder.Sql(PostgreSqlTenantConstraintSql.CreateTenantQuestionReferenceGuard);
|
||||
migrationBuilder.Sql(PostgreSqlTenantConstraintSql.CreateTaxonomyParentGuard);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("""
|
||||
drop trigger if exists trg_validate_question_bank_preference on tenant_question_bank_preferences;
|
||||
drop function if exists validate_question_bank_preference();
|
||||
drop trigger if exists trg_validate_taxonomy_owner_boundary on taxonomy_nodes;
|
||||
drop function if exists validate_taxonomy_owner_boundary();
|
||||
drop trigger if exists trg_validate_tenant_question_reference on tenant_question_references;
|
||||
drop function if exists validate_tenant_question_reference();
|
||||
""");
|
||||
migrationBuilder.Sql(PostgreSqlTenantConstraintSql.DropTenantGuards);
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_content_entries_regions_tenant_id_region_id",
|
||||
@@ -8970,6 +9353,9 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
migrationBuilder.DropTable(
|
||||
name: "auth_sessions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "background_jobs");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "banners");
|
||||
|
||||
@@ -9042,6 +9428,15 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
migrationBuilder.DropTable(
|
||||
name: "platform_audit_alerts");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "platform_backend_role_menus");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "platform_backend_role_permissions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "platform_backend_user_roles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "platform_dunning_notification_events");
|
||||
|
||||
@@ -9114,6 +9509,15 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
migrationBuilder.DropTable(
|
||||
name: "subject_shares");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "tenant_backend_role_menus");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "tenant_backend_role_permissions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "tenant_backend_user_roles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "tenant_billing_profiles");
|
||||
|
||||
@@ -9225,6 +9629,9 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
migrationBuilder.DropTable(
|
||||
name: "platform_audit_alert_rules");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "platform_backend_roles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "platform_dunning_notification_channels");
|
||||
|
||||
@@ -9249,6 +9656,12 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
migrationBuilder.DropTable(
|
||||
name: "reports");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "backend_menus");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "tenant_backend_roles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "tenant_role_templates");
|
||||
|
||||
@@ -9285,6 +9698,9 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
migrationBuilder.DropTable(
|
||||
name: "practice_sessions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "backend_permissions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "vocabulary_units");
|
||||
|
||||
@@ -9876,6 +9876,268 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
b.ToTable("audit_logs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.BackendMenu", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<string>("Area")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("area");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(160)
|
||||
.HasColumnType("character varying(160)")
|
||||
.HasColumnName("code");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<string>("Icon")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("icon");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_active");
|
||||
|
||||
b.Property<string>("ParentCode")
|
||||
.HasMaxLength(160)
|
||||
.HasColumnType("character varying(160)")
|
||||
.HasColumnName("parent_code");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<string>("PermissionCode")
|
||||
.HasMaxLength(160)
|
||||
.HasColumnType("character varying(160)")
|
||||
.HasColumnName("permission_code");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_backend_menus");
|
||||
|
||||
b.HasAlternateKey("Code")
|
||||
.HasName("ak_backend_menus_code");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_backend_menus_code");
|
||||
|
||||
b.HasIndex("PermissionCode")
|
||||
.HasDatabaseName("ix_backend_menus_permission_code");
|
||||
|
||||
b.HasIndex("Area", "ParentCode", "SortOrder")
|
||||
.HasDatabaseName("ix_backend_menus_area_parent_code_sort_order");
|
||||
|
||||
b.ToTable("backend_menus", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.BackendPermission", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<string>("Area")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("area");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(160)
|
||||
.HasColumnType("character varying(160)")
|
||||
.HasColumnName("code");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<bool>("IsSystem")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_system");
|
||||
|
||||
b.Property<string>("Module")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("module");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_backend_permissions");
|
||||
|
||||
b.HasAlternateKey("Code")
|
||||
.HasName("ak_backend_permissions_code");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_backend_permissions_code");
|
||||
|
||||
b.HasIndex("Area", "Module", "SortOrder")
|
||||
.HasDatabaseName("ix_backend_permissions_area_module_sort_order");
|
||||
|
||||
b.ToTable("backend_permissions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.BackgroundJob", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<DateTimeOffset?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("completed_at");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<string>("JobType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("job_type");
|
||||
|
||||
b.Property<string>("LastError")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("character varying(4000)")
|
||||
.HasColumnName("last_error");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockExpiresAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("lock_expires_at");
|
||||
|
||||
b.Property<string>("LockedBy")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)")
|
||||
.HasColumnName("locked_by");
|
||||
|
||||
b.Property<int>("MaxRetries")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("max_retries");
|
||||
|
||||
b.Property<Guid?>("OutputAssetId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("output_asset_id");
|
||||
|
||||
b.Property<JsonElement>("Payload")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("payload")
|
||||
.HasDefaultValueSql("'{}'::jsonb");
|
||||
|
||||
b.Property<JsonElement>("Result")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("result")
|
||||
.HasDefaultValueSql("'{}'::jsonb");
|
||||
|
||||
b.Property<int>("RetryCount")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("retry_count");
|
||||
|
||||
b.Property<DateTimeOffset?>("RunAfter")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("run_after");
|
||||
|
||||
b.Property<DateTimeOffset?>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("tenant_id");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_background_jobs");
|
||||
|
||||
b.HasAlternateKey("TenantId", "Id")
|
||||
.HasName("ak_background_jobs_tenant_id_id");
|
||||
|
||||
b.HasIndex("TenantId", "OutputAssetId")
|
||||
.HasDatabaseName("ix_background_jobs_tenant_id_output_asset_id");
|
||||
|
||||
b.HasIndex("TenantId", "JobType", "Status", "CreatedAt")
|
||||
.HasDatabaseName("ix_background_jobs_tenant_id_job_type_status_created_at");
|
||||
|
||||
b.HasIndex("TenantId", "Status", "RunAfter", "CreatedAt")
|
||||
.HasDatabaseName("ix_background_jobs_tenant_id_status_run_after_created_at");
|
||||
|
||||
b.ToTable("background_jobs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.Badge", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -10135,6 +10397,381 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
b.ToTable("faqs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.PlatformBackendRole", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("code");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<bool>("IsSystem")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_system");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_platform_backend_roles");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_platform_backend_roles_code");
|
||||
|
||||
b.HasIndex("Status", "Code")
|
||||
.HasDatabaseName("ix_platform_backend_roles_status_code");
|
||||
|
||||
b.ToTable("platform_backend_roles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.PlatformBackendRoleMenu", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<string>("MenuCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(160)
|
||||
.HasColumnType("character varying(160)")
|
||||
.HasColumnName("menu_code");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("role_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_platform_backend_role_menus");
|
||||
|
||||
b.HasIndex("MenuCode")
|
||||
.HasDatabaseName("ix_platform_backend_role_menus_menu_code");
|
||||
|
||||
b.HasIndex("RoleId", "MenuCode")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_platform_backend_role_menus_role_id_menu_code");
|
||||
|
||||
b.ToTable("platform_backend_role_menus", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.PlatformBackendRolePermission", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<string>("PermissionCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(160)
|
||||
.HasColumnType("character varying(160)")
|
||||
.HasColumnName("permission_code");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("role_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_platform_backend_role_permissions");
|
||||
|
||||
b.HasIndex("PermissionCode")
|
||||
.HasDatabaseName("ix_platform_backend_role_permissions_permission_code");
|
||||
|
||||
b.HasIndex("RoleId", "PermissionCode")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_platform_backend_role_permissions_role_id_permission_code");
|
||||
|
||||
b.ToTable("platform_backend_role_permissions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.PlatformBackendUserRole", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("role_id");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_platform_backend_user_roles");
|
||||
|
||||
b.HasIndex("RoleId")
|
||||
.HasDatabaseName("ix_platform_backend_user_roles_role_id");
|
||||
|
||||
b.HasIndex("UserId", "RoleId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_platform_backend_user_roles_user_id_role_id");
|
||||
|
||||
b.ToTable("platform_backend_user_roles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.TenantBackendRole", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("code");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<JsonElement>("DataScope")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("data_scope")
|
||||
.HasDefaultValueSql("'{}'::jsonb");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<bool>("IsSystem")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_system");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("tenant_id");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tenant_backend_roles");
|
||||
|
||||
b.HasAlternateKey("TenantId", "Id")
|
||||
.HasName("ak_tenant_backend_roles_tenant_id_id");
|
||||
|
||||
b.HasIndex("TenantId", "Code")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_tenant_backend_roles_tenant_id_code");
|
||||
|
||||
b.HasIndex("TenantId", "Status", "Code")
|
||||
.HasDatabaseName("ix_tenant_backend_roles_tenant_id_status_code");
|
||||
|
||||
b.ToTable("tenant_backend_roles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.TenantBackendRoleMenu", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<string>("MenuCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(160)
|
||||
.HasColumnType("character varying(160)")
|
||||
.HasColumnName("menu_code");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("role_id");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("tenant_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tenant_backend_role_menus");
|
||||
|
||||
b.HasAlternateKey("TenantId", "Id")
|
||||
.HasName("ak_tenant_backend_role_menus_tenant_id_id");
|
||||
|
||||
b.HasIndex("MenuCode")
|
||||
.HasDatabaseName("ix_tenant_backend_role_menus_menu_code");
|
||||
|
||||
b.HasIndex("TenantId", "RoleId", "MenuCode")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_tenant_backend_role_menus_tenant_id_role_id_menu_code");
|
||||
|
||||
b.ToTable("tenant_backend_role_menus", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.TenantBackendRolePermission", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<string>("PermissionCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(160)
|
||||
.HasColumnType("character varying(160)")
|
||||
.HasColumnName("permission_code");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("role_id");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("tenant_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tenant_backend_role_permissions");
|
||||
|
||||
b.HasAlternateKey("TenantId", "Id")
|
||||
.HasName("ak_tenant_backend_role_permissions_tenant_id_id");
|
||||
|
||||
b.HasIndex("PermissionCode")
|
||||
.HasDatabaseName("ix_tenant_backend_role_permissions_permission_code");
|
||||
|
||||
b.HasIndex("TenantId", "RoleId", "PermissionCode")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_tenant_backend_role_permissions_tenant_id_role_id_permissio~");
|
||||
|
||||
b.ToTable("tenant_backend_role_permissions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.TenantBackendUserRole", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("role_id");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("tenant_id");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tenant_backend_user_roles");
|
||||
|
||||
b.HasAlternateKey("TenantId", "Id")
|
||||
.HasName("ak_tenant_backend_user_roles_tenant_id_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_tenant_backend_user_roles_user_id");
|
||||
|
||||
b.HasIndex("TenantId", "RoleId")
|
||||
.HasDatabaseName("ix_tenant_backend_user_roles_tenant_id_role_id");
|
||||
|
||||
b.HasIndex("TenantId", "UserId", "RoleId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_tenant_backend_user_roles_tenant_id_user_id_role_id");
|
||||
|
||||
b.ToTable("tenant_backend_user_roles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.TenantContentNotification", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -15991,6 +16628,33 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
.HasConstraintName("fk_audit_logs_tenants_tenant_id");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.BackendMenu", b =>
|
||||
{
|
||||
b.HasOne("Tiku.Domain.Operations.BackendPermission", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("PermissionCode")
|
||||
.HasPrincipalKey("Code")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("fk_backend_menus_backend_permissions_permission_code");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.BackgroundJob", b =>
|
||||
{
|
||||
b.HasOne("Tiku.Domain.Tenancy.Tenant", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TenantId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_background_jobs_tenants_tenant_id");
|
||||
|
||||
b.HasOne("Tiku.Domain.Content.ContentAsset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TenantId", "OutputAssetId")
|
||||
.HasPrincipalKey("TenantId", "Id")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("fk_background_jobs_content_assets_tenant_id_output_asset_id");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.Badge", b =>
|
||||
{
|
||||
b.HasOne("Tiku.Domain.Tenancy.Tenant", null)
|
||||
@@ -16035,6 +16699,125 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
.HasConstraintName("fk_faqs_regions_tenant_id_region_id");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.PlatformBackendRoleMenu", b =>
|
||||
{
|
||||
b.HasOne("Tiku.Domain.Operations.BackendMenu", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MenuCode")
|
||||
.HasPrincipalKey("Code")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_platform_backend_role_menus_backend_menus_menu_code");
|
||||
|
||||
b.HasOne("Tiku.Domain.Operations.PlatformBackendRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_platform_backend_role_menus_platform_backend_roles_role_id");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.PlatformBackendRolePermission", b =>
|
||||
{
|
||||
b.HasOne("Tiku.Domain.Operations.BackendPermission", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("PermissionCode")
|
||||
.HasPrincipalKey("Code")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_platform_backend_role_permissions_backend_permissions_permi~");
|
||||
|
||||
b.HasOne("Tiku.Domain.Operations.PlatformBackendRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_platform_backend_role_permissions_platform_backend_roles_ro~");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.PlatformBackendUserRole", b =>
|
||||
{
|
||||
b.HasOne("Tiku.Domain.Operations.PlatformBackendRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_platform_backend_user_roles_platform_backend_roles_role_id");
|
||||
|
||||
b.HasOne("Tiku.Domain.Identity.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_platform_backend_user_roles_users_user_id");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.TenantBackendRole", b =>
|
||||
{
|
||||
b.HasOne("Tiku.Domain.Tenancy.Tenant", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TenantId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tenant_backend_roles_tenants_tenant_id");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.TenantBackendRoleMenu", b =>
|
||||
{
|
||||
b.HasOne("Tiku.Domain.Operations.BackendMenu", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MenuCode")
|
||||
.HasPrincipalKey("Code")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tenant_backend_role_menus_backend_menus_menu_code");
|
||||
|
||||
b.HasOne("Tiku.Domain.Operations.TenantBackendRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TenantId", "RoleId")
|
||||
.HasPrincipalKey("TenantId", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tenant_backend_role_menus_tenant_backend_roles_tenant_id_ro~");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.TenantBackendRolePermission", b =>
|
||||
{
|
||||
b.HasOne("Tiku.Domain.Operations.BackendPermission", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("PermissionCode")
|
||||
.HasPrincipalKey("Code")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tenant_backend_role_permissions_backend_permissions_permiss~");
|
||||
|
||||
b.HasOne("Tiku.Domain.Operations.TenantBackendRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TenantId", "RoleId")
|
||||
.HasPrincipalKey("TenantId", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tenant_backend_role_permissions_tenant_backend_roles_tenant~");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.TenantBackendUserRole", b =>
|
||||
{
|
||||
b.HasOne("Tiku.Domain.Identity.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tenant_backend_user_roles_users_user_id");
|
||||
|
||||
b.HasOne("Tiku.Domain.Operations.TenantBackendRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TenantId", "RoleId")
|
||||
.HasPrincipalKey("TenantId", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tenant_backend_user_roles_tenant_backend_roles_tenant_id_ro~");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Operations.TenantContentNotification", b =>
|
||||
{
|
||||
b.HasOne("Tiku.Domain.Identity.User", null)
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
namespace Tiku.Infrastructure.Persistence;
|
||||
|
||||
internal static class PostgreSqlTenantConstraintSql
|
||||
{
|
||||
public const string CreateTenantQuestionReferenceGuard = """
|
||||
create or replace function tiku_guard_tenant_question_reference()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
declare
|
||||
owner_mode text;
|
||||
begin
|
||||
select mode into owner_mode
|
||||
from tenants
|
||||
where id = new.question_owner_tenant_id;
|
||||
|
||||
if owner_mode = 'platform_owned' then
|
||||
if new.source <> 'platform' then
|
||||
raise exception 'tenant question reference to platform question must use platform source'
|
||||
using errcode = '23514',
|
||||
constraint = 'ck_tenant_question_references_platform_or_self_owner';
|
||||
end if;
|
||||
|
||||
return new;
|
||||
end if;
|
||||
|
||||
if new.question_owner_tenant_id = new.tenant_id then
|
||||
if new.source <> 'tenant' then
|
||||
raise exception 'tenant question reference to tenant-owned question must use tenant source'
|
||||
using errcode = '23514',
|
||||
constraint = 'ck_tenant_question_references_platform_or_self_owner';
|
||||
end if;
|
||||
|
||||
return new;
|
||||
end if;
|
||||
|
||||
raise exception 'tenant question reference cannot point to another tenant private question'
|
||||
using errcode = '23514',
|
||||
constraint = 'ck_tenant_question_references_platform_or_self_owner';
|
||||
end;
|
||||
$$;
|
||||
|
||||
create trigger trg_tenant_question_references_platform_or_self_owner
|
||||
before insert or update of tenant_id, question_owner_tenant_id, question_id, source
|
||||
on tenant_question_references
|
||||
for each row
|
||||
execute function tiku_guard_tenant_question_reference();
|
||||
""";
|
||||
|
||||
public const string CreateTaxonomyParentGuard = """
|
||||
create or replace function tiku_guard_taxonomy_parent_owner()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
declare
|
||||
parent_owner_mode text;
|
||||
begin
|
||||
if new.parent_owner_tenant_id is null and new.parent_id is null then
|
||||
return new;
|
||||
end if;
|
||||
|
||||
select mode into parent_owner_mode
|
||||
from tenants
|
||||
where id = new.parent_owner_tenant_id;
|
||||
|
||||
if parent_owner_mode = 'platform_owned' or new.parent_owner_tenant_id = new.tenant_id then
|
||||
return new;
|
||||
end if;
|
||||
|
||||
raise exception 'taxonomy parent must belong to platform or current tenant'
|
||||
using errcode = '23514',
|
||||
constraint = 'ck_taxonomy_nodes_parent_platform_or_self_owner';
|
||||
end;
|
||||
$$;
|
||||
|
||||
create trigger trg_taxonomy_nodes_parent_platform_or_self_owner
|
||||
before insert or update of tenant_id, parent_owner_tenant_id, parent_id
|
||||
on taxonomy_nodes
|
||||
for each row
|
||||
execute function tiku_guard_taxonomy_parent_owner();
|
||||
""";
|
||||
|
||||
public const string DropTenantGuards = """
|
||||
drop trigger if exists trg_tenant_question_references_platform_or_self_owner on tenant_question_references;
|
||||
drop function if exists tiku_guard_tenant_question_reference();
|
||||
drop trigger if exists trg_taxonomy_nodes_parent_platform_or_self_owner on taxonomy_nodes;
|
||||
drop function if exists tiku_guard_taxonomy_parent_owner();
|
||||
""";
|
||||
}
|
||||
@@ -153,6 +153,17 @@ public sealed class TikuDbContext(
|
||||
public DbSet<Faq> Faqs => Set<Faq>();
|
||||
public DbSet<Announcement> Announcements => Set<Announcement>();
|
||||
public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
|
||||
public DbSet<BackendPermission> BackendPermissions => Set<BackendPermission>();
|
||||
public DbSet<BackendMenu> BackendMenus => Set<BackendMenu>();
|
||||
public DbSet<TenantBackendRole> TenantBackendRoles => Set<TenantBackendRole>();
|
||||
public DbSet<TenantBackendRolePermission> TenantBackendRolePermissions => Set<TenantBackendRolePermission>();
|
||||
public DbSet<TenantBackendRoleMenu> TenantBackendRoleMenus => Set<TenantBackendRoleMenu>();
|
||||
public DbSet<TenantBackendUserRole> TenantBackendUserRoles => Set<TenantBackendUserRole>();
|
||||
public DbSet<PlatformBackendRole> PlatformBackendRoles => Set<PlatformBackendRole>();
|
||||
public DbSet<PlatformBackendRolePermission> PlatformBackendRolePermissions => Set<PlatformBackendRolePermission>();
|
||||
public DbSet<PlatformBackendRoleMenu> PlatformBackendRoleMenus => Set<PlatformBackendRoleMenu>();
|
||||
public DbSet<PlatformBackendUserRole> PlatformBackendUserRoles => Set<PlatformBackendUserRole>();
|
||||
public DbSet<BackgroundJob> BackgroundJobs => Set<BackgroundJob>();
|
||||
public DbSet<UserNotification> UserNotifications => Set<UserNotification>();
|
||||
public DbSet<Badge> Badges => Set<Badge>();
|
||||
public DbSet<UserBadge> UserBadges => Set<UserBadge>();
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AlibabaCloud.OSS.V2" />
|
||||
<PackageReference Include="AlibabaCloud.SDK.Dysmsapi20170525" />
|
||||
<PackageReference Include="AlipaySDKNet.Standard" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
||||
@@ -17,8 +18,8 @@
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
|
||||
<PackageReference Include="Senparc.Weixin" />
|
||||
<PackageReference Include="Senparc.Weixin.MP" />
|
||||
<PackageReference Include="Senparc.Weixin.TenPayV3" />
|
||||
<PackageReference Include="Senparc.Weixin.WxOpen" />
|
||||
<PackageReference Include="SKIT.FlurlHttpClient.Wechat.TenpayV3" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user