admin/src/Oms.Application/Services/System/SystemOtherConfigService.cs
zzh b17e6639a4
All checks were successful
Build and Deploy / deploy (push) Successful in 1m3s
fix: 主数据新增兼容软删唯一键,避免 Duplicate entry
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-05 11:53:08 +08:00

328 lines
13 KiB
C#

using System.Linq.Expressions;
using Fake.Application;
using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Shared;
using Oms.Application.Dtos.System;
using Oms.Application.Shared;
using Oms.Domain.SystemAggregate.Consts;
using Oms.Domain.SystemAggregate.Entities;
using Oms.Domain.SystemAggregate.Repositories;
namespace Oms.Application.Services.System;
/// <summary>
/// 系统其他配置 API 服务。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-System")]
public class SystemOtherConfigService : ApplicationService
{
private const string SecretMask = "******";
private ISystemOtherConfigRepository SystemOtherConfigRepository =>
LazyServiceProvider.GetRequiredService<ISystemOtherConfigRepository>();
/// <summary>
/// 查询系统其他配置列表。
/// </summary>
[HttpGet("/system/other-config/list")]
public async Task<OmsPageResult<SystemOtherConfigOutput>> GetListAsync([FromQuery] SystemOtherConfigListInput input)
{
var predicate = BuildListPredicate(input);
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
var items = await SystemOtherConfigRepository.GetPagedListAsync(
predicate,
pageIndex,
pageSize,
new Dictionary<string, bool> { [nameof(SystemOtherConfig.Id)] = false });
var total = await SystemOtherConfigRepository.CountAsync(predicate);
return new OmsPageResult<SystemOtherConfigOutput>
{
Items = items.Select(x => MapOutput(x, maskSecret: true)).ToList(),
Total = total
};
}
/// <summary>
/// 查询系统其他配置详情。
/// </summary>
[HttpGet("/system/other-config/detail")]
public async Task<SystemOtherConfigOutput> GetDetailAsync([FromQuery] int id)
{
var entity = await GetConfigOrThrowAsync(id);
return MapOutput(entity, maskSecret: false);
}
/// <summary>
/// 新增系统其他配置。
/// </summary>
[HttpPost("/system/other-config/create")]
public async Task CreateAsync(SystemOtherConfigCreateInput input)
{
await ValidateInputAsync(input, null);
var configCode = input.ConfigCode.Trim();
var deleted = await SystemOtherConfigRepository.GetByConfigCodeIncludingDeletedAsync(configCode);
if (deleted != null && deleted.IsDeleted)
{
deleted.IsDeleted = false;
deleted.ConfigCode = configCode;
deleted.ConfigName = input.ConfigName.Trim();
deleted.Enabled = input.Enabled;
deleted.BaseUrl = NormalizeOptional(input.BaseUrl);
deleted.AppKey = NormalizeOptional(input.AppKey);
deleted.AppSecret = NormalizeOptional(input.AppSecret);
deleted.TimeoutSeconds = NormalizeTimeoutSeconds(input.TimeoutSeconds);
deleted.Remark = NormalizeOptional(input.Remark);
await SystemOtherConfigRepository.UpdateAsync(deleted);
return;
}
await SystemOtherConfigRepository.InsertAsync(new SystemOtherConfig
{
ConfigCode = configCode,
ConfigName = input.ConfigName.Trim(),
Enabled = input.Enabled,
BaseUrl = NormalizeOptional(input.BaseUrl),
AppKey = NormalizeOptional(input.AppKey),
AppSecret = NormalizeOptional(input.AppSecret),
TimeoutSeconds = NormalizeTimeoutSeconds(input.TimeoutSeconds),
Remark = NormalizeOptional(input.Remark)
});
}
/// <summary>
/// 更新系统其他配置。
/// </summary>
[HttpPost("/system/other-config/update")]
public async Task UpdateAsync(SystemOtherConfigUpdateInput input)
{
var entity = await GetConfigOrThrowAsync(input.Id);
await ValidateInputAsync(input, entity.Id);
var configCode = input.ConfigCode.Trim();
var softDeleted = await SystemOtherConfigRepository.GetByConfigCodeIncludingDeletedAsync(configCode);
if (softDeleted != null && softDeleted.IsDeleted && softDeleted.Id != entity.Id)
{
softDeleted.ConfigCode = $"{configCode}__deleted_{softDeleted.Id}";
await SystemOtherConfigRepository.UpdateAsync(softDeleted);
}
var clearWeChatAccessToken = ShouldClearWeChatAccessToken(entity, input);
entity.ConfigCode = configCode;
entity.ConfigName = input.ConfigName.Trim();
entity.Enabled = input.Enabled;
entity.BaseUrl = NormalizeOptional(input.BaseUrl);
entity.AppKey = NormalizeOptional(input.AppKey);
if (!ShouldKeepExistingSecret(input.AppSecret))
{
entity.AppSecret = NormalizeOptional(input.AppSecret);
}
entity.TimeoutSeconds = NormalizeTimeoutSeconds(input.TimeoutSeconds);
entity.Remark = NormalizeOptional(input.Remark);
if (clearWeChatAccessToken)
{
entity.AccessToken = null;
entity.AccessTokenExpiresIn = 0;
entity.AccessTokenUpdatedAt = null;
}
await SystemOtherConfigRepository.UpdateAsync(entity);
}
/// <summary>
/// 删除系统其他配置。
/// </summary>
[HttpPost("/system/other-config/delete")]
public async Task DeleteAsync(IdInput input)
{
var entity = await GetConfigOrThrowAsync(input.Id);
await SystemOtherConfigRepository.DeleteAsync(entity);
}
private static Expression<Func<SystemOtherConfig, bool>> BuildListPredicate(SystemOtherConfigListInput input)
{
var keyword = input.Keyword?.Trim();
return x =>
string.IsNullOrWhiteSpace(keyword) ||
x.ConfigCode.Contains(keyword) ||
x.ConfigName.Contains(keyword);
}
private async Task<SystemOtherConfig> GetConfigOrThrowAsync(int id)
{
var entity = await SystemOtherConfigRepository.FirstOrDefaultAsync(x => x.Id == id);
if (entity == null)
{
throw new BusinessException("配置不存在");
}
return entity;
}
private async Task ValidateInputAsync(SystemOtherConfigCreateInput input, int? excludeId)
{
if (string.IsNullOrWhiteSpace(input.ConfigCode))
{
throw new BusinessException("配置编码不能为空");
}
if (string.IsNullOrWhiteSpace(input.ConfigName))
{
throw new BusinessException("配置名称不能为空");
}
var configCode = input.ConfigCode.Trim();
var existed = await SystemOtherConfigRepository.GetByConfigCodeAsync(configCode);
if (existed != null && existed.Id != excludeId)
{
throw new BusinessException("配置编码已存在");
}
if (string.Equals(configCode, SystemOtherConfigCodes.TrackService, StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrWhiteSpace(input.BaseUrl))
{
throw new BusinessException("轨迹服务地址不能为空");
}
if (string.IsNullOrWhiteSpace(input.AppKey))
{
throw new BusinessException("轨迹服务 AppKey 不能为空");
}
if (excludeId == null && ShouldKeepExistingSecret(input.AppSecret))
{
throw new BusinessException("轨迹服务 AppSecret 不能为空");
}
}
else if (string.Equals(configCode, SystemOtherConfigCodes.ApiCenter, StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrWhiteSpace(input.BaseUrl))
{
throw new BusinessException("API 中心服务地址不能为空");
}
if (string.IsNullOrWhiteSpace(input.AppKey))
{
throw new BusinessException("API 中心应用 AppKey 不能为空");
}
if (excludeId == null && ShouldKeepExistingSecret(input.AppSecret))
{
throw new BusinessException("API 中心应用 AppSecret 不能为空");
}
}
else if (string.Equals(configCode, SystemOtherConfigCodes.PickupOpenApi, StringComparison.OrdinalIgnoreCase))
{
// 鉴权已改由 itfx 上游完成;本配置不再强制 AppKey
}
else if (string.Equals(configCode, SystemOtherConfigCodes.WeChatMp, StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrWhiteSpace(input.AppKey))
{
throw new BusinessException("微信公众号 AppId 不能为空");
}
if (string.IsNullOrWhiteSpace(input.Remark))
{
throw new BusinessException("微信公众号 Token 不能为空");
}
if (excludeId == null && ShouldKeepExistingSecret(input.AppSecret))
{
throw new BusinessException("微信公众号 AppSecret 不能为空");
}
}
else if (string.Equals(configCode, SystemOtherConfigCodes.PlatformShop, StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrWhiteSpace(input.BaseUrl))
{
throw new BusinessException("店铺授权前端回调根地址不能为空");
}
}
else if (IsPlatformShopAppConfig(configCode))
{
if (string.IsNullOrWhiteSpace(input.AppKey))
{
throw new BusinessException("平台 AppKey / PartnerId 不能为空");
}
if (excludeId == null && ShouldKeepExistingSecret(input.AppSecret))
{
throw new BusinessException("平台 AppSecret / PartnerKey 不能为空");
}
if (string.Equals(configCode, SystemOtherConfigCodes.PlatformShopShopee, StringComparison.OrdinalIgnoreCase)
&& !int.TryParse(input.AppKey.Trim(), out _))
{
throw new BusinessException("Shopee PartnerId 必须为数字");
}
if (string.IsNullOrWhiteSpace(input.BaseUrl))
{
throw new BusinessException("平台授权/API 地址不能为空");
}
}
}
private static bool IsPlatformShopAppConfig(string configCode) =>
string.Equals(configCode, SystemOtherConfigCodes.PlatformShopLazada, StringComparison.OrdinalIgnoreCase)
|| string.Equals(configCode, SystemOtherConfigCodes.PlatformShopShopee, StringComparison.OrdinalIgnoreCase)
|| string.Equals(configCode, SystemOtherConfigCodes.PlatformShopTiktok, StringComparison.OrdinalIgnoreCase)
|| string.Equals(configCode, SystemOtherConfigCodes.PlatformShopTemu, StringComparison.OrdinalIgnoreCase);
private static SystemOtherConfigOutput MapOutput(SystemOtherConfig entity, bool maskSecret)
{
return new SystemOtherConfigOutput
{
Id = entity.Id,
ConfigCode = entity.ConfigCode,
ConfigName = entity.ConfigName,
Enabled = entity.Enabled,
BaseUrl = entity.BaseUrl,
AppKey = entity.AppKey,
AppSecret = maskSecret ? MaskSecret(entity.AppSecret) : entity.AppSecret,
TimeoutSeconds = entity.TimeoutSeconds,
Remark = entity.Remark,
CreateTime = entity.CreateTime,
UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime)
};
}
private static string? MaskSecret(string? secret) =>
string.IsNullOrWhiteSpace(secret) ? secret : SecretMask;
private static bool ShouldKeepExistingSecret(string? secret) =>
string.IsNullOrWhiteSpace(secret) || string.Equals(secret.Trim(), SecretMask, StringComparison.Ordinal);
private static bool ShouldClearWeChatAccessToken(SystemOtherConfig entity, SystemOtherConfigUpdateInput input)
{
if (!string.Equals(entity.ConfigCode, SystemOtherConfigCodes.WeChatMp, StringComparison.OrdinalIgnoreCase)
&& !string.Equals(input.ConfigCode.Trim(), SystemOtherConfigCodes.WeChatMp, StringComparison.OrdinalIgnoreCase))
{
return false;
}
var nextAppKey = NormalizeOptional(input.AppKey);
if (!string.Equals(entity.AppKey, nextAppKey, StringComparison.Ordinal))
{
return true;
}
if (!ShouldKeepExistingSecret(input.AppSecret)
&& !string.Equals(entity.AppSecret, NormalizeOptional(input.AppSecret), StringComparison.Ordinal))
{
return true;
}
return false;
}
private static string? NormalizeOptional(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
private static int NormalizeTimeoutSeconds(int timeoutSeconds) =>
timeoutSeconds <= 0 ? 30 : timeoutSeconds;
}