fix(withdrawal): 修复提现详情收款二维码使用公网URL导致鉴权失败加载错误
- backend: withdrawal presenter 增加 privateCertificateURLs 兼容转换历史公网 payment-cert 路径 - frontend: AuthImage 与 fetchFileBlobByURL 兼容私有凭证 key,后台上下文通过管理员私有对象接口安全加载 - frontend: 按对象接口路径与 key 前缀精确判定,避免外链带 ?key= 被误代理到后台私有对象接口 - tests: 补充 TestPrivateCertificateURLs 单元测试
This commit is contained in:
@@ -39,3 +39,20 @@ func TestReviewWithdrawalRequestBindMissingApproved(t *testing.T) {
|
|||||||
t.Fatal("缺少 approved 字段时应该参数绑定失败")
|
t.Fatal("缺少 approved 字段时应该参数绑定失败")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPrivateCertificateURLs(t *testing.T) {
|
||||||
|
values := privateCertificateURLs([]string{
|
||||||
|
"/api/public/files/object?key=payment-cert%2F2026%2F09%2F16%2Fqr.webp",
|
||||||
|
"/api/public/files/object?key=avatar%2Fuser.jpg",
|
||||||
|
"/api/files/object?key=payment-cert%2Falready_private.webp",
|
||||||
|
})
|
||||||
|
if values[0] != "/api/files/object?key=payment-cert%2F2026%2F09%2F16%2Fqr.webp" {
|
||||||
|
t.Fatalf("expected private URL for payment-cert, got %q", values[0])
|
||||||
|
}
|
||||||
|
if values[1] != "/api/public/files/object?key=avatar%2Fuser.jpg" {
|
||||||
|
t.Fatalf("non-payment-cert URL should not be changed, got %q", values[1])
|
||||||
|
}
|
||||||
|
if values[2] != "/api/files/object?key=payment-cert%2Falready_private.webp" {
|
||||||
|
t.Fatalf("already private URL should not be changed, got %q", values[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package withdrawal
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"hfb_sys/backend/internal/model"
|
"hfb_sys/backend/internal/model"
|
||||||
)
|
)
|
||||||
@@ -72,6 +74,7 @@ func (r *Repository) toDetailDTO(ctx context.Context, w model.WithdrawalRequest)
|
|||||||
if paymentAccount.CertificateURLs != nil {
|
if paymentAccount.CertificateURLs != nil {
|
||||||
json.Unmarshal(paymentAccount.CertificateURLs, &certificateURLs)
|
json.Unmarshal(paymentAccount.CertificateURLs, &certificateURLs)
|
||||||
}
|
}
|
||||||
|
certificateURLs = privateCertificateURLs(certificateURLs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,4 +121,18 @@ func (r *Repository) toDetailDTO(ctx context.Context, w model.WithdrawalRequest)
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成提现单号
|
// privateCertificateURLs 兼容历史公开收款凭证 URL,统一经私有对象接口读取。
|
||||||
|
func privateCertificateURLs(values []string) []string {
|
||||||
|
for index, value := range values {
|
||||||
|
parsed, err := url.Parse(value)
|
||||||
|
if err != nil || parsed.Path != "/api/public/files/object" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(parsed.Query().Get("key"), "payment-cert/") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parsed.Path = "/api/files/object"
|
||||||
|
values[index] = parsed.String()
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|||||||
@@ -57,6 +57,14 @@ export async function fetchFileBlobByURL(fileURL: string) {
|
|||||||
if (apiPath.startsWith('/api/')) {
|
if (apiPath.startsWith('/api/')) {
|
||||||
apiPath = apiPath.slice(4)
|
apiPath = apiPath.slice(4)
|
||||||
}
|
}
|
||||||
|
// 历史收款凭证存的是公开 URL,但 payment-cert 已转为私有,需精确识别 key 前缀后改走私有对象接口。
|
||||||
|
if (apiPath.startsWith('/public/files/object')) {
|
||||||
|
const queryIndex = apiPath.indexOf('?')
|
||||||
|
const search = new URLSearchParams(queryIndex >= 0 ? apiPath.slice(queryIndex) : '')
|
||||||
|
if ((search.get('key') || '').startsWith('payment-cert/')) {
|
||||||
|
apiPath = `/files/object${queryIndex >= 0 ? apiPath.slice(queryIndex) : ''}`
|
||||||
|
}
|
||||||
|
}
|
||||||
const { data } = await apiClient.get<Blob>(apiPath, {
|
const { data } = await apiClient.get<Blob>(apiPath, {
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -67,17 +67,26 @@ function withAuthImageLoadSlot<T>(task: () => Promise<T>): Promise<T> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractObjectKey(value: string) {
|
function tryParseURL(value: string): URL | null {
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(value, window.location.origin)
|
return new URL(value, window.location.origin)
|
||||||
return parsed.searchParams.get('key') || ''
|
|
||||||
} catch {
|
} catch {
|
||||||
return ''
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractObjectKey(value: string) {
|
||||||
|
return tryParseURL(value)?.searchParams.get('key') || ''
|
||||||
|
}
|
||||||
|
|
||||||
function shouldFetchWithAuth(value: string) {
|
function shouldFetchWithAuth(value: string) {
|
||||||
return value.includes('/api/files/object') || value.includes('/api/admin/files/object')
|
const parsed = tryParseURL(value)
|
||||||
|
if (!parsed) return false
|
||||||
|
// 公开对象接口仅对已转私有的收款凭证做鉴权兜底,其余公开资源维持直连,避免外链被误代理。
|
||||||
|
if (parsed.pathname.endsWith('/public/files/object')) {
|
||||||
|
return (parsed.searchParams.get('key') || '').startsWith('payment-cert/')
|
||||||
|
}
|
||||||
|
return parsed.pathname.endsWith('/files/object')
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldFetchAsAdmin(value: string) {
|
function shouldFetchAsAdmin(value: string) {
|
||||||
|
|||||||
Reference in New Issue
Block a user