diff --git a/src/Oms.Application/Storage/FileStoragePath.cs b/src/Oms.Application/Storage/FileStoragePath.cs
index a9af070..6a17bbe 100644
--- a/src/Oms.Application/Storage/FileStoragePath.cs
+++ b/src/Oms.Application/Storage/FileStoragePath.cs
@@ -28,7 +28,32 @@ public static class FileStoragePath
throw new ArgumentException("object key 不能为空", nameof(pathOrUrl));
}
- return key;
+ // 自定义域公开 URL 常带 PathBase「admin/」;库内/COS 逻辑 key 为 uploads/...。
+ // 若不剥离,GetUrl 再拼 KeyPrefix/PathBase 会变成 admin/admin/uploads → NoSuchKey。
+ return StripPublicPathBase(key);
+ }
+
+ ///
+ /// 去掉公开 URL 上的 PathBase(可重复的 admin/),保留 uploads/... 等逻辑 key。
+ ///
+ public static string StripPublicPathBase(string key)
+ {
+ var value = key.Replace('\\', '/').Trim().TrimStart('/');
+ while (value.StartsWith("admin/", StringComparison.OrdinalIgnoreCase))
+ {
+ var rest = value["admin/".Length..];
+ // 仅剥「PathBase + 业务前缀」;避免误伤 admin 业务目录(若有)
+ if (rest.StartsWith("uploads/", StringComparison.OrdinalIgnoreCase)
+ || rest.StartsWith("admin/", StringComparison.OrdinalIgnoreCase))
+ {
+ value = rest;
+ continue;
+ }
+
+ break;
+ }
+
+ return value;
}
///
diff --git a/src/Oms.Tests/FileStoragePathTests.cs b/src/Oms.Tests/FileStoragePathTests.cs
index 9a26ce1..493c7d9 100644
--- a/src/Oms.Tests/FileStoragePathTests.cs
+++ b/src/Oms.Tests/FileStoragePathTests.cs
@@ -15,8 +15,9 @@ public class FileStoragePathTests
var keys = FileStoragePath.ExpandReadKeys(url);
- keys.Should().Contain("admin/uploads/prints/orders/IDE2610061059117673-20261006105912.pdf");
+ // NormalizeKey 已剥 PathBase,主 key 为 uploads/...;Expand 仍保留 admin/ 候选以兼容历史对象
keys.Should().Contain("uploads/prints/orders/IDE2610061059117673-20261006105912.pdf");
+ keys.Should().Contain("admin/uploads/prints/orders/IDE2610061059117673-20261006105912.pdf");
}
[Theory]
@@ -41,6 +42,47 @@ public class FileStoragePathTests
.Be("uploads/prints/orders/IDE2610081704036507-20261008170404.pdf");
}
+ [Fact]
+ public void ToStorageKey_ShouldStripPathBaseAdminFromCustomDomainUrl()
+ {
+ var url =
+ "https://cos.test.ideship.com/admin/uploads/prints/orders/IDE2610231052073356-20261023105208.pdf?q-sign-algorithm=sha1";
+
+ OmsObjectStorageExtensions.ToStorageKey(url)
+ .Should()
+ .Be("uploads/prints/orders/IDE2610231052073356-20261023105208.pdf");
+ }
+
+ [Fact]
+ public void NormalizeKey_ShouldCollapseDoubleAdminPathBase()
+ {
+ FileStoragePath.NormalizeKey("admin/admin/uploads/prints/orders/a.pdf")
+ .Should()
+ .Be("uploads/prints/orders/a.pdf");
+ }
+
+ [Fact]
+ public async Task GetPublicUrl_ShouldResignCustomDomainUrlWithoutDoubleAdmin()
+ {
+ var expired =
+ "https://cos.test.ideship.com/admin/uploads/prints/orders/IDE2610231052073356-20261023105208.pdf?q-sign-algorithm=sha1&q-sign-time=1;2";
+ const string fresh =
+ "https://cos.test.ideship.com/admin/uploads/prints/orders/IDE2610231052073356-20261023105208.pdf?q-sign-algorithm=sha1&fresh=1";
+
+ var storage = new Mock(MockBehavior.Strict);
+ storage
+ .Setup(x => x.GetUrlAsync(
+ "uploads/prints/orders/IDE2610231052073356-20261023105208.pdf",
+ It.IsAny(),
+ It.IsAny()))
+ .ReturnsAsync(fresh);
+
+ var url = await storage.Object.GetPublicUrlAsync(expired);
+
+ url.Should().Be(fresh);
+ storage.VerifyAll();
+ }
+
[Fact]
public void ToStorageKey_ShouldKeepExternalAbsoluteUrl()
{