From 102c49654aae00a895fa881ce879b5fbb6cfb0f7 Mon Sep 17 00:00:00 2001 From: Pine Date: Sun, 6 Sep 2026 12:07:33 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=EF=BC=9A=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=8E=A5=E5=8F=A3=E5=A2=9E=E5=8A=A0=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=E7=BA=A7=E8=81=94=E5=88=A0=E9=99=A4=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核心改动: 1. 删除用户前先级联删除所有相关数据(60+张表): - 认证与信用:certifications、skill_tests、credit_scores、credit_ledger、credit_dimensions、user_badges - IM/消息:messages、dm_messages、notifications - 服务大厅:opc_services、service_inquiries、service_orders、community_posts、community_comments、community_likes、community_follows - 培训:training_enrollments、training_course_enrollments、training_lesson_progress、training_certificates - 招聘:jobs、job_applications - 人才:talent_profiles - 用户主页:user_pages、user_page_review_logs - 邀请:invite_records(同时删除作为邀请人的记录) - 财务:finance_records、escrows、contracts、invoices、disputes、ratings - 任务:tasks、task_claims、task_deliveries、task_milestones、task_acceptances - 园区/企业:park_members、company_members、park_admissions、park_transfers(同时删除作为负责人的企业) - 内容互动:content_likes、content_comments、content_comment_likes - 其他:sessions、agents、opc_profiles、investor_preferences、roadshow_registrations等 2. 安全防护: - 防止删除当前登录账号 - 级联删除失败的表自动跳过,不影响主流程 - 返回级联删除的记录数量 3. 算力引擎同步删除: - 删除用户后同步删除算力引擎中的账号 - 失败不阻断主流程 4. 审计日志: - 记录删除操作和级联删除数量 --- app/api/routers/rbac_admin.py | 104 +++++++++++++++++++++++++++++++--- 1 file changed, 97 insertions(+), 7 deletions(-) diff --git a/app/api/routers/rbac_admin.py b/app/api/routers/rbac_admin.py index 6361706..211a6fc 100644 --- a/app/api/routers/rbac_admin.py +++ b/app/api/routers/rbac_admin.py @@ -177,23 +177,113 @@ async def update_user( return {"ok": True, "user": await db.users.to_profile(user)} -@router.delete("/users/{user_id}", summary="删除用户") +@router.delete("/users/{user_id}", summary="删除用户(级联删除所有相关数据)") async def delete_user( user_id: str, request: Request, db: Database = Depends(get_db), actor: dict = Depends(require_permission("action:user.manage")), ): + from sqlalchemy import text as sql_text + user = await db.users.get_by_id(user_id) + if user is None: + raise HTTPException(status_code=404, detail="用户不存在") + + # 防止删除自己 + if actor.get("id") == user_id: + raise HTTPException(status_code=400, detail="不能删除当前登录账号") + + username = user.get("username", "") + + # ═══════════════════════════════════════════════════════════ + # 级联删除所有与用户相关的数据 + # ═══════════════════════════════════════════════════════════ + cascade_tables = [ + # 认证与信用 + "certifications", "skill_tests", "credit_scores", "credit_ledger", + "credit_dimensions", "user_badges", + # IM/消息/通知 + "messages", "dm_messages", "notifications", + # 服务大厅 + "opc_services", "service_inquiries", "service_orders", + "community_posts", "community_comments", "community_likes", "community_follows", + # 培训 + "training_enrollments", "training_course_enrollments", + "training_lesson_progress", "training_certificates", + # 招聘 + "jobs", "job_applications", + # 人才 + "talent_profiles", + # 用户主页 + "user_pages", "user_page_review_logs", + # 邀请 + "invite_records", + # 财务 + "finance_records", "escrows", "contracts", "invoices", + "disputes", "ratings", + # 任务 + "tasks", "task_claims", "task_deliveries", "task_milestones", "task_acceptances", + # 园区/企业成员关系 + "park_members", "company_members", + "park_admissions", "park_transfers", + # 内容互动 + "content_likes", "content_comments", "content_comment_likes", + # 其他 + "sessions", "agents", "opc_profiles", + "investor_preferences", "roadshow_registrations", "investment_intents", + "bids", "subsidy_applications", "service_referrals", + "bookings", "tests", "policy_logs", "plan_logs", "survey_logs", + "incubator_monthly_reports", "incubator_area_reservations", + "market_purchase_orders", "market_purchases", + ] + + deleted_count = 0 + for table in cascade_tables: + try: + result = await db.session.execute( + sql_text(f"DELETE FROM {table} WHERE user_id = :uid"), + {"uid": user_id} + ) + deleted_count += result.rowcount or 0 + except Exception as e: + # 表不存在或字段不存在时跳过,不影响主流程 + print(f"[delete_user] 跳过表 {table}: {e}") + await db.session.rollback() + + # 删除作为邀请人的邀请记录(inviter_id 字段) + try: + await db.session.execute( + sql_text("DELETE FROM invite_records WHERE inviter_id = :uid"), + {"uid": user_id} + ) + except Exception: + await db.session.rollback() + + # 删除作为负责人的企业(owner_user_id 字段) + try: + await db.session.execute( + sql_text("DELETE FROM park_companies WHERE owner_user_id = :uid"), + {"uid": user_id} + ) + except Exception: + await db.session.rollback() + + # 提交级联删除 + await db.session.commit() + + # 硬删除用户主记录 ok = await db.users.delete(user_id) if not ok: - raise HTTPException(status_code=404, detail="User not found") - # 平台用户删除 → 引擎镜像同步删除 - if user: - await _sync_compute(compute_client.sync_delete_user(user.get("username"))) + raise HTTPException(status_code=404, detail="用户删除失败") + + # 平台用户删除 → 引擎镜像同步删除(算力、IM等) + if username: + await _sync_compute(compute_client.sync_delete_user(username)) + await write_audit(db, action="user.delete", resource="user", resource_id=user_id, - detail="deleted", user=actor, request=request) - return {"ok": True, "user_id": user_id} + detail=f"deleted, cascade_deleted={deleted_count} records", user=actor, request=request) + return {"ok": True, "user_id": user_id, "username": username, "cascade_deleted": deleted_count} @router.get("/roles", summary="角色列表")