From 0b3a4d4d3192f7a2cc4e6d79a06861ed5dbc30aa Mon Sep 17 00:00:00 2001 From: Cheng Zhou Date: Thu, 4 Jun 2026 16:30:54 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=84=E8=8C=83=E5=AE=A1=E8=AE=A1=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E7=9B=AE=E6=A0=87=E5=90=8D=E7=A7=B0=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 药品运输审计日志写入运单号、批号或中心名称作为可读目标名\n- FAQ 分类审计日志写入分类名称,删除时提前保留展示名称\n- 前端审计归一化隐藏 UUID-only 目标名,并清理旧文本中的 UUID 标识\n- 新增审计归一化测试覆盖 FAQ 分类和药品运输记录 --- backend/app/api/v1/drug_shipments.py | 18 ++++++++-- backend/app/api/v1/faq_categories.py | 8 +++-- frontend/src/audit/auditNormalize.test.ts | 40 +++++++++++++++++++++++ frontend/src/audit/index.ts | 14 ++++++-- 4 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 frontend/src/audit/auditNormalize.test.ts 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,