diff --git a/app/api/v1/auth.py b/app/api/v1/auth.py index dfb9307..eb54fb1 100644 --- a/app/api/v1/auth.py +++ b/app/api/v1/auth.py @@ -9,6 +9,7 @@ from app.schemas.auth import ( TokenResponse, UserPublic, ) +from app.core.heartbeat_verify import compute_server_verify_code from app.schemas.common import ApiResponse from app.services import auth as auth_service @@ -64,15 +65,29 @@ async def heartbeat( db: DbSession, device_serial: str | None = Header(default=None, convert_underscores=False), ) -> ApiResponse[HeartbeatResponse]: + verify_code = body.verify_code.strip() + if not verify_code: + return ApiResponse(ok=False, message="verify_code 无效") + + # 无论业务成败都返回 server_verify_code,供客户端校验响应真实性 + server_verify = compute_server_verify_code(verify_code) try: data = await auth_service.process_heartbeat( db, current_user, device_serial, - body.verify_code, + verify_code, ) except auth_service.AuthError as exc: - return ApiResponse(ok=False, message=exc.message) + return ApiResponse( + ok=False, + message=exc.message, + data=HeartbeatResponse( + vip_end_time=current_user.vip_end_time, + vip_active=False, + server_verify_code=server_verify, + ), + ) return ApiResponse(ok=True, message="", data=data) diff --git a/app/api/v1/oem_users.py b/app/api/v1/oem_users.py index 4c4ae7d..234cc0c 100644 --- a/app/api/v1/oem_users.py +++ b/app/api/v1/oem_users.py @@ -1,13 +1,13 @@ -"""管理员用户 CRUD。""" +"""OEM 用户 CRUD。""" from fastapi import APIRouter, Query from app.dependencies import OemUser, DbSession from app.schemas.admin_user import AdminUserCreate, AdminUserOut, AdminUserUpdate from app.schemas.common import ApiResponse, PaginatedData -from app.services import admin_user as oem_user_service +from app.services import oem_user as oem_user_service -router = APIRouter(prefix="/oem/users", tags=["管理-用户"]) +router = APIRouter(prefix="/oem/users", tags=["OEM-用户"]) @router.get("", response_model=ApiResponse[PaginatedData[AdminUserOut]]) @@ -25,13 +25,12 @@ async def list_users( agent_id: int | None = Query(None, description="按所属代理用户 ID 筛选"), role_id: int | None = Query(None, ge=0, le=3, description="按角色筛选"), ) -> ApiResponse[PaginatedData[AdminUserOut]]: - oem_id=_oem.id users, total = await oem_user_service.list_users( db, page=page, page_size=page_size, username=username, - oem_id=oem_id, + oem_id=_oem.id, agent_id=agent_id, role_id=role_id, ) @@ -54,7 +53,7 @@ async def create_user( db: DbSession, ) -> ApiResponse[AdminUserOut]: try: - user = await oem_user_service.create_user(db, body,oem) + user = await oem_user_service.create_user(db, body, oem) except oem_user_service.AdminUserError as exc: return ApiResponse(ok=False, message=exc.message) @@ -75,6 +74,7 @@ async def update_user( try: user = await oem_user_service.update_user( db, + oem, user_id, body, actor_id=oem.id, @@ -96,7 +96,7 @@ async def delete_user( db: DbSession, ) -> ApiResponse[None]: try: - await oem_user_service.delete_user(db, user_id, actor_id=admin.id) + await oem_user_service.delete_user(db, oem, user_id, actor_id=oem.id) except oem_user_service.AdminUserError as exc: return ApiResponse(ok=False, message=exc.message) diff --git a/app/models/card_key.py b/app/models/card_key.py index f733e29..b330bca 100644 --- a/app/models/card_key.py +++ b/app/models/card_key.py @@ -29,3 +29,4 @@ class CardKey(Base): Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True ) remark: Mapped[str | None] = mapped_column(String(255), nullable=True) + oem_remark: Mapped[str | None] = mapped_column(String(255), nullable=True) \ No newline at end of file diff --git a/app/services/oem_user.py b/app/services/oem_user.py index 09a67eb..1e7789b 100644 --- a/app/services/oem_user.py +++ b/app/services/oem_user.py @@ -1,7 +1,7 @@ from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession -from app.core.roles import ROLE_ADMIN, ROLE_AGENT, ROLE_OEM +from app.core.roles import ROLE_AGENT from app.core.security import hash_password from app.models.user import User from app.schemas.admin_user import AdminUserCreate, AdminUserUpdate @@ -9,6 +9,9 @@ from app.schemas.admin_user import AdminUserCreate, AdminUserUpdate DEFAULT_PAGE_SIZE = 20 MAX_PAGE_SIZE = 200 +# OEM 创建用户时允许的角色:普通用户、代理 +OEM_CREATABLE_ROLE_IDS = frozenset({0, ROLE_AGENT}) + class AdminUserError(Exception): def __init__(self, message: str) -> None: @@ -66,23 +69,31 @@ async def list_users( return list(result.all()), total -async def _validate_owner_ids( +async def _validate_agent_under_oem( db: AsyncSession, *, - oem_id: int | None, + oem_id: int, agent_id: int | None, ) -> None: - if oem_id is not None: - oem = await db.get(User, oem_id) - if oem is None or oem.role_id != ROLE_OEM: - raise AdminUserError("所选 OEM 不存在") - if agent_id is not None: - agent = await db.get(User, agent_id) - if agent is None or agent.role_id != ROLE_AGENT: - raise AdminUserError("所选代理不存在") + if agent_id is None: + return + agent = await db.get(User, agent_id) + if agent is None or agent.role_id != ROLE_AGENT: + raise AdminUserError("所选代理不存在") + if agent.oem_id != oem_id: + raise AdminUserError("所选代理不属于当前 OEM") -async def create_user(db: AsyncSession, body: AdminUserCreate) -> User: +def _validate_creatable_role(role_id: int) -> None: + if role_id not in OEM_CREATABLE_ROLE_IDS: + raise AdminUserError("只能创建普通用户或代理") + + +async def create_user(db: AsyncSession, body: AdminUserCreate, oem: User) -> User: + _validate_creatable_role(body.role_id) + if body.vip_end_time is not None: + raise AdminUserError("创建用户时不能设置 VIP 到期时间") + existing = await db.scalar(select(User).where(User.username == body.username)) if existing is not None: raise AdminUserError("用户名已存在") @@ -92,16 +103,19 @@ async def create_user(db: AsyncSession, body: AdminUserCreate) -> User: if phone_taken is not None: raise AdminUserError("手机号已被使用") - await _validate_owner_ids(db, oem_id=body.oem_id, agent_id=body.agent_id) + agent_id = body.agent_id + if body.role_id == ROLE_AGENT: + agent_id = None + await _validate_agent_under_oem(db, oem_id=oem.id, agent_id=agent_id) user = User( username=body.username, password_hash=hash_password(body.password), phone=body.phone, role_id=body.role_id, - oem_id=body.oem_id, - agent_id=body.agent_id, - vip_end_time=body.vip_end_time, + oem_id=oem.id, + agent_id=agent_id, + vip_end_time=None, ) db.add(user) await db.flush() @@ -109,16 +123,24 @@ async def create_user(db: AsyncSession, body: AdminUserCreate) -> User: return user +async def _get_user_under_oem(db: AsyncSession, user_id: int, oem: User) -> User: + user = await db.get(User, user_id) + if user is None: + raise AdminUserError("用户不存在") + if user.oem_id != oem.id: + raise AdminUserError("无权限操作该用户") + return user + + async def update_user( db: AsyncSession, + oem: User, user_id: int, body: AdminUserUpdate, *, actor_id: int, ) -> User: - user = await db.get(User, user_id) - if user is None: - raise AdminUserError("用户不存在") + user = await _get_user_under_oem(db, user_id, oem) if body.username is not None and body.username != user.username: taken = await db.scalar(select(User).where(User.username == body.username)) @@ -134,40 +156,34 @@ async def update_user( user.phone = body.phone if body.role_id is not None: - if user_id == actor_id and body.role_id != ROLE_ADMIN: - raise AdminUserError("不能修改自己的管理员角色") + if user_id == actor_id: + raise AdminUserError("不能修改自己的角色") + _validate_creatable_role(body.role_id) user.role_id = body.role_id - if body.oem_id is not None or body.agent_id is not None: - await _validate_owner_ids( - db, - oem_id=body.oem_id if body.oem_id is not None else user.oem_id, - agent_id=body.agent_id if body.agent_id is not None else user.agent_id, - ) - if body.oem_id is not None: - user.oem_id = body.oem_id - if body.agent_id is not None: - user.agent_id = body.agent_id + new_agent_id = body.agent_id if body.agent_id is not None else user.agent_id + effective_role = body.role_id if body.role_id is not None else user.role_id + if effective_role == ROLE_AGENT: + new_agent_id = None + elif body.agent_id is not None: + await _validate_agent_under_oem(db, oem_id=oem.id, agent_id=new_agent_id) + if body.agent_id is not None or effective_role == ROLE_AGENT: + user.agent_id = new_agent_id if body.password: user.password_hash = hash_password(body.password) - if body.clear_vip_end_time: - user.vip_end_time = None - elif body.vip_end_time is not None: - user.vip_end_time = body.vip_end_time + if body.clear_vip_end_time or body.vip_end_time is not None: + raise AdminUserError("不能设置 VIP 到期时间") await db.flush() await db.refresh(user) return user -async def delete_user(db: AsyncSession, user_id: int, *, actor_id: int) -> None: +async def delete_user(db: AsyncSession, oem: User, user_id: int, *, actor_id: int) -> None: if user_id == actor_id: raise AdminUserError("不能删除当前登录账号") - user = await db.get(User, user_id) - if user is None: - raise AdminUserError("用户不存在") - + user = await _get_user_under_oem(db, user_id, oem) await db.delete(user)