187 lines
6.8 KiB
Python
187 lines
6.8 KiB
Python
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy.orm import selectinload
|
|
|
|
from app.core.deps import get_current_user
|
|
from app.db.database import get_db
|
|
from app.db.models import ClassMembership, Class_, User
|
|
from app.schemas.user import TokenResponse, build_user_out
|
|
from app.schemas.wechat import (
|
|
WeChatBindRequest,
|
|
WeChatCurrentBindRequest,
|
|
WeChatLoginRequest,
|
|
WeChatLoginResponse,
|
|
WeChatPhoneUpdateRequest,
|
|
)
|
|
from app.services.member_activation_service import validate_registration
|
|
from app.services.wechat_service import (
|
|
build_wechat_login_result,
|
|
create_bind_token,
|
|
decode_bind_token,
|
|
exchange_code_for_session,
|
|
exchange_phone_code,
|
|
get_user_by_openid,
|
|
mark_wechat_bound,
|
|
)
|
|
|
|
router = APIRouter(prefix="/api/wechat", tags=["wechat"])
|
|
|
|
|
|
async def _find_approved_member_for_wechat_bind(
|
|
db: AsyncSession,
|
|
invite_code: str,
|
|
student_id: str,
|
|
) -> tuple[User, int] | None:
|
|
result = await db.execute(
|
|
select(User, ClassMembership.class_id)
|
|
.join(ClassMembership, ClassMembership.user_id == User.id)
|
|
.join(Class_, Class_.id == ClassMembership.class_id)
|
|
.options(
|
|
selectinload(User.memberships),
|
|
selectinload(User.memberships).selectinload(ClassMembership.class_),
|
|
)
|
|
.where(
|
|
Class_.invite_code == invite_code,
|
|
User.student_id == student_id,
|
|
User.status == "approved",
|
|
)
|
|
)
|
|
row = result.first()
|
|
if row is None:
|
|
return None
|
|
return row[0], row[1]
|
|
|
|
|
|
@router.post("/login", response_model=WeChatLoginResponse)
|
|
async def wechat_login(req: WeChatLoginRequest, db: AsyncSession = Depends(get_db)):
|
|
session = await exchange_code_for_session(req.code)
|
|
openid = session["openid"]
|
|
unionid = session.get("unionid")
|
|
|
|
user = await get_user_by_openid(db, openid)
|
|
if user is None:
|
|
return WeChatLoginResponse(
|
|
binding_required=True,
|
|
bind_token=create_bind_token(openid, unionid),
|
|
)
|
|
if user.status == "inactive":
|
|
raise HTTPException(status_code=403, detail="账号尚未激活")
|
|
if user.status == "disabled":
|
|
raise HTTPException(status_code=403, detail="账号已被禁用")
|
|
return WeChatLoginResponse(**build_wechat_login_result(user))
|
|
|
|
|
|
@router.post("/bind", response_model=TokenResponse)
|
|
async def wechat_bind(req: WeChatBindRequest, db: AsyncSession = Depends(get_db)):
|
|
openid, unionid = decode_bind_token(req.bind_token)
|
|
existing = await get_user_by_openid(db, openid)
|
|
if existing is not None and existing.status != "inactive":
|
|
raise HTTPException(status_code=400, detail="该微信已绑定账号")
|
|
|
|
phone = await exchange_phone_code(req.phone_code)
|
|
activation_target = await validate_registration(
|
|
db,
|
|
req.invite_code.strip().upper(),
|
|
req.student_id.strip(),
|
|
)
|
|
approved_target = None
|
|
if activation_target is None:
|
|
approved_target = await _find_approved_member_for_wechat_bind(
|
|
db,
|
|
req.invite_code.strip().upper(),
|
|
req.student_id.strip(),
|
|
)
|
|
if activation_target is None and approved_target is None:
|
|
raise HTTPException(status_code=400, detail="邀请码或学号无效")
|
|
|
|
user, class_id = activation_target or approved_target
|
|
if existing is not None and existing.id != user.id:
|
|
raise HTTPException(status_code=400, detail="该微信已绑定其他账号")
|
|
if user.wechat_openid and user.wechat_openid != openid:
|
|
raise HTTPException(status_code=400, detail="该账号已绑定其他微信")
|
|
if user.phone and user.phone != phone:
|
|
raise HTTPException(status_code=400, detail="手机号与预留手机号不一致")
|
|
|
|
other_phone_result = await db.execute(
|
|
select(User).where(User.phone == phone, User.id != user.id, User.status != "inactive")
|
|
)
|
|
if other_phone_result.scalar_one_or_none() is not None:
|
|
raise HTTPException(status_code=400, detail="该手机号已绑定其他账号")
|
|
|
|
mark_wechat_bound(user, openid, unionid, phone)
|
|
await db.commit()
|
|
|
|
result = await db.execute(
|
|
select(User)
|
|
.options(
|
|
selectinload(User.memberships),
|
|
selectinload(User.memberships).selectinload(ClassMembership.class_),
|
|
)
|
|
.where(User.id == user.id)
|
|
)
|
|
bound_user = result.scalar_one()
|
|
bound_user.set_active_membership(class_id)
|
|
login_result = build_wechat_login_result(bound_user, class_id)
|
|
return TokenResponse(token=login_result["token"], user=login_result["user"])
|
|
|
|
|
|
@router.post("/phone", response_model=TokenResponse)
|
|
async def update_wechat_phone(
|
|
req: WeChatPhoneUpdateRequest,
|
|
user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
phone = await exchange_phone_code(req.phone_code)
|
|
other_result = await db.execute(
|
|
select(User).where(User.phone == phone, User.id != user.id, User.status != "inactive")
|
|
)
|
|
if other_result.scalar_one_or_none() is not None:
|
|
raise HTTPException(status_code=400, detail="该手机号已绑定其他账号")
|
|
user.phone = phone
|
|
from datetime import datetime, timezone
|
|
|
|
user.phone_verified_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
|
await db.commit()
|
|
await db.refresh(user)
|
|
return TokenResponse(
|
|
token=build_wechat_login_result(user)["token"],
|
|
user=build_user_out(user),
|
|
)
|
|
|
|
|
|
@router.post("/bind-current", response_model=TokenResponse)
|
|
async def bind_current_user_wechat(
|
|
req: WeChatCurrentBindRequest,
|
|
user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
session = await exchange_code_for_session(req.code)
|
|
openid = session["openid"]
|
|
unionid = session.get("unionid")
|
|
existing = await get_user_by_openid(db, openid)
|
|
if existing is not None and existing.id != user.id:
|
|
raise HTTPException(status_code=400, detail="该微信已绑定其他账号")
|
|
|
|
phone = await exchange_phone_code(req.phone_code)
|
|
other_result = await db.execute(
|
|
select(User).where(User.phone == phone, User.id != user.id, User.status != "inactive")
|
|
)
|
|
if other_result.scalar_one_or_none() is not None:
|
|
raise HTTPException(status_code=400, detail="该手机号已绑定其他账号")
|
|
|
|
mark_wechat_bound(user, openid, unionid, phone)
|
|
await db.commit()
|
|
|
|
result = await db.execute(
|
|
select(User)
|
|
.options(
|
|
selectinload(User.memberships),
|
|
selectinload(User.memberships).selectinload(ClassMembership.class_),
|
|
)
|
|
.where(User.id == user.id)
|
|
)
|
|
bound_user = result.scalar_one()
|
|
login_result = build_wechat_login_result(bound_user)
|
|
return TokenResponse(token=login_result["token"], user=login_result["user"])
|