11
This commit is contained in:
103
app/api/v1/oem_users.py
Normal file
103
app/api/v1/oem_users.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
"""管理员用户 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
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/oem/users", tags=["管理-用户"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=ApiResponse[PaginatedData[AdminUserOut]])
|
||||||
|
async def list_users(
|
||||||
|
_oem: OemUser,
|
||||||
|
db: DbSession,
|
||||||
|
page: int = Query(1, ge=1, description="页码,从 1 开始"),
|
||||||
|
page_size: int = Query(
|
||||||
|
oem_user_service.DEFAULT_PAGE_SIZE,
|
||||||
|
ge=1,
|
||||||
|
le=oem_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(
|
||||||
|
db,
|
||||||
|
page=page,
|
||||||
|
page_size=page_size,
|
||||||
|
username=username,
|
||||||
|
oem_id=oem_id,
|
||||||
|
agent_id=agent_id,
|
||||||
|
role_id=role_id,
|
||||||
|
)
|
||||||
|
return ApiResponse(
|
||||||
|
ok=True,
|
||||||
|
message="",
|
||||||
|
data=PaginatedData.build(
|
||||||
|
[AdminUserOut.model_validate(u) for u in users],
|
||||||
|
total=total,
|
||||||
|
page=page,
|
||||||
|
page_size=page_size,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=ApiResponse[AdminUserOut])
|
||||||
|
async def create_user(
|
||||||
|
body: AdminUserCreate,
|
||||||
|
oem: OemUser,
|
||||||
|
db: DbSession,
|
||||||
|
) -> ApiResponse[AdminUserOut]:
|
||||||
|
try:
|
||||||
|
user = await oem_user_service.create_user(db, body,oem)
|
||||||
|
except oem_user_service.AdminUserError as exc:
|
||||||
|
return ApiResponse(ok=False, message=exc.message)
|
||||||
|
|
||||||
|
return ApiResponse(
|
||||||
|
ok=True,
|
||||||
|
message="用户已创建",
|
||||||
|
data=AdminUserOut.model_validate(user),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/{user_id}", response_model=ApiResponse[AdminUserOut])
|
||||||
|
async def update_user(
|
||||||
|
user_id: int,
|
||||||
|
body: AdminUserUpdate,
|
||||||
|
oem: OemUser,
|
||||||
|
db: DbSession,
|
||||||
|
) -> ApiResponse[AdminUserOut]:
|
||||||
|
try:
|
||||||
|
user = await oem_user_service.update_user(
|
||||||
|
db,
|
||||||
|
user_id,
|
||||||
|
body,
|
||||||
|
actor_id=oem.id,
|
||||||
|
)
|
||||||
|
except oem_user_service.AdminUserError as exc:
|
||||||
|
return ApiResponse(ok=False, message=exc.message)
|
||||||
|
|
||||||
|
return ApiResponse(
|
||||||
|
ok=True,
|
||||||
|
message="用户已更新",
|
||||||
|
data=AdminUserOut.model_validate(user),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{user_id}", response_model=ApiResponse[None])
|
||||||
|
async def delete_user(
|
||||||
|
user_id: int,
|
||||||
|
oem: OemUser,
|
||||||
|
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:
|
||||||
|
return ApiResponse(ok=False, message=exc.message)
|
||||||
|
|
||||||
|
return ApiResponse(ok=True, message="用户已删除")
|
||||||
@@ -7,12 +7,14 @@ from app.api.v1 import (
|
|||||||
admin_users,
|
admin_users,
|
||||||
app_config,
|
app_config,
|
||||||
oem_public,
|
oem_public,
|
||||||
|
oem_users,
|
||||||
auth,
|
auth,
|
||||||
nodejs_scripts,
|
nodejs_scripts,
|
||||||
quickjs_scripts,
|
quickjs_scripts,
|
||||||
)
|
)
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
|
api_router.include_router(oem_users.router)
|
||||||
api_router.include_router(oem_public.router)
|
api_router.include_router(oem_public.router)
|
||||||
api_router.include_router(auth.router)
|
api_router.include_router(auth.router)
|
||||||
api_router.include_router(admin_users.router)
|
api_router.include_router(admin_users.router)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import redis.asyncio as redis
|
|||||||
from fastapi import Depends, Header, HTTPException, status
|
from fastapi import Depends, Header, HTTPException, status
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.core.roles import ROLE_ADMIN
|
from app.core.roles import ROLE_ADMIN, ROLE_OEM,ROLE_AGENT
|
||||||
from app.core.security import decode_access_token
|
from app.core.security import decode_access_token
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
@@ -63,5 +63,23 @@ async def require_admin(current_user: CurrentUser) -> User:
|
|||||||
)
|
)
|
||||||
return current_user
|
return current_user
|
||||||
|
|
||||||
|
async def require_oem(current_user: CurrentUser) -> User:
|
||||||
|
if current_user.role_id != ROLE_OEM:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="需要 OEM 权限",
|
||||||
|
)
|
||||||
|
return current_user
|
||||||
|
|
||||||
|
async def require_agent(current_user: CurrentUser) -> User:
|
||||||
|
if current_user.role_id != ROLE_AGENT:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="需要代理权限",
|
||||||
|
)
|
||||||
|
return current_user
|
||||||
|
|
||||||
|
|
||||||
AdminUser = Annotated[User, Depends(require_admin)]
|
AdminUser = Annotated[User, Depends(require_admin)]
|
||||||
|
OemUser = Annotated[User, Depends(require_oem)]
|
||||||
|
AgentUser = Annotated[User, Depends(require_agent)]
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||||
|
|
||||||
|
|
||||||
@@ -38,6 +40,7 @@ class UserPublic(BaseModel):
|
|||||||
id: int
|
id: int
|
||||||
username: str
|
username: str
|
||||||
role_id: int
|
role_id: int
|
||||||
|
vip_end_time: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
class TokenResponse(BaseModel):
|
class TokenResponse(BaseModel):
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ class OemPublicBrandingOut(BaseModel):
|
|||||||
software_name: str = ""
|
software_name: str = ""
|
||||||
logo_url: str | None = None
|
logo_url: str | None = None
|
||||||
wechat: str | None = None
|
wechat: str | None = None
|
||||||
|
wechat_qrcode_url: str | None = None
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ async def _validate_owner_ids(
|
|||||||
raise AdminUserError("所选代理不存在")
|
raise AdminUserError("所选代理不存在")
|
||||||
|
|
||||||
|
|
||||||
async def create_user(db: AsyncSession, body: AdminUserCreate) -> User:
|
async def create_user(db: AsyncSession, body: AdminUserCreate,oem: User) -> User:
|
||||||
existing = await db.scalar(select(User).where(User.username == body.username))
|
existing = await db.scalar(select(User).where(User.username == body.username))
|
||||||
if existing is not None:
|
if existing is not None:
|
||||||
raise AdminUserError("用户名已存在")
|
raise AdminUserError("用户名已存在")
|
||||||
@@ -92,14 +92,14 @@ async def create_user(db: AsyncSession, body: AdminUserCreate) -> User:
|
|||||||
if phone_taken is not None:
|
if phone_taken is not None:
|
||||||
raise AdminUserError("手机号已被使用")
|
raise AdminUserError("手机号已被使用")
|
||||||
|
|
||||||
await _validate_owner_ids(db, oem_id=body.oem_id, agent_id=body.agent_id)
|
await _validate_owner_ids(db, oem_id=oem.id, agent_id=body.agent_id)
|
||||||
|
|
||||||
user = User(
|
user = User(
|
||||||
username=body.username,
|
username=body.username,
|
||||||
password_hash=hash_password(body.password),
|
password_hash=hash_password(body.password),
|
||||||
phone=body.phone,
|
phone=body.phone,
|
||||||
role_id=body.role_id,
|
role_id=body.role_id,
|
||||||
oem_id=body.oem_id,
|
oem_id=oem.id,
|
||||||
agent_id=body.agent_id,
|
agent_id=body.agent_id,
|
||||||
vip_end_time=body.vip_end_time,
|
vip_end_time=body.vip_end_time,
|
||||||
)
|
)
|
||||||
@@ -111,6 +111,7 @@ async def create_user(db: AsyncSession, body: AdminUserCreate) -> User:
|
|||||||
|
|
||||||
async def update_user(
|
async def update_user(
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
|
oem: User,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
body: AdminUserUpdate,
|
body: AdminUserUpdate,
|
||||||
*,
|
*,
|
||||||
@@ -119,7 +120,7 @@ async def update_user(
|
|||||||
user = await db.get(User, user_id)
|
user = await db.get(User, user_id)
|
||||||
if user is None:
|
if user is None:
|
||||||
raise AdminUserError("用户不存在")
|
raise AdminUserError("用户不存在")
|
||||||
|
body.oem_id=oem.id
|
||||||
if body.username is not None and body.username != user.username:
|
if body.username is not None and body.username != user.username:
|
||||||
taken = await db.scalar(select(User).where(User.username == body.username))
|
taken = await db.scalar(select(User).where(User.username == body.username))
|
||||||
if taken is not None:
|
if taken is not None:
|
||||||
@@ -162,12 +163,13 @@ async def update_user(
|
|||||||
return 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:
|
if user_id == actor_id:
|
||||||
raise AdminUserError("不能删除当前登录账号")
|
raise AdminUserError("不能删除当前登录账号")
|
||||||
|
|
||||||
user = await db.get(User, user_id)
|
user = await db.get(User, user_id)
|
||||||
if user is None:
|
if user is None:
|
||||||
raise AdminUserError("用户不存在")
|
raise AdminUserError("用户不存在")
|
||||||
|
if user.oem_id != oem.id:
|
||||||
|
raise AdminUserError("无权限删除该用户")
|
||||||
await db.delete(user)
|
await db.delete(user)
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ from app.models.oem import Oem
|
|||||||
from app.schemas.oem_public import OemPublicBrandingOut
|
from app.schemas.oem_public import OemPublicBrandingOut
|
||||||
|
|
||||||
|
|
||||||
def _logo_url(logo_path: str | None, base_url: str) -> str | None:
|
def _image_url(image_path: str | None, base_url: str) -> str | None:
|
||||||
if not logo_path or not logo_path.strip():
|
if not image_path or not image_path.strip():
|
||||||
return None
|
return None
|
||||||
path = logo_path.strip().replace("\\", "/").lstrip("/")
|
path = image_path.strip().replace("\\", "/").lstrip("/")
|
||||||
if path.startswith("http://") or path.startswith("https://"):
|
if path.startswith("http://") or path.startswith("https://"):
|
||||||
return path
|
return path
|
||||||
if not path.startswith("images/"):
|
if not path.startswith("images/"):
|
||||||
@@ -21,11 +21,18 @@ async def get_oem_public_branding(db: AsyncSession, oem_id: int) -> OemPublicBra
|
|||||||
base_url = settings.api_public_base.rstrip("/")
|
base_url = settings.api_public_base.rstrip("/")
|
||||||
oem = await db.get(Oem, oem_id)
|
oem = await db.get(Oem, oem_id)
|
||||||
if oem is None:
|
if oem is None:
|
||||||
return OemPublicBrandingOut(oem_id=oem_id, software_name="", logo_url=None, wechat=None)
|
return OemPublicBrandingOut(
|
||||||
|
oem_id=oem_id,
|
||||||
|
software_name="",
|
||||||
|
logo_url=None,
|
||||||
|
wechat=None,
|
||||||
|
wechat_qrcode_url=None,
|
||||||
|
)
|
||||||
|
|
||||||
return OemPublicBrandingOut(
|
return OemPublicBrandingOut(
|
||||||
oem_id=oem.id,
|
oem_id=oem.id,
|
||||||
software_name=oem.software_name or "",
|
software_name=oem.software_name or "",
|
||||||
logo_url=_logo_url(oem.logo_path, base_url),
|
logo_url=_image_url(oem.logo_path, base_url),
|
||||||
wechat=oem.wechat,
|
wechat=oem.wechat,
|
||||||
|
wechat_qrcode_url=_image_url(oem.wechat_qrcode, base_url),
|
||||||
)
|
)
|
||||||
|
|||||||
173
app/services/oem_user.py
Normal file
173
app/services/oem_user.py
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
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) -> 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=body.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=body.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,
|
||||||
|
user_id: int,
|
||||||
|
body: AdminUserUpdate,
|
||||||
|
*,
|
||||||
|
actor_id: int,
|
||||||
|
) -> User:
|
||||||
|
user = await db.get(User, user_id)
|
||||||
|
if user is None:
|
||||||
|
raise AdminUserError("用户不存在")
|
||||||
|
|
||||||
|
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, 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("用户不存在")
|
||||||
|
|
||||||
|
await db.delete(user)
|
||||||
Reference in New Issue
Block a user