fix: 服务渠道分组软删同名可恢复,并修正 COS 自定义域名签名
All checks were successful
Build and Deploy / deploy (push) Successful in 1m44s

同名软删分组创建时恢复记录,更新时释放唯一键冲突;COS 自定义 CDN 域名关闭 signHost,并补充常见图片 Content-Type。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-09-22 19:00:12 +08:00
parent 8ce8b82f48
commit 623db4ffdb
6 changed files with 80 additions and 3 deletions

View File

@ -64,6 +64,21 @@ public partial class ServiceChannelService
} }
} }
/// <summary>
/// 更新时若目标名称被软删记录占用,则改名释放唯一键,避免 uk_scg_name 冲突。
/// </summary>
private async Task FreeSoftDeletedNameIfNeededAsync(string name, int currentId)
{
var conflict = await ServiceChannelGroupRepository.GetByNameIncludingDeletedAsync(name);
if (conflict == null || !conflict.IsDeleted || conflict.Id == currentId)
{
return;
}
conflict.Name = $"{name}__deleted_{conflict.Id}";
await ServiceChannelGroupRepository.UpdateAsync(conflict);
}
/// <summary> /// <summary>
/// 校验渠道编码唯一性。 /// 校验渠道编码唯一性。
/// </summary> /// </summary>

View File

@ -128,9 +128,21 @@ public partial class ServiceChannelService : ApplicationService
public async Task CreateGroupAsync(ServiceChannelGroupSaveInput input) public async Task CreateGroupAsync(ServiceChannelGroupSaveInput input)
{ {
await ValidateGroupInputAsync(input, null); await ValidateGroupInputAsync(input, null);
var name = input.Name.Trim();
var deleted = await ServiceChannelGroupRepository.GetByNameIncludingDeletedAsync(name);
if (deleted != null && deleted.IsDeleted)
{
deleted.IsDeleted = false;
deleted.Name = name;
deleted.AppId = input.AppId;
deleted.ApiServiceProviderId = input.ApiServiceProviderId;
await ServiceChannelGroupRepository.UpdateAsync(deleted);
return;
}
await ServiceChannelGroupRepository.InsertAsync(new ServiceChannelGroup await ServiceChannelGroupRepository.InsertAsync(new ServiceChannelGroup
{ {
Name = input.Name.Trim(), Name = name,
AppId = input.AppId, AppId = input.AppId,
ApiServiceProviderId = input.ApiServiceProviderId ApiServiceProviderId = input.ApiServiceProviderId
}); });
@ -146,7 +158,9 @@ public partial class ServiceChannelService : ApplicationService
{ {
var group = await GetGroupOrThrowAsync(input.Id); var group = await GetGroupOrThrowAsync(input.Id);
await ValidateGroupInputAsync(input, group.Id); await ValidateGroupInputAsync(input, group.Id);
group.Name = input.Name.Trim(); var name = input.Name.Trim();
await FreeSoftDeletedNameIfNeededAsync(name, group.Id);
group.Name = name;
group.AppId = input.AppId; group.AppId = input.AppId;
group.ApiServiceProviderId = input.ApiServiceProviderId; group.ApiServiceProviderId = input.ApiServiceProviderId;
await ServiceChannelGroupRepository.UpdateAsync(group); await ServiceChannelGroupRepository.UpdateAsync(group);

View File

@ -245,6 +245,32 @@ public class FileStorageService : ApplicationService
return "text/html; charset=utf-8"; return "text/html; charset=utf-8";
} }
if (path.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
{
return "image/png";
}
if (path.EndsWith(".webp", StringComparison.OrdinalIgnoreCase))
{
return "image/webp";
}
if (path.EndsWith(".gif", StringComparison.OrdinalIgnoreCase))
{
return "image/gif";
}
if (path.EndsWith(".bmp", StringComparison.OrdinalIgnoreCase))
{
return "image/bmp";
}
if (path.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase)
|| path.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase))
{
return "image/jpeg";
}
return "application/octet-stream"; return "application/octet-stream";
} }

View File

@ -365,8 +365,11 @@ public sealed class TencentCosFileStorage : IFileStorage, IDisposable
if (!string.IsNullOrWhiteSpace(_customHost) if (!string.IsNullOrWhiteSpace(_customHost)
&& !_customHost.EndsWith(".myqcloud.com", StringComparison.OrdinalIgnoreCase)) && !_customHost.EndsWith(".myqcloud.com", StringComparison.OrdinalIgnoreCase))
{ {
// 自定义 CDN 域名:URL 走 cos.ideship.com,但不要把 Host 签进 q-header-list。
// signHost=true 时浏览器直开签名链常因 CDN 回源 Host 不一致而 403(img naturalWidth=0),
// 对象其实在桶里(/file-storage/stream 可读)。
preSign.host = _customHost; preSign.host = _customHost;
preSign.signHost = true; preSign.signHost = false;
} }
} }

View File

@ -6,4 +6,9 @@ namespace Oms.Domain.ChannelsAggregate.Repositories;
public interface IServiceChannelGroupRepository : IRepository<ServiceChannelGroup> public interface IServiceChannelGroupRepository : IRepository<ServiceChannelGroup>
{ {
Task<ServiceChannelGroup?> GetByNameAsync(string name, CancellationToken cancellationToken = default); Task<ServiceChannelGroup?> GetByNameAsync(string name, CancellationToken cancellationToken = default);
/// <summary>
/// 按名称查询分组(含软删除),用于唯一名冲突时恢复或释放名称。
/// </summary>
Task<ServiceChannelGroup?> GetByNameIncludingDeletedAsync(string name, CancellationToken cancellationToken = default);
} }

View File

@ -2,6 +2,7 @@ using Fake.DependencyInjection;
using Oms.Domain.ChannelsAggregate.Entities; using Oms.Domain.ChannelsAggregate.Entities;
using Oms.Domain.ChannelsAggregate.Repositories; using Oms.Domain.ChannelsAggregate.Repositories;
using Oms.SqlSugarCore.Repositories; using Oms.SqlSugarCore.Repositories;
using SqlSugar;
namespace Oms.SqlSugarCore.Channels; namespace Oms.SqlSugarCore.Channels;
@ -11,4 +12,17 @@ public class ServiceChannelGroupRepository : OmsSqlSugarRepository<ServiceChanne
{ {
return FirstOrDefaultAsync(x => x.Name == name, cancellationToken); return FirstOrDefaultAsync(x => x.Name == name, cancellationToken);
} }
public async Task<ServiceChannelGroup?> GetByNameIncludingDeletedAsync(
string name,
CancellationToken cancellationToken = default)
{
var db = await GetDbContextAsync(cancellationToken);
var rows = await db.Queryable<ServiceChannelGroup>()
.ClearFilter()
.Where(x => x.Name == name)
.Take(1)
.ToListAsync(cancellationToken);
return rows.FirstOrDefault();
}
} }