注册码 + 管理员系统:

User 模型新增 is_admin 字段
新增 InviteCode 模型(邀请码表)
注册接口必须提供有效邀请码,使用后自动标记
管理员接口:查看所有用户、启用/禁用用户、生成/删除邀请码
前端新增管理面板页面 /admin,导航栏对管理员显示入口
注册页面新增邀请码输入框
选择性超话签到:

新增 GET /api/v1/accounts/{id}/topics 接口获取超话列表
POST /signin 接口支持 {"topic_indices": [0,1,3]} 选择性签到
新增超话选择页面 /accounts/{id}/topics,支持全选/手动勾选
账号详情页新增"选择超话签到"按钮
This commit is contained in:
2026-03-17 17:05:28 +08:00
parent 2fb27aa714
commit e514a11e62
26 changed files with 649 additions and 18 deletions

View File

@@ -7,12 +7,14 @@ from fastapi import FastAPI, Depends, HTTPException, status, Security
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from sqlalchemy import select, func as sa_func
import uvicorn
import os
import logging
import secrets
from datetime import datetime
from shared.models import get_db, User
from shared.models import get_db, User, InviteCode
from shared.config import shared_settings
from auth_service.app.models.database import create_tables
from auth_service.app.schemas.user import (
@@ -113,11 +115,26 @@ async def health_check():
@app.post("/auth/register", response_model=AuthResponse, status_code=status.HTTP_201_CREATED)
async def register_user(user_data: UserCreate, db: AsyncSession = Depends(get_db)):
"""
Register a new user account and return tokens
Register a new user account and return tokens.
Requires a valid invite code.
"""
auth_service = AuthService(db)
# Validate invite code
result = await db.execute(
select(InviteCode).where(
InviteCode.code == user_data.invite_code,
InviteCode.is_used == False,
)
)
invite = result.scalar_one_or_none()
if not invite:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="邀请码无效或已被使用",
)
# Check if user already exists - optimized with single query
# Check if user already exists
email_user, username_user = await auth_service.check_user_exists(user_data.email, user_data.username)
if email_user:
@@ -135,6 +152,12 @@ async def register_user(user_data: UserCreate, db: AsyncSession = Depends(get_db
# Create new user
try:
user = await auth_service.create_user(user_data)
# Mark invite code as used
invite.is_used = True
invite.used_by = str(user.id)
invite.used_at = datetime.utcnow()
await db.commit()
# Create tokens for auto-login
access_token = create_access_token(data={"sub": str(user.id), "username": user.username})
@@ -335,3 +358,128 @@ async def wx_login(body: WxLoginRequest, db: AsyncSession = Depends(get_db)):
expires_in=3600,
user=UserResponse.from_orm(user),
)
# ===================== Admin Endpoints =====================
async def require_admin(
credentials: HTTPAuthorizationCredentials = Security(security),
db: AsyncSession = Depends(get_db),
) -> User:
"""Dependency: require admin user."""
payload = decode_access_token(credentials.credentials)
if not payload:
raise HTTPException(status_code=401, detail="Invalid token")
user_id = payload.get("sub")
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user or not user.is_active:
raise HTTPException(status_code=401, detail="User not found")
if not user.is_admin:
raise HTTPException(status_code=403, detail="需要管理员权限")
return user
@app.get("/admin/users")
async def admin_list_users(
admin: User = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
"""管理员查看所有用户"""
result = await db.execute(select(User).order_by(User.created_at.desc()))
users = result.scalars().all()
return {
"success": True,
"data": [
{
"id": str(u.id),
"username": u.username,
"email": u.email,
"is_active": u.is_active,
"is_admin": u.is_admin,
"created_at": str(u.created_at) if u.created_at else None,
}
for u in users
],
}
@app.put("/admin/users/{user_id}/toggle")
async def admin_toggle_user(
user_id: str,
admin: User = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
"""管理员启用/禁用用户"""
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
if str(user.id) == str(admin.id):
raise HTTPException(status_code=400, detail="不能禁用自己")
user.is_active = not user.is_active
await db.commit()
return {"success": True, "is_active": user.is_active}
@app.post("/admin/invite-codes")
async def admin_create_invite_code(
admin: User = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
"""管理员生成邀请码"""
code = secrets.token_urlsafe(8)[:12].upper()
invite = InviteCode(code=code, created_by=str(admin.id))
db.add(invite)
await db.commit()
await db.refresh(invite)
return {
"success": True,
"data": {
"id": str(invite.id),
"code": invite.code,
"created_at": str(invite.created_at),
},
}
@app.get("/admin/invite-codes")
async def admin_list_invite_codes(
admin: User = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
"""管理员查看所有邀请码"""
result = await db.execute(select(InviteCode).order_by(InviteCode.created_at.desc()))
codes = result.scalars().all()
return {
"success": True,
"data": [
{
"id": str(c.id),
"code": c.code,
"is_used": c.is_used,
"used_by": c.used_by,
"created_at": str(c.created_at) if c.created_at else None,
"used_at": str(c.used_at) if c.used_at else None,
}
for c in codes
],
}
@app.delete("/admin/invite-codes/{code_id}")
async def admin_delete_invite_code(
code_id: str,
admin: User = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
"""管理员删除未使用的邀请码"""
result = await db.execute(select(InviteCode).where(InviteCode.id == code_id))
invite = result.scalar_one_or_none()
if not invite:
raise HTTPException(status_code=404, detail="邀请码不存在")
if invite.is_used:
raise HTTPException(status_code=400, detail="已使用的邀请码不能删除")
await db.delete(invite)
await db.commit()
return {"success": True}