fix(file): 修复商品详情截图与提现打款凭证等私有图片 403 权限问题

- 放宽公开商品截图 key 提取规则,支持任意合规 object key 转换为免鉴权公开端点
- 补充后台截图上传 listing_screenshot 与 operating-expense 场景识别
- dependencies 授权器支持已审核上架商品截图兜底放行、提现人打款凭证鉴权及变体后缀匹配
- /api/files 路由组支持用户与管理员双重鉴权 (AuthUserOrAdmin)
- 前端收款账号凭证与提现详情凭证统一升级为 AuthImage 组件并支持大图预览
This commit is contained in:
yml2213
2026-09-16 20:33:09 +08:00
parent 64a4e5bb16
commit 69774458b0
13 changed files with 215 additions and 27 deletions
+5 -1
View File
@@ -118,8 +118,12 @@ func fileURLForScene(scene string, key string) string {
func normalizeScene(scene string) string {
scene = strings.TrimSpace(strings.ToLower(scene))
switch scene {
case "listing", "handoff", "dispute", "realname", "avatar", "home-banner", "chat", "payment-cert", "announcement", "qrcode", "mohong", "crash", "aw-recycle", "cooperation-feedback", "manual-disbursement":
case "listing", "handoff", "dispute", "realname", "avatar", "home-banner", "chat", "payment-cert", "announcement", "qrcode", "mohong", "crash", "aw-recycle", "cooperation-feedback", "manual-disbursement", "operating-expense":
return scene
case "listing_screenshot", "listing-screenshot":
return "listing"
case "operating_expense":
return "operating-expense"
default:
return "misc"
}
@@ -76,3 +76,19 @@ func TestCooperationFeedbackSceneUsesPublicFileURL(t *testing.T) {
t.Fatalf("cooperation-feedback file url = %q, want public file endpoint", got)
}
}
func TestNormalizeSceneListingScreenshotAndOperatingExpense(t *testing.T) {
if got := normalizeScene("listing_screenshot"); got != "listing" {
t.Fatalf("normalize listing_screenshot scene = %q, want listing", got)
}
if got := normalizeScene("listing-screenshot"); got != "listing" {
t.Fatalf("normalize listing-screenshot scene = %q, want listing", got)
}
if got := normalizeScene("operating-expense"); got != "operating-expense" {
t.Fatalf("normalize operating-expense scene = %q, want operating-expense", got)
}
if got := normalizeScene("operating_expense"); got != "operating-expense" {
t.Fatalf("normalize operating_expense scene = %q, want operating-expense", got)
}
}
@@ -452,11 +452,11 @@ func extractListingObjectKey(fileURL string) string {
if err != nil {
return ""
}
key := parsed.Query().Get("key")
if key == "" {
if !strings.HasSuffix(parsed.Path, "/files/object") {
return ""
}
if !strings.HasPrefix(key, "listing/") || strings.Contains(key, "..") {
key := strings.TrimSpace(parsed.Query().Get("key"))
if key == "" || strings.Contains(key, "..") || strings.HasPrefix(key, "/") || strings.HasPrefix(key, "\\") {
return ""
}
return key
@@ -769,6 +769,44 @@ func TestApplyPublicScreenshotGroups(t *testing.T) {
}
}
func TestPublicScreenshotURLsWithMiscAndListingKeys(t *testing.T) {
rawURLs := []string{
"/api/files/object?key=misc%2F2026%2F09%2F16%2F984a80162e94e3b89090d1f8.webp",
"/api/files/object?key=listing%2Fnormal.webp",
"https://example.com/external.jpg",
}
publicURLs := publicScreenshotURLs(29969, rawURLs, "published", "approved")
if len(publicURLs) != 3 {
t.Fatalf("expected 3 public URLs, got %d", len(publicURLs))
}
if publicURLs[0] != "/api/listings/29969/screenshots/0" {
t.Fatalf("expected /api/listings/29969/screenshots/0, got %q", publicURLs[0])
}
if publicURLs[1] != "/api/listings/29969/screenshots/1" {
t.Fatalf("expected /api/listings/29969/screenshots/1, got %q", publicURLs[1])
}
if publicURLs[2] != "https://example.com/external.jpg" {
t.Fatalf("external URL should remain unchanged, got %q", publicURLs[2])
}
coverURL := publicCoverURL(29969, rawURLs, "published", "approved")
if coverURL != "/api/listings/29969/cover" {
t.Fatalf("expected /api/listings/29969/cover, got %q", coverURL)
}
summary := map[string]any{
"screenshot_groups": map[string]any{
"coin": []string{"/api/files/object?key=misc%2F2026%2F09%2F16%2F984a80162e94e3b89090d1f8.webp"},
},
}
applyPublicScreenshotGroups(29969, summary, rawURLs, publicURLs)
groups := summary["screenshot_groups"].(map[string]any)
coinList := groups["coin"].([]string)
if len(coinList) != 1 || coinList[0] != "/api/listings/29969/screenshots/0" {
t.Fatalf("expected mapped /api/listings/29969/screenshots/0, got %#v", coinList)
}
}
func TestUpdateScreenshots(t *testing.T) {
db := database.NewTestDB()
if err := database.MigrateListingLifecycleTestSchema(db); err != nil {