跳转至

答题管理系统 设计文档(升级方案)

面向现有"局域网联机答题"项目(FastAPI + SQLite + 原生 JS),将其从昵称制实时抢答工具升级为账号制答题管理系统。 本文档覆盖:管理员端(账号管理 / 题目管理 / 统计查看 / 用户管理)与用户端(登录 / 注册 / 账号认领 / 答题记录)的完整功能规格,先评审、后编码。

v1.1 实现修订(校园落地,以本段为准) 学生通常没有手机号:注册 / 登录 / 认领不再依赖手机与短信验证码,统一使用 用户名 或 邮箱 作为身份凭据; - 注册:姓名 + (用户名 或 邮箱,至少一项)+ 密码;用户名留空时服务端自动生成(只留邮箱也能登录); - 登录:输入框接受 用户名 或 邮箱 任一,密码校验; - 认领:身份凭证下拉项为 用户名 / 邮箱(导入模板中的手机号仅为可选项,填了也可作认领键); - 忘记密码:登录页不再提供自助找回,提示联系老师 → 老师于【账号管理 → 重置密码】一键生成新密码(POST /api/users/{id}/pwd); - 批量导入模板示例已去手机列;.xlsx 解析依赖 openpyxl。 本文后续章节提到的"验证码登录 Tab、手机必填、注册字段手机"等均以本修订为准(视为不存在或已弱化)。


1. 总体架构与升级思路

1.1 现状与目标

