218 lines
9.8 KiB
C#
218 lines
9.8 KiB
C#
using System.Text.Json;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Tiku.Application.PlatformAdmin;
|
|
using Tiku.Application.Security;
|
|
using Tiku.Application.Tenancy;
|
|
using Tiku.Domain.Common;
|
|
using Tiku.Domain.Growth;
|
|
using Tiku.Domain.Operations;
|
|
using Tiku.Domain.Platform;
|
|
using Tiku.Domain.Tenancy;
|
|
using Tiku.Infrastructure.Persistence;
|
|
|
|
namespace Tiku.Infrastructure.PlatformAdmin;
|
|
|
|
internal sealed class PlatformCrmAdminService(ITenantExecutionScope tenantExecutionScope) : IPlatformCrmAdminService
|
|
{
|
|
public Task<PlatformTenantCapabilityList<PlatformCrmConfigItem>> GetConfigsAsync(
|
|
PlatformCapabilityActor actor,
|
|
PlatformCapabilityQuery query,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return ExecuteAsync("platform crm configs list", async (services, token) =>
|
|
{
|
|
var db = services.GetRequiredService<IPlatformTenantCapabilitiesPersistence>();
|
|
var values =
|
|
from config in db.CrmConfigs.AsNoTracking()
|
|
join tenant in db.Tenants.AsNoTracking() on config.TenantId equals tenant.Id
|
|
select new { config, tenant.Name };
|
|
if (query.TenantId.HasValue) values = values.Where(value => value.config.TenantId == query.TenantId);
|
|
if (!string.IsNullOrWhiteSpace(query.Status))
|
|
{
|
|
var enabled = IsEnabledStatus(query.Status);
|
|
values = values.Where(value => value.config.Enabled == enabled);
|
|
}
|
|
|
|
var items = await values
|
|
.OrderByDescending(value => value.config.UpdatedAt)
|
|
.Take(Limit(query.Limit))
|
|
.Select(value => ToCrmConfigItem(value.config, value.Name))
|
|
.ToArrayAsync(token);
|
|
return new PlatformTenantCapabilityList<PlatformCrmConfigItem>(items);
|
|
}, cancellationToken);
|
|
}
|
|
|
|
public Task<PlatformCrmConfigItem> UpsertConfigAsync(
|
|
PlatformCapabilityActor actor,
|
|
UpsertPlatformCrmConfigCommand command,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return ExecuteAsync("platform crm config upsert", async (services, token) =>
|
|
{
|
|
var db = services.GetRequiredService<IPlatformTenantCapabilitiesPersistence>();
|
|
var tenantName = await db.Tenants
|
|
.Where(tenant => tenant.Id == command.TenantId)
|
|
.Select(tenant => tenant.Name)
|
|
.SingleOrDefaultAsync(token)
|
|
?? throw Error("Tenant was not found.", "tenant_not_found");
|
|
var item = command.Id.HasValue
|
|
? await db.CrmConfigs.SingleOrDefaultAsync(
|
|
value => value.Id == command.Id.Value && value.TenantId == command.TenantId, token)
|
|
: await db.CrmConfigs.SingleOrDefaultAsync(value => value.TenantId == command.TenantId, token);
|
|
if (item is null)
|
|
{
|
|
item = new CrmConfig { TenantId = command.TenantId };
|
|
db.CrmConfigs.Add(item);
|
|
}
|
|
|
|
item.Enabled = command.Enabled;
|
|
item.Url = Normalize(command.Url);
|
|
item.SecretRef = Normalize(command.SecretRef);
|
|
item.FormName = Normalize(command.FormName);
|
|
item.ExamType = Normalize(command.ExamType);
|
|
item.TimeoutSeconds = command.TimeoutSeconds;
|
|
item.DelaySeconds = command.DelaySeconds;
|
|
item.AssignmentMode = ParseEnum(command.AssignmentMode, ReferralAssignmentMode.None);
|
|
item.AssignmentPool = command.AssignmentPool ?? JsonDefaults.Array();
|
|
item.AssignmentConfig = command.AssignmentConfig ?? JsonDefaults.Object();
|
|
item.UpdatedAt = DateTimeOffset.UtcNow;
|
|
AddAudit(db, actor, command.TenantId, "platform.crm.config.upserted", "crm_config", item.Id,
|
|
new { item.Enabled, item.Url, item.AssignmentMode });
|
|
await db.SaveChangesAsync(token);
|
|
return ToCrmConfigItem(item, tenantName);
|
|
}, cancellationToken);
|
|
}
|
|
|
|
public Task<PlatformTenantCapabilityList<CrmWebhookQueueItem>> GetLeadsAsync(
|
|
PlatformCapabilityActor actor,
|
|
PlatformCapabilityQuery query,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return ExecuteAsync("platform crm leads list", async (services, token) =>
|
|
{
|
|
var db = services.GetRequiredService<IPlatformTenantCapabilitiesPersistence>();
|
|
var values = db.CrmWebhookQueue.AsNoTracking();
|
|
if (query.TenantId.HasValue) values = values.Where(value => value.TenantId == query.TenantId);
|
|
if (!string.IsNullOrWhiteSpace(query.Status))
|
|
values = values.Where(value => value.Status == ParseEnum(query.Status, CrmWebhookQueueStatus.Pending));
|
|
var items = await values.OrderByDescending(value => value.UpdatedAt).Take(Limit(query.Limit))
|
|
.ToArrayAsync(token);
|
|
return new PlatformTenantCapabilityList<CrmWebhookQueueItem>(items);
|
|
}, cancellationToken);
|
|
}
|
|
|
|
public Task<CrmWebhookQueueItem> RetryLeadAsync(
|
|
PlatformCapabilityActor actor,
|
|
PlatformCrmLeadRetryCommand command,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return ExecuteAsync("platform crm lead retry", async (services, token) =>
|
|
{
|
|
var db = services.GetRequiredService<IPlatformTenantCapabilitiesPersistence>();
|
|
var item = await db.CrmWebhookQueue.SingleOrDefaultAsync(value => value.Id == command.QueueId, token)
|
|
?? throw Error("CRM queue item was not found.", "crm_queue_not_found");
|
|
if (item.Status is not (CrmWebhookQueueStatus.Failed or CrmWebhookQueueStatus.Discarded
|
|
or CrmWebhookQueueStatus.Retrying))
|
|
throw Error("Only failed CRM queue items can be retried.", "crm_queue_retry_invalid");
|
|
|
|
item.Status = CrmWebhookQueueStatus.Retrying;
|
|
item.NextAttemptAt = DateTimeOffset.UtcNow;
|
|
item.LastError = null;
|
|
item.UpdatedAt = DateTimeOffset.UtcNow;
|
|
AddAudit(db, actor, item.TenantId, "platform.crm.lead.retry", "crm_webhook_queue", item.Id,
|
|
new { command.Note });
|
|
await db.SaveChangesAsync(token);
|
|
return item;
|
|
}, cancellationToken);
|
|
}
|
|
|
|
public Task<PlatformTenantCapabilityList<CrmWebhookLog>> GetLogsAsync(
|
|
PlatformCapabilityActor actor,
|
|
Guid? tenantId,
|
|
Guid? queueId,
|
|
int limit,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return ExecuteAsync("platform crm logs list", async (services, token) =>
|
|
{
|
|
var db = services.GetRequiredService<IPlatformTenantCapabilitiesPersistence>();
|
|
var values = db.CrmWebhookLogs.AsNoTracking();
|
|
if (tenantId.HasValue) values = values.Where(value => value.TenantId == tenantId);
|
|
if (queueId.HasValue)
|
|
{
|
|
var queueRecordId = await db.CrmWebhookQueue.AsNoTracking()
|
|
.Where(value => value.Id == queueId.Value)
|
|
.Select(value => value.RecordId)
|
|
.SingleOrDefaultAsync(token);
|
|
values = values.Where(value => value.RecordId == queueRecordId);
|
|
}
|
|
|
|
var items = await values.OrderByDescending(value => value.CreatedAt).Take(Limit(limit)).ToArrayAsync(token);
|
|
return new PlatformTenantCapabilityList<CrmWebhookLog>(items);
|
|
}, cancellationToken);
|
|
}
|
|
|
|
private static PlatformCrmConfigItem ToCrmConfigItem(CrmConfig config, string tenantName)
|
|
{
|
|
return new PlatformCrmConfigItem(config.Id, config.TenantId, tenantName, config.Enabled, config.Url,
|
|
config.SecretRef,
|
|
config.FormName, config.ExamType,
|
|
config.TimeoutSeconds, config.DelaySeconds, config.AssignmentMode, config.AssignmentPool,
|
|
config.AssignmentConfig, config.UpdatedAt);
|
|
}
|
|
|
|
private Task<TResult> ExecuteAsync<TResult>(string reason,
|
|
Func<IServiceProvider, CancellationToken, Task<TResult>> operation, CancellationToken cancellationToken)
|
|
{
|
|
return tenantExecutionScope.ExecuteAsync(
|
|
new SystemScopeRequest(null, SystemScopeCallerType.Platform, nameof(PlatformCrmAdminService), reason,
|
|
Guid.NewGuid().ToString("N"), true), operation, cancellationToken);
|
|
}
|
|
|
|
private static bool IsEnabledStatus(string value)
|
|
{
|
|
return value.Equals("active", StringComparison.OrdinalIgnoreCase) ||
|
|
value.Equals("enabled", StringComparison.OrdinalIgnoreCase) || value == "正常";
|
|
}
|
|
|
|
private static PlatformCapabilityException Error(string message, string code)
|
|
{
|
|
return new PlatformCapabilityException(message, code);
|
|
}
|
|
|
|
private static int Limit(int value)
|
|
{
|
|
return Math.Clamp(value, 1, 500);
|
|
}
|
|
|
|
private static string? Normalize(string? value)
|
|
{
|
|
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
|
}
|
|
|
|
private static T ParseEnum<T>(string? value, T fallback) where T : struct, Enum
|
|
{
|
|
return Enum.TryParse<T>(NormalizeEnum(value), true, out var parsed) ? parsed : fallback;
|
|
}
|
|
|
|
private static string? NormalizeEnum(string? value)
|
|
{
|
|
return value?.Trim().Replace("-", "_", StringComparison.Ordinal);
|
|
}
|
|
|
|
private static void AddAudit(IPlatformTenantCapabilitiesPersistence db, PlatformCapabilityActor actor, Guid tenantId, string action,
|
|
string targetType, Guid targetId, object details)
|
|
{
|
|
db.AuditLogs.Add(new AuditLog
|
|
{
|
|
TenantId = tenantId,
|
|
ActorUserId = actor.UserId,
|
|
Action = action,
|
|
TargetType = targetType,
|
|
TargetId = targetId.ToString(),
|
|
Details = JsonSerializer.SerializeToElement(details)
|
|
});
|
|
}
|
|
}
|