feat(plan): 添加周计划打卡名单查询功能

- 新增 USER_STATUS_NORMAL 常量用于筛选正常状态学员
- 修改小组成员查询接口,增加 student_id 字段返回
- 扩展打卡记录查询,支持 content 和 attachments 字段
- 在打卡结果中添加 student_id、content 和 attachments 信息
- 新增 /records 接口供教师查询周计划打卡名单
- 名单统计仅包含状态为'正常'的学员
- 实现按组别和学号排序的打卡状态统计功能
This commit is contained in:
王宏豪
2026-09-16 16:23:45 +08:00
parent 65ec4e6a52
commit 0395b58eaf

View File

@@ -19,6 +19,7 @@ router = APIRouter(prefix="/api/v1/plan", tags=["plan"])
PLAN_TYPES = {"weekly", "monthly", "yearly"} PLAN_TYPES = {"weekly", "monthly", "yearly"}
PLAN_STATUSES = {"已完成", "未完成"} PLAN_STATUSES = {"已完成", "未完成"}
DEFAULT_STATUS = "未完成" DEFAULT_STATUS = "未完成"
USER_STATUS_NORMAL = "正常" # auth_users.status,名单只统计状态正常的学员
MAX_CONTENT_LEN = 1000 MAX_CONTENT_LEN = 1000
MAX_FILE_SIZE = 10 * 1024 * 1024 MAX_FILE_SIZE = 10 * 1024 * 1024
MAX_FILES = 4 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="小组不存在") raise HTTPException(status_code=404, detail="小组不存在")
members = db.fetch_all( 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,), (group_name,),
) )
uids = [m["user_id"] for m in members] 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: if uids:
placeholders = ",".join(["%s"] * len(uids)) placeholders = ",".join(["%s"] * len(uids))
records = db.fetch_all( 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 + ")", "WHERE plan_type=%s AND period_key=%s AND user_id IN (" + placeholders + ")",
tuple([type, selected] + uids), 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"]) rec = rec_map.get(m["user_id"])
result.append({ result.append({
"user_id": m["user_id"], "user_id": m["user_id"],
"student_id": m.get("student_id") or "",
"name": m["name"], "name": m["name"],
"filled": rec is not None, "filled": rec is not None,
"record": { "record": {
"status": rec["status"], "status": rec["status"],
"checked_at": fmt_dt(rec.get("checked_at")), "checked_at": fmt_dt(rec.get("checked_at")),
"content": rec.get("content") or "",
"attachments": _parse_attachments(rec.get("attachments")),
} if rec else None, } if rec else None,
}) })
result.sort(key=lambda item: (0 if not item["filled"] else 1, item["user_id"])) 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") @router.post("/checkin")
def checkin(req: PlanCheckinRequest, user=Depends(get_current_user)): def checkin(req: PlanCheckinRequest, user=Depends(get_current_user)):
plan_type = req.plan_type.strip() plan_type = req.plan_type.strip()