release(main): 同步 dev 最新候选改动
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled

This commit is contained in:
Cheng Zhou
2026-07-16 17:15:50 +08:00
parent 32167fba02
commit d5279b124f
393 changed files with 51630 additions and 9711 deletions
+1 -68
View File
@@ -1,12 +1,9 @@
import json
import uuid
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.deps import get_current_user, get_db_session, get_operator_role_label, is_system_admin, require_study_not_locked, require_api_permission
from app.core.project_permissions import role_has_api_permission
from app.crud import audit as audit_crud
from app.core.deps import get_current_user, get_db_session, is_system_admin, require_study_not_locked, require_api_permission
from app.crud import faq_category as category_crud
from app.crud import faq_item as faq_crud
from app.crud import faq_reply as reply_crud
@@ -31,13 +28,6 @@ def _is_system_admin(current_user) -> bool:
return is_system_admin(current_user)
def _compact_text(value: str | None, max_length: int = 40) -> str:
text = " ".join(str(value or "").split())
if len(text) <= max_length:
return text
return f"{text[:max_length]}..."
@router.post(
"/",
response_model=FaqRead,
@@ -74,17 +64,6 @@ async def create_faq(
reply_in=FaqReplyCreate(content=payload.answer),
)
await faq_crud.set_status(db, item.id, "PROCESSING")
question_name = _compact_text(item.question)
await audit_crud.log_action(
db,
study_id=payload.study_id,
entity_type="faq_item",
entity_id=item.id,
action="CREATE_FAQ_ITEM",
detail=json.dumps({"targetName": question_name, "description": f"创建医学咨询问题“{question_name}"}, ensure_ascii=False),
operator_id=current_user.id,
operator_role=await get_operator_role_label(db, payload.study_id, current_user),
)
return FaqRead.model_validate(item)
@@ -178,19 +157,6 @@ async def update_faq(
if not item:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在")
updated = await faq_crud.update_item(db, item, payload)
action = "UPDATE_FAQ_ITEM"
question_name = _compact_text(updated.question)
detail = json.dumps({"targetName": question_name, "description": f"更新医学咨询问题“{question_name}"}, ensure_ascii=False)
await audit_crud.log_action(
db,
study_id=item.study_id,
entity_type="faq_item",
entity_id=item_id,
action=action,
detail=detail,
operator_id=current_user.id,
operator_role=await get_operator_role_label(db, item.study_id, current_user),
)
return FaqRead.model_validate(updated)
@@ -337,17 +303,6 @@ async def create_reply(
if item.status != "RESOLVED":
await faq_crud.set_status(db, item.id, "PROCESSING")
await faq_crud.touch_item(db, item.id)
question_name = _compact_text(item.question)
await audit_crud.log_action(
db,
study_id=item.study_id,
entity_type="faq_reply",
entity_id=reply.id,
action="CREATE_FAQ_REPLY",
detail=json.dumps({"targetName": question_name, "description": f"回复医学咨询问题“{question_name}"}, ensure_ascii=False),
operator_id=current_user.id,
operator_role=await get_operator_role_label(db, item.study_id, current_user),
)
data = FaqReplyRead.model_validate(reply)
if quote:
if quote.is_deleted:
@@ -380,20 +335,9 @@ async def delete_faq(
item = await faq_crud.get_item(db, item_id)
if not item:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在")
question_name = _compact_text(item.question)
await reply_crud.delete_replies_by_faq_id(db, item.id)
await db.delete(item)
await db.commit()
await audit_crud.log_action(
db,
study_id=item.study_id,
entity_type="faq_item",
entity_id=item_id,
action="DELETE_FAQ_ITEM",
detail=json.dumps({"targetName": question_name, "description": f"删除医学咨询问题“{question_name}"}, ensure_ascii=False),
operator_id=current_user.id,
operator_role=await get_operator_role_label(db, item.study_id, current_user),
)
@router.delete(
@@ -415,7 +359,6 @@ async def delete_reply(
item = await faq_crud.get_item(db, item_id)
if not item:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在")
question_name = _compact_text(item.question)
reply = await reply_crud.get_reply(db, reply_id)
if not reply or reply.faq_id != item.id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="回复不存在")
@@ -438,13 +381,3 @@ async def delete_reply(
resolved_by_confirm=False,
)
await faq_crud.touch_item(db, item.id)
await audit_crud.log_action(
db,
study_id=item.study_id,
entity_type="faq_reply",
entity_id=reply_id,
action="DELETE_FAQ_REPLY",
detail=json.dumps({"targetName": question_name, "description": f"删除医学咨询问题“{question_name}”的回复"}, ensure_ascii=False),
operator_id=current_user.id,
operator_role=await get_operator_role_label(db, item.study_id, current_user),
)