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}