6d70520e79
- 新增后端模块:Alert、APIAsset、Compliance、Lineage、Masking、Risk、SchemaChange、Unstructured、Watermark - 新增前端模块页面与API接口 - 新增Alembic迁移脚本(002-014)覆盖全量业务表 - 新增测试数据生成脚本与集成测试脚本 - 修复metadata模型JSON类型导入缺失导致启动失败的问题 - 修复前端Alert/APIAsset页面request模块路径错误 - 更新docker-compose与开发计划文档
281 lines
11 KiB
Python
281 lines
11 KiB
Python
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.classification import (
|
|
CategoryCreate, CategoryUpdate, CategoryOut, CategoryTree,
|
|
DataLevelOut, RecognitionRuleCreate, RecognitionRuleUpdate, RecognitionRuleOut,
|
|
TemplateOut,
|
|
)
|
|
from app.schemas.common import ResponseModel, ListResponse
|
|
from app.services import classification_service, classification_engine
|
|
from app.api.deps import get_current_user, require_admin, require_labeler, require_guest_or_above, _is_admin, ROLE_PROJECT_MANAGER
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/categories/tree", response_model=ResponseModel[list])
|
|
def get_category_tree(
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tree = classification_service.build_category_tree(db)
|
|
return ResponseModel(data=tree)
|
|
|
|
|
|
@router.get("/categories", response_model=ResponseModel[list[CategoryOut]])
|
|
def list_categories(
|
|
parent_id: Optional[int] = Query(None),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
items = classification_service.list_categories(db, parent_id=parent_id)
|
|
return ResponseModel(data=[CategoryOut.model_validate(i) for i in items])
|
|
|
|
|
|
@router.post("/categories", response_model=ResponseModel[CategoryOut])
|
|
def create_category(
|
|
req: CategoryCreate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_admin),
|
|
):
|
|
item = classification_service.create_category(db, req)
|
|
return ResponseModel(data=CategoryOut.model_validate(item))
|
|
|
|
|
|
@router.put("/categories/{category_id}", response_model=ResponseModel[CategoryOut])
|
|
def update_category(
|
|
category_id: int,
|
|
req: CategoryUpdate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_admin),
|
|
):
|
|
db_obj = classification_service.get_category(db, category_id)
|
|
if not db_obj:
|
|
from fastapi import HTTPException, status
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="分类不存在")
|
|
item = classification_service.update_category(db, db_obj, req)
|
|
return ResponseModel(data=CategoryOut.model_validate(item))
|
|
|
|
|
|
@router.delete("/categories/{category_id}")
|
|
def delete_category(
|
|
category_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_admin),
|
|
):
|
|
classification_service.delete_category(db, category_id)
|
|
return ResponseModel(message="删除成功")
|
|
|
|
|
|
@router.get("/levels", response_model=ResponseModel[list[DataLevelOut]])
|
|
def list_levels(
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
items = classification_service.list_data_levels(db)
|
|
return ResponseModel(data=[DataLevelOut.model_validate(i) for i in items])
|
|
|
|
|
|
@router.get("/rules", response_model=ListResponse[RecognitionRuleOut])
|
|
def list_rules(
|
|
template_id: Optional[int] = Query(None),
|
|
keyword: 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 = classification_service.list_rules(db, template_id=template_id, keyword=keyword, page=page, page_size=page_size)
|
|
out = []
|
|
for i in items:
|
|
data = RecognitionRuleOut.model_validate(i)
|
|
data.category_name = i.category.name if i.category else None
|
|
data.level_name = i.level.name if i.level else None
|
|
data.level_color = i.level.color if i.level else None
|
|
out.append(data)
|
|
return ListResponse(data=out, total=total, page=page, page_size=page_size)
|
|
|
|
|
|
@router.post("/rules", response_model=ResponseModel[RecognitionRuleOut])
|
|
def create_rule(
|
|
req: RecognitionRuleCreate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_admin),
|
|
):
|
|
item = classification_service.create_rule(db, req)
|
|
data = RecognitionRuleOut.model_validate(item)
|
|
data.category_name = item.category.name if item.category else None
|
|
data.level_name = item.level.name if item.level else None
|
|
data.level_color = item.level.color if item.level else None
|
|
return ResponseModel(data=data)
|
|
|
|
|
|
@router.put("/rules/{rule_id}", response_model=ResponseModel[RecognitionRuleOut])
|
|
def update_rule(
|
|
rule_id: int,
|
|
req: RecognitionRuleUpdate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_admin),
|
|
):
|
|
db_obj = classification_service.get_rule(db, rule_id)
|
|
if not db_obj:
|
|
from fastapi import HTTPException, status
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="规则不存在")
|
|
item = classification_service.update_rule(db, db_obj, req)
|
|
data = RecognitionRuleOut.model_validate(item)
|
|
data.category_name = item.category.name if item.category else None
|
|
data.level_name = item.level.name if item.level else None
|
|
data.level_color = item.level.color if item.level else None
|
|
return ResponseModel(data=data)
|
|
|
|
|
|
@router.delete("/rules/{rule_id}")
|
|
def delete_rule(
|
|
rule_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_admin),
|
|
):
|
|
classification_service.delete_rule(db, rule_id)
|
|
return ResponseModel(message="删除成功")
|
|
|
|
|
|
@router.get("/templates", response_model=ResponseModel[list[TemplateOut]])
|
|
def list_templates(
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
items = classification_service.list_templates(db)
|
|
return ResponseModel(data=[TemplateOut.model_validate(i) for i in items])
|
|
|
|
|
|
@router.get("/results", response_model=ListResponse)
|
|
def list_results(
|
|
project_id: Optional[int] = Query(None),
|
|
level_id: Optional[int] = Query(None),
|
|
keyword: 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(require_guest_or_above),
|
|
):
|
|
from app.services.project_service import list_results as _list_results
|
|
from app.models.project import ClassificationProject, ClassificationTask
|
|
from app.api.deps import _has_role
|
|
|
|
# Data isolation: compute allowed project IDs for non-admin users
|
|
allowed_project_ids = None
|
|
if not _is_admin(current_user):
|
|
if _has_role(current_user, ROLE_PROJECT_MANAGER):
|
|
# Project managers see their own projects
|
|
allowed_project_ids = [
|
|
p.id for p in db.query(ClassificationProject.id).filter(
|
|
ClassificationProject.created_by == current_user.id
|
|
).all()
|
|
]
|
|
elif _has_role(current_user, 'labeler') or _has_role(current_user, 'reviewer'):
|
|
# Labelers/reviewers see projects where they have tasks
|
|
task_projects = db.query(ClassificationTask.project_id).filter(
|
|
ClassificationTask.assignee_id == current_user.id
|
|
).distinct().all()
|
|
allowed_project_ids = [p[0] for p in task_projects]
|
|
else:
|
|
# Guests see all results (read-only)
|
|
allowed_project_ids = None
|
|
if allowed_project_ids is not None and not allowed_project_ids:
|
|
allowed_project_ids = []
|
|
|
|
# If a specific project_id is requested, check permission
|
|
if project_id and allowed_project_ids is not None and project_id not in allowed_project_ids:
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权查看此项目")
|
|
|
|
items, total = _list_results(
|
|
db, project_id=project_id, keyword=keyword, page=page, page_size=page_size,
|
|
project_ids=allowed_project_ids,
|
|
)
|
|
|
|
data = []
|
|
for r in items:
|
|
col = r.column
|
|
table = col.table if col else None
|
|
database = table.database if table else None
|
|
source = database.source if database else None
|
|
|
|
if level_id and r.level_id != level_id:
|
|
continue
|
|
|
|
data.append({
|
|
"id": r.id,
|
|
"project_id": r.project_id,
|
|
"column_id": col.id if col else None,
|
|
"column_name": col.name if col else None,
|
|
"data_type": col.data_type if col else None,
|
|
"comment": col.comment if col else None,
|
|
"table_name": table.name if table else None,
|
|
"database_name": database.name if database else None,
|
|
"source_name": source.name if source else None,
|
|
"category_id": r.category_id,
|
|
"category_name": r.category.name if r.category else None,
|
|
"level_id": r.level_id,
|
|
"level_name": r.level.name if r.level else None,
|
|
"level_color": r.level.color if r.level else None,
|
|
"source": r.source,
|
|
"confidence": r.confidence,
|
|
"status": r.status,
|
|
"created_at": r.created_at.isoformat() if r.created_at else None,
|
|
})
|
|
|
|
return ListResponse(data=data, total=total, page=page, page_size=page_size)
|
|
|
|
|
|
@router.post("/auto-classify/{project_id}")
|
|
def auto_classify(
|
|
project_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
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"])
|