Files
wechat_backend/routes/checkin.py
王宏豪 f3fbd0377a feat(init): 添加完整的认证和业务路由模块
- 实现微信登录、手机号绑定、token验证和登出功能
- 添加打卡系统,支持正常、迟到、请假等状态记录
- 集成课程投票功能,支持多课程选择和评分评论
- 开发反馈系统,支持图文反馈提交和管理员处理
- 构建首页页面模块,动态展示学习资源和个人信息
- 设计计划管理系统,支持周/月/年计划制定和跟踪
- 实现小组计划统计和历史记录查询功能
- 完善用户认证会话管理和数据库交互逻辑
2026-09-03 16:43:11 +08:00

172 lines
5.9 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
import re
from datetime import date, datetime, timedelta
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from .. import db
from .auth import get_current_user
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1/checkin", tags=["checkin"])
CHECKIN_STATUSES = {"正常", "迟到", "请假", "不参加讨论", "早退"}
DEFAULT_STATUS = "正常"
DEFAULT_COURSE_TYPE = "会员课程"
MAX_NOTE_LEN = 500
DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
SELECT_COLS = "id,user_id,class_date,checked_at,status,course_type,note"
def get_class_date():
today = date.today()
return today + timedelta(days=(6 - today.weekday()))
def _is_valid_date(value):
if not isinstance(value, str) or not DATE_RE.match(value):
return False
try:
return datetime.strptime(value, "%Y-%m-%d").date().isoformat() == value
except ValueError as exc:
logger.warning("日期校验失败: %s", exc)
return False
def fmt_dt(value):
if isinstance(value, datetime):
return value.strftime("%Y-%m-%d %H:%M:%S")
return str(value) if value else ""
def fmt_rec(rec, name=""):
return {
"id": rec["id"],
"user_id": rec["user_id"],
"name": name,
"class_date": str(rec["class_date"]),
"checked_at": fmt_dt(rec.get("checked_at")),
"status": rec.get("status", DEFAULT_STATUS),
"course_type": rec.get("course_type", DEFAULT_COURSE_TYPE),
"note": rec.get("note", ""),
}
class CheckinRequest(BaseModel):
status: str = DEFAULT_STATUS
class_date: str = ""
note: str = ""
course_type: str = DEFAULT_COURSE_TYPE
@router.get("/week")
def week(user=Depends(get_current_user)):
class_date = str(get_class_date())
rec = db.fetch_one(
"SELECT " + SELECT_COLS + " FROM checkin_records_old WHERE user_id=%s AND class_date=%s",
(user["id"], class_date),
)
return {"class_date": class_date, "checked": rec is not None, "record": fmt_rec(rec) if rec else None}
@router.post("")
def checkin(req: CheckinRequest, user=Depends(get_current_user)):
status = req.status.strip()
if status not in CHECKIN_STATUSES:
raise HTTPException(status_code=400, detail="无效的打卡状态")
class_date = req.class_date.strip() or str(get_class_date())
if not _is_valid_date(class_date):
raise HTTPException(status_code=400, detail="无效的上课日期")
note = req.note.strip() if status != DEFAULT_STATUS else ""
if len(note) > MAX_NOTE_LEN:
raise HTTPException(status_code=400, detail="备注过长")
now = datetime.now()
# 根据 user_id 判断当天是否已打卡:已打卡 → 更新;未打卡 → 新增
existing = db.fetch_one(
"SELECT id FROM checkin_records_old WHERE user_id=%s AND class_date=%s",
(user["id"], class_date),
)
if existing:
db.execute(
"UPDATE checkin_records_old SET checked_at=%s, status=%s, course_type=%s, note=%s WHERE id=%s",
(now, status, DEFAULT_COURSE_TYPE, note, existing["id"]),
)
else:
# course_type 固定为会员课程(规范),忽略客户端传入值
db.execute(
"INSERT INTO checkin_records_old (user_id, class_date, checked_at, status, course_type, note) "
"VALUES (%s, %s, %s, %s, %s, %s)",
(user["id"], class_date, now, status, DEFAULT_COURSE_TYPE, note),
)
rec = db.fetch_one(
"SELECT " + SELECT_COLS + " FROM checkin_records_old WHERE user_id=%s AND class_date=%s",
(user["id"], class_date),
)
return {"success": True, "record": fmt_rec(rec, user.get("name", "")) if rec else None}
@router.get("/list")
def list_records(page: int = 1, size: int = 10, user=Depends(get_current_user)):
page = max(1, page)
size = max(1, min(50, size))
offset = (page - 1) * size
total_row = db.fetch_one(
"SELECT COUNT(*) AS cnt FROM checkin_records_old WHERE user_id=%s", (user["id"],)
)
total = total_row["cnt"] if total_row else 0
rows = db.fetch_all(
"SELECT " + SELECT_COLS + " FROM checkin_records_old "
"WHERE user_id=%s ORDER BY class_date DESC LIMIT %s OFFSET %s",
(user["id"], size, offset),
)
return {"list": [fmt_rec(r, user.get("name", "")) for r in rows], "total": total, "page": page, "size": size}
@router.get("/group")
def group_view(class_date: str = "", user=Depends(get_current_user)):
class_date = class_date.strip() or str(get_class_date())
if not _is_valid_date(class_date):
raise HTTPException(status_code=400, detail="无效的上课日期")
user_row = db.fetch_one("SELECT `group` FROM auth_users WHERE id=%s", (user["id"],))
if not user_row or not user_row.get("group"):
return {"class_date": class_date, "group_name": "", "members": []}
group_name = user_row["group"]
# 单查询:成员 + 同组成员该上课日的打卡记录,LEFT JOIN 一次取回
rows = db.fetch_all(
"SELECT m.id AS user_id, m.student_id, m.name, "
" r.status, r.checked_at, r.note "
"FROM auth_users m "
"LEFT JOIN checkin_records_old r "
" ON r.user_id = m.id AND r.class_date = %s "
"WHERE m.`group` = %s "
"ORDER BY m.student_id",
(class_date, group_name),
)
result_members = []
for row in rows:
rec = None
if row.get("status") is not None:
rec = {
"status": row["status"],
"checked_at": fmt_dt(row.get("checked_at")),
"note": row.get("note", ""),
}
result_members.append({
"user_id": row["user_id"],
"student_id": row["student_id"],
"name": row["name"],
"role": "",
"checked": rec is not None,
"record": rec,
})
return {"class_date": class_date, "group_name": group_name, "members": result_members}