diff --git a/app/api/v1/agent_card_keys.py b/app/api/v1/agent_card_keys.py index 605db17..a352948 100644 --- a/app/api/v1/agent_card_keys.py +++ b/app/api/v1/agent_card_keys.py @@ -4,7 +4,7 @@ from typing import Literal from fastapi import APIRouter, Query -from app.dependencies import AdminUser, DbSession +from app.dependencies import AgentUser, DbSession from app.schemas.admin_card_key import ( CardKeyBatchCreateResult, CardKeyCreate, @@ -12,14 +12,14 @@ from app.schemas.admin_card_key import ( CardKeyUpdate, ) from app.schemas.common import ApiResponse, PaginatedData -from app.services import admin_card_key as card_key_service +from app.services import agent_card_key as card_key_service router = APIRouter(prefix="/agent/card-keys", tags=["管理-卡密"]) @router.get("", response_model=ApiResponse[PaginatedData[CardKeyOut]]) async def list_card_keys( - _admin: AdminUser, + _agent: AgentUser, db: DbSession, page: int = Query(1, ge=1), page_size: int = Query( @@ -42,8 +42,7 @@ async def list_card_keys( page_size=page_size, status=filter_status, username=username, - oem_id=oem_id, - agent_id=agent_id, + agent_id=_agent.id, ) return ApiResponse( ok=True, @@ -60,7 +59,7 @@ async def list_card_keys( @router.post("", response_model=ApiResponse[CardKeyBatchCreateResult]) async def create_card_keys( body: CardKeyCreate, - _admin: AdminUser, + _admin: AgentUser, db: DbSession, ) -> ApiResponse[CardKeyBatchCreateResult]: try: @@ -80,7 +79,7 @@ async def create_card_keys( async def update_card_key( card_id: int, body: CardKeyUpdate, - _admin: AdminUser, + _admin: AgentUser, db: DbSession, ) -> ApiResponse[CardKeyOut]: try: @@ -98,7 +97,7 @@ async def update_card_key( @router.delete("/{card_id}", response_model=ApiResponse[None]) async def delete_card_key( card_id: int, - _admin: AdminUser, + _admin: AgentUser, db: DbSession, ) -> ApiResponse[None]: try: diff --git a/app/api/v1/agent_users.py b/app/api/v1/agent_users.py index 76a276a..eebb8b8 100644 --- a/app/api/v1/agent_users.py +++ b/app/api/v1/agent_users.py @@ -2,37 +2,37 @@ from fastapi import APIRouter, Query -from app.dependencies import OemUser, DbSession +from app.dependencies import AgentUser, 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 agent_user as agent_user_service router = APIRouter(prefix="/agent/users", tags=["管理-用户"]) @router.get("", response_model=ApiResponse[PaginatedData[AdminUserOut]]) async def list_users( - _oem: OemUser, + _agent: AgentUser, db: DbSession, page: int = Query(1, ge=1, description="页码,从 1 开始"), page_size: int = Query( - oem_user_service.DEFAULT_PAGE_SIZE, + agent_user_service.DEFAULT_PAGE_SIZE, ge=1, - le=oem_user_service.MAX_PAGE_SIZE, + le=agent_user_service.MAX_PAGE_SIZE, description="每页条数", ), username: str | None = Query(None, description="用户名模糊搜索"), 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( + agent_id=_agent.id + users, total = await agent_user_service.list_users( db, page=page, page_size=page_size, username=username, - oem_id=oem_id, agent_id=agent_id, + role_id=role_id, ) return ApiResponse( @@ -50,12 +50,12 @@ async def list_users( @router.post("", response_model=ApiResponse[AdminUserOut]) async def create_user( body: AdminUserCreate, - oem: OemUser, + agent: AgentUser, db: DbSession, ) -> ApiResponse[AdminUserOut]: try: - user = await oem_user_service.create_user(db, body,oem) - except oem_user_service.AdminUserError as exc: + user = await agent_user_service.create_user(db, body,agent) + except agent_user_service.AdminUserError as exc: return ApiResponse(ok=False, message=exc.message) return ApiResponse( @@ -69,17 +69,17 @@ async def create_user( async def update_user( user_id: int, body: AdminUserUpdate, - oem: OemUser, + agent: AgentUser, db: DbSession, ) -> ApiResponse[AdminUserOut]: try: - user = await oem_user_service.update_user( + user = await agent_user_service.update_user( db, user_id, body, - actor_id=oem.id, + actor_id=agent.id, ) - except oem_user_service.AdminUserError as exc: + except agent_user_service.AdminUserError as exc: return ApiResponse(ok=False, message=exc.message) return ApiResponse( @@ -92,12 +92,12 @@ async def update_user( @router.delete("/{user_id}", response_model=ApiResponse[None]) async def delete_user( user_id: int, - oem: OemUser, + agent: AgentUser, db: DbSession, ) -> ApiResponse[None]: try: - await oem_user_service.delete_user(db, user_id, actor_id=admin.id) - except oem_user_service.AdminUserError as exc: + await agent_user_service.delete_user(db, user_id, actor_id=agent.id) + except agent_user_service.AdminUserError as exc: return ApiResponse(ok=False, message=exc.message) return ApiResponse(ok=True, message="用户已删除") diff --git a/app/services/agent_card_key.py b/app/services/agent_card_key.py new file mode 100644 index 0000000..89f882e --- /dev/null +++ b/app/services/agent_card_key.py @@ -0,0 +1,176 @@ +import secrets +import string + +from sqlalchemy import func, select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.roles import ROLE_AGENT, ROLE_OEM +from app.models.card_key import CardKey +from app.models.user import User +from app.schemas.admin_card_key import CardKeyCreate, CardKeyUpdate + +DEFAULT_PAGE_SIZE = 20 +MAX_PAGE_SIZE = 200 + +_SERIAL_ALPHABET = string.ascii_uppercase + string.digits +_SERIAL_LENGTH = 16 + + +class AdminCardKeyError(Exception): + def __init__(self, message: str) -> None: + self.message = message + super().__init__(message) + + +def _generate_serial() -> str: + return "".join(secrets.choice(_SERIAL_ALPHABET) for _ in range(_SERIAL_LENGTH)) + + +async def _unique_serial(db: AsyncSession, preferred: str | None = None) -> str: + if preferred: + existing = await db.scalar( + select(CardKey.id).where(CardKey.serial_number == preferred) + ) + if existing is not None: + raise AdminCardKeyError("序列号已存在") + return preferred + + for _ in range(20): + candidate = _generate_serial() + existing = await db.scalar( + select(CardKey.id).where(CardKey.serial_number == candidate) + ) + if existing is None: + return candidate + raise AdminCardKeyError("生成序列号失败,请重试") + + +async def _validate_assign_ids( + db: AsyncSession, + *, + oem_id: int | None, + agent_id: int | None, +) -> None: + if oem_id is not None and agent_id is not None: + raise AdminCardKeyError("OEM 与代理只能指定其一") + if oem_id is not None: + oem = await db.get(User, oem_id) + if oem is None or oem.role_id != ROLE_OEM: + raise AdminCardKeyError("所选 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 AdminCardKeyError("所选代理不存在") + + +def _list_card_keys_filters( + *, + status: str | None = None, + username: str | None = None, + oem_id: int | None = None, + agent_id: int | None = None, +) -> list: + filters = [] + if status == "unused": + filters.append(CardKey.activated_at.is_(None)) + elif status == "used": + filters.append(CardKey.activated_at.is_not(None)) + if username and username.strip(): + filters.append(CardKey.username.ilike(f"%{username.strip()}%")) + if oem_id is not None: + filters.append(CardKey.oem_id == oem_id) + if agent_id is not None: + filters.append(CardKey.agent_id == agent_id) + return filters + + +async def list_card_keys( + db: AsyncSession, + *, + page: int = 1, + page_size: int = DEFAULT_PAGE_SIZE, + status: str | None = None, + username: str | None = None, + oem_id: int | None = None, + agent_id: int | None = None, +) -> tuple[list[CardKey], int]: + page_size = min(max(page_size, 1), MAX_PAGE_SIZE) + page = max(page, 1) + offset = (page - 1) * page_size + + filters = _list_card_keys_filters( + status=status, + username=username, + oem_id=oem_id, + agent_id=agent_id, + ) + + count_stmt = select(func.count()).select_from(CardKey) + list_stmt = select(CardKey) + for clause in filters: + count_stmt = count_stmt.where(clause) + list_stmt = list_stmt.where(clause) + + total = await db.scalar(count_stmt) or 0 + result = await db.scalars( + list_stmt.order_by(CardKey.id.desc()).offset(offset).limit(page_size) + ) + return list(result.all()), total + + +async def create_card_keys(db: AsyncSession, body: CardKeyCreate) -> list[CardKey]: + await _validate_assign_ids(db, oem_id=body.oem_id, agent_id=body.agent_id) + created: list[CardKey] = [] + + for index in range(body.count): + serial = await _unique_serial( + db, + body.serial_number if index == 0 else None, + ) + card = CardKey( + serial_number=serial, + duration_days=body.duration_days, + remark=body.remark, + oem_id=body.oem_id, + agent_id=body.agent_id, + ) + db.add(card) + created.append(card) + + await db.flush() + for card in created: + await db.refresh(card) + return created + + +async def update_card_key( + db: AsyncSession, + card_id: int, + body: CardKeyUpdate, +) -> CardKey: + card = await db.get(CardKey, card_id) + if card is None: + raise AdminCardKeyError("卡密不存在") + + if card.activated_at is not None: + if body.duration_days is not None: + raise AdminCardKeyError("已激活卡密不能修改时长") + elif body.duration_days is not None: + card.duration_days = body.duration_days + + if body.remark is not None: + card.remark = body.remark + + await db.flush() + await db.refresh(card) + return card + + +async def delete_card_key(db: AsyncSession, card_id: int) -> None: + card = await db.get(CardKey, card_id) + if card is None: + raise AdminCardKeyError("卡密不存在") + if card.activated_at is not None: + raise AdminCardKeyError("已激活卡密不能删除") + + await db.delete(card) diff --git a/app/services/agent_user.py b/app/services/agent_user.py new file mode 100644 index 0000000..55259c0 --- /dev/null +++ b/app/services/agent_user.py @@ -0,0 +1,175 @@ +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.security import hash_password +from app.models.user import User +from app.schemas.admin_user import AdminUserCreate, AdminUserUpdate + +DEFAULT_PAGE_SIZE = 20 +MAX_PAGE_SIZE = 200 + + +class AdminUserError(Exception): + def __init__(self, message: str) -> None: + self.message = message + super().__init__(message) + + +def _list_users_filters( + *, + username: str | None = None, + oem_id: int | None = None, + agent_id: int | None = None, + role_id: int | None = None, +) -> list: + conditions = [] + if username and username.strip(): + conditions.append(User.username.ilike(f"%{username.strip()}%")) + if oem_id is not None: + conditions.append(User.oem_id == oem_id) + if agent_id is not None: + conditions.append(User.agent_id == agent_id) + if role_id is not None: + conditions.append(User.role_id == role_id) + return conditions + + +async def list_users( + db: AsyncSession, + *, + page: int = 1, + page_size: int = DEFAULT_PAGE_SIZE, + username: str | None = None, + oem_id: int | None = None, + agent_id: int | None = None, + role_id: int | None = None, +) -> tuple[list[User], int]: + page_size = min(max(page_size, 1), MAX_PAGE_SIZE) + page = max(page, 1) + offset = (page - 1) * page_size + conditions = _list_users_filters( + username=username, + oem_id=oem_id, + agent_id=agent_id, + role_id=role_id, + ) + + count_stmt = select(func.count()).select_from(User) + list_stmt = select(User).order_by(User.id.desc()) + if conditions: + count_stmt = count_stmt.where(*conditions) + list_stmt = list_stmt.where(*conditions) + + total = await db.scalar(count_stmt) or 0 + result = await db.scalars(list_stmt.offset(offset).limit(page_size)) + return list(result.all()), total + + +async def _validate_owner_ids( + db: AsyncSession, + *, + oem_id: int | None, + 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("所选代理不存在") + + +async def create_user(db: AsyncSession, body: AdminUserCreate,oem: User) -> User: + existing = await db.scalar(select(User).where(User.username == body.username)) + if existing is not None: + raise AdminUserError("用户名已存在") + + if body.phone: + phone_taken = await db.scalar(select(User).where(User.phone == body.phone)) + if phone_taken is not None: + raise AdminUserError("手机号已被使用") + + await _validate_owner_ids(db, oem_id=oem.id, agent_id=body.agent_id) + + user = User( + username=body.username, + password_hash=hash_password(body.password), + phone=body.phone, + role_id=body.role_id, + oem_id=oem.id, + agent_id=body.agent_id, + vip_end_time=body.vip_end_time, + ) + db.add(user) + await db.flush() + await db.refresh(user) + 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("用户不存在") + body.oem_id=oem.id + if body.username is not None and body.username != user.username: + taken = await db.scalar(select(User).where(User.username == body.username)) + if taken is not None: + raise AdminUserError("用户名已存在") + user.username = body.username + + if body.phone is not None: + if body.phone != user.phone: + taken = await db.scalar(select(User).where(User.phone == body.phone)) + if taken is not None: + raise AdminUserError("手机号已被使用") + user.phone = body.phone + + if body.role_id is not None: + if user_id == actor_id and body.role_id != ROLE_ADMIN: + raise AdminUserError("不能修改自己的管理员角色") + 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 + + 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 + + await db.flush() + await db.refresh(user) + return user + + +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("用户不存在") + if user.oem_id != oem.id: + raise AdminUserError("无权限删除该用户") + await db.delete(user)