Your Name 0ef7650c09 feat: 审核体系全面改造 — 多维度评分 + 卖点优先级 + AI 语义匹配 + 品牌方 AI 状态通知
后端:
- 审核结果拆分为 4 个独立维度 (法规合规/平台规则/品牌安全/Brief匹配度)
- 卖点优先级从 required:bool 改为三级 (core/recommended/reference)
- AI 语义匹配卖点覆盖 + AI 整体 Brief 匹配度分析
- BriefMatchDetail 评分详情 (覆盖率+亮点+问题点)
- min_selling_points 代理商可配置最少卖点数 + Alembic 迁移
- AI 语境复核过滤误报
- Brief AI 解析 + 规则 AI 解析
- AI 未配置/异常时通知品牌方
- 种子数据更新 (新格式审核结果+brief_match_detail)

前端:
- 三端审核页面展示四维度评分卡片
- 卖点编辑改为三级优先级选择器
- BriefMatchDetail 展示 (覆盖率进度条+亮点+问题)
- min_selling_points 配置 UI
- AI 配置页未配置时静默处理
- 文件预览/下载/签名 URL 优化

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 19:11:54 +08:00

102 lines
2.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 获取私有桶文件的预签名访问 URL
*
* 用于展示/下载 TOS 私有桶中的文件。
* 自动缓存签名 URL过期前 5 分钟刷新。
*/
import { useState, useEffect, useCallback, useRef } from 'react'
import { api } from '@/lib/api'
import { USE_MOCK } from '@/contexts/AuthContext'
const urlCache = new Map<string, { signedUrl: string; expireAt: number }>()
export function useSignedUrl(originalUrl: string | undefined | null) {
const [signedUrl, setSignedUrl] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
const mountedRef = useRef(true)
useEffect(() => {
mountedRef.current = true
return () => { mountedRef.current = false }
}, [])
const fetchSignedUrl = useCallback(async () => {
if (!originalUrl) {
setSignedUrl(null)
return
}
// Mock 模式直接返回原始 URL
if (USE_MOCK) {
setSignedUrl(originalUrl)
return
}
// 非 TOS URL如外部链接直接返回
if (!originalUrl.includes('tos-cn-') && !originalUrl.includes('volces.com') && !originalUrl.startsWith('uploads/')) {
setSignedUrl(originalUrl)
return
}
// 检查缓存(提前 5 分钟过期)
const cached = urlCache.get(originalUrl)
if (cached && cached.expireAt > Date.now() + 5 * 60 * 1000) {
setSignedUrl(cached.signedUrl)
return
}
setLoading(true)
try {
const expireSeconds = 3600
const url = await api.getSignedUrl(originalUrl)
if (mountedRef.current) {
setSignedUrl(url)
urlCache.set(originalUrl, {
signedUrl: url,
expireAt: Date.now() + expireSeconds * 1000,
})
}
} catch {
// 签名失败时回退到原始 URL
if (mountedRef.current) {
setSignedUrl(originalUrl)
}
} finally {
if (mountedRef.current) {
setLoading(false)
}
}
}, [originalUrl])
useEffect(() => {
fetchSignedUrl()
}, [fetchSignedUrl])
return { signedUrl, loading, refresh: fetchSignedUrl }
}
/**
* 批量获取签名 URL 的工具函数
*/
export async function getSignedUrls(urls: string[]): Promise<Map<string, string>> {
const result = new Map<string, string>()
if (USE_MOCK) {
urls.forEach(u => result.set(u, u))
return result
}
await Promise.all(
urls.map(async (url) => {
try {
const signed = await api.getSignedUrl(url)
result.set(url, signed)
} catch {
result.set(url, url)
}
})
)
return result
}