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, 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"]) @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(), ) if activation_target is None: raise HTTPException(status_code=400, detail="邀请码或学号无效,或账号已激活") user, class_id = activation_target if existing is not None and existing.id != user.id: 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"])