维度 现状 升级后
身份 考生输入昵称进入(players 表,无密码) 必须登录(账号体系),成绩自动归属账号
玩法 主持人逐题过题:顺序 / 随机 / 抢答,实时排行榜 保留上述"实时场";新增"整卷测验场"(整卷限时作答、交卷判分)
记录 answers 表只有 (player_id, qid, sel),无时间/场次,跨场累计无法统计 引入"答题场次 session",每次答题(每场)独立归档,可统计
题目 questions 表(A/B/C/D 单选,无解析)+ qb_questions 题库(多题型、富文本、含解析 analysis 字段 题目统一收敛到 qb_questions解析必填,按场次策略展示
管理端 4 个面板(模式/控制/题目/排行) 页签化:现场控制 / 题库组卷 / 账号管理 / 用户管理 / 统计报告

1.2 答题"场次"概念(统计与记录的基础)

每一次答题 = 一个场次(session),两种产生方式:

  1. 实时场(live):沿用主持人逐题控制。主持人在现场控制区点新增按钮【结束本场并结算】时,把本轮所有作答与得分归档为一个 session(players 表计分清零,可开启下一场)。清空数据 = 放弃本场、不归档(保留现状语义)。
  2. 整卷测验场(exam):主持人在题库页"组卷"后或从题库抽题发起一场测验(设置标题、时长、每题分值沿用组卷/默认),考生从大厅进入作答,可逐题保存、提前交卷或到时自动交卷,交卷即时结算归档。

两种场次落库结构完全一致,统计、用户记录、报表共用同一套查询。

1.3 页面与路由规划(★=新增,▲=改造)

用户端

页面 路由 说明
答题大厅 / 未登录显示登录卡(内嵌),已登录显示等待/答题界面与"我的记录"入口
登录 /login ★(或首页内嵌卡片,二选一,推荐内嵌卡片 + 独立页双入口) 密码登录 / 手机号验证码登录 两个 Tab
注册 /register 自助注册(管理端可关闭)
账号认领 /claim 认领管理员导入的账号
个人中心 /me 答题历史列表
答题明细 /me/sessions/{id} 逐题作答情况 + 解析回顾
整卷测验作答 /exam/{sid} 考场页(计时、答题卡、交卷)

管理端(/admin 页签化)

页签 说明
现场控制 现有实时控制 + 抢答 + 排行榜(玩家名=账号名)▲
题库与组卷 现有 /qbank(迁移入口保留,去掉右上角外链改为页签内嵌或新窗口)▲
账号管理 手工新增 / CSV·Excel 批量导入 / 模板下载 ★
用户管理 用户列表、认领状态、答题记录下钻 ★
统计报告 场次统计:整体指标、各题正确率、得分分布、导出 ★

API 前缀约定:认证 /api/auth/*、用户管理 /api/users/*、导入 /api/import/*、场次与记录 /api/sessions/*、统计 /api/stats/*

1.4 技术决策(离线优先)

  • 局域网环境可能无外网,页面图表不使用 CDN(ECharts 等),改用纯 CSS/SVG 自绘柱状图、分布图;
  • 密码哈希用 Python 内置 hashlib.scrypt(或 PBKDF2-HMAC-SHA256),不新增依赖;会话用 users.session_token + HttpOnly Cookie;
  • Excel .xlsx 解析需新增依赖 openpyxl(同步更新 requirements.txt);.csv 用内置 csv(自动识别 UTF-8 / GBK,读 BOM);
  • 手机短信验证码在纯局域网内没有短信网关,落地为三级方案(见 5.1.2):邮件验证码(可选 SMTP 配置)→ 调试码通道 → 短信网关接口预留。

2. 数据模型设计(DDL 草案)

全部写入现有 quiz.dbqbank.py 的建表沿用)。

-- ============ 账号体系 ============
CREATE TABLE IF NOT EXISTS users (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  username      TEXT NOT NULL UNIQUE,            -- 登录名:3-20位,任意字符开头,字母/数字/下划线/中文
  display_name  TEXT NOT NULL,                   -- 姓名(导入列"姓名",注册字段"姓名")
  phone         TEXT UNIQUE,                     -- 手机号,可空,唯一
  email         TEXT UNIQUE,                     -- 邮箱,可空,唯一
  password_hash TEXT NOT NULL,                   -- scrypt(随机盐 + 密码)
  role          TEXT NOT NULL DEFAULT 'student', -- student / admin(预留)
  status        TEXT NOT NULL DEFAULT 'unclaimed', -- unclaimed待认领|normal正常|disabled停用|locked锁定
  source        TEXT NOT NULL DEFAULT 'import',  -- import批量导入|self自助注册
  batch_id      INTEGER,                         -- 导入批次,见 import_batches
  remark        TEXT DEFAULT '',                 -- 备注/班级(导入可选列)
  fail_count    INTEGER DEFAULT 0,               -- 连续认证失败次数
  lock_until    REAL,                            -- 锁定截止时间戳(0/空=未锁定)
  claimed_at    REAL,                            -- 认领时间(待认领→正常 的时刻)
  last_login_at REAL,
  created_at    REAL NOT NULL,
  updated_at    REAL
);
CREATE INDEX IF NOT EXISTS idx_users_status ON users(status);
CREATE INDEX IF NOT EXISTS idx_users_batch  ON users(batch_id);

CREATE TABLE IF NOT EXISTS import_batches (
  id         INTEGER PRIMARY KEY AUTOINCREMENT,
  filename   TEXT NOT NULL,
  total_rows INTEGER NOT NULL,       -- 文件总行数(不含表头)
  ok_rows    INTEGER NOT NULL,       -- 成功导入
  fail_rows  INTEGER NOT NULL,       -- 失败行数(明细可下载)
  created_at REAL NOT NULL
);

-- ============ 答题场次与作答明细(统计/记录的数据源) ============
CREATE TABLE IF NOT EXISTS sessions (
  id          INTEGER PRIMARY KEY AUTOINCREMENT,
  title       TEXT NOT NULL,                       -- 场次名(live: "实时场 第N场 20xx-xx-xx 14:00")
  mode        TEXT NOT NULL,                       -- live_sequence|live_random|live_buzz|exam
  paper_id    INTEGER,                             -- exam 场关联组卷;live 场为空
  quiz_plan   TEXT,                                -- 题单快照 JSON: [{qn_id, qn_type, points, ord}]
  duration    INTEGER,                             -- exam:限时(秒);live:0
  reveal_policy TEXT NOT NULL DEFAULT 'after_submit', -- 解析展示策略,见 4.2.2
  pass_line   REAL DEFAULT 60,                     -- 及格线(百分制),统计用,可配置
  started_at  REAL,                                -- 开考时间
  ended_at    REAL,                                -- 结束/结算时间
  status      TEXT NOT NULL DEFAULT 'open'         -- open答题中|ended已结束|aborted作废
);
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);

CREATE TABLE IF NOT EXISTS session_users (         -- 场次-用户 汇总(一人一场一条)
  id          INTEGER PRIMARY KEY AUTOINCREMENT,
  session_id  INTEGER NOT NULL,
  user_id     INTEGER NOT NULL,
  score       REAL DEFAULT 0,                      -- 本场得分(live=实时计分规则得分;exam=卷面得分)
  max_score   REAL DEFAULT 0,                      -- 满分(统计百分化用)
  correct_cnt INTEGER DEFAULT 0,                   -- 答对题数
  answered_cnt INTEGER DEFAULT 0,                  -- 已作答题数
  used_secs   INTEGER,                             -- 用时(exam=交卷-开考;live=入场到结算)
  submit_at   REAL,                                -- 交卷/结算时间
  UNIQUE(session_id, user_id)
);

CREATE TABLE IF NOT EXISTS attempt_items (         -- 逐题作答明细(一题一条,两种场次统一)
  id          INTEGER PRIMARY KEY AUTOINCREMENT,
  session_id  INTEGER NOT NULL,
  user_id     INTEGER NOT NULL,
  qn_type     TEXT NOT NULL DEFAULT 'qb',          -- qb=新题库题 | legacy=旧表题(迁移过渡期)
  qn_id       INTEGER NOT NULL,                    -- 对应 qb_questions.id 或 questions.id
  sel         TEXT,                                -- 作答原文:single "A" / multi "A,C" / judge "T"/"F" / code 文本截断
  is_correct  INTEGER,                             -- 1对 0错 NULL未答(exam 到点未答的题写一行 is_correct=NULL)
  points_got  REAL DEFAULT 0,                      -- 本题得分
  answered_at REAL,                                -- 作答时间戳
  latency     INTEGER                              -- 本题用时(毫秒;实时场=发题到作答)
);
CREATE INDEX IF NOT EXISTS idx_att_session ON attempt_items(session_id);
CREATE INDEX IF NOT EXISTS idx_att_user    ON attempt_items(user_id);
CREATE INDEX IF NOT EXISTS idx_att_qn      ON attempt_items(qn_type, qn_id);

升级改造(对现有表,幂等 ALTER)

ALTER TABLE players ADD COLUMN user_id INTEGER;    -- 实时玩法玩家绑定账号;ALTER 需判列存在再执行
ALTER TABLE players ADD COLUMN display TEXT DEFAULT '';
-- players.name 保留为唯一标识的"参赛名",登录后自动以 user 维度 upsert(一个账号对应一条 player)

questions 表处置(迁移方案,见确认清单 Q7):提供迁移脚本把旧 A/B/C/D 单选题导入 qb_questionstype='single',选项转 JSON,analysis='' 标记"缺解析"),此后实时场题目源也读 qb_questions;迁移完成前 attempt_items.qn_type 字段保证双源兼容。players.score 结算后清零,answers 表数据在首次结算历史场次时归入 attempt_items 后弃用。


3. 管理员端——账号管理(导入)

3.1 功能入口与界面

管理端【账号管理】页签,包含:手工新增单条表单、批量导入区(上传 + 下载模板)、导入历史列表(批次、时间、成功/失败数、下载失败明细)。

3.2 导入文件格式

支持格式.csv(UTF-8 带/不带 BOM、GBK 自动识别,逗号/制表符分隔均可)、.xlsx(openpyxl 解析,取第一个工作表)。 表头:中文列名与英文列名均接受(列名不区分顺序,多余列忽略)。

列名(中文 / 英文) 必填 规则
姓名 / name 2–30 字符
用户名 / username 视规则 唯一;不填时自动生成拼音_姓名不可靠 → 默认规则:s + 4 位随机,导入后可在用户列表改名;建议校内统一填学号/工号
手机号 / phone 见下 11 位数字(^1\d{10}$),全库唯一;可空
邮箱 / email 见下 标准邮箱格式,全库唯一;可空
初始密码 / password 条件 6–20 位;留空则系统生成随机 8 位(含大小写字母+数字),导入完成后一次性展示,并提供"导出初始密码.csv"(含用户名、姓名、明文密码,仅管理员可下载一次)
账号状态 / status 可填:待认领(默认)/正常/停用;不填默认待认领
备注 / remark 班级/部门等,任意文本

身份键规则:用户名、手机号、邮箱三者必须至少填一个且全局唯一;批量内不允许出现重复的(用户名 / 手机号 / 邮箱)。填了手机号或邮箱的账号将作为认领的匹配键。

3.3 示例文件(模板下载内容即此,含两行数据)

姓名,用户名,手机号,邮箱,初始密码,账号状态,备注
张三,20260001,13800138001,zhangsan@example.com,Init@2026,待认领,高一(3)班
李四,20260002,13800138002,,,正常,高一(3)班
王五,,,wangwu@example.com,,待认领,高一(4)班
  • 张三:待认领,可凭 手机/邮箱+初始密码 Init@2026 认领;
  • 李四:用户名+手机,状态正常且留空密码 → 系统生成随机密码,管理员导出后线下发给本人,可直接登录、无需认领
  • 王五:未填用户名 → 自动生成;只有邮箱 → 仅邮箱通道认领。

3.4 导入流程(两步确认式)

  1. 上传解析:选择文件 → 服务端逐行校验(格式、唯一性、与库内已有数据冲突)→ 返回预览:表格展示前 20 行 + 顶部统计(总行数 / 可导入 / 冲突行数)与错误清单(行号、列、原因,逐条列出);
  2. 确认导入:点击"确认导入 N 行"→ 落库(同一批次内重复用户名自动加后缀并列入提示)→ 完成页:成功 N / 失败 M,提供【下载失败明细.csv】(含行号与原因,修好后可直接再传)。

冲突判定:与库内已存在用户的冲突(用户名/手机/邮箱撞库)默认跳过该行(不覆盖),并提供选项"覆盖更新已存在账号的资料与状态"(密码字段留空则保持原密码不变),防止误导入造成账号覆盖。


4. 管理员端——题目管理(增删改查 + 解析)

4.1 现有基础与升级点

题库 CRUD、分类/标签、富文本题干(插图/表格/代码)、多题型(单选/多选/判断/编程/材料大题及小题)、组卷与打印已实现于 /qbank(数据在 qb_questions,其中 analysis 即解析字段)。升级点:

  1. 解析必填:新建/编辑保存时若解析为空 → 拒绝保存并提示(前端 + 后端双重校验)。历史缺解析题目列表标"缺解析"徽标,支持按"缺解析"筛选,批量补录完成后才可在正式场次中被抽中;
  2. 列表/搜索新增筛选条件:有无解析
  3. 删改规则维持现有(删大题时小题转独立题、试卷引用位自动移除,二次确认)。

4.2 解析的展示方式(考生侧三时机 + 管理侧)

每道题解析由"场次级开关 reveal_policy"统管(创建场次时选择),实现层:/api/current、交卷响应、记录页接口按策略决定是否下发 analysis。四种策略:

策略 名称 适用 规则
instant 答后立现 随堂练习/实时场 每题提交判分后,立即在本题卡片下方展开"答案解析"(对错 + 解析同屏)
after_submit(默认) 交卷后逐题回顾 整卷测验/考试 答题中不显示;交卷后跳"答题明细"页,逐题列表每行对错标记 + 可展开解析;个人记录页此后永久可见
after_end 主持人统一公布 实时场/课堂讲解 答题与结算阶段仅显示对错;主持人在现场控制/统计页点【公布解析】后,全体可见(含个人记录),未公布前接口不下发
report_only 仅报告可见 存档测试 考生全程看不到解析;解析只出现在管理端统计报告与教师卷打印中

补充规则: - 管理端不受限制:题库编辑、教师卷打印、统计报告始终展示解析; - 材料大题:解析展示按小题逐条展示(题干材料本身无解析); - 编程题:解析内容约定为"参考思路 + 关键代码"两段富文本,展示时与判分结果(通过/未通过)并列。


5. 管理员端——统计查看

5.1 入口与范围

【统计报告】页签。顶部场次选择器(下拉:全部 / 最近场次列表,含模式徽标与时间);选定后展示:整体指标卡片区、每题表现表、得分分布图、场次列表(多场对比)。所有图表 CSS/SVG 自绘,无外部依赖。

5.2 指标定义与计算方式(口径先行)

有效作答者 = 该场 attempt_items 中至少作答 1 题的去重 user_id 数(实时场即参加人数;整卷场含未交卷但已作答者)。

指标 计算公式(口径) 展示形式
答题人数 有效作答者总数 顶部卡片(与"交卷人数"并列)
交卷人数 session_users.submit_at 非空 的行数(exam 场) 顶部卡片
平均分 Σscore ÷ 有效作答者数,保留 1 位小数 卡片
最高分 / 最低分 有效作答者中 score 的 max / min 卡片(并列)
得分分布 按百分化 score ÷ max_score × 100,分 10 档 [0-9,…,90-100],统计每档人数与占比;max_score=0 或非百分制场次则按实际分数组距(如满分 20 分按 1 分一档) SVG 柱状图 + 悬停显示人数/占比;下方附人数表
标准差 √(Σ(x−x̄)² ÷ n) 卡片(反映两极分化)
及格率 ≥ pass_line(默认 60 分,场次创建可改)人数 ÷ 有效作答者数 卡片
平均用时 交卷者 Σused_secs ÷ 交卷人数(exam);实时场不展示 卡片
总分/满分 卷面 Σpoints(exam);实时场展示 Σ每题 10 分制的总分 卡片副文本

各题表现表(核心行表):行=题目(序号、题干摘要、点击展开全文),列如下——

计算方式 展示形式
题型 徽标
作答率 本题作答人次 ÷ 有效作答者数 ×100%(未答不计入正确率分母) 百分比 + 细条
正确率 本题答对人次 ÷ 本题作答人次 ×100% 百分比 + 细条,≤60% 红、60–85% 黄、>85% 绿
选项分布 各选项被选人次及占比(判断:对/错;多选:按组合统计前 6;编程:通过/未通过/未答) 堆叠细条(A/B/C/D 分色)+ 悬停明细
人均得分 Σpoints_got ÷ 作答人次 数值

汇总行:全卷平均正确率、平均作答率。

5.3 核心 SQL(实现口径示例)

-- 单场指标(live 与 exam 统一,得分以 session_users 计)
SELECT COUNT(*) AS n, AVG(score) AS avg, MAX(score) AS hi, MIN(score) AS lo,
       ROUND(AVG((score*100.0/NULLIF(max_score,0))),1) AS pct_avg
FROM session_users WHERE session_id = ?;

-- 各题表现(题目信息 join 题单快照 quiz_plan 或 qb_questions)
SELECT a.qn_id, COUNT(*) AS answered, SUM(a.is_correct) AS correct
FROM attempt_items a WHERE a.session_id = ?
GROUP BY a.qn_id;

5.4 展示补充

  • 场次趋势:多场次平均分/及格率折线(场次选择器切"对比模式"),最多 30 场;
  • 导出:本场报告导出 CSV(整体指标一行 + 每题一行);明细导出(每用户每题的答案与判分,管理端用户管理页也有入口);
  • 仅管理端可见解析(无论策略),教师卷导出复用 qbank 现有打印。

6. 管理员端——用户管理

6.1 列表字段

说明
用户名 主键展示,可点击编辑
姓名 显示名
手机号 / 邮箱 脱敏展示(138****8001),认领前为空显示"—"
账号状态 徽标:待认领(黄)/ 正常(绿)/ 停用(灰)/ 锁定(红,附剩余时间)
来源 导入(批次号) / 自助注册
创建时间 / 最近登录 时间戳
累计答题次数 session_users 按 user 计数(仅已结算场次)
最近答题 最近一场的标题 + 得分,可点击下钻
操作 编辑 / 重置密码 / 停用·启用 / 导出该用户记录

6.2 筛选与搜索

  • 筛选:状态(待认领/正常/停用/锁定)、来源、导入批次、是否答过题(是/否)、有无手机/邮箱(判断认领通道是否完备);
  • 搜索:用户名 / 姓名 / 手机号 / 邮箱 任意片段模糊匹配,支持组合;
  • 分页 20 条/页,可切换 50;当前筛选结果可一键导出 CSV

6.3 账号认领状态与答题记录下钻

  • "待认领"行显示醒目提示条与【催认领】操作(无短信能力,仅界面提示);
  • 点击某用户行 → 右侧抽屉:
  • 基本信息 + 认领时间/渠道(若已认领)+ 停用/锁定操作日志(简);
  • 答题记录:按场次列出——场次标题/模式、时间、得分/满分、答对数/总题、用时、交卷时间;
  • 点击任一场次 → 该用户逐题明细:题号、题干、他的答案 vs 正确答案、判分、得分、用时(解析可见)。管理端不依赖解析策略,始终可见。

7. 用户端——登录

7.1 登录方式(两个 Tab)

Tab 验证方式 说明
密码登录 用户名 + 密码 主通道;用户名支持大小写不敏感
验证码登录 手机号 + 短信验证码 点【发送验证码】→ 校验码 6 位、5 分钟有效、60 秒倒计时重发;手机号必须已绑定账号(未绑定提示:可去注册或认领)

7.2 验证码通道的现实说明与实现(局域网约束)

纯局域网无短信网关,因此实现三级通道(服务端接口统一,便于日后替换): 1. 调试通道(默认启用):验证码写入服务端日志并在管理端"现场控制 → 验证码代收"小面板实时展示(局域网演示、教学场景直接用); 2. 邮件通道(可选配置):在 config 中填 SMTP 后自动启用,发到账号绑定邮箱; 3. 短信通道(预留):定义 sms_gateway.send(phone, code) 接口与配置占位,接网关即启用,无需改业务代码。

7.3 错误处理逻辑(含防爆破)

场景 行为 提示文案
用户名不存在 / 密码错误 统一文案(不泄露账号是否存在),fail_count+1 "用户名或密码错误"
连续错误 5 次 账号锁定 5 分钟(lock_until),锁定期间即使密码正确也拒绝 "尝试次数过多,账号已锁定,请 X 分钟后再试"(前端倒计时)
锁定中 拒绝登录 同上(显示剩余秒数)
验证码错误 fail_count+1,同一规则锁定;验证码 5 次错误作废需重发 "验证码错误,请重试"
验证码过期 需重新发送 "验证码已过期,请重新获取"
账号停用 disabled 直接拒绝 "该账号已停用,请联系管理员"
账号待认领 unclaimed 拒绝直接进入,响应携带提示,前端自动弹出认领引导(跳转 /claim "该账号为管理员导入,请先完成账号认领"
会话过期 任何 API 返回 401,前端弹登录卡,完成后回跳原操作 "登录已过期,请重新登录"

成功:签发 session_token(写入 users.session_token,HttpOnly Cookie 30 天),跳转答题大厅;/api/me 返回用户名、姓名、状态。


8. 用户端——注册

8.1 开关与前提

自助注册由管理端【账号管理】开关控制(默认关闭,校内建议以批量导入+认领为主;开启后新注册账号直接可用,或可选"注册后待审核"模式)。

8.2 字段、校验规则与重复性检查

字段 必填 校验规则 重复性检查
用户名 3–20 位;任意字符开头,仅字母/数字/下划线/中文;前端即时校验(输入变化即提示格式错误) 失焦即 AJAX 查询 /api/auth/check?username=,被占用红字"该用户名已被注册",实时刷新;提交时服务端唯一索引兜底
姓名 2–30 字符 不查重(可与他人同名)
手机号 ^1\d{10}$ 同用户名:失焦 AJAX 查重 + 服务端兜底,占用提示"该手机号已注册(可尝试直接登录或找回)"
邮箱 选填 标准邮箱格式(正则 + 基本域名校验) 同上
密码 8–20 位,必须同时含字母与数字;不得与用户名相同;输入时强度条(弱/中/强)
确认密码 与密码一致(不一致即红字提示,不提交)

注册页注明:注册后即登录;若你是老师下发的账号,请走"账号认领"而非注册。

提交语义:前端全部通过才可点提交;服务端逐项复验,失败返回字段级错误({field, msg}),不回滚已占用检查提示。成功 → 自动登录 → 大厅。


9. 用户端——账号认领

9.1 概念与触发入口

认领 = 管理员通过 CSV/Excel 导入的账号(初始状态 unclaimed)由"本人"确权激活。触发入口:① 登录页底部常驻"认领账号"链接(独立页 /claim);② 待认领账号尝试登录时自动引导。

9.2 认领流程(步骤明细)

步骤 表单内容 说明
① 填写身份 姓名(必填)+ 下面三种任选其一作为匹配键:用户名 / 手机号 / 邮箱 表单说明:"与老师导入信息一致"
② 输入初始密码 初始密码(导入时下发或管理员导出) 若选择"用户名"为匹配键,密码校验照常
③ 系统匹配 服务端按唯一键查账号 匹配规则:键命中 且 display_name 与"姓名"一致;命中多条(理论上不可能,唯一键约束)则拒绝并要求联系管理员
④ 设置新密码 新密码 + 确认 规则同注册密码(8–20 位含字母数字);成功后自动登录并跳大厅
⑤ 完成 状态 → normal,写入 claimed_atlast_login_atsession_token 轮换 页面提示"认领成功,欢迎 XXX"

9.3 身份校验所需字段与安全设计

  • 匹配键(三选一):username / phone / email——来自管理员导入数据;
  • 双因子校验:唯一键命中 + 姓名一致(双重确认防同名误领)+ 初始密码正确(密钥);
  • 安全:初始密码只能用于"登录→强制改密"或"认领改密",认领成功后作废;认领接口与登录共用 fail_count 锁定策略;已登录用户不可认领(先退出)。

9.4 校验失败处理

场景 提示文案 附加行为
键未命中任何账号 "未找到匹配的账号,请核对姓名与手机/邮箱/用户名,或联系管理员"
键命中但姓名不一致 "姓名与账号信息不匹配,请核对"
初始密码错误 "初始密码错误" fail_count+1,5 次锁定 10 分钟
账号已被认领 "该账号已被认领,请直接登录" 提供登录跳转
账号停用 "该账号已停用,请联系管理员"
账号为自助注册(非导入) "该账号非管理员导入账号,无需认领,直接登录即可"
锁定中 "尝试次数过多,请 X 分钟后再试" 剩余时间展示

10. 用户端——答题记录(个人中心 /me

10.1 记录列表字段

说明
场次标题 + 模式徽标 实时·顺序 / 实时·随机 / 实时·抢答 / 测验
答题时间 场次开始时间(实时场=入场时间附近;测验场=交卷时间)
得分 / 满分 85 / 100(含各题分汇总);实时场换算为其规则总分
正确 / 总题数 12/20
用时 秒 → 自动格式 mm:ss
状态 已交卷 / 自动交卷 / 已结算 / 进行中(占位,结算后完整)
场次排名(实时场) 结算时名次(同分按答对时长并列规则:先完成者靠前,或并列)
操作 【查看明细】→ /me/sessions/{id}

顶部汇总条:累计场次数、累计答题数、累计正确率、平均分。筛选:时间范围、模式、标题关键词。分页 10 条/页。

10.2 答题明细页字段(逐题回顾)

每题卡片:序号 + 题型徽标;题干全文(富文本渲染);我的答案(未答灰显"未作答")与正确答案对照,对错彩色标记;本题得分与满分;本题用时;解析(可见性规则:场次策略为 after_submit/已公布 after_end/instant → 显示;report_only 或未公布 → 隐藏并提示"解析暂未开放");多选显示组合答案、判断显示对/错、编程题显示提交片段(截断)+ 通过状态。


11. 升级对现有功能的改造点清单

  1. index.html:昵称登录框移除 → 登录卡(含注册/认领入口);考生标识显示账号姓名;答题逻辑不变;
  2. main.py 认证中间件:新增 require_user 依赖,/api/join/api/submit/api/buzz/api/current(name) 等从 Cookie 取账号,服务端按账号自动 upsert players(一账号一条),所有管理 API 维持现状(见确认清单 Q1)
  3. 实时排行榜显示账号名 + 本场得分;/api/admin/clear 语义改为"放弃本场"(不清 users);
  4. /admin 页签化整合:现有面板并入"现场控制",新增账号/用户/统计页签,/qbank 作为题库子入口保留独立页;
  5. 结算动作:现场控制新增【结束本场并结算】→ 生成 sessions + session_users + 迁移 answers 明细到 attempt_itemsplayers.score 清零;
  6. 打印:教师卷/考生卷随解析策略(教师卷始终带解析)。

12. 分阶段实施计划(评审通过后按此排期)

阶段 内容 验证点
M1 用户体系 users/import_batches 表、密码哈希、会话 Cookie、注册/登录/退出 API + 页面、字段查重、锁定策略 冒烟脚本:注册→登录→错 5 次锁定→解锁
M2 账号认领 认领页 + 后端匹配/改密/失败处理、待认领拦截登录 导入样例数据 → 认领成功/各失败分支
M3 导入 CSV/xlsx 解析、模板下载、两步确认、批次、失败明细、初始密码导出 三种表头样例 + 冲突/非法行用例
M4 场次与记录 sessions 系建表、结算按钮、exam 整卷作答页、/me 记录与明细、解析策略下发 实时场结算 + 测验场全流程 → 记录页核对
M5 统计与用户管理 指标 SQL、报告页与 SVG 图、各题表现表、用户列表/筛选/下钻、CSV 导出 数据核对:手算 vs 页面指标
M6 整合与回归 admin 页签化、导航、旧数据迁移脚本、readme 更新 全链路回归 + 双模式并存验证

13. 待确认清单(请逐条勾选或修改)

  • Q1 主持人后台 /admin 是否需要登录/口令?(现状:局域网内任何人可开,添加"主持人口令"为可选加固)
  • Q2 注册开关默认值?(建议:默认关闭,导入为主)
  • Q3 待认领账号初始密码若留空 → 自动生成并一次性导出,是否接受?(或要求管理员必填密码列)
  • Q4 登录账号与实时玩法的"参赛名"关系:直接用账号姓名显示,还是允许每场另取昵称?(建议:直接用账号姓名,排行榜简洁)
  • Q5 整卷测验场默认题目来源:从"组卷"发起 还是 从题库按分类/标签筛选抽题?满分=100 按组卷分自动归一?
  • Q6 及格线默认 60 分(可场次级修改)?
  • Q7 历史 A/B/C/D 单选(questions 表)是否同意迁移脚本并入 qb_questions(迁移后解析为空需补录或批量标注"缺解析")?
  • Q8 验证码通道:默认调试通道(管理端代收面板)是否可接受?邮件通道是否需要一并做 SMTP 配置?
  • Q9 实时场结算时机:以主持人点【结束本场并结算】为准(建议),还是每题公布后自动按整场归档?
  • Q10 导入时发现与库内用户手机/邮箱冲突的行:默认跳过还是可选手动合并?

以上各条均有默认建议值,全部按默认执行也可;请在回复中标注例外项(如"Q2 改为开启")。