feat: 全量功能模块开发与集成测试修复

- 新增后端模块:Alert、APIAsset、Compliance、Lineage、Masking、Risk、SchemaChange、Unstructured、Watermark
- 新增前端模块页面与API接口
- 新增Alembic迁移脚本(002-014)覆盖全量业务表
- 新增测试数据生成脚本与集成测试脚本
- 修复metadata模型JSON类型导入缺失导致启动失败的问题
- 修复前端Alert/APIAsset页面request模块路径错误
- 更新docker-compose与开发计划文档
This commit is contained in:
hiderfong
2026-04-25 08:51:38 +08:00
parent 8b2bc84399
commit 6d70520e79
110 changed files with 6125 additions and 87 deletions
+10 -1
View File
@@ -1,6 +1,6 @@
from fastapi import APIRouter
from app.api.v1 import auth, user, datasource, metadata, classification, project, task, report, dashboard
from app.api.v1 import auth, user, datasource, metadata, classification, project, task, report, dashboard, masking, watermark, unstructured, schema_change, risk, compliance, lineage, alert, api_asset
api_router = APIRouter()
api_router.include_router(auth.router, prefix="/auth", tags=["认证"])
@@ -12,3 +12,12 @@ api_router.include_router(project.router, prefix="/projects", tags=["项目管
api_router.include_router(task.router, prefix="/tasks", tags=["任务管理"])
api_router.include_router(report.router, prefix="/reports", tags=["报告管理"])
api_router.include_router(dashboard.router, prefix="/dashboard", tags=["仪表盘"])
api_router.include_router(masking.router, prefix="/masking", tags=["数据脱敏"])
api_router.include_router(watermark.router, prefix="/watermark", tags=["数据水印"])
api_router.include_router(unstructured.router, prefix="/unstructured", tags=["非结构化文件"])
api_router.include_router(schema_change.router, prefix="/schema-changes", tags=["Schema变更"])
api_router.include_router(risk.router, prefix="/risk", tags=["风险评估"])
api_router.include_router(compliance.router, prefix="/compliance", tags=["合规检查"])
api_router.include_router(lineage.router, prefix="/lineage", tags=["数据血缘"])
api_router.include_router(alert.router, prefix="/alerts", tags=["告警与工单"])
api_router.include_router(api_asset.router, prefix="/api-assets", tags=["API资产"])
+115
View File
@@ -0,0 +1,115 @@
from typing import Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.models.user import User
from app.schemas.common import ResponseModel, ListResponse
from app.services import alert_service
from app.api.deps import get_current_user, require_admin
router = APIRouter()
@router.post("/init-rules")
def init_alert_rules(
db: Session = Depends(get_db),
current_user: User = Depends(require_admin),
):
alert_service.init_builtin_alert_rules(db)
return ResponseModel(message="初始化完成")
@router.post("/check")
def check_alerts(
db: Session = Depends(get_db),
current_user: User = Depends(require_admin),
):
records = alert_service.check_alerts(db)
return ResponseModel(data={"alerts_created": len(records)})
@router.get("/records")
def list_alert_records(
status: Optional[str] = Query(None),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=500),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
query = db.query(alert_service.AlertRecord)
if status:
query = query.filter(alert_service.AlertRecord.status == status)
total = query.count()
items = query.order_by(alert_service.AlertRecord.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
return ListResponse(
data=[{
"id": r.id,
"rule_id": r.rule_id,
"title": r.title,
"content": r.content,
"severity": r.severity,
"status": r.status,
"created_at": r.created_at.isoformat() if r.created_at else None,
} for r in items],
total=total,
page=page,
page_size=page_size,
)
@router.post("/work-orders")
def create_work_order(
alert_id: int,
title: str,
description: str = "",
assignee_id: Optional[int] = Query(None),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
wo = alert_service.create_work_order(db, alert_id, title, description, assignee_id)
return ResponseModel(data={"id": wo.id})
@router.get("/work-orders")
def list_work_orders(
status: Optional[str] = Query(None),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=500),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
from app.models.alert import WorkOrder
query = db.query(WorkOrder)
if status:
query = query.filter(WorkOrder.status == status)
total = query.count()
items = query.order_by(WorkOrder.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
return ListResponse(
data=[{
"id": w.id,
"alert_id": w.alert_id,
"title": w.title,
"status": w.status,
"assignee_name": w.assignee.username if w.assignee else None,
"created_at": w.created_at.isoformat() if w.created_at else None,
} for w in items],
total=total,
page=page,
page_size=page_size,
)
@router.post("/work-orders/{wo_id}/status")
def update_work_order(
wo_id: int,
status: str,
resolution: str = "",
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
wo = alert_service.update_work_order_status(db, wo_id, status, resolution or None)
if not wo:
from fastapi import HTTPException, status
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="工单不存在")
return ResponseModel(data={"id": wo.id, "status": wo.status})
+131
View File
@@ -0,0 +1,131 @@
from typing import Optional, List
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from pydantic import BaseModel
from app.core.database import get_db
from app.models.user import User
from app.schemas.common import ResponseModel, ListResponse
from app.services import api_asset_service
from app.api.deps import get_current_user
router = APIRouter()
class APIAssetCreate(BaseModel):
name: str
base_url: str
swagger_url: Optional[str] = None
auth_type: Optional[str] = "none"
headers: Optional[dict] = None
description: Optional[str] = None
class APIAssetUpdate(BaseModel):
name: Optional[str] = None
base_url: Optional[str] = None
swagger_url: Optional[str] = None
auth_type: Optional[str] = None
headers: Optional[dict] = None
description: Optional[str] = None
@router.post("")
def create_asset(
body: APIAssetCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
asset = api_asset_service.create_asset(db, body.dict(), current_user.id)
return ResponseModel(data={"id": asset.id})
@router.get("")
def list_assets(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=500),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
from app.models.api_asset import APIAsset
query = db.query(APIAsset)
total = query.count()
items = query.order_by(APIAsset.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
return ListResponse(
data=[{
"id": a.id,
"name": a.name,
"base_url": a.base_url,
"swagger_url": a.swagger_url,
"auth_type": a.auth_type,
"scan_status": a.scan_status,
"total_endpoints": a.total_endpoints,
"sensitive_endpoints": a.sensitive_endpoints,
"created_at": a.created_at.isoformat() if a.created_at else None,
} for a in items],
total=total,
page=page,
page_size=page_size,
)
@router.put("/{asset_id}")
def update_asset(
asset_id: int,
body: APIAssetUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
asset = api_asset_service.update_asset(db, asset_id, body.dict(exclude_unset=True))
if not asset:
from fastapi import HTTPException, status
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="资产不存在")
return ResponseModel(data={"id": asset.id})
@router.delete("/{asset_id}")
def delete_asset(
asset_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
ok = api_asset_service.delete_asset(db, asset_id)
if not ok:
from fastapi import HTTPException, status
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="资产不存在")
return ResponseModel()
@router.post("/{asset_id}/scan")
def scan_asset(
asset_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
result = api_asset_service.scan_swagger(db, asset_id)
return ResponseModel(data=result)
@router.get("/{asset_id}/endpoints")
def list_endpoints(
asset_id: int,
risk_level: Optional[str] = Query(None),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=500),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
from app.models.api_asset import APIEndpoint
query = db.query(APIEndpoint).filter(APIEndpoint.asset_id == asset_id)
if risk_level:
query = query.filter(APIEndpoint.risk_level == risk_level)
total = query.count()
items = query.order_by(APIEndpoint.id.asc()).offset((page - 1) * page_size).limit(page_size).all()
return ListResponse(
data=[{
"id": e.id,
"method": e.method,
"path": e.path,
"summary": e.summary,
"tags": e.tags,
"parameters": e.parameters,
"sensitive_fields": e.sensitive_fields,
"risk_level": e.risk_level,
"is_active": e.is_active,
} for e in items],
total=total,
page=page,
page_size=page_size,
)
+40
View File
@@ -238,3 +238,43 @@ def auto_classify(
):
result = classification_engine.run_auto_classification(db, project_id)
return ResponseModel(data=result)
@router.post("/ml-train")
def ml_train(
background: bool = True,
model_name: Optional[str] = None,
algorithm: str = "logistic_regression",
db: Session = Depends(get_db),
current_user: User = Depends(require_admin),
):
from app.tasks.ml_tasks import train_ml_model_task
from app.services.ml_service import train_model
if background:
task = train_ml_model_task.delay(model_name=model_name, algorithm=algorithm)
return ResponseModel(data={"task_id": task.id, "status": task.state})
else:
mv = train_model(db, model_name=model_name, algorithm=algorithm)
if mv:
return ResponseModel(data={"model_id": mv.id, "accuracy": mv.accuracy, "train_samples": mv.train_samples})
return ResponseModel(message="训练失败:样本不足或发生错误")
@router.get("/ml-suggest/{project_id}")
def ml_suggest(
project_id: int,
column_ids: Optional[str] = Query(None),
top_k: int = Query(3, ge=1, le=5),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
from app.services.ml_service import suggest_for_project_columns
ids = None
if column_ids:
ids = [int(x) for x in column_ids.split(",") if x.strip().isdigit()]
result = suggest_for_project_columns(db, project_id, column_ids=ids, top_k=top_k)
if not result.get("success"):
from fastapi import HTTPException, status
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=result.get("message"))
return ResponseModel(data=result["suggestions"])
+72
View File
@@ -0,0 +1,72 @@
from typing import Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.models.user import User
from app.schemas.common import ResponseModel, ListResponse
from app.services import compliance_service
from app.api.deps import get_current_user, require_admin
router = APIRouter()
@router.post("/init-rules")
def init_rules(
db: Session = Depends(get_db),
current_user: User = Depends(require_admin),
):
compliance_service.init_builtin_rules(db)
return ResponseModel(message="初始化完成")
@router.post("/scan")
def scan_compliance(
project_id: Optional[int] = Query(None),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
issues = compliance_service.scan_compliance(db, project_id=project_id)
return ResponseModel(data={"issues_found": len(issues)})
@router.get("/issues")
def list_issues(
project_id: Optional[int] = Query(None),
status: Optional[str] = Query(None),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=500),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
items, total = compliance_service.list_issues(db, project_id=project_id, status=status, page=page, page_size=page_size)
return ListResponse(
data=[{
"id": i.id,
"rule_id": i.rule_id,
"project_id": i.project_id,
"entity_type": i.entity_type,
"entity_name": i.entity_name,
"severity": i.severity,
"description": i.description,
"suggestion": i.suggestion,
"status": i.status,
"created_at": i.created_at.isoformat() if i.created_at else None,
} for i in items],
total=total,
page=page,
page_size=page_size,
)
@router.post("/issues/{issue_id}/resolve")
def resolve_issue(
issue_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
issue = compliance_service.resolve_issue(db, issue_id)
if not issue:
from fastapi import HTTPException, status
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="问题不存在")
return ResponseModel(message="已标记为已解决")
+32
View File
@@ -0,0 +1,32 @@
from typing import Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.models.user import User
from app.schemas.common import ResponseModel
from app.services import lineage_service
from app.api.deps import get_current_user
router = APIRouter()
@router.post("/parse")
def parse_lineage(
sql: str,
target_table: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
records = lineage_service.parse_sql_lineage(db, sql, target_table)
return ResponseModel(data={"records_created": len(records)})
@router.get("/graph")
def get_graph(
table_name: Optional[str] = Query(None),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
graph = lineage_service.get_lineage_graph(db, table_name=table_name)
return ResponseModel(data=graph)
+88
View File
@@ -0,0 +1,88 @@
from typing import Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.models.user import User
from app.schemas.common import ResponseModel, ListResponse
from app.services import masking_service
from app.api.deps import get_current_user, require_admin
router = APIRouter()
@router.get("/rules")
def list_masking_rules(
level_id: Optional[int] = Query(None),
category_id: Optional[int] = Query(None),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=500),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
items, total = masking_service.list_masking_rules(db, level_id=level_id, category_id=category_id, page=page, page_size=page_size)
return ListResponse(
data=[{
"id": r.id,
"name": r.name,
"level_id": r.level_id,
"category_id": r.category_id,
"algorithm": r.algorithm,
"params": r.params,
"is_active": r.is_active,
"description": r.description,
"level_name": r.level.name if r.level else None,
"category_name": r.category.name if r.category else None,
} for r in items],
total=total,
page=page,
page_size=page_size,
)
@router.post("/rules")
def create_masking_rule(
req: dict,
db: Session = Depends(get_db),
current_user: User = Depends(require_admin),
):
item = masking_service.create_masking_rule(db, req)
return ResponseModel(data={"id": item.id})
@router.put("/rules/{rule_id}")
def update_masking_rule(
rule_id: int,
req: dict,
db: Session = Depends(get_db),
current_user: User = Depends(require_admin),
):
db_obj = masking_service.get_masking_rule(db, rule_id)
if not db_obj:
from fastapi import HTTPException, status
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="规则不存在")
item = masking_service.update_masking_rule(db, db_obj, req)
return ResponseModel(data={"id": item.id})
@router.delete("/rules/{rule_id}")
def delete_masking_rule(
rule_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(require_admin),
):
masking_service.delete_masking_rule(db, rule_id)
return ResponseModel(message="删除成功")
@router.post("/preview")
def preview_masking(
source_id: int,
table_name: str,
project_id: Optional[int] = None,
limit: int = Query(20, ge=1, le=100),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
result = masking_service.preview_masking(db, source_id, table_name, project_id=project_id, limit=limit)
return ResponseModel(data=result)
+67 -3
View File
@@ -101,9 +101,73 @@ def delete_project(
@router.post("/{project_id}/auto-classify")
def project_auto_classify(
project_id: int,
background: bool = True,
db: Session = Depends(get_db),
current_user: User = Depends(require_manager),
):
from app.services.classification_engine import run_auto_classification
result = run_auto_classification(db, project_id)
return ResponseModel(data=result)
from app.tasks.classification_tasks import auto_classify_task
from celery.result import AsyncResult
project = project_service.get_project(db, project_id)
if not project:
from fastapi import HTTPException, status
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在")
if background:
# Check if already running
if project.celery_task_id:
existing = AsyncResult(project.celery_task_id)
if existing.state in ("PENDING", "PROGRESS", "STARTED"):
return ResponseModel(data={"task_id": project.celery_task_id, "status": existing.state})
task = auto_classify_task.delay(project_id)
project.celery_task_id = task.id
project.status = "scanning"
db.commit()
return ResponseModel(data={"task_id": task.id, "status": task.state})
else:
from app.services.classification_engine import run_auto_classification
project.status = "scanning"
db.commit()
result = run_auto_classification(db, project_id)
if result.get("success"):
project.status = "assigning"
else:
project.status = "created"
db.commit()
return ResponseModel(data=result)
@router.get("/{project_id}/auto-classify-status")
def project_auto_classify_status(
project_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
from celery.result import AsyncResult
import json
project = project_service.get_project(db, project_id)
if not project:
from fastapi import HTTPException, status
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在")
task_id = project.celery_task_id
if not task_id:
# Return persisted progress if any
progress = json.loads(project.scan_progress) if project.scan_progress else None
return ResponseModel(data={"status": project.status, "progress": progress})
result = AsyncResult(task_id)
progress = None
if result.state == "PROGRESS" and result.info:
progress = result.info
elif project.scan_progress:
progress = json.loads(project.scan_progress)
return ResponseModel(data={
"status": result.state,
"task_id": task_id,
"progress": progress,
"project_status": project.status,
})
+18
View File
@@ -44,12 +44,30 @@ def get_report_stats(
@router.get("/projects/{project_id}/download")
def download_report(
project_id: int,
format: str = "docx",
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
if format == "excel":
content = report_service.generate_excel_report(db, project_id)
return Response(
content=content,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f"attachment; filename=report_project_{project_id}.xlsx"},
)
content = report_service.generate_classification_report(db, project_id)
return Response(
content=content,
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
headers={"Content-Disposition": f"attachment; filename=report_project_{project_id}.docx"},
)
@router.get("/projects/{project_id}/summary")
def report_summary(
project_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
data = report_service.get_report_summary(db, project_id)
return ResponseModel(data=data)
+73
View File
@@ -0,0 +1,73 @@
from typing import Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.models.user import User
from app.schemas.common import ResponseModel, ListResponse
from app.services import risk_service
from app.api.deps import get_current_user
router = APIRouter()
@router.post("/recalculate")
def recalculate_risk(
project_id: Optional[int] = Query(None),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
if project_id:
result = risk_service.calculate_project_risk(db, project_id)
return ResponseModel(data={"project_id": project_id, "risk_score": result.risk_score if result else 0})
result = risk_service.calculate_all_projects_risk(db)
return ResponseModel(data=result)
@router.get("/top")
def risk_top(
entity_type: str = Query("project"),
n: int = Query(10, ge=1, le=100),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
items = risk_service.get_risk_top_n(db, entity_type=entity_type, n=n)
return ListResponse(
data=[{
"id": r.id,
"entity_type": r.entity_type,
"entity_id": r.entity_id,
"entity_name": r.entity_name,
"risk_score": r.risk_score,
"sensitivity_score": r.sensitivity_score,
"exposure_score": r.exposure_score,
"protection_score": r.protection_score,
"updated_at": r.updated_at.isoformat() if r.updated_at else None,
} for r in items],
total=len(items),
page=1,
page_size=n,
)
@router.get("/projects/{project_id}")
def project_risk(
project_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
from app.models.risk import RiskAssessment
item = db.query(RiskAssessment).filter(
RiskAssessment.entity_type == "project",
RiskAssessment.entity_id == project_id,
).first()
if not item:
return ResponseModel(data=None)
return ResponseModel(data={
"risk_score": item.risk_score,
"sensitivity_score": item.sensitivity_score,
"exposure_score": item.exposure_score,
"protection_score": item.protection_score,
"details": item.details,
"updated_at": item.updated_at.isoformat() if item.updated_at else None,
})
+45
View File
@@ -0,0 +1,45 @@
from typing import Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.models.user import User
from app.schemas.common import ResponseModel, ListResponse
from app.models.schema_change import SchemaChangeLog
from app.api.deps import get_current_user
router = APIRouter()
@router.get("/logs")
def list_schema_changes(
source_id: Optional[int] = Query(None),
change_type: Optional[str] = Query(None),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=500),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
query = db.query(SchemaChangeLog)
if source_id:
query = query.filter(SchemaChangeLog.source_id == source_id)
if change_type:
query = query.filter(SchemaChangeLog.change_type == change_type)
total = query.count()
items = query.order_by(SchemaChangeLog.detected_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
return ListResponse(
data=[{
"id": log.id,
"source_id": log.source_id,
"database_id": log.database_id,
"table_id": log.table_id,
"column_id": log.column_id,
"change_type": log.change_type,
"old_value": log.old_value,
"new_value": log.new_value,
"detected_at": log.detected_at.isoformat() if log.detected_at else None,
} for log in items],
total=total,
page=page,
page_size=page_size,
)
+108
View File
@@ -0,0 +1,108 @@
from typing import Optional
from fastapi import APIRouter, Depends, Query, UploadFile, File
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.models.user import User
from app.schemas.common import ResponseModel, ListResponse
from app.services import unstructured_service
from app.api.deps import get_current_user
from app.core.events import minio_client
from app.core.config import settings
from app.models.metadata import UnstructuredFile
router = APIRouter()
@router.post("/upload")
def upload_file(
file: UploadFile = File(...),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
# Determine file type
filename = file.filename or "unknown"
ext = filename.split(".")[-1].lower() if "." in filename else ""
type_map = {
"docx": "word", "doc": "word",
"xlsx": "excel", "xls": "excel",
"pdf": "pdf",
"txt": "txt",
}
file_type = type_map.get(ext, "unknown")
# Upload to MinIO
storage_path = f"unstructured/{current_user.id}/{filename}"
try:
data = file.file.read()
minio_client.put_object(
settings.MINIO_BUCKET_NAME,
storage_path,
data=data,
length=len(data),
content_type=file.content_type or "application/octet-stream",
)
except Exception as e:
return ResponseModel(message=f"上传失败: {e}")
db_obj = UnstructuredFile(
original_name=filename,
file_type=file_type,
file_size=len(data),
storage_path=storage_path,
status="pending",
created_by=current_user.id,
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
# Trigger processing
try:
result = unstructured_service.process_unstructured_file(db, db_obj.id)
return ResponseModel(data={"id": db_obj.id, "matches": result.get("matches", []), "status": "processed"})
except Exception as e:
return ResponseModel(data={"id": db_obj.id, "status": "error", "error": str(e)})
@router.get("/files")
def list_files(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=500),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
query = db.query(UnstructuredFile).filter(UnstructuredFile.created_by == current_user.id)
total = query.count()
items = query.order_by(UnstructuredFile.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
return ListResponse(
data=[{
"id": f.id,
"original_name": f.original_name,
"file_type": f.file_type,
"file_size": f.file_size,
"status": f.status,
"analysis_result": f.analysis_result,
"created_at": f.created_at.isoformat() if f.created_at else None,
} for f in items],
total=total,
page=page,
page_size=page_size,
)
@router.post("/files/{file_id}/reprocess")
def reprocess_file(
file_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
file_obj = db.query(UnstructuredFile).filter(
UnstructuredFile.id == file_id,
UnstructuredFile.created_by == current_user.id,
).first()
if not file_obj:
from fastapi import HTTPException, status
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="文件不存在")
result = unstructured_service.process_unstructured_file(db, file_id)
return ResponseModel(data={"matches": result.get("matches", []), "status": "processed"})
+23
View File
@@ -0,0 +1,23 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.models.user import User
from app.schemas.common import ResponseModel
from app.services import watermark_service
from app.api.deps import get_current_user
router = APIRouter()
@router.post("/trace")
def trace_watermark(
req: dict,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
text = req.get("text", "")
result = watermark_service.trace_watermark(db, text)
if not result:
return ResponseModel(data=None, message="未检测到水印")
return ResponseModel(data=result)