Initial MVP for QA asset backend
This commit is contained in:
2
hy_qa_asset_backend/backend/app/api/__init__.py
Normal file
2
hy_qa_asset_backend/backend/app/api/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
"""API routers."""
|
||||
|
||||
42
hy_qa_asset_backend/backend/app/api/dashboard.py
Normal file
42
hy_qa_asset_backend/backend/app/api/dashboard.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import models, schemas
|
||||
from app.database import get_db
|
||||
|
||||
router = APIRouter(prefix="/dashboard", tags=["dashboard"])
|
||||
|
||||
|
||||
@router.get("/summary", response_model=schemas.DashboardSummary)
|
||||
def dashboard_summary(db: Session = Depends(get_db)) -> schemas.DashboardSummary:
|
||||
last_task = db.query(models.TaskRun).order_by(models.TaskRun.created_at.desc()).first()
|
||||
return schemas.DashboardSummary(
|
||||
total_sessions=db.query(models.FeishuSession).count(),
|
||||
processed_sessions=db.query(models.FeishuSession)
|
||||
.filter(models.FeishuSession.process_status.in_([models.ProcessStatus.pending_review, models.ProcessStatus.completed]))
|
||||
.count(),
|
||||
pending_review_count=db.query(models.RawQAItem)
|
||||
.filter(models.RawQAItem.review_status.in_([models.ReviewStatus.pending, models.ReviewStatus.reviewing]))
|
||||
.count(),
|
||||
high_risk_count=db.query(models.RawQAItem)
|
||||
.filter(or_(models.RawQAItem.risk_level == models.RiskLevel.high, models.RawQAItem.risk_level == models.RiskLevel.forbidden))
|
||||
.count(),
|
||||
approved_qa_count=db.query(models.RawQAItem).filter(models.RawQAItem.review_status == models.ReviewStatus.approved).count(),
|
||||
standard_qa_count=db.query(models.StandardQAItem).count(),
|
||||
callable_qa_count=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.done, models.DesensitizationStatus.not_needed]
|
||||
),
|
||||
)
|
||||
.count(),
|
||||
last_task_status=last_task.task_status.value if last_task else None,
|
||||
last_task_time=last_task.ended_at if last_task else None,
|
||||
last_task_error=last_task.error_message if last_task else None,
|
||||
)
|
||||
|
||||
112
hy_qa_asset_backend/backend/app/api/feishu.py
Normal file
112
hy_qa_asset_backend/backend/app/api/feishu.py
Normal file
@@ -0,0 +1,112 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app import schemas
|
||||
from app.services.feishu_client import FeishuClient
|
||||
|
||||
router = APIRouter(prefix="/feishu", tags=["feishu"])
|
||||
|
||||
|
||||
@router.get("/status", response_model=schemas.FeishuStatus)
|
||||
def feishu_status() -> dict:
|
||||
return FeishuClient().connection_status()
|
||||
|
||||
|
||||
@router.get("/setup-guide", response_model=schemas.FeishuSetupGuide)
|
||||
def setup_guide() -> schemas.FeishuSetupGuide:
|
||||
return schemas.FeishuSetupGuide(
|
||||
required_env=[
|
||||
"FEISHU_APP_ID",
|
||||
"FEISHU_APP_SECRET",
|
||||
"FEISHU_APP_TOKEN 或 FEISHU_WIKI_NODE_TOKEN",
|
||||
"FEISHU_TABLE_ID_SESSION",
|
||||
"FEISHU_TABLE_ID_RAW_QA",
|
||||
"FEISHU_TABLE_ID_STANDARD_QA",
|
||||
"MOCK_MODE=false",
|
||||
],
|
||||
required_permissions=[
|
||||
"bitable:app:readonly",
|
||||
"bitable:app:readwrite",
|
||||
"docx:document:readonly",
|
||||
"drive:drive:readonly",
|
||||
"如果要读取文档链接里的内容,需要把飞书应用/机器人加入目标文档协作者。",
|
||||
],
|
||||
tables=[
|
||||
schemas.FeishuTableTemplate(
|
||||
table_key="session",
|
||||
table_name="答疑场次",
|
||||
env_name="FEISHU_TABLE_ID_SESSION",
|
||||
required_fields=[
|
||||
"场次编号",
|
||||
"标题",
|
||||
"日期",
|
||||
"答疑老师",
|
||||
"飞书资料链接",
|
||||
"转写稿",
|
||||
"处理状态",
|
||||
"问答总数",
|
||||
"待审核数",
|
||||
"已审核数",
|
||||
"已入库数",
|
||||
"失败原因",
|
||||
],
|
||||
),
|
||||
schemas.FeishuTableTemplate(
|
||||
table_key="raw_qa",
|
||||
table_name="原始问答",
|
||||
env_name="FEISHU_TABLE_ID_RAW_QA",
|
||||
required_fields=[
|
||||
"问答编号",
|
||||
"场次编号",
|
||||
"原始问题",
|
||||
"问题整理版",
|
||||
"原始回答",
|
||||
"回答整理版",
|
||||
"AI建议标准问题",
|
||||
"AI建议标准回答",
|
||||
"回答人",
|
||||
"一级主题",
|
||||
"问题标签",
|
||||
"课程阶段",
|
||||
"适用人群",
|
||||
"情绪强度",
|
||||
"风险等级",
|
||||
"风险类型",
|
||||
"风险说明",
|
||||
"是否需脱敏",
|
||||
"脱敏状态",
|
||||
"审核状态",
|
||||
"建议入库",
|
||||
"来源时间戳",
|
||||
"审核备注",
|
||||
],
|
||||
),
|
||||
schemas.FeishuTableTemplate(
|
||||
table_key="standard_qa",
|
||||
table_name="标准问答",
|
||||
env_name="FEISHU_TABLE_ID_STANDARD_QA",
|
||||
required_fields=[
|
||||
"标准编号",
|
||||
"来源原始问答ID",
|
||||
"来源场次ID",
|
||||
"标准问题",
|
||||
"标准回答",
|
||||
"相似问法",
|
||||
"一级主题",
|
||||
"问题标签",
|
||||
"课程阶段",
|
||||
"适用人群",
|
||||
"回答边界",
|
||||
"禁止表达",
|
||||
"风险等级",
|
||||
"审核状态",
|
||||
"调用状态",
|
||||
"禁用原因",
|
||||
],
|
||||
),
|
||||
],
|
||||
notes=[
|
||||
"场次表中只有处理状态为空、unprocessed 或 未处理 的记录会被后台扫描处理。",
|
||||
"如果暂时不接文档 API,可以直接把完整文字稿粘到场次表的 转写稿 字段。",
|
||||
"敏感密钥不要写入 system_settings,请写入 backend/.env 或运行环境变量。",
|
||||
],
|
||||
)
|
||||
18
hy_qa_asset_backend/backend/app/api/logs.py
Normal file
18
hy_qa_asset_backend/backend/app/api/logs.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import models, schemas
|
||||
from app.database import get_db
|
||||
|
||||
router = APIRouter(prefix="/logs", tags=["logs"])
|
||||
|
||||
|
||||
@router.get("/audit", response_model=list[schemas.AuditLogRead])
|
||||
def audit_logs(db: Session = Depends(get_db)) -> list[models.AuditLog]:
|
||||
return db.query(models.AuditLog).order_by(models.AuditLog.created_at.desc()).limit(200).all()
|
||||
|
||||
|
||||
@router.get("/tasks", response_model=list[schemas.TaskRunRead])
|
||||
def task_logs(db: Session = Depends(get_db)) -> list[models.TaskRun]:
|
||||
return db.query(models.TaskRun).order_by(models.TaskRun.created_at.desc()).limit(200).all()
|
||||
|
||||
150
hy_qa_asset_backend/backend/app/api/raw_qa.py
Normal file
150
hy_qa_asset_backend/backend/app/api/raw_qa.py
Normal file
@@ -0,0 +1,150 @@
|
||||
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, approve_raw_qa, refresh_session_counts, set_raw_status
|
||||
|
||||
router = APIRouter(prefix="/raw-qa", tags=["raw_qa"])
|
||||
|
||||
|
||||
def _get_raw(db: Session, raw_id: int) -> models.RawQAItem:
|
||||
raw = db.get(models.RawQAItem, raw_id)
|
||||
if not raw:
|
||||
raise HTTPException(status_code=404, detail="原始问答不存在")
|
||||
return raw
|
||||
|
||||
|
||||
@router.get("", response_model=list[schemas.RawQAItemRead])
|
||||
def list_raw_qa(
|
||||
review_status: str | None = None,
|
||||
risk_level: str | None = None,
|
||||
session_id: int | None = None,
|
||||
primary_topic: str | None = None,
|
||||
course_stage: str | None = None,
|
||||
db: Session = Depends(get_db),
|
||||
) -> list[models.RawQAItem]:
|
||||
query = db.query(models.RawQAItem)
|
||||
if review_status:
|
||||
query = query.filter(models.RawQAItem.review_status == review_status)
|
||||
if risk_level:
|
||||
query = query.filter(models.RawQAItem.risk_level == risk_level)
|
||||
if session_id:
|
||||
query = query.filter(models.RawQAItem.session_id == session_id)
|
||||
if primary_topic:
|
||||
query = query.filter(models.RawQAItem.primary_topic == primary_topic)
|
||||
if course_stage:
|
||||
query = query.filter(models.RawQAItem.course_stage == course_stage)
|
||||
return query.order_by(models.RawQAItem.created_at.desc()).all()
|
||||
|
||||
|
||||
@router.get("/{raw_id}", response_model=schemas.RawQAItemRead)
|
||||
def get_raw_qa(raw_id: int, db: Session = Depends(get_db)) -> models.RawQAItem:
|
||||
return _get_raw(db, raw_id)
|
||||
|
||||
|
||||
@router.patch("/{raw_id}", response_model=schemas.RawQAItemRead)
|
||||
def update_raw_qa(raw_id: int, payload: schemas.RawQAItemUpdate, db: Session = Depends(get_db)) -> models.RawQAItem:
|
||||
raw = _get_raw(db, raw_id)
|
||||
for key, value in payload.model_dump(exclude_unset=True).items():
|
||||
setattr(raw, key, value)
|
||||
db.commit()
|
||||
db.refresh(raw)
|
||||
return raw
|
||||
|
||||
|
||||
@router.post("/{raw_id}/approve", response_model=schemas.RawQAItemRead)
|
||||
def approve(raw_id: int, payload: schemas.ReviewActionRequest | None = None, db: Session = Depends(get_db)) -> models.RawQAItem:
|
||||
payload = payload or schemas.ReviewActionRequest()
|
||||
return approve_raw_qa(db, _get_raw(db, raw_id), payload.reviewer_id, payload.comment)
|
||||
|
||||
|
||||
@router.post("/{raw_id}/revise", response_model=schemas.RawQAItemRead)
|
||||
def revise(raw_id: int, payload: schemas.ReviewActionRequest | None = None, db: Session = Depends(get_db)) -> models.RawQAItem:
|
||||
payload = payload or schemas.ReviewActionRequest()
|
||||
return set_raw_status(
|
||||
db,
|
||||
_get_raw(db, raw_id),
|
||||
models.ReviewStatus.needs_revision,
|
||||
models.AuditAction.revise,
|
||||
payload.reviewer_id,
|
||||
payload.comment,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{raw_id}/reject", response_model=schemas.RawQAItemRead)
|
||||
def reject(raw_id: int, payload: schemas.ReviewActionRequest | None = None, db: Session = Depends(get_db)) -> models.RawQAItem:
|
||||
payload = payload or schemas.ReviewActionRequest()
|
||||
return set_raw_status(
|
||||
db,
|
||||
_get_raw(db, raw_id),
|
||||
models.ReviewStatus.rejected,
|
||||
models.AuditAction.reject,
|
||||
payload.reviewer_id,
|
||||
payload.comment,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{raw_id}/forbid", response_model=schemas.RawQAItemRead)
|
||||
def forbid(raw_id: int, payload: schemas.ReviewActionRequest | None = None, db: Session = Depends(get_db)) -> models.RawQAItem:
|
||||
payload = payload or schemas.ReviewActionRequest()
|
||||
raw = _get_raw(db, raw_id)
|
||||
raw.suggested_to_standard_qa = False
|
||||
return set_raw_status(
|
||||
db,
|
||||
raw,
|
||||
models.ReviewStatus.forbidden,
|
||||
models.AuditAction.forbid,
|
||||
payload.reviewer_id,
|
||||
payload.comment,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{raw_id}/mark-high-risk", response_model=schemas.RawQAItemRead)
|
||||
def mark_high_risk(
|
||||
raw_id: int, payload: schemas.ReviewActionRequest | None = None, db: Session = Depends(get_db)
|
||||
) -> models.RawQAItem:
|
||||
payload = payload or schemas.ReviewActionRequest()
|
||||
raw = _get_raw(db, raw_id)
|
||||
before = raw.risk_level.value
|
||||
raw.risk_level = models.RiskLevel.high
|
||||
raw.suggested_to_standard_qa = False
|
||||
add_audit_log(
|
||||
db,
|
||||
user_id=payload.reviewer_id,
|
||||
target_type=models.TargetType.raw_qa,
|
||||
target_id=raw.id,
|
||||
action=models.AuditAction.mark_high_risk,
|
||||
before_status=before,
|
||||
after_status=raw.risk_level.value,
|
||||
comment=payload.comment,
|
||||
)
|
||||
refresh_session_counts(db, raw.session_id)
|
||||
db.commit()
|
||||
db.refresh(raw)
|
||||
return raw
|
||||
|
||||
|
||||
@router.post("/{raw_id}/mark-desensitization", response_model=schemas.RawQAItemRead)
|
||||
def mark_desensitization(
|
||||
raw_id: int, payload: schemas.ReviewActionRequest | None = None, db: Session = Depends(get_db)
|
||||
) -> models.RawQAItem:
|
||||
payload = payload or schemas.ReviewActionRequest()
|
||||
raw = _get_raw(db, raw_id)
|
||||
before = raw.desensitization_status.value
|
||||
raw.need_desensitization = True
|
||||
raw.desensitization_status = models.DesensitizationStatus.needed
|
||||
add_audit_log(
|
||||
db,
|
||||
user_id=payload.reviewer_id,
|
||||
target_type=models.TargetType.raw_qa,
|
||||
target_id=raw.id,
|
||||
action=models.AuditAction.mark_desensitization,
|
||||
before_status=before,
|
||||
after_status=raw.desensitization_status.value,
|
||||
comment=payload.comment,
|
||||
)
|
||||
db.commit()
|
||||
db.refresh(raw)
|
||||
return raw
|
||||
|
||||
72
hy_qa_asset_backend/backend/app/api/sessions.py
Normal file
72
hy_qa_asset_backend/backend/app/api/sessions.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from datetime import date
|
||||
|
||||
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.task_runner import TaskRunner
|
||||
|
||||
router = APIRouter(prefix="/sessions", tags=["sessions"])
|
||||
|
||||
|
||||
@router.get("", response_model=list[schemas.FeishuSessionRead])
|
||||
def list_sessions(db: Session = Depends(get_db)) -> list[models.FeishuSession]:
|
||||
return db.query(models.FeishuSession).order_by(models.FeishuSession.created_at.desc()).all()
|
||||
|
||||
|
||||
@router.get("/{session_id}", response_model=schemas.FeishuSessionRead)
|
||||
def get_session(session_id: int, db: Session = Depends(get_db)) -> models.FeishuSession:
|
||||
session = db.get(models.FeishuSession, session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="场次不存在")
|
||||
return session
|
||||
|
||||
|
||||
@router.post("", response_model=schemas.FeishuSessionRead)
|
||||
def create_session(payload: schemas.FeishuSessionCreate, db: Session = Depends(get_db)) -> models.FeishuSession:
|
||||
next_id = db.query(models.FeishuSession).count() + 1
|
||||
session = models.FeishuSession(
|
||||
session_code=payload.session_code or f"MANUAL-{next_id:04d}",
|
||||
title=payload.title,
|
||||
date=payload.date or date.today(),
|
||||
teachers=payload.teachers,
|
||||
feishu_doc_url=payload.feishu_doc_url,
|
||||
feishu_record_id=payload.feishu_record_id,
|
||||
source_file_name=payload.source_file_name,
|
||||
source_text=payload.source_text,
|
||||
process_status=models.ProcessStatus.unprocessed,
|
||||
)
|
||||
db.add(session)
|
||||
db.commit()
|
||||
db.refresh(session)
|
||||
return session
|
||||
|
||||
|
||||
@router.patch("/{session_id}", response_model=schemas.FeishuSessionRead)
|
||||
def update_session(
|
||||
session_id: int, payload: schemas.FeishuSessionUpdate, db: Session = Depends(get_db)
|
||||
) -> models.FeishuSession:
|
||||
session = db.get(models.FeishuSession, session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="场次不存在")
|
||||
for key, value in payload.model_dump(exclude_unset=True).items():
|
||||
setattr(session, key, value)
|
||||
db.commit()
|
||||
db.refresh(session)
|
||||
return session
|
||||
|
||||
|
||||
@router.post("/{session_id}/process", response_model=schemas.TaskRunRead)
|
||||
async def process_session(session_id: int, db: Session = Depends(get_db)) -> models.TaskRun:
|
||||
runner = TaskRunner()
|
||||
task = await runner.run(db, task_type=models.TaskType.manual_process, session_id=session_id)
|
||||
return task
|
||||
|
||||
|
||||
@router.post("/{session_id}/reprocess", response_model=schemas.TaskRunRead)
|
||||
async def reprocess_session(session_id: int, db: Session = Depends(get_db)) -> models.TaskRun:
|
||||
runner = TaskRunner()
|
||||
task = await runner.run(db, task_type=models.TaskType.manual_process, session_id=session_id, reprocess=True)
|
||||
return task
|
||||
|
||||
51
hy_qa_asset_backend/backend/app/api/settings.py
Normal file
51
hy_qa_asset_backend/backend/app/api/settings.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import models, schemas
|
||||
from app.config import get_settings
|
||||
from app.database import get_db
|
||||
|
||||
router = APIRouter(prefix="/settings", tags=["settings"])
|
||||
|
||||
SENSITIVE_MARKERS = {"secret", "token", "key", "password", "api_key"}
|
||||
|
||||
|
||||
@router.get("", response_model=schemas.SafeSettingsRead)
|
||||
def read_settings(db: Session = Depends(get_db)) -> schemas.SafeSettingsRead:
|
||||
settings = get_settings()
|
||||
rows = db.query(models.SystemSetting).order_by(models.SystemSetting.key.asc()).all()
|
||||
driver = settings.resolved_database_url.split(":", 1)[0]
|
||||
return schemas.SafeSettingsRead(
|
||||
feishu_configured=settings.feishu_configured,
|
||||
openai_configured=settings.openai_configured,
|
||||
model_name=settings.model_name,
|
||||
schedule_cron=settings.schedule_cron,
|
||||
schedule_timezone=settings.schedule_timezone,
|
||||
mock_mode=settings.mock_mode or not settings.feishu_configured,
|
||||
database_url=f"{driver}://***",
|
||||
feishu_tables_configured={
|
||||
"session": bool(settings.feishu_table_id_session),
|
||||
"raw_qa": bool(settings.feishu_table_id_raw_qa),
|
||||
"standard_qa": bool(settings.feishu_table_id_standard_qa),
|
||||
},
|
||||
system_settings=[
|
||||
{"key": row.key, "value": row.value, "description": row.description, "updated_at": row.updated_at}
|
||||
for row in rows
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@router.patch("", response_model=schemas.SafeSettingsRead)
|
||||
def patch_settings(payload: schemas.SettingsPatch, db: Session = Depends(get_db)) -> schemas.SafeSettingsRead:
|
||||
for key, value in payload.settings.items():
|
||||
lower = key.lower()
|
||||
if any(marker in lower for marker in SENSITIVE_MARKERS):
|
||||
raise HTTPException(status_code=400, detail="敏感密钥不能写入 system_settings,请使用 .env")
|
||||
row = db.query(models.SystemSetting).filter(models.SystemSetting.key == key).first()
|
||||
if row:
|
||||
row.value = value
|
||||
else:
|
||||
db.add(models.SystemSetting(key=key, value=value, description="后台页面写入的非敏感配置"))
|
||||
db.commit()
|
||||
return read_settings(db)
|
||||
|
||||
146
hy_qa_asset_backend/backend/app/api/standard_qa.py
Normal file
146
hy_qa_asset_backend/backend/app/api/standard_qa.py
Normal 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
|
||||
|
||||
28
hy_qa_asset_backend/backend/app/api/tasks.py
Normal file
28
hy_qa_asset_backend/backend/app/api/tasks.py
Normal file
@@ -0,0 +1,28 @@
|
||||
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.task_runner import TaskRunner
|
||||
|
||||
router = APIRouter(prefix="/tasks", tags=["tasks"])
|
||||
|
||||
|
||||
@router.get("/runs", response_model=list[schemas.TaskRunRead])
|
||||
def list_task_runs(db: Session = Depends(get_db)) -> list[models.TaskRun]:
|
||||
return db.query(models.TaskRun).order_by(models.TaskRun.created_at.desc()).limit(100).all()
|
||||
|
||||
|
||||
@router.get("/runs/{task_id}", response_model=schemas.TaskRunRead)
|
||||
def get_task_run(task_id: int, db: Session = Depends(get_db)) -> models.TaskRun:
|
||||
task = db.get(models.TaskRun, task_id)
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
return task
|
||||
|
||||
|
||||
@router.post("/run-now", response_model=schemas.TaskRunRead)
|
||||
async def run_now(db: Session = Depends(get_db)) -> models.TaskRun:
|
||||
runner = TaskRunner()
|
||||
return await runner.run(db, task_type=models.TaskType.manual_scan)
|
||||
|
||||
Reference in New Issue
Block a user