diff --git a/backend/app/api/v1/drug_shipments.py b/backend/app/api/v1/drug_shipments.py index f172837c..f38fca80 100644 --- a/backend/app/api/v1/drug_shipments.py +++ b/backend/app/api/v1/drug_shipments.py @@ -1,4 +1,5 @@ import uuid +import json from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession @@ -18,6 +19,10 @@ from app.schemas.drug_shipment import ( router = APIRouter() +def _shipment_audit_name(shipment) -> str: + return shipment.tracking_no or shipment.batch_no or shipment.site_name or "药品运输记录" + + async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID): study = await study_crud.get(db, study_id) if not study: @@ -59,7 +64,10 @@ async def create_shipment( entity_type="drug_shipment", entity_id=shipment.id, action="CREATE_DRUG_SHIPMENT", - detail=f"药品运输 {shipment.id} 已创建", + detail=json.dumps( + {"targetName": _shipment_audit_name(shipment), "description": f"药品运输 {_shipment_audit_name(shipment)} 已创建"}, + ensure_ascii=False, + ), operator_id=current_user.id, operator_role=await get_operator_role_label(db, study_id, current_user), ) @@ -169,7 +177,10 @@ async def update_shipment( entity_type="drug_shipment", entity_id=shipment_id, action="UPDATE_DRUG_SHIPMENT", - detail=f"药品运输 {shipment_id} 已更新", + detail=json.dumps( + {"targetName": _shipment_audit_name(shipment), "description": f"药品运输 {_shipment_audit_name(shipment)} 已更新"}, + ensure_ascii=False, + ), operator_id=current_user.id, operator_role=await get_operator_role_label(db, study_id, current_user), ) @@ -195,6 +206,7 @@ async def delete_shipment( if cra_scope and shipment.center_id not in cra_scope[0]: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") await _ensure_center_active(db, study_id, shipment.center_id) + shipment_name = _shipment_audit_name(shipment) await shipment_crud.delete_shipment(db, shipment) await audit_crud.log_action( db, @@ -202,7 +214,7 @@ async def delete_shipment( entity_type="drug_shipment", entity_id=shipment_id, action="DELETE_DRUG_SHIPMENT", - detail=f"药品运输 {shipment_id} 已删除", + detail=json.dumps({"targetName": shipment_name, "description": f"药品运输 {shipment_name} 已删除"}, ensure_ascii=False), operator_id=current_user.id, operator_role=await get_operator_role_label(db, study_id, current_user), ) diff --git a/backend/app/api/v1/faq_categories.py b/backend/app/api/v1/faq_categories.py index efbdb0ef..907541c3 100644 --- a/backend/app/api/v1/faq_categories.py +++ b/backend/app/api/v1/faq_categories.py @@ -1,4 +1,5 @@ import uuid +import json from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession @@ -48,7 +49,7 @@ async def create_category( entity_type="faq_category", entity_id=category.id, action="CREATE_FAQ_CATEGORY", - detail=f"FAQ 分类 {category.name} 已创建", + detail=json.dumps({"targetName": category.name, "description": f"FAQ 分类 {category.name} 已创建"}, ensure_ascii=False), operator_id=current_user.id, operator_role=await get_operator_role_label(db, payload.study_id, current_user), ) @@ -110,7 +111,7 @@ async def update_category( entity_type="faq_category", entity_id=category_id, action="UPDATE_FAQ_CATEGORY", - detail=f"FAQ 分类 {category_id} 已更新", + detail=json.dumps({"targetName": updated.name, "description": f"FAQ 分类 {updated.name} 已更新"}, ensure_ascii=False), operator_id=current_user.id, operator_role=await get_operator_role_label(db, updated.study_id, current_user), ) @@ -140,6 +141,7 @@ async def delete_category( item_count = await item_crud.count_items_by_category(db, category_id) if item_count > 0: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="分类下存在 FAQ,无法删除") + category_name = category.name await db.delete(category) await db.commit() await audit_crud.log_action( @@ -148,7 +150,7 @@ async def delete_category( entity_type="faq_category", entity_id=category_id, action="DELETE_FAQ_CATEGORY", - detail=f"FAQ 分类 {category_id} 已删除", + detail=json.dumps({"targetName": category_name, "description": f"FAQ 分类 {category_name} 已删除"}, ensure_ascii=False), operator_id=current_user.id, operator_role=await get_operator_role_label(db, category.study_id, current_user), ) diff --git a/frontend/src/audit/auditNormalize.test.ts b/frontend/src/audit/auditNormalize.test.ts new file mode 100644 index 00000000..2d46559c --- /dev/null +++ b/frontend/src/audit/auditNormalize.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; +import { normalizeAuditEvent } from "."; + +describe("normalizeAuditEvent", () => { + it("removes UUID tokens from legacy audit detail text", () => { + const event = normalizeAuditEvent( + { + action: "DELETE_FAQ_CATEGORY", + detail: "FAQ 分类 295c4072-3868-416e-9a35-24a63a3f97fb 已删除", + operator_id: "operator-1", + operator_role: "ADMIN", + entity_type: "faq_category", + entity_id: "295c4072-3868-416e-9a35-24a63a3f97fb", + created_at: "2026-06-04T11:16:04Z", + }, + {} + ); + + expect(event.diffText).toEqual(["FAQ 分类已删除"]); + expect(event.targetName).toBe(""); + }); + + it("keeps readable shipment identifiers but hides UUID-only shipment identifiers", () => { + const event = normalizeAuditEvent( + { + action: "CREATE_DRUG_SHIPMENT", + detail: "药品运输 835d6a00-f696-4aec-a35d-8c359be201d2 已创建", + operator_id: "operator-1", + operator_role: "ADMIN", + entity_type: "drug_shipment", + entity_id: "835d6a00-f696-4aec-a35d-8c359be201d2", + created_at: "2026-06-04T10:27:02Z", + }, + {} + ); + + expect(event.diffText).toEqual(["药品运输已创建"]); + expect(event.targetName).toBe(""); + }); +}); diff --git a/frontend/src/audit/index.ts b/frontend/src/audit/index.ts index 3763df6d..c71448ea 100644 --- a/frontend/src/audit/index.ts +++ b/frontend/src/audit/index.ts @@ -111,7 +111,17 @@ const normalizeLegacyDetailLine = (line: string): string => { } if (text === "立项配置已发布") return "立项配置已发布"; if (text.includes("siteEnrollmentPlans")) return text.replaceAll("siteEnrollmentPlans", "中心入组计划"); - return text; + return removeUuidOnlyIdentifier(text); +}; + +const uuidTextPattern = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"; + +const removeUuidOnlyIdentifier = (text: string): string => { + return text.replace(new RegExp(`\\s+${uuidTextPattern}\\s+(?=已(?:创建|更新|删除))`, "g"), ""); +}; + +const isUuidText = (value: unknown): boolean => { + return new RegExp(`^${uuidTextPattern}$`).test(String(value || "").trim()); }; const splitAuditDetailSegments = (line: string): string[] => { @@ -280,7 +290,7 @@ export const normalizeAuditEvent = (raw: any, userMap: Record): targetType: raw.entity_type || "", targetTypeLabel: dict.targetLabel, targetId: raw.entity_id || "", - targetName: detailObj.targetName || raw.entity_id || "", + targetName: detailObj.targetName || (isUuidText(raw.entity_id) ? "" : raw.entity_id || ""), actionText: dict.actionText, before, after,