feat(plan): 添加周计划打卡名单查询功能
- 新增 USER_STATUS_NORMAL 常量用于筛选正常状态学员 - 修改小组成员查询接口,增加 student_id 字段返回 - 扩展打卡记录查询,支持 content 和 attachments 字段 - 在打卡结果中添加 student_id、content 和 attachments 信息 - 新增 /records 接口供教师查询周计划打卡名单 - 名单统计仅包含状态为'正常'的学员 - 实现按组别和学号排序的打卡状态统计功能
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user