refactor: 清理无用模块、修复前后端对齐、添加注册页面

- 删除后端 risk_exceptions 模块(API/Model/Schema/迁移/测试)
- 删除后端 metrics 模块(API/测试)
- 删除后端 ManualTask 模型和相关 Schema
- 修复搜索接口响应缺少 total 字段的问题
- 统一 Platform 枚举(前端去掉后端不支持的 weibo/wechat)
- 新增前端注册页面 /register,登录页添加注册链接

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-02-09 14:51:17 +08:00
co-authored by Claude Opus 4.6
parent a32102f583
commit 4a3c7e7923
22 changed files with 725 additions and 1298 deletions
-87
View File
@@ -1,87 +0,0 @@
"""
一致性指标 API
按达人、规则类型、时间窗口查询
"""
from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, HTTPException, Query, status
from app.schemas.review import (
ConsistencyMetricsResponse,
ConsistencyWindow,
RuleConsistencyMetric,
ViolationType,
)
router = APIRouter(prefix="/metrics", tags=["metrics"])
@router.get("/consistency", response_model=ConsistencyMetricsResponse)
async def get_consistency_metrics(
influencer_id: str = Query(None, description="达人 ID(必填)"),
window: ConsistencyWindow = Query(ConsistencyWindow.ROLLING_30D, description="计算周期"),
rule_type: ViolationType = Query(None, description="规则类型筛选"),
) -> ConsistencyMetricsResponse:
"""
查询一致性指标
- 按达人 ID 查询
- 支持 Rolling 30 天、周度快照、月度快照
- 可按规则类型筛选
"""
# 验证必填参数
if not influencer_id:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="缺少必填参数: influencer_id",
)
# 计算时间范围
now = datetime.now(timezone.utc)
if window == ConsistencyWindow.ROLLING_30D:
period_start = now - timedelta(days=30)
period_end = now
elif window == ConsistencyWindow.SNAPSHOT_WEEK:
# 本周一到现在
days_since_monday = now.weekday()
period_start = (now - timedelta(days=days_since_monday)).replace(
hour=0, minute=0, second=0, microsecond=0
)
period_end = now
else: # SNAPSHOT_MONTH
# 本月1号到现在
period_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
period_end = now
# 生成模拟数据(实际应从数据库查询)
all_metrics = [
RuleConsistencyMetric(
rule_type=ViolationType.FORBIDDEN_WORD,
total_reviews=100,
violation_count=5,
violation_rate=0.05,
),
RuleConsistencyMetric(
rule_type=ViolationType.COMPETITOR_LOGO,
total_reviews=100,
violation_count=2,
violation_rate=0.02,
),
RuleConsistencyMetric(
rule_type=ViolationType.DURATION_SHORT,
total_reviews=100,
violation_count=8,
violation_rate=0.08,
),
]
# 按规则类型筛选
if rule_type:
all_metrics = [m for m in all_metrics if m.rule_type == rule_type]
return ConsistencyMetricsResponse(
influencer_id=influencer_id,
window=window,
period_start=period_start,
period_end=period_end,
metrics=all_metrics,
)
+23 -25
View File
@@ -279,18 +279,17 @@ async def search_agencies(
)
agencies = list(result.scalars().all())
return {
"items": [
AgencySummary(
id=a.id,
name=a.name,
logo=a.logo,
contact_name=a.contact_name,
force_pass_enabled=a.force_pass_enabled,
).model_dump()
for a in agencies
]
}
items = [
AgencySummary(
id=a.id,
name=a.name,
logo=a.logo,
contact_name=a.contact_name,
force_pass_enabled=a.force_pass_enabled,
).model_dump()
for a in agencies
]
return {"items": items, "total": len(items)}
@router.get("/search/creators")
@@ -307,16 +306,15 @@ async def search_creators(
)
creators = list(result.scalars().all())
return {
"items": [
CreatorSummary(
id=c.id,
name=c.name,
avatar=c.avatar,
douyin_account=c.douyin_account,
xiaohongshu_account=c.xiaohongshu_account,
bilibili_account=c.bilibili_account,
).model_dump()
for c in creators
]
}
items = [
CreatorSummary(
id=c.id,
name=c.name,
avatar=c.avatar,
douyin_account=c.douyin_account,
xiaohongshu_account=c.xiaohongshu_account,
bilibili_account=c.bilibili_account,
).model_dump()
for c in creators
]
return {"items": items, "total": len(items)}
-226
View File
@@ -1,226 +0,0 @@
"""
特例审批 API
创建、查询、审批特例记录
"""
import uuid
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, Header, HTTPException, status
from sqlalchemy import select, and_
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.models.tenant import Tenant
from app.models.risk_exception import (
RiskException,
RiskTargetType as DBRiskTargetType,
RiskExceptionStatus as DBRiskExceptionStatus,
)
from app.schemas.review import (
RiskExceptionCreateRequest,
RiskExceptionRecord,
RiskExceptionStatus,
RiskExceptionDecisionRequest,
RiskTargetType,
)
router = APIRouter(prefix="/risk-exceptions", tags=["risk-exceptions"])
async def _ensure_tenant_exists(tenant_id: str, db: AsyncSession) -> Tenant:
"""确保租户存在,不存在则自动创建"""
result = await db.execute(
select(Tenant).where(Tenant.id == tenant_id)
)
tenant = result.scalar_one_or_none()
if not tenant:
tenant = Tenant(id=tenant_id, name=f"租户-{tenant_id}")
db.add(tenant)
await db.flush()
return tenant
def _exception_to_response(record: RiskException) -> RiskExceptionRecord:
"""将数据库模型转换为响应模型"""
return RiskExceptionRecord(
record_id=record.id,
applicant_id=record.applicant_id,
apply_time=record.apply_time,
target_type=RiskTargetType(record.target_type.value),
target_id=record.target_id,
risk_rule_id=record.risk_rule_id,
status=RiskExceptionStatus(record.status.value),
valid_start_time=record.valid_start_time,
valid_end_time=record.valid_end_time,
reason_category=record.reason_category,
justification=record.justification,
attachment_url=record.attachment_url,
current_approver_id=record.current_approver_id,
approval_chain_log=record.approval_chain_log or [],
auto_rejected=record.auto_rejected,
rejection_reason=record.rejection_reason,
last_status_at=record.last_status_at,
)
@router.post("", response_model=RiskExceptionRecord, status_code=status.HTTP_201_CREATED)
async def create_exception(
request: RiskExceptionCreateRequest,
x_tenant_id: str = Header(..., alias="X-Tenant-ID"),
db: AsyncSession = Depends(get_db),
) -> RiskExceptionRecord:
"""创建特例申请"""
# 确保租户存在
await _ensure_tenant_exists(x_tenant_id, db)
record_id = f"exc-{uuid.uuid4().hex[:12]}"
now = datetime.now(timezone.utc)
record = RiskException(
id=record_id,
tenant_id=x_tenant_id,
applicant_id=request.applicant_id,
apply_time=now,
target_type=DBRiskTargetType(request.target_type.value),
target_id=request.target_id,
risk_rule_id=request.risk_rule_id,
status=DBRiskExceptionStatus.PENDING,
valid_start_time=request.valid_start_time,
valid_end_time=request.valid_end_time,
reason_category=request.reason_category,
justification=request.justification,
attachment_url=request.attachment_url,
current_approver_id=request.current_approver_id,
approval_chain_log=[],
auto_rejected=False,
rejection_reason=None,
last_status_at=now,
)
db.add(record)
await db.flush()
await db.refresh(record)
return _exception_to_response(record)
@router.get("/{record_id}", response_model=RiskExceptionRecord)
async def get_exception(
record_id: str,
x_tenant_id: str = Header(..., alias="X-Tenant-ID"),
db: AsyncSession = Depends(get_db),
) -> RiskExceptionRecord:
"""查询特例记录"""
result = await db.execute(
select(RiskException).where(
and_(
RiskException.id == record_id,
RiskException.tenant_id == x_tenant_id,
)
)
)
record = result.scalar_one_or_none()
if not record:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"特例记录不存在: {record_id}",
)
return _exception_to_response(record)
@router.post("/{record_id}/approve", response_model=RiskExceptionRecord)
async def approve_exception(
record_id: str,
request: RiskExceptionDecisionRequest,
x_tenant_id: str = Header(..., alias="X-Tenant-ID"),
db: AsyncSession = Depends(get_db),
) -> RiskExceptionRecord:
"""审批通过"""
result = await db.execute(
select(RiskException).where(
and_(
RiskException.id == record_id,
RiskException.tenant_id == x_tenant_id,
)
)
)
record = result.scalar_one_or_none()
if not record:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"特例记录不存在: {record_id}",
)
now = datetime.now(timezone.utc)
record.status = DBRiskExceptionStatus.APPROVED
record.last_status_at = now
# 更新审批日志
approval_log = record.approval_chain_log or []
approval_log.append({
"approver_id": request.approver_id,
"action": "approve",
"comment": request.comment,
"timestamp": now.isoformat(),
})
record.approval_chain_log = approval_log
await db.flush()
await db.refresh(record)
return _exception_to_response(record)
@router.post("/{record_id}/reject", response_model=RiskExceptionRecord)
async def reject_exception(
record_id: str,
request: RiskExceptionDecisionRequest,
x_tenant_id: str = Header(..., alias="X-Tenant-ID"),
db: AsyncSession = Depends(get_db),
) -> RiskExceptionRecord:
"""驳回申请"""
result = await db.execute(
select(RiskException).where(
and_(
RiskException.id == record_id,
RiskException.tenant_id == x_tenant_id,
)
)
)
record = result.scalar_one_or_none()
if not record:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"特例记录不存在: {record_id}",
)
# 驳回必须填写原因
if not request.comment:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="驳回必须填写原因",
)
now = datetime.now(timezone.utc)
record.status = DBRiskExceptionStatus.REJECTED
record.rejection_reason = request.comment
record.last_status_at = now
# 更新审批日志
approval_log = record.approval_chain_log or []
approval_log.append({
"approver_id": request.approver_id,
"action": "reject",
"comment": request.comment,
"timestamp": now.isoformat(),
})
record.approval_chain_log = approval_log
await db.flush()
await db.refresh(record)
return _exception_to_response(record)