Files
wechat_backend/routes/auth.py
王宏豪 9a7681112b feat(database): 迁移用户关联字段并优化打卡系统
- 修改数据库配置将MYSQL_DB从mys_info_v3改为mys_info_test
- 新增auth_users表wechat_openid字段并建立唯一索引
- 为plan_checkins表添加计划性能索引(idx_plan_period_student)
- 将checkin_records_old表关联方式从student_id改为user_id
- 将plan_checkins表关联方式从student_id改为user_id并更新附件路径
- 移除routes/auth.py中的uuid导入使用安全token生成函数
- 重构routes/checkin.py打卡逻辑去除course_type字段并优化返回结构
- 添加打卡时间限制验证功能防止过期打卡操作
- 实现小组统计功能支持老师查看各组打卡情况
- 更新主页index.html界面适配新的打卡和计划功能
- 调整计划提交时间规则为周五至周四周期
- 添加月度和年度计划提交状态验证机制
2026-09-10 14:11:59 +08:00

124 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import logging
from datetime import datetime, timedelta
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel
from .. import config, db, security, wechat
from ..user import user_payload
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1/auth", tags=["auth"])
class WeChatLoginRequest(BaseModel):
code: str
class BindPhoneRequest(BaseModel):
code: str
phone_code: str
def get_current_user(request: Request):
token = ""
# 1. 优先从 Authorization header 取(小程序 API 调用方式)
auth = request.headers.get("Authorization", "")
if auth.startswith("Bearer "):
token = auth[7:]
# 2. header 没有则从 cookie 取(webview H5 页面内请求方式)
if not token:
token = request.cookies.get("auth_token", "")
if not token:
raise HTTPException(status_code=401, detail="缺少 token")
session = db.fetch_one(
"SELECT s.user_id AS id FROM auth_sessions s "
"WHERE s.token=%s AND s.expires_at>NOW()",
(token,),
)
if not session:
raise HTTPException(status_code=401, detail="token 无效或过期")
user = db.fetch_one("SELECT * FROM auth_users WHERE id=%s", (session["id"],))
if not user:
raise HTTPException(status_code=401, detail="用户不存在")
return user
def _record_session(user_id):
"""创建会话 token 并写入 auth_sessions。"""
token = security.create_session_token()
expires = datetime.now() + timedelta(seconds=config.JWT_TTL_SECONDS)
db.execute(
"INSERT INTO auth_sessions (user_id, token, expires_at) VALUES (%s, %s, %s)",
(user_id, token, expires),
)
return token
@router.post("/wechat-login")
def wechat_login(req: WeChatLoginRequest):
try:
openid = wechat.code_to_openid(req.code)
except wechat.WeChatError as exc:
logger.warning("微信登录 code 换取 openid 失败: %s", exc)
raise HTTPException(status_code=400, detail=str(exc))
user = db.fetch_one("SELECT * FROM auth_users WHERE wechat_openid=%s", (openid,))
if user:
token = _record_session(user["id"])
return {"success": True, "is_new": False, "token": token, **user_payload(user)}
return {"success": False, "need_phone": True, "error": "需要绑定手机号"}
@router.post("/bind-phone")
def bind_phone(req: BindPhoneRequest):
try:
openid = wechat.code_to_openid(req.code)
phone = wechat.get_phone_number(req.phone_code)
except wechat.WeChatError as exc:
logger.warning("绑定手机号时获取微信信息失败: %s", exc)
raise HTTPException(status_code=400, detail=str(exc))
if not phone:
raise HTTPException(status_code=400, detail="获取手机号失败")
encrypted = security.encrypt_phone(phone)
user = db.fetch_one("SELECT * FROM auth_users WHERE phone_encrypted=%s", (encrypted,))
if not user:
return {"success": False, "error": "手机号未匹配,无法登录"}
db.execute("UPDATE auth_users SET wechat_openid=%s WHERE id=%s", (openid, user["id"]))
user["wechat_openid"] = openid
token = _record_session(user["id"])
return {"success": True, "is_new": True, "token": token, **user_payload(user)}
@router.get("/verify")
def verify(user=Depends(get_current_user)):
return {"success": True, **user_payload(user)}
@router.post("/logout")
def logout(request: Request, user=Depends(get_current_user)):
from fastapi.responses import JSONResponse
auth = request.headers.get("Authorization", "")
if auth.startswith("Bearer "):
db.execute("DELETE FROM auth_sessions WHERE token=%s", (auth[7:],))
# 退出登录时解除该用户手机号绑定的微信 openid,下次需重新授权手机号绑定
db.execute(
"UPDATE auth_users SET wechat_openid=NULL WHERE id=%s",
(user["id"],),
)
resp = JSONResponse(content={"success": True})
# 同时清除 webview cookie 登录态
resp.delete_cookie(key="auth_token", path="/")
return resp