Преглед на файлове

fix: 创建用户时正确处理软删账号占用的用户名唯一约束

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu преди 3 месеца
родител
ревизия
94c3f54e29
променени са 2 файла, в които са добавени 37 реда и са изтрити 7 реда
  1. 17 4
      backend/app/api/admin.py
  2. 20 3
      backend/app/crud/user.py

+ 17 - 4
backend/app/api/admin.py

@@ -1304,13 +1304,20 @@ def create_user(
     request, req_err = _resolve_admin_payload_with_crypto_fallback(request, current_user)
     if req_err is not None:
         return req_err
-    # 检查用户名唯一性
-    existing = user_crud.get_by_username(db, request.get("username"))
+    username = (request.get("username") or "").strip() if isinstance(request, dict) else ""
+    if not username:
+        return error_response(400, "用户名不能为空")
+    # uk_username 覆盖软删行:活跃账号直接拒绝;已软删则释放用户名后允许重建
+    existing = user_crud.get_by_username_any(db, username)
     if existing:
-        return error_response(400, "用户名已存在")
-    
+        if getattr(existing, "is_del", 0) == 0:
+            return error_response(400, "用户名已存在")
+        existing.username = user_crud._release_username(existing.username, existing.id)
+        db.flush()
+
     creator = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")}
     body = dict(request) if request else {}
+    body["username"] = username
     region_ids = body.pop("regionIds", None)
     if body.get("avatarUrl") == "":
         body["avatarUrl"] = None
@@ -1318,6 +1325,12 @@ def create_user(
         user, initial_password = user_crud.create_with_password(db, body, creator)
     except ValueError as e:
         return error_response(400, str(e))
+    except IntegrityError as e:
+        db.rollback()
+        err = str(getattr(e.orig, "args", e) or e)
+        if "uk_username" in err or "username" in err.lower():
+            return error_response(400, "用户名已存在")
+        raise
 
     if isinstance(region_ids, list):
         _replace_user_region_scopes(db, user.id, region_ids, current_user.get("user_id"), current_user.get("real_name"))

+ 20 - 3
backend/app/crud/user.py

@@ -16,11 +16,24 @@ class UserCRUD(CRUDBase[SysUser]):
     """用户CRUD操作"""
 
     def get_by_username(self, db: Session, username: str) -> Optional[SysUser]:
-        """根据用户名获取用户"""
+        """根据用户名获取用户(仅未删除)"""
         return db.query(SysUser).filter(
             and_(SysUser.username == username, SysUser.is_del == 0)
         ).first()
 
+    def get_by_username_any(self, db: Session, username: str) -> Optional[SysUser]:
+        """根据用户名获取用户(含已软删;用于 uk_username 唯一约束校验)"""
+        if not username:
+            return None
+        return db.query(SysUser).filter(SysUser.username == username).first()
+
+    @staticmethod
+    def _release_username(original_username: str, user_id: str) -> str:
+        """软删后释放 uk_username:改写为不可登录的占位名。"""
+        suffix = (user_id or "")[:8] or "x"
+        base = (original_username or "user")[:30]
+        return f"{base}__del__{suffix}"
+
     def get_by_phone(self, db: Session, phone: str) -> Optional[SysUser]:
         """根据手机号获取用户"""
         return db.query(SysUser).filter(
@@ -169,7 +182,7 @@ class UserCRUD(CRUDBase[SysUser]):
         return self.update(db, id, obj_in, updater)
 
     def soft_delete_with_history(self, db: Session, id: str) -> bool:
-        """删除用户并记录历史"""
+        """删除用户并记录历史;同时释放用户名唯一索引,便于后续重建同名账号。"""
         original = self.get(db, id)
         if not original:
             return False
@@ -194,7 +207,11 @@ class UserCRUD(CRUDBase[SysUser]):
         his = SysUserHis(**his_data)
         db.add(his)
 
-        return self.delete(db, id)
+        # 历史表已保留原用户名;主表改写以释放 uk_username
+        original.username = self._release_username(original.username, original.id)
+        original.is_del = 1
+        db.commit()
+        return True
 
 
 user_crud = UserCRUD(SysUser)