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

258 lines
7.4 KiB
Python
Raw 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.

"""
火山引擎 TOS (Volcengine Object Storage) 服务 — 表单直传签名 (V4)
"""
import time
import hmac
import base64
import hashlib
import json
from typing import Optional
from datetime import datetime, timezone
from app.config import settings
def generate_upload_policy(
max_size_mb: int = 500,
expire_seconds: int = 3600,
upload_dir: Optional[str] = None,
) -> dict:
"""
生成前端直传 TOS 所需的 Policy 和签名 (V4 HMAC-SHA256)
TOS 表单直传签名流程 (PostObject):
1. 构建 policy JSON → Base64 编码
2. 派生签名密钥: kDate → kRegion → kService → kSigning
3. signature = HMAC-SHA256(kSigning, policy_base64)
Returns:
{
"x_tos_algorithm": "TOS4-HMAC-SHA256",
"x_tos_credential": "AKIDxxxx/20260210/cn-beijing/tos/request",
"x_tos_date": "20260210T120000Z",
"x_tos_signature": "...",
"policy": "base64 encoded policy",
"host": "https://bucket.tos-cn-beijing.volces.com",
"dir": "uploads/2026/02/",
"expire": 1234567890,
}
"""
if not settings.TOS_ACCESS_KEY_ID or not settings.TOS_SECRET_ACCESS_KEY:
raise ValueError("TOS 配置未设置")
# 计算时间
now_utc = datetime.now(timezone.utc)
date_stamp = now_utc.strftime("%Y%m%d") # 20260210
tos_date = now_utc.strftime("%Y%m%dT%H%M%SZ") # 20260210T120000Z
expire_time = int(time.time()) + expire_seconds
expiration = datetime.fromtimestamp(expire_time, tz=timezone.utc).strftime(
"%Y-%m-%dT%H:%M:%S.000Z"
)
# Credential scope
region = settings.TOS_REGION
credential = f"{settings.TOS_ACCESS_KEY_ID}/{date_stamp}/{region}/tos/request"
# 默认上传目录uploads/年/月/
if upload_dir is None:
now = datetime.now()
upload_dir = f"uploads/{now.year}/{now.month:02d}/"
# 1. 构建 Policy
policy_dict = {
"expiration": expiration,
"conditions": [
{"bucket": settings.TOS_BUCKET_NAME},
["starts-with", "$key", upload_dir],
{"x-tos-algorithm": "TOS4-HMAC-SHA256"},
{"x-tos-credential": credential},
{"x-tos-date": tos_date},
["content-length-range", 0, max_size_mb * 1024 * 1024],
],
}
# 2. Base64 编码 Policy
policy_json = json.dumps(policy_dict)
policy_base64 = base64.b64encode(policy_json.encode()).decode()
# 3. 派生签名密钥 (V4 Signing Key)
k_date = hmac.new(
f"TOS4{settings.TOS_SECRET_ACCESS_KEY}".encode(),
date_stamp.encode(),
hashlib.sha256,
).digest()
k_region = hmac.new(k_date, region.encode(), hashlib.sha256).digest()
k_service = hmac.new(k_region, b"tos", hashlib.sha256).digest()
k_signing = hmac.new(k_service, b"request", hashlib.sha256).digest()
# 4. signature = HMAC-SHA256(kSigning, policy_base64)
signature = hmac.new(
k_signing,
policy_base64.encode(),
hashlib.sha256,
).hexdigest()
# 构建 Host
endpoint = settings.TOS_ENDPOINT or f"tos-cn-{region}.volces.com"
host = f"https://{settings.TOS_BUCKET_NAME}.{endpoint}"
return {
"x_tos_algorithm": "TOS4-HMAC-SHA256",
"x_tos_credential": credential,
"x_tos_date": tos_date,
"x_tos_signature": signature,
"policy": policy_base64,
"host": host,
"dir": upload_dir,
"expire": expire_time,
}
def get_file_url(file_key: str) -> str:
"""
获取文件的访问 URL
优先使用 CDN 域名,否则用 TOS 源站域名。
Args:
file_key: 文件在 TOS 中的 key"uploads/2026/02/video.mp4"
Returns:
完整的访问 URL
"""
if settings.TOS_CDN_DOMAIN:
host = settings.TOS_CDN_DOMAIN
else:
endpoint = settings.TOS_ENDPOINT or f"tos-cn-{settings.TOS_REGION}.volces.com"
host = f"https://{settings.TOS_BUCKET_NAME}.{endpoint}"
# 确保 host 以 https:// 开头
if not host.startswith("http"):
host = f"https://{host}"
# 确保 host 不以 / 结尾
host = host.rstrip("/")
# 确保 file_key 不以 / 开头
file_key = file_key.lstrip("/")
return f"{host}/{file_key}"
def generate_presigned_url(
file_key: str,
expire_seconds: int = 3600,
) -> str:
"""
为私有桶中的文件生成预签名访问 URL (TOS V4 Query String Auth)
签名流程:
1. 构建 CanonicalRequest
2. 构建 StringToSign
3. 用派生密钥签名
4. 拼接查询参数
Args:
file_key: 文件在 TOS 中的 key
expire_seconds: URL 有效期(秒),默认 1 小时
Returns:
预签名 URL
"""
if not settings.TOS_ACCESS_KEY_ID or not settings.TOS_SECRET_ACCESS_KEY:
raise ValueError("TOS 配置未设置")
from urllib.parse import quote
region = settings.TOS_REGION
endpoint = settings.TOS_ENDPOINT or f"tos-cn-{region}.volces.com"
bucket = settings.TOS_BUCKET_NAME
host = f"{bucket}.{endpoint}"
now_utc = datetime.now(timezone.utc)
date_stamp = now_utc.strftime("%Y%m%d")
tos_date = now_utc.strftime("%Y%m%dT%H%M%SZ")
credential = f"{settings.TOS_ACCESS_KEY_ID}/{date_stamp}/{region}/tos/request"
# 对 file_key 中的路径段分别编码
encoded_key = "/".join(quote(seg, safe="") for seg in file_key.split("/"))
# 查询参数(按字母序排列)
query_params = (
f"X-Tos-Algorithm=TOS4-HMAC-SHA256"
f"&X-Tos-Credential={quote(credential, safe='')}"
f"&X-Tos-Date={tos_date}"
f"&X-Tos-Expires={expire_seconds}"
f"&X-Tos-SignedHeaders=host"
)
# CanonicalRequest
canonical_request = (
f"GET\n"
f"/{encoded_key}\n"
f"{query_params}\n"
f"host:{host}\n"
f"\n"
f"host\n"
f"UNSIGNED-PAYLOAD"
)
# StringToSign
canonical_request_hash = hashlib.sha256(canonical_request.encode()).hexdigest()
string_to_sign = (
f"TOS4-HMAC-SHA256\n"
f"{tos_date}\n"
f"{date_stamp}/{region}/tos/request\n"
f"{canonical_request_hash}"
)
# 派生签名密钥
k_date = hmac.new(
f"TOS4{settings.TOS_SECRET_ACCESS_KEY}".encode(),
date_stamp.encode(),
hashlib.sha256,
).digest()
k_region = hmac.new(k_date, region.encode(), hashlib.sha256).digest()
k_service = hmac.new(k_region, b"tos", hashlib.sha256).digest()
k_signing = hmac.new(k_service, b"request", hashlib.sha256).digest()
# 计算签名
signature = hmac.new(
k_signing,
string_to_sign.encode(),
hashlib.sha256,
).hexdigest()
return (
f"https://{host}/{encoded_key}"
f"?{query_params}"
f"&X-Tos-Signature={signature}"
)
def parse_file_key_from_url(url: str) -> str:
"""
从完整 URL 解析出文件 key
Args:
url: 完整的 TOS URL
Returns:
文件 key
"""
# 尝试移除 CDN 域名
if settings.TOS_CDN_DOMAIN:
cdn = settings.TOS_CDN_DOMAIN.rstrip("/")
if not cdn.startswith("http"):
cdn = f"https://{cdn}"
if url.startswith(cdn):
return url[len(cdn):].lstrip("/")
# 尝试移除 TOS 源站域名
endpoint = settings.TOS_ENDPOINT or f"tos-cn-{settings.TOS_REGION}.volces.com"
tos_host = f"https://{settings.TOS_BUCKET_NAME}.{endpoint}"
if url.startswith(tos_host):
return url[len(tos_host):].lstrip("/")
return url