From 0039a809ac3705366a9a6261d662f463836e5909 Mon Sep 17 00:00:00 2001 From: Pine Date: Tue, 25 Aug 2026 16:24:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(user):=20=E7=94=A8=E6=88=B7=E8=AE=A4?= =?UTF-8?q?=E8=AF=81=E7=8A=B6=E6=80=81/=E6=89=80=E5=B1=9E/=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E7=B1=BB=E5=9E=8B=20=E5=88=86=E7=B1=BB=E5=AD=97?= =?UTF-8?q?=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - users 增: certification_status(uncertified|pending|certified|rejected)+certification_time, affiliation(independent|park)+park_id/park_name, account_type(opc_default|park_staff|platform_staff|provider|enterprise); alembic 0007 - repo: set_classification; _user_to_dict 含全部新字段; profile 返回认证/所属/账号类型 - admin: POST /admin/users/{id}/classification(认证/分类设置, action:user.manage) + UserClassificationRequest schema - 迁移已应用, 导入通过 --- alembic/versions/0007_user_classification.py | 35 ++++++++++++++++++++ app/api/routers/auth.py | 5 +++ app/api/routers/rbac_admin.py | 19 ++++++++++- app/api/schemas/admin.py | 10 ++++++ app/infrastructure/models.py | 7 ++++ app/infrastructure/repositories.py | 21 ++++++++++++ 6 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 alembic/versions/0007_user_classification.py diff --git a/alembic/versions/0007_user_classification.py b/alembic/versions/0007_user_classification.py new file mode 100644 index 0000000..6fc18dd --- /dev/null +++ b/alembic/versions/0007_user_classification.py @@ -0,0 +1,35 @@ +"""user classification/certification + +Revision ID: 0007_user_classification +Revises: 0006_user_source_fields +Create Date: 2026-08-25 +""" +from __future__ import annotations + +from alembic import op +import sqlalchemy as sa + +revision = "0007_user_classification" +down_revision = "0006_user_source_fields" +branch_labels = None +depends_on = None + + +def _add(table, col): + conn = op.get_bind() + cols = {r[1] for r in conn.execute(sa.text(f"PRAGMA table_info({table})"))} + if col.name not in cols: + op.add_column(table, col) + + +def upgrade() -> None: + _add("users", sa.Column("certification_status", sa.String, server_default="uncertified")) + _add("users", sa.Column("certification_time", sa.String, server_default="")) + _add("users", sa.Column("affiliation", sa.String, server_default="independent")) + _add("users", sa.Column("park_id", sa.String, server_default="")) + _add("users", sa.Column("park_name", sa.String, server_default="")) + _add("users", sa.Column("account_type", sa.String, server_default="opc_default")) + + +def downgrade() -> None: + pass diff --git a/app/api/routers/auth.py b/app/api/routers/auth.py index 02fc377..3363b49 100644 --- a/app/api/routers/auth.py +++ b/app/api/routers/auth.py @@ -153,6 +153,11 @@ async def _profile_for( profile["compute_provisioned"] = bool(user.get("compute_provisioned", False)) profile["compute_quota"] = user.get("compute_quota", 0) profile["compute_used_quota"] = user.get("compute_used_quota", 0) + profile["certification_status"] = user.get("certification_status", "uncertified") + profile["affiliation"] = user.get("affiliation", "independent") + profile["park_id"] = user.get("park_id", "") + profile["park_name"] = user.get("park_name", "") + profile["account_type"] = user.get("account_type", "opc_default") profile["role"] = role profile["sub_role"] = sub_role profile["org_id"] = org_id diff --git a/app/api/routers/rbac_admin.py b/app/api/routers/rbac_admin.py index 14687a9..772ebfb 100644 --- a/app/api/routers/rbac_admin.py +++ b/app/api/routers/rbac_admin.py @@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel from ..dependencies import get_db -from ..schemas.admin import SetUserRoleRequest, SetUserStatusRequest +from ..schemas.admin import SetUserRoleRequest, SetUserStatusRequest, UserClassificationRequest from ..schemas.admin import UserCreateRequest, RolePermissionRequest, UpdateUserRequest from ...rbac import require_permission, require_roles, write_audit from ...infrastructure.repositories import Database @@ -82,6 +82,23 @@ async def set_user_role( return {"ok": True, "user": await db.users.to_profile(updated)} +@router.post("/users/{user_id}/classification", summary="设置用户认证状态/所属/账号类型") +async def set_user_classification( + user_id: str, + req: UserClassificationRequest, + request: Request, + db: Database = Depends(get_db), + actor: dict = Depends(require_permission("action:user.manage")), +): + fields = req.model_dump(exclude_none=True) + updated = await db.users.set_classification(user_id, **fields) + if updated is None: + raise HTTPException(status_code=404, detail="User not found") + await write_audit(db, action="user.classification", resource="user", resource_id=user_id, + detail=str(fields), user=actor, request=request) + return {"ok": True, "user": updated} + + @router.post("/users/{user_id}/status", summary="禁用/启用用户") async def set_user_status( user_id: str, diff --git a/app/api/schemas/admin.py b/app/api/schemas/admin.py index dec30c8..a84b044 100644 --- a/app/api/schemas/admin.py +++ b/app/api/schemas/admin.py @@ -9,6 +9,16 @@ class SetUserRoleRequest(BaseModel): class SetUserStatusRequest(BaseModel): status: str = "active" # active | disabled + +class UserClassificationRequest(BaseModel): + """用户 认证状态/所属/账号类型 分类字段。""" + certification_status: str | None = None # uncertified|pending|certified|rejected + certification_time: str | None = None + affiliation: str | None = None # independent|park + park_id: str | None = None + park_name: str | None = None + account_type: str | None = None # opc_default|park_staff|platform_staff|provider|enterprise + class UpdateUserRequest(BaseModel): nickname: str | None = None role: str | None = None diff --git a/app/infrastructure/models.py b/app/infrastructure/models.py index 7390c25..fea619b 100644 --- a/app/infrastructure/models.py +++ b/app/infrastructure/models.py @@ -67,6 +67,13 @@ class User(Base): compute_username: Mapped[str] = mapped_column(String, default="") # 引擎用户名(= username) compute_quota: Mapped[int] = mapped_column(Integer, default=0) # 缓存引擎可用额度 compute_used_quota: Mapped[int] = mapped_column(Integer, default=0) # 缓存引擎已用额度 + # ── 状态与身份分类 ── + certification_status: Mapped[str] = mapped_column(String, default="uncertified") # uncertified|pending|certified|rejected + certification_time: Mapped[str] = mapped_column(String, default="") + affiliation: Mapped[str] = mapped_column(String, default="independent") # independent:独立OPC | park:所属园区 + park_id: Mapped[str] = mapped_column(String, default="") # 所属园区(id) + park_name: Mapped[str] = mapped_column(String, default="") # 所属园区(名) + account_type: Mapped[str] = mapped_column(String, default="opc_default") # opc_default|park_staff|platform_staff|provider|enterprise created_at: Mapped[str] = mapped_column(String, default="") updated_at: Mapped[str] = mapped_column(String, default="") diff --git a/app/infrastructure/repositories.py b/app/infrastructure/repositories.py index fcb4c11..5077b5e 100644 --- a/app/infrastructure/repositories.py +++ b/app/infrastructure/repositories.py @@ -167,6 +167,12 @@ def _user_to_dict(u: User) -> dict: "compute_username": u.compute_username, "compute_quota": u.compute_quota, "compute_used_quota": u.compute_used_quota, + "certification_status": u.certification_status, + "certification_time": u.certification_time, + "affiliation": u.affiliation, + "park_id": u.park_id, + "park_name": u.park_name, + "account_type": u.account_type, "created_at": u.created_at, "updated_at": u.updated_at, } @@ -287,6 +293,21 @@ class UserRepository: u.updated_at = utcnow_iso() await self.session.commit() + async def set_classification(self, user_id: str, **fields) -> dict | None: + """更新用户 认证状态/所属/账号类型 等分类字段。""" + u = await self.session.get(User, user_id) + if u is None: + return None + for k, v in fields.items(): + if k in { + "certification_status", "certification_time", "affiliation", + "park_id", "park_name", "account_type", + }: + setattr(u, k, v) + u.updated_at = utcnow_iso() + await self.session.commit() + return _user_to_dict(u) + async def set_compute_mirror(self, user_id: str, *, provisioned: bool, username: str, quota: int, used_quota: int) -> None: """回写算力引擎镜像(全局用户一致)。"""