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:
co-authored by
Claude Opus 4.6
parent
a32102f583
commit
4a3c7e7923
@@ -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,
|
||||
)
|
||||
@@ -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)}
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user