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:
@@ -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资产"])
|
||||
|
||||
@@ -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})
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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"])
|
||||
|
||||
@@ -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="已标记为已解决")
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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"})
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user