Initial MVP for QA asset backend

This commit is contained in:
2026-07-05 17:44:15 +08:00
commit 95b5e09fd4
71 changed files with 6546 additions and 0 deletions

View File

@@ -0,0 +1,146 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app import models, schemas
from app.database import get_db
from app.services.standard_qa_service import add_audit_log, mark_standard_call_status
router = APIRouter(prefix="/standard-qa", tags=["standard_qa"])
def _get_standard(db: Session, standard_id: int) -> models.StandardQAItem:
standard = db.get(models.StandardQAItem, standard_id)
if not standard:
raise HTTPException(status_code=404, detail="标准问答不存在")
return standard
@router.get("/callable", response_model=list[schemas.CallableQARead])
def callable_qa(db: Session = Depends(get_db)) -> list[models.StandardQAItem]:
return (
db.query(models.StandardQAItem)
.join(models.RawQAItem)
.filter(
models.StandardQAItem.audit_status == models.AuditStatus.approved,
models.StandardQAItem.call_status == models.CallStatus.callable,
models.StandardQAItem.risk_level.in_([models.RiskLevel.low, models.RiskLevel.medium]),
models.RawQAItem.desensitization_status.in_(
[models.DesensitizationStatus.not_needed, models.DesensitizationStatus.done]
),
)
.order_by(models.StandardQAItem.updated_at.desc())
.all()
)
@router.get("", response_model=list[schemas.StandardQARead])
def list_standard_qa(
audit_status: str | None = None,
call_status: str | None = None,
risk_level: str | None = None,
primary_topic: str | None = None,
course_stage: str | None = None,
db: Session = Depends(get_db),
) -> list[models.StandardQAItem]:
query = db.query(models.StandardQAItem)
if audit_status:
query = query.filter(models.StandardQAItem.audit_status == audit_status)
if call_status:
query = query.filter(models.StandardQAItem.call_status == call_status)
if risk_level:
query = query.filter(models.StandardQAItem.risk_level == risk_level)
if primary_topic:
query = query.filter(models.StandardQAItem.primary_topic == primary_topic)
if course_stage:
query = query.filter(models.StandardQAItem.course_stage == course_stage)
return query.order_by(models.StandardQAItem.updated_at.desc()).all()
@router.get("/{standard_id}", response_model=schemas.StandardQARead)
def get_standard_qa(standard_id: int, db: Session = Depends(get_db)) -> models.StandardQAItem:
return _get_standard(db, standard_id)
@router.patch("/{standard_id}", response_model=schemas.StandardQARead)
def update_standard_qa(
standard_id: int, payload: schemas.StandardQAUpdate, db: Session = Depends(get_db)
) -> models.StandardQAItem:
standard = _get_standard(db, standard_id)
for key, value in payload.model_dump(exclude_unset=True).items():
setattr(standard, key, value)
db.commit()
db.refresh(standard)
return standard
@router.post("/{standard_id}/mark-callable", response_model=schemas.StandardQARead)
def mark_callable(
standard_id: int, payload: schemas.ReviewActionRequest | None = None, db: Session = Depends(get_db)
) -> models.StandardQAItem:
payload = payload or schemas.ReviewActionRequest()
try:
return mark_standard_call_status(
db, _get_standard(db, standard_id), models.CallStatus.callable, payload.reviewer_id, payload.comment
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@router.post("/{standard_id}/mark-not-callable", response_model=schemas.StandardQARead)
def mark_not_callable(
standard_id: int, payload: schemas.ReviewActionRequest | None = None, db: Session = Depends(get_db)
) -> models.StandardQAItem:
payload = payload or schemas.ReviewActionRequest()
return mark_standard_call_status(
db, _get_standard(db, standard_id), models.CallStatus.not_callable, payload.reviewer_id, payload.comment
)
@router.post("/{standard_id}/disable", response_model=schemas.StandardQARead)
def disable_standard(
standard_id: int, payload: schemas.ReviewActionRequest | None = None, db: Session = Depends(get_db)
) -> models.StandardQAItem:
payload = payload or schemas.ReviewActionRequest()
standard = _get_standard(db, standard_id)
before = standard.audit_status.value
standard.audit_status = models.AuditStatus.disabled
standard.call_status = models.CallStatus.forbidden
standard.disabled_reason = payload.comment
add_audit_log(
db,
user_id=payload.reviewer_id,
target_type=models.TargetType.standard_qa,
target_id=standard.id,
action=models.AuditAction.disable,
before_status=before,
after_status=standard.audit_status.value,
comment=payload.comment,
)
db.commit()
db.refresh(standard)
return standard
@router.post("/{standard_id}/need-review", response_model=schemas.StandardQARead)
def need_review(
standard_id: int, payload: schemas.ReviewActionRequest | None = None, db: Session = Depends(get_db)
) -> models.StandardQAItem:
payload = payload or schemas.ReviewActionRequest()
standard = _get_standard(db, standard_id)
before = standard.audit_status.value
standard.audit_status = models.AuditStatus.needs_revision
standard.call_status = models.CallStatus.need_review
add_audit_log(
db,
user_id=payload.reviewer_id,
target_type=models.TargetType.standard_qa,
target_id=standard.id,
action=models.AuditAction.revise,
before_status=before,
after_status=standard.audit_status.value,
comment=payload.comment,
)
db.commit()
db.refresh(standard)
return standard