Files
bxh/app/api/vocabulary.py

95 lines
3.3 KiB
Python

"""STEP 05 — Vocabulary Terms (权威词表) CRUD + lookup + merge."""
from fastapi import APIRouter, Depends, HTTPException
from app.auth import CurrentUser
from app.config import settings
from app.contracts import VocabularyTermCreate, VocabularyTermUpdate
from app.db import (
list_vocabulary_terms,
create_vocabulary_term,
update_vocabulary_term,
lookup_vocabulary,
get_conn,
)
from app.project_context import ProjectContext, get_project_context
router = APIRouter()
@router.get("/vocabulary")
async def _list(
entity_type: str | None = None,
search: str | None = None,
context: ProjectContext = Depends(get_project_context),
_user: CurrentUser = None,
):
return await list_vocabulary_terms(
context.tenant_id, context.project_id, entity_type, search
)
@router.post("/vocabulary")
async def _create(
body: VocabularyTermCreate,
user: CurrentUser,
context: ProjectContext = Depends(get_project_context),
):
data = body.model_dump()
data["tenant_id"] = context.tenant_id
data["project_id"] = context.project_id
data["created_by"] = user["username"]
return await create_vocabulary_term(data)
@router.patch("/vocabulary/{term_id}")
async def _update(term_id: int, body: VocabularyTermUpdate, _user: CurrentUser):
row = await update_vocabulary_term(term_id, body.model_dump(exclude_none=True))
if not row:
raise HTTPException(404, "Term not found")
return row
@router.get("/vocabulary/lookup")
async def _lookup(
name: str,
context: ProjectContext = Depends(get_project_context),
_user: CurrentUser = None,
):
"""Normalize a name against the vocabulary."""
result = await lookup_vocabulary(context.tenant_id, context.project_id, name)
if not result:
return {"found": False, "name": name}
return {"found": True, "canonical_name": result["canonical_name"], "term": result}
@router.post("/vocabulary/{term_id}/merge-into/{target_id}")
async def _merge(term_id: int, target_id: int, _user: CurrentUser):
"""Merge aliases from term_id into target_id, then delete term_id."""
import json
s = settings.db_schema
async with get_conn() as conn:
async with conn.cursor() as cur:
await cur.execute(f"SELECT * FROM {s}.vocabulary_terms WHERE id=%s", (term_id,))
source = await cur.fetchone()
await cur.execute(f"SELECT * FROM {s}.vocabulary_terms WHERE id=%s", (target_id,))
target = await cur.fetchone()
if not source or not target:
raise HTTPException(404, "Term not found")
source_aliases = source["aliases"] if isinstance(source["aliases"], list) else json.loads(source["aliases"] or "[]")
target_aliases = target["aliases"] if isinstance(target["aliases"], list) else json.loads(target["aliases"] or "[]")
merged = list(set(target_aliases + [source["canonical_name"]] + source_aliases))
if target["canonical_name"] in merged:
merged.remove(target["canonical_name"])
await update_vocabulary_term(target_id, {"aliases": json.dumps(merged)})
async with conn.cursor() as cur:
await cur.execute(f"DELETE FROM {s}.vocabulary_terms WHERE id=%s", (term_id,))
await conn.commit()
return {"merged": term_id, "into": target_id, "aliases": merged}