1
This commit is contained in:
@@ -9,6 +9,7 @@ from app.schemas.auth import (
|
|||||||
TokenResponse,
|
TokenResponse,
|
||||||
UserPublic,
|
UserPublic,
|
||||||
)
|
)
|
||||||
|
from app.core.heartbeat_verify import compute_server_verify_code
|
||||||
from app.schemas.common import ApiResponse
|
from app.schemas.common import ApiResponse
|
||||||
from app.services import auth as auth_service
|
from app.services import auth as auth_service
|
||||||
|
|
||||||
@@ -64,15 +65,29 @@ async def heartbeat(
|
|||||||
db: DbSession,
|
db: DbSession,
|
||||||
device_serial: str | None = Header(default=None, convert_underscores=False),
|
device_serial: str | None = Header(default=None, convert_underscores=False),
|
||||||
) -> ApiResponse[HeartbeatResponse]:
|
) -> 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:
|
try:
|
||||||
data = await auth_service.process_heartbeat(
|
data = await auth_service.process_heartbeat(
|
||||||
db,
|
db,
|
||||||
current_user,
|
current_user,
|
||||||
device_serial,
|
device_serial,
|
||||||
body.verify_code,
|
verify_code,
|
||||||
)
|
)
|
||||||
except auth_service.AuthError as exc:
|
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)
|
return ApiResponse(ok=True, message="", data=data)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
"""管理员用户 CRUD。"""
|
"""OEM 用户 CRUD。"""
|
||||||
|
|
||||||
from fastapi import APIRouter, Query
|
from fastapi import APIRouter, Query
|
||||||
|
|
||||||
from app.dependencies import OemUser, DbSession
|
from app.dependencies import OemUser, DbSession
|
||||||
from app.schemas.admin_user import AdminUserCreate, AdminUserOut, AdminUserUpdate
|
from app.schemas.admin_user import AdminUserCreate, AdminUserOut, AdminUserUpdate
|
||||||
from app.schemas.common import ApiResponse, PaginatedData
|
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]])
|
@router.get("", response_model=ApiResponse[PaginatedData[AdminUserOut]])
|
||||||
@@ -25,13 +25,12 @@ async def list_users(
|
|||||||
agent_id: int | None = Query(None, description="按所属代理用户 ID 筛选"),
|
agent_id: int | None = Query(None, description="按所属代理用户 ID 筛选"),
|
||||||
role_id: int | None = Query(None, ge=0, le=3, description="按角色筛选"),
|
role_id: int | None = Query(None, ge=0, le=3, description="按角色筛选"),
|
||||||
) -> ApiResponse[PaginatedData[AdminUserOut]]:
|
) -> ApiResponse[PaginatedData[AdminUserOut]]:
|
||||||
oem_id=_oem.id
|
|
||||||
users, total = await oem_user_service.list_users(
|
users, total = await oem_user_service.list_users(
|
||||||
db,
|
db,
|
||||||
page=page,
|
page=page,
|
||||||
page_size=page_size,
|
page_size=page_size,
|
||||||
username=username,
|
username=username,
|
||||||
oem_id=oem_id,
|
oem_id=_oem.id,
|
||||||
agent_id=agent_id,
|
agent_id=agent_id,
|
||||||
role_id=role_id,
|
role_id=role_id,
|
||||||
)
|
)
|
||||||
@@ -75,6 +74,7 @@ async def update_user(
|
|||||||
try:
|
try:
|
||||||
user = await oem_user_service.update_user(
|
user = await oem_user_service.update_user(
|
||||||
db,
|
db,
|
||||||
|
oem,
|
||||||
user_id,
|
user_id,
|
||||||
body,
|
body,
|
||||||
actor_id=oem.id,
|
actor_id=oem.id,
|
||||||
@@ -96,7 +96,7 @@ async def delete_user(
|
|||||||
db: DbSession,
|
db: DbSession,
|
||||||
) -> ApiResponse[None]:
|
) -> ApiResponse[None]:
|
||||||
try:
|
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:
|
except oem_user_service.AdminUserError as exc:
|
||||||
return ApiResponse(ok=False, message=exc.message)
|
return ApiResponse(ok=False, message=exc.message)
|
||||||
|
|
||||||
|
|||||||
@@ -29,3 +29,4 @@ class CardKey(Base):
|
|||||||
Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True
|
Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True
|
||||||
)
|
)
|
||||||
remark: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
remark: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||||
|
oem_remark: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
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.core.security import hash_password
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.schemas.admin_user import AdminUserCreate, AdminUserUpdate
|
from app.schemas.admin_user import AdminUserCreate, AdminUserUpdate
|
||||||
@@ -9,6 +9,9 @@ from app.schemas.admin_user import AdminUserCreate, AdminUserUpdate
|
|||||||
DEFAULT_PAGE_SIZE = 20
|
DEFAULT_PAGE_SIZE = 20
|
||||||
MAX_PAGE_SIZE = 200
|
MAX_PAGE_SIZE = 200
|
||||||
|
|
||||||
|
# OEM 创建用户时允许的角色:普通用户、代理
|
||||||
|
OEM_CREATABLE_ROLE_IDS = frozenset({0, ROLE_AGENT})
|
||||||
|
|
||||||
|
|
||||||
class AdminUserError(Exception):
|
class AdminUserError(Exception):
|
||||||
def __init__(self, message: str) -> None:
|
def __init__(self, message: str) -> None:
|
||||||
@@ -66,23 +69,31 @@ async def list_users(
|
|||||||
return list(result.all()), total
|
return list(result.all()), total
|
||||||
|
|
||||||
|
|
||||||
async def _validate_owner_ids(
|
async def _validate_agent_under_oem(
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
*,
|
*,
|
||||||
oem_id: int | None,
|
oem_id: int,
|
||||||
agent_id: int | None,
|
agent_id: int | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if oem_id is not None:
|
if agent_id is None:
|
||||||
oem = await db.get(User, oem_id)
|
return
|
||||||
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)
|
agent = await db.get(User, agent_id)
|
||||||
if agent is None or agent.role_id != ROLE_AGENT:
|
if agent is None or agent.role_id != ROLE_AGENT:
|
||||||
raise AdminUserError("所选代理不存在")
|
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))
|
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,16 +103,19 @@ 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)
|
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(
|
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=agent_id,
|
||||||
vip_end_time=body.vip_end_time,
|
vip_end_time=None,
|
||||||
)
|
)
|
||||||
db.add(user)
|
db.add(user)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
@@ -109,16 +123,24 @@ async def create_user(db: AsyncSession, body: AdminUserCreate) -> User:
|
|||||||
return 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(
|
async def update_user(
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
|
oem: User,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
body: AdminUserUpdate,
|
body: AdminUserUpdate,
|
||||||
*,
|
*,
|
||||||
actor_id: int,
|
actor_id: int,
|
||||||
) -> User:
|
) -> User:
|
||||||
user = await db.get(User, user_id)
|
user = await _get_user_under_oem(db, user_id, oem)
|
||||||
if user is None:
|
|
||||||
raise AdminUserError("用户不存在")
|
|
||||||
|
|
||||||
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))
|
||||||
@@ -134,40 +156,34 @@ async def update_user(
|
|||||||
user.phone = body.phone
|
user.phone = body.phone
|
||||||
|
|
||||||
if body.role_id is not None:
|
if body.role_id is not None:
|
||||||
if user_id == actor_id and body.role_id != ROLE_ADMIN:
|
if user_id == actor_id:
|
||||||
raise AdminUserError("不能修改自己的管理员角色")
|
raise AdminUserError("不能修改自己的角色")
|
||||||
|
_validate_creatable_role(body.role_id)
|
||||||
user.role_id = body.role_id
|
user.role_id = body.role_id
|
||||||
|
|
||||||
if body.oem_id is not None or body.agent_id is not None:
|
new_agent_id = body.agent_id if body.agent_id is not None else user.agent_id
|
||||||
await _validate_owner_ids(
|
effective_role = body.role_id if body.role_id is not None else user.role_id
|
||||||
db,
|
if effective_role == ROLE_AGENT:
|
||||||
oem_id=body.oem_id if body.oem_id is not None else user.oem_id,
|
new_agent_id = None
|
||||||
agent_id=body.agent_id if body.agent_id is not None else user.agent_id,
|
elif body.agent_id is not None:
|
||||||
)
|
await _validate_agent_under_oem(db, oem_id=oem.id, agent_id=new_agent_id)
|
||||||
if body.oem_id is not None:
|
if body.agent_id is not None or effective_role == ROLE_AGENT:
|
||||||
user.oem_id = body.oem_id
|
user.agent_id = new_agent_id
|
||||||
if body.agent_id is not None:
|
|
||||||
user.agent_id = body.agent_id
|
|
||||||
|
|
||||||
if body.password:
|
if body.password:
|
||||||
user.password_hash = hash_password(body.password)
|
user.password_hash = hash_password(body.password)
|
||||||
|
|
||||||
if body.clear_vip_end_time:
|
if body.clear_vip_end_time or body.vip_end_time is not None:
|
||||||
user.vip_end_time = None
|
raise AdminUserError("不能设置 VIP 到期时间")
|
||||||
elif body.vip_end_time is not None:
|
|
||||||
user.vip_end_time = body.vip_end_time
|
|
||||||
|
|
||||||
await db.flush()
|
await db.flush()
|
||||||
await db.refresh(user)
|
await db.refresh(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 _get_user_under_oem(db, user_id, oem)
|
||||||
if user is None:
|
|
||||||
raise AdminUserError("用户不存在")
|
|
||||||
|
|
||||||
await db.delete(user)
|
await db.delete(user)
|
||||||
|
|||||||
Reference in New Issue
Block a user