diff --git a/src/Oms.Application/Storage/FileStoragePath.cs b/src/Oms.Application/Storage/FileStoragePath.cs
index 45cae8f..a9af070 100644
--- a/src/Oms.Application/Storage/FileStoragePath.cs
+++ b/src/Oms.Application/Storage/FileStoragePath.cs
@@ -39,6 +39,37 @@ public static class FileStoragePath
&& (value.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|| value.StartsWith("https://", StringComparison.OrdinalIgnoreCase));
+ ///
+ /// 是否为本系统对象存储域名(自定义域 cos.* / 腾讯云默认域)。
+ /// 这类地址带临时签名,库内应存 object key,读出时重新签名;不可当外链原样返回。
+ ///
+ public static bool IsManagedObjectStorageUrl(string? value)
+ {
+ if (!IsAbsoluteUrl(value))
+ {
+ return false;
+ }
+
+ try
+ {
+ var host = new Uri(value!).Host;
+ if (string.IsNullOrWhiteSpace(host))
+ {
+ return false;
+ }
+
+ host = host.ToLowerInvariant();
+ return host.StartsWith("cos.", StringComparison.Ordinal)
+ || host.Contains("myqcloud.com", StringComparison.Ordinal)
+ || host.Contains("tencentcos.cn", StringComparison.Ordinal)
+ || host.Contains("tencentcos.com", StringComparison.Ordinal);
+ }
+ catch (UriFormatException)
+ {
+ return false;
+ }
+ }
+
///
/// 读取对象时尝试的 key:含/不含 admin/ 前缀(自定义域路径常带 PathBase)。
///
diff --git a/src/Oms.Application/Storage/OmsObjectStorageExtensions.cs b/src/Oms.Application/Storage/OmsObjectStorageExtensions.cs
index 28910bc..20db8cc 100644
--- a/src/Oms.Application/Storage/OmsObjectStorageExtensions.cs
+++ b/src/Oms.Application/Storage/OmsObjectStorageExtensions.cs
@@ -8,7 +8,7 @@ namespace Oms.Application.Storage;
public static class OmsObjectStorageExtensions
{
///
- /// 将 key / 相对路径规范为库内 object key;绝对外链原样返回;空返回 null。
+ /// 将 key / 相对路径规范为库内 object key;本系统 COS 签名 URL 剥签后存 key;其它绝对外链原样返回;空返回 null。
///
public static string? ToStorageKey(string? keyOrUrl)
{
@@ -23,6 +23,12 @@ public static class OmsObjectStorageExtensions
return "0";
}
+ // 私有桶签名 URL 会过期,禁止写入库;只保留 object key。
+ if (FileStoragePath.IsManagedObjectStorageUrl(raw))
+ {
+ return FileStoragePath.NormalizeKey(raw);
+ }
+
if (FileStoragePath.IsAbsoluteUrl(raw))
{
return raw;
@@ -32,7 +38,7 @@ public static class OmsObjectStorageExtensions
}
///
- /// 临时访问地址。空值返回空串;外链原样返回;逻辑 key 走 。
+ /// 临时访问地址。空值返回空串;真正外链原样返回;逻辑 key / 本系统 COS URL 走 重新签名。
/// Local 存储目录不可写/不可读时降级返回 key,避免拖垮下单等主流程。
///
public static async Task GetPublicUrlAsync(
@@ -46,7 +52,12 @@ public static class OmsObjectStorageExtensions
}
var raw = keyOrUrl.Trim();
- if (FileStoragePath.IsAbsoluteUrl(raw))
+ // 历史数据可能把带 q-sign 的完整 URL 存进库;直接返回会 AccessDenied/Request has expired。
+ if (FileStoragePath.IsManagedObjectStorageUrl(raw))
+ {
+ raw = FileStoragePath.NormalizeKey(raw);
+ }
+ else if (FileStoragePath.IsAbsoluteUrl(raw))
{
return raw;
}
diff --git a/src/Oms.Tests/FileStoragePathTests.cs b/src/Oms.Tests/FileStoragePathTests.cs
index 07b0843..9a26ce1 100644
--- a/src/Oms.Tests/FileStoragePathTests.cs
+++ b/src/Oms.Tests/FileStoragePathTests.cs
@@ -1,4 +1,6 @@
+using Fake.ObjectStorage;
using FluentAssertions;
+using Moq;
using Oms.Application.Storage;
namespace Oms.Tests;
@@ -16,4 +18,69 @@ public class FileStoragePathTests
keys.Should().Contain("admin/uploads/prints/orders/IDE2610061059117673-20261006105912.pdf");
keys.Should().Contain("uploads/prints/orders/IDE2610061059117673-20261006105912.pdf");
}
+
+ [Theory]
+ [InlineData("https://cos.ideship.com/uploads/a.pdf?q-sign-algorithm=sha1", true)]
+ [InlineData("http://cos.test.ideship.com/admin/uploads/a.pdf", true)]
+ [InlineData("https://bucket-123.cos.ap-guangzhou.myqcloud.com/uploads/a.pdf", true)]
+ [InlineData("https://third-party.example.com/label.pdf", false)]
+ [InlineData("uploads/prints/orders/a.pdf", false)]
+ public void IsManagedObjectStorageUrl_ShouldDetectCosHosts(string value, bool expected)
+ {
+ FileStoragePath.IsManagedObjectStorageUrl(value).Should().Be(expected);
+ }
+
+ [Fact]
+ public void ToStorageKey_ShouldStripExpiredCosSignedUrlToObjectKey()
+ {
+ var expired =
+ "https://cos.ideship.com/uploads/prints/orders/IDE2610081704036507-20261008170404.pdf?q-sign-algorithm=sha1&q-sign-time=1;2";
+
+ OmsObjectStorageExtensions.ToStorageKey(expired)
+ .Should()
+ .Be("uploads/prints/orders/IDE2610081704036507-20261008170404.pdf");
+ }
+
+ [Fact]
+ public void ToStorageKey_ShouldKeepExternalAbsoluteUrl()
+ {
+ const string external = "https://third-party.example.com/label.pdf?token=1";
+ OmsObjectStorageExtensions.ToStorageKey(external).Should().Be(external);
+ }
+
+ [Fact]
+ public async Task GetPublicUrl_ShouldResignManagedCosAbsoluteUrl()
+ {
+ var expired =
+ "https://cos.ideship.com/uploads/prints/orders/IDE2610081704036507-20261008170404.pdf?q-sign-algorithm=sha1&q-sign-time=1;2";
+ const string fresh =
+ "https://cos.ideship.com/uploads/prints/orders/IDE2610081704036507-20261008170404.pdf?q-sign-algorithm=sha1&fresh=1";
+
+ var storage = new Mock(MockBehavior.Strict);
+ storage
+ .Setup(x => x.GetUrlAsync(
+ "uploads/prints/orders/IDE2610081704036507-20261008170404.pdf",
+ It.IsAny(),
+ It.IsAny()))
+ .ReturnsAsync(fresh);
+
+ var url = await storage.Object.GetPublicUrlAsync(expired);
+
+ url.Should().Be(fresh);
+ storage.VerifyAll();
+ }
+
+ [Fact]
+ public async Task GetPublicUrl_ShouldPassThroughExternalAbsoluteUrl()
+ {
+ const string external = "https://third-party.example.com/label.pdf";
+ var storage = new Mock(MockBehavior.Strict);
+
+ var url = await storage.Object.GetPublicUrlAsync(external);
+
+ url.Should().Be(external);
+ storage.Verify(
+ x => x.GetUrlAsync(It.IsAny(), It.IsAny(), It.IsAny()),
+ Times.Never);
+ }
}