From 0395b58eafd296ef274b667de53240c8d9dbdb84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=AE=8F=E8=B1=AA?= <2443753184@qq.com> Date: Wed, 16 Sep 2026 16:23:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(plan):=20=E6=B7=BB=E5=8A=A0=E5=91=A8?= =?UTF-8?q?=E8=AE=A1=E5=88=92=E6=89=93=E5=8D=A1=E5=90=8D=E5=8D=95=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 USER_STATUS_NORMAL 常量用于筛选正常状态学员 - 修改小组成员查询接口,增加 student_id 字段返回 - 扩展打卡记录查询,支持 content 和 attachments 字段 - 在打卡结果中添加 student_id、content 和 attachments 信息 - 新增 /records 接口供教师查询周计划打卡名单 - 名单统计仅包含状态为'正常'的学员 - 实现按组别和学号排序的打卡状态统计功能 --- routes/plan.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/routes/plan.py b/routes/plan.py index f681dff..6586356 100644 --- a/routes/plan.py +++ b/routes/plan.py @@ -19,6 +19,7 @@ router = APIRouter(prefix="/api/v1/plan", tags=["plan"]) PLAN_TYPES = {"weekly", "monthly", "yearly"} PLAN_STATUSES = {"已完成", "未完成"} DEFAULT_STATUS = "未完成" +USER_STATUS_NORMAL = "正常" # auth_users.status,名单只统计状态正常的学员 MAX_CONTENT_LEN = 1000 MAX_FILE_SIZE = 10 * 1024 * 1024 MAX_FILES = 4 @@ -521,7 +522,7 @@ def group(type: str, group_name: str, period_key: str = "", user=Depends(get_cur raise HTTPException(status_code=404, detail="小组不存在") members = db.fetch_all( - "SELECT id AS user_id, name FROM auth_users WHERE `group`=%s ORDER BY id", + "SELECT id AS user_id, student_id, name FROM auth_users WHERE `group`=%s ORDER BY id", (group_name,), ) uids = [m["user_id"] for m in members] @@ -529,7 +530,7 @@ def group(type: str, group_name: str, period_key: str = "", user=Depends(get_cur if uids: placeholders = ",".join(["%s"] * len(uids)) records = db.fetch_all( - "SELECT user_id, status, checked_at FROM plan_checkins " + "SELECT user_id, status, checked_at, content, attachments FROM plan_checkins " "WHERE plan_type=%s AND period_key=%s AND user_id IN (" + placeholders + ")", tuple([type, selected] + uids), ) @@ -540,11 +541,14 @@ def group(type: str, group_name: str, period_key: str = "", user=Depends(get_cur rec = rec_map.get(m["user_id"]) result.append({ "user_id": m["user_id"], + "student_id": m.get("student_id") or "", "name": m["name"], "filled": rec is not None, "record": { "status": rec["status"], "checked_at": fmt_dt(rec.get("checked_at")), + "content": rec.get("content") or "", + "attachments": _parse_attachments(rec.get("attachments")), } if rec else None, }) result.sort(key=lambda item: (0 if not item["filled"] else 1, item["user_id"])) @@ -558,6 +562,51 @@ def group(type: str, group_name: str, period_key: str = "", user=Depends(get_cur } +@router.get("/records") +def records(period_key: str = "", user=Depends(get_current_user)): + """ + 周计划打卡名单(仅老师可查)。 + + 请求参数: + - period_key 周(ISO 周,如 `2026-W38`);不传取当前应填周(周五~周日为下一周) + + 返回:`user_id` / `student_id` / `name` / `group_name` / `checked` + + 判定口径:该周在 `plan_checkins` 中存在记录 = 已打卡,与 `/groups` 的小组统计口径一致。 + 仅统计 `auth_users.status = '正常'` 的学员(`暂停` 等状态不计入名单)。 + 学员(非 teacher)一律拒绝。 + """ + if not _is_teacher(user): + raise HTTPException(status_code=403, detail="无权限") + selected = period_key.strip() or get_period_key("weekly") + if parse_iso_week(selected) is None: + raise HTTPException(status_code=400, detail="无效的周周期") + # 用户表 LEFT JOIN 周计划打卡记录表,一次取回全员(含未打卡)避免 N+1 + rows = db.fetch_all( + "SELECT u.id AS user_id, u.student_id, u.name, u.`group` AS group_name, " + " pc.id AS record_id " + "FROM auth_users u " + "LEFT JOIN plan_checkins pc ON pc.user_id = u.id " + " AND pc.plan_type = %s AND pc.period_key = %s " + "WHERE u.`status` = %s " + "ORDER BY u.`group`, u.student_id", + ("weekly", selected, USER_STATUS_NORMAL), + ) + return { + "period_key": selected, + "list": [ + { + "user_id": r["user_id"], + "student_id": r.get("student_id") or "", + "name": r.get("name") or "", + "group_name": r.get("group_name") or "", + "checked": r.get("record_id") is not None, + } + for r in rows or [] + ], + } + + @router.post("/checkin") def checkin(req: PlanCheckinRequest, user=Depends(get_current_user)): plan_type = req.plan_type.strip()