diff --git a/02_K210识别端文档.md b/02_K210识别端文档.md index d3f1b8a..96b39e2 100644 --- a/02_K210识别端文档.md +++ b/02_K210识别端文档.md @@ -158,7 +158,7 @@ UNKNOWN 4. 再短按确认添加。 5. K210 分配一个未使用的 `face_id`,例如 `1`。 6. 特征写入 `/sd/recordftr.txt`,名称为 `ID:1`。 -7. 当前画面压缩为 JPEG。 +7. 裁剪人脸框附近区域并压缩为 JPEG,避免整帧压缩导致 K210 内存峰值过高。 8. JPEG 保存到 `/sd/face_photos/1.jpg`。 9. `1` 加入照片上传队列。 10. 后续网络任务会上传到服务端。 diff --git a/数据库结构详细文档.md b/数据库结构详细文档.md new file mode 100644 index 0000000..91d421c --- /dev/null +++ b/数据库结构详细文档.md @@ -0,0 +1,737 @@ +# 智慧课堂考勤系统数据库结构详细文档 + +## 1. 文档说明 + +本文档说明智慧课堂考勤系统服务端的 SQLite 数据库结构、字段含义、表关系、同步机制以及主要业务数据流。数据库结构来自服务端源码 `code/server/server_encrypted.cpp` 中的初始化逻辑和业务访问逻辑。 + +系统服务端承担三类核心数据职责: + +1. 保存 STM32 上传的考勤签到记录。 +2. 保存 Qt 管理端维护的班级、学生、人脸绑定和时间段配置。 +3. 保存 K210 人脸识别端的人脸照片索引、删除任务和三端同步状态。 + +## 2. 数据库总体设计 + +### 2.1 数据库类型 + +系统使用 SQLite 作为嵌入式数据库,默认数据库文件为: + +```text +attendance.db +``` + +服务端在启动时会自动执行数据库初始化。所有建表语句均使用 `CREATE TABLE IF NOT EXISTS`,因此重复启动服务端不会删除或覆盖已有数据。 + +### 2.2 运行时存储文件 + +| 路径 | 类型 | 作用 | +| --- | --- | --- | +| `attendance.db` | SQLite 数据库 | 保存考勤、学生、班级、人脸档案、配置、同步状态等结构化数据 | +| `faces/` | 文件目录 | 保存 K210 上传的人脸照片,命名格式为 `faces/.jpg` | +| `backups/` | 文件目录 | 保存服务端启动前生成的数据库备份 | +| `attendance.log` | 日志文件名 | 源码中预留,当前主要使用控制台输出运行日志 | + +### 2.3 初始化策略 + +服务端启动时执行以下初始化动作: + +1. 打开 `attendance.db`。 +2. 设置 `PRAGMA journal_mode=WAL`,提升读写并发能力。 +3. 创建业务表和索引。 +4. 初始化 `kv_store` 中的默认配置。 +5. 创建默认班级 `class_id = 0`,名称为 `默认班级`。 +6. 创建 `faces/` 目录。 + +默认配置项如下: + +| key | 默认值 | 说明 | +| --- | --- | --- | +| `time_slots` | `{"time_slots":[]}` | 默认无签到时间段配置 | +| `sync_request` | `{"sync":false}` | 默认不主动请求 K210 同步 | +| `sync_version` | `1` | 全局同步版本号初始值 | + +## 3. 表结构总览 + +| 表名 | 中文含义 | 主要用途 | +| --- | --- | --- | +| `records` | 考勤记录表 | 保存每一次签到上传结果 | +| `kv_store` | 键值配置表 | 保存时间段配置、同步请求标志、同步版本号 | +| `classes` | 班级信息表 | 保存班级编号和班级名称 | +| `students` | 学生档案表 | 保存学生基础信息和软删除状态 | +| `face_profiles` | 人脸档案表 | 保存 K210 `face_id` 与真实学号的绑定关系 | +| `face_photos` | 人脸照片索引表 | 保存人脸照片文件路径和文件元数据 | +| `pending_delete` | 待删除人脸任务表 | 保存等待 K210 本地删除的人脸任务 | +| `device_sync_state` | 设备同步状态表 | 保存 K210、STM32、Qt 等客户端的同步进度 | + +## 4. 详细表结构 + +### 4.1 `records` 考勤记录表 + +`records` 用于保存每一次签到结果。STM32 通过 `/api/v1/attendance/checkins` 上传签到数据后,服务端写入该表。 + +建表结构: + +```sql +CREATE TABLE IF NOT EXISTS records( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp TEXT NOT NULL, + student_id TEXT NOT NULL, + name TEXT, + confidence TEXT, + status TEXT, + class_id TEXT DEFAULT '0' +); +``` + +字段说明: + +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| `id` | INTEGER | 主键,自增 | 考勤记录内部编号,用于排序和唯一标识 | +| `timestamp` | TEXT | 非空 | 签到时间,格式通常为 `YYYY-MM-DD HH:MM:SS` | +| `student_id` | TEXT | 非空 | 学号,也可能是由人脸编号映射后的学生编号 | +| `name` | TEXT | 可空 | 学生姓名或终端上传的显示名称 | +| `confidence` | TEXT | 可空 | 人脸识别置信度,业务校验范围为 `0.0` 到 `1.0` | +| `status` | TEXT | 可空 | 签到状态,服务端接受 `success`、`failed`、`late` | +| `class_id` | TEXT | 默认 `0` | 班级编号,空值会归入默认班级 | + +索引: + +```sql +CREATE INDEX IF NOT EXISTS idx_records_student_time +ON records(student_id,timestamp); + +CREATE INDEX IF NOT EXISTS idx_records_timestamp +ON records(timestamp); +``` + +索引用途: + +| 索引 | 用途 | +| --- | --- | +| `idx_records_student_time` | 加速按学生和时间查询重复签到、个人记录 | +| `idx_records_timestamp` | 加速按日期筛选今日记录和统计 | + +主要写入场景: + +1. STM32 上传签到记录。 +2. 服务端校验 `student_id`、`class_id`、`confidence`、`status` 等字段。 +3. 写入 `records`。 +4. 自动调用学生档案 upsert 逻辑,保证 `students` 中存在该学生。 +5. 调用 `bumpSyncVersion("checkin")` 推进同步版本,Qt 管理端可感知记录变化。 + +主要读取场景: + +| 接口 | 说明 | +| --- | --- | +| `GET /api/v1/attendance/records` | 分页读取考勤记录,按 `id DESC` 返回最新记录 | +| `GET /api/v1/attendance/statistics` | 统计总记录数和今日记录数 | +| `GET /api/v1/attendance/duplicates` | 检查指定学生在某时间段内是否已经签到 | +| `GET /api/v1/attendance/today` | 返回今日指定时间段内已签到学生列表 | + +### 4.2 `kv_store` 键值配置表 + +`kv_store` 是轻量级配置表,用于保存不适合单独建表的小型全局状态。 + +建表结构: + +```sql +CREATE TABLE IF NOT EXISTS kv_store( + key TEXT PRIMARY KEY, + value TEXT NOT NULL +); +``` + +字段说明: + +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| `key` | TEXT | 主键 | 配置项名称 | +| `value` | TEXT | 非空 | 配置项内容,通常为 JSON 字符串或数字字符串 | + +当前核心键: + +| key | value 示例 | 说明 | +| --- | --- | --- | +| `time_slots` | `{"time_slots":[...]}` | 签到时间段配置,STM32 和 Qt 会同步使用 | +| `sync_request` | `{"sync":true}` | 是否请求 K210 尽快执行同步 | +| `sync_version` | `12` | 全局同步版本号 | + +业务说明: + +1. `time_slots` 由 Qt 管理端维护,STM32 根据该配置判断是否处于可签到时间段。 +2. `sync_request` 是给 K210 轮询的轻量标志。管理端保存人脸档案或删除人脸后会置为 `true`。 +3. `sync_version` 是统一同步机制的核心版本号,所有影响端侧状态的操作都会递增该值。 + +### 4.3 `classes` 班级信息表 + +`classes` 保存班级基础信息。系统默认创建编号为 `0` 的默认班级。 + +建表结构: + +```sql +CREATE TABLE IF NOT EXISTS classes( + class_id TEXT PRIMARY KEY, + class_name TEXT NOT NULL, + updated_at TEXT NOT NULL +); +``` + +字段说明: + +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| `class_id` | TEXT | 主键 | 班级编号,空值会归一化为 `0` | +| `class_name` | TEXT | 非空 | 班级名称 | +| `updated_at` | TEXT | 非空 | 最近更新时间 | + +业务规则: + +1. 空班级编号统一归入 `0`。 +2. `class_id = 0` 的班级名称默认为 `默认班级`。 +3. 保存学生时,如果传入新的 `class_id`,服务端会先确保班级存在。 +4. 删除班级前会检查该班级下是否仍有未删除学生;如果存在 active 学生,则拒绝删除该班级。 + +主要接口: + +| 接口 | 说明 | +| --- | --- | +| `GET /api/v1/classes` | 读取班级列表 | +| `POST /api/v1/classes` | 保存班级配置 | + +### 4.4 `students` 学生档案表 + +`students` 保存学生基础档案。它支持软删除,避免直接删除造成历史考勤记录无法解释。 + +建表结构: + +```sql +CREATE TABLE IF NOT EXISTS students( + student_id TEXT PRIMARY KEY, + student_name TEXT, + class_id TEXT DEFAULT '0', + status TEXT NOT NULL DEFAULT 'active', + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); +``` + +字段说明: + +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| `student_id` | TEXT | 主键 | 学号 | +| `student_name` | TEXT | 可空 | 学生姓名 | +| `class_id` | TEXT | 默认 `0` | 所属班级编号 | +| `status` | TEXT | 非空,默认 `active` | 学生状态,主要包括 `active`、`deleted` | +| `created_at` | TEXT | 非空 | 创建时间 | +| `updated_at` | TEXT | 非空 | 最近更新时间 | + +索引: + +```sql +CREATE INDEX IF NOT EXISTS idx_students_class +ON students(class_id); +``` + +业务规则: + +1. 管理端保存学生列表时,服务端使用事务批量写入。 +2. 学号必须满足安全 ID 校验。 +3. 姓名长度限制为 64 字符以内。 +4. 本次上传列表中不存在的旧学生会被标记为 `deleted`,不是物理删除。 +5. 学生被软删除后,绑定到该学生的人脸档案会解除 `student_id` 绑定。 +6. 考勤上传时若学生不存在,服务端会自动创建基础学生档案。 + +主要接口: + +| 接口 | 说明 | +| --- | --- | +| `GET /api/v1/students` | 读取未被软删除的学生列表 | +| `POST /api/v1/students` | 保存学生档案列表 | + +### 4.5 `face_profiles` 人脸档案表 + +`face_profiles` 是人脸识别业务中的核心关系表,用于维护 K210 原始人脸编号与真实学生学号之间的绑定关系。 + +建表结构: + +```sql +CREATE TABLE IF NOT EXISTS face_profiles( + face_id TEXT PRIMARY KEY, + student_id TEXT, + device_id TEXT DEFAULT 'k210-01', + status TEXT NOT NULL DEFAULT 'active', + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); +``` + +字段说明: + +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| `face_id` | TEXT | 主键 | K210 本地生成的原始人脸编号 | +| `student_id` | TEXT | 可空 | 绑定的真实学号,空值表示未绑定 | +| `device_id` | TEXT | 默认 `k210-01` | 来源识别设备编号 | +| `status` | TEXT | 非空,默认 `active` | 人脸档案状态 | +| `created_at` | TEXT | 非空 | 创建时间 | +| `updated_at` | TEXT | 非空 | 最近更新时间 | + +索引: + +```sql +CREATE INDEX IF NOT EXISTS idx_face_profiles_student +ON face_profiles(student_id); +``` + +业务规则: + +1. `face_id` 始终保持为 K210 原始编号,不因学生学号变化而改变。 +2. 绑定学生时只更新 `student_id`,K210 本地特征文件无需重写。 +3. `student_id` 为空表示人脸照片存在但尚未绑定学生。 +4. 管理端提交人脸绑定时,若 `student_id` 非空,服务端会确保 `students` 中存在该学生。 +5. 管理端上传的人脸列表中未出现的旧 `face_id` 不会被删除,只会解除绑定,避免误删 K210 本地特征。 +6. 如果照片仍存在但解除绑定,状态保持 `active`;如果照片不存在,则可能标记为 `inactive` 或 `unbound`。 + +与照片的合并读取规则: + +读取人脸档案时,服务端会合并 `face_profiles` 和 `face_photos`: + +1. 同时有档案和照片的 `face_id` 正常返回。 +2. 只有照片但无档案的 `face_id` 以 `status = photo_only` 返回。 +3. 返回前会尝试修复历史数据中“档案 `face_id` 与照片 `face_id` 不一致”的情况。 + +主要接口: + +| 接口 | 说明 | +| --- | --- | +| `GET /api/v1/face_profiles` | 读取人脸档案及照片状态 | +| `POST /api/v1/face_profiles` | 保存人脸与学生绑定关系 | + +### 4.6 `face_photos` 人脸照片索引表 + +`face_photos` 保存照片文件元数据,实际 JPEG 图片保存在 `faces/` 目录。 + +建表结构: + +```sql +CREATE TABLE IF NOT EXISTS face_photos( + face_id TEXT PRIMARY KEY, + file_path TEXT NOT NULL, + content_type TEXT DEFAULT 'image/jpeg', + file_size INTEGER DEFAULT 0, + uploaded_at TEXT NOT NULL +); +``` + +字段说明: + +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| `face_id` | TEXT | 主键 | K210 原始人脸编号 | +| `file_path` | TEXT | 非空 | 服务端本地照片路径 | +| `content_type` | TEXT | 默认 `image/jpeg` | 图片 MIME 类型 | +| `file_size` | INTEGER | 默认 `0` | 图片文件大小,单位字节 | +| `uploaded_at` | TEXT | 非空 | 上传时间 | + +业务规则: + +1. K210 上传照片时,请求路径中携带 `face_id`,请求体为 JPEG 二进制。 +2. 服务端将照片保存为 `faces/.jpg`。 +3. 照片保存成功后使用 `REPLACE INTO face_photos` 更新索引。 +4. 照片上传成功后,服务端会确保 `face_profiles` 中至少存在一条同 `face_id` 的 active 档案。 +5. 如果该 `face_id` 曾经存在待删除任务,重新上传照片会取消旧删除任务,避免 K210 后续误删。 + +主要接口: + +| 接口 | 说明 | +| --- | --- | +| `GET /api/v1/faces/photos` | 获取人脸照片列表 | +| `GET /api/v1/faces/photos/` | 获取指定人脸照片 | +| `POST /api/v1/faces/photos/` | 上传指定人脸照片 | + +### 4.7 `pending_delete` 待删除人脸任务表 + +`pending_delete` 用于保存等待 K210 执行的远程删除任务。由于服务端无法直接删除 K210 SD 卡中的人脸特征,因此采用“服务端登记任务,K210 同步后删除并 ACK”的模式。 + +建表结构: + +```sql +CREATE TABLE IF NOT EXISTS pending_delete( + task_id TEXT PRIMARY KEY, + face_id TEXT NOT NULL, + student_id TEXT, + created_at TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + ack_at TEXT +); +``` + +字段说明: + +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| `task_id` | TEXT | 主键 | 删除任务编号,格式通常为 `-` | +| `face_id` | TEXT | 非空 | K210 需要删除的本地人脸编号 | +| `student_id` | TEXT | 可空 | 关联学生学号或管理端请求删除的 ID | +| `created_at` | TEXT | 非空 | 任务创建时间 | +| `status` | TEXT | 非空,默认 `pending` | 删除任务状态,主要为 `pending`、`acked` | +| `ack_at` | TEXT | 可空 | K210 确认删除的时间 | + +索引: + +```sql +CREATE INDEX IF NOT EXISTS idx_pending_delete_face +ON pending_delete(face_id); +``` + +业务规则: + +1. 管理端删除人脸时,服务端删除照片索引并解除绑定,然后写入 `pending_delete`。 +2. K210 通过统一同步接口获取 `delete_tasks`。 +3. K210 删除本地特征和照片后调用 `/api/v1/sync/ack` 确认。 +4. 服务端收到 ACK 后不会物理删除任务,而是将 `status` 标记为 `acked` 并写入 `ack_at`,保留审计信息。 +5. 如果同一 `face_id` 后续重新上传照片或重新绑定,服务端会将旧的未确认删除任务标记为 `acked`,防止误删。 + +### 4.8 `device_sync_state` 设备同步状态表 + +`device_sync_state` 保存各客户端同步版本进度,便于服务端存储状态接口展示每个端是否已跟上最新版本。 + +建表结构: + +```sql +CREATE TABLE IF NOT EXISTS device_sync_state( + client TEXT PRIMARY KEY, + last_seen_version INTEGER NOT NULL DEFAULT 0, + last_ack_version INTEGER NOT NULL DEFAULT 0, + last_seen_at TEXT NOT NULL +); +``` + +字段说明: + +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| `client` | TEXT | 主键 | 客户端名称,例如 `k210`、`stm32`、`qt`、`all` | +| `last_seen_version` | INTEGER | 非空,默认 `0` | 客户端最近拉取到的服务端版本 | +| `last_ack_version` | INTEGER | 非空,默认 `0` | 客户端最近确认完成处理的版本 | +| `last_seen_at` | TEXT | 非空 | 最近一次访问或同步时间 | + +业务规则: + +1. 客户端访问 `/api/v1/sync/state` 时更新 `last_seen_version`。 +2. 客户端访问 `/api/v1/sync/ack` 时更新 `last_ack_version`。 +3. 更新时使用 `MAX` 保留更高版本,防止旧请求覆盖新状态。 +4. `/api/v1/system/storage` 会返回该表摘要,辅助判断三端同步是否正常。 + +## 5. 表关系说明 + +系统没有在 SQLite 层显式声明外键,但业务逻辑中维护了以下关系。 + +### 5.1 班级与学生 + +```text +classes.class_id 1 ---- N students.class_id +``` + +说明: + +1. 一个班级可包含多个学生。 +2. 学生保存时会确保对应班级存在。 +3. 删除班级时必须保证该班级下没有 active 学生。 + +### 5.2 学生与考勤记录 + +```text +students.student_id 1 ---- N records.student_id +``` + +说明: + +1. 一个学生可以有多条考勤记录。 +2. 考勤记录中的 `student_id` 保存签到时上报的学生编号。 +3. 学生软删除不会清理历史考勤记录,保证统计和追溯仍可进行。 + +### 5.3 学生与人脸档案 + +```text +students.student_id 1 ---- N face_profiles.student_id +``` + +说明: + +1. 一个学生理论上可以绑定多个 `face_id`。 +2. 人脸档案允许 `student_id` 为空,表示只有人脸照片或特征但尚未绑定学生。 +3. 删除学生档案时,会解除相关人脸绑定。 + +### 5.4 人脸档案与照片索引 + +```text +face_profiles.face_id 1 ---- 0/1 face_photos.face_id +``` + +说明: + +1. `face_profiles` 维护识别身份和学生绑定。 +2. `face_photos` 维护照片文件路径和元数据。 +3. 可能存在只有照片、尚未绑定学生的记录,此时读取接口返回 `status = photo_only`。 +4. 删除照片不会立即删除 K210 本地特征,需要通过 `pending_delete` 完成端侧删除。 + +### 5.5 人脸档案与删除任务 + +```text +face_profiles.face_id 1 ---- N pending_delete.face_id +``` + +说明: + +1. 同一人脸可能在不同时间产生多个删除任务。 +2. 新版同步使用 `task_id` 精确 ACK,避免只按 `face_id` 确认造成历史任务混淆。 +3. 旧版兼容逻辑仍允许按 `face_id` ACK。 + +## 6. 统一同步版本机制 + +### 6.1 设计目标 + +系统包含 K210、STM32、Qt 管理端三个客户端。它们关注的数据不同: + +| 客户端 | 关注数据 | +| --- | --- | +| K210 | 人脸绑定关系、待删除人脸任务 | +| STM32 | 签到时间段配置、今日签到数据、重复签到检查 | +| Qt 管理端 | 考勤记录、统计数据、学生、班级、人脸档案、照片列表 | + +为了避免每个端维护独立同步逻辑,服务端通过 `kv_store.sync_version` 建立统一版本流。只要某个操作影响终端可见状态,就调用 `bumpSyncVersion(reason)` 递增版本号。 + +### 6.2 版本递增场景 + +| 操作 | reason 示例 | 影响 | +| --- | --- | --- | +| STM32 上传签到 | `checkin` | Qt 刷新记录和统计 | +| 清空考勤记录 | `clear_records` | Qt 刷新记录 | +| 删除单条考勤 | `delete_record` | Qt 刷新记录 | +| 修改时间段 | `timeslots` | STM32、Qt 同步时间段 | +| 保存学生档案 | `students` | Qt、K210 相关绑定状态可能变化 | +| 保存班级配置 | `classes` | Qt 刷新班级和学生展示 | +| 保存人脸绑定 | `face_profiles` | K210 同步 face_id 到 student_id 映射 | +| 上传人脸照片 | `face_photo` | Qt 展示照片,人脸档案状态更新 | +| 删除人脸 | `delete_face` | K210 获取删除任务 | +| K210 确认删除任务 | `sync_ack_delete` | 删除任务状态变化 | +| 人脸档案自动修复 | `face_profile_repair` | 同步修复后的绑定状态 | + +### 6.3 同步接口 + +| 接口 | 作用 | +| --- | --- | +| `GET /api/v1/sync/version` | 获取当前全局同步版本 | +| `GET /api/v1/sync/state?client=&last_version=` | 按客户端类型获取需要同步的数据 | +| `POST /api/v1/sync/ack` | 客户端确认已处理版本或删除任务 | +| `GET /api/v1/sync/request` | K210 轮询是否需要主动同步 | +| `POST /api/v1/sync/request/ack` | K210 确认收到同步请求并清除标志 | + +### 6.4 `sync/state` 返回数据差异 + +当客户端传入的 `last_version` 小于服务端 `sync_version` 时,服务端返回 `changed = true` 和对应状态数据。 + +| client | 返回内容 | +| --- | --- | +| `k210` | `face_profiles`、`pending_delete`、`delete_tasks` | +| `stm32` | `time_slots` | +| `qt` | `time_slots`、`students`、`classes`、`face_profiles`、`face_photos`、`records`、`statistics` | +| `all` 或空 | 返回全部可同步状态 | + +## 7. 主要业务数据流 + +### 7.1 签到记录写入流程 + +```text +STM32 -> POST /api/v1/attendance/checkins + -> 服务端校验字段 + -> records 写入考勤记录 + -> students upsert 学生基础档案 + -> sync_version + 1 + -> Qt 管理端刷新记录和统计 +``` + +关键校验: + +| 字段 | 校验规则 | +| --- | --- | +| `student_id` | 安全 ID,长度不超过 32 | +| `name` | 长度不超过 64 | +| `class_id` | 安全 ID,长度不超过 32 | +| `confidence` | 数值,范围 `0.0` 到 `1.0` | +| `status` | 只允许 `success`、`failed`、`late` | + +### 7.2 学生档案保存流程 + +```text +Qt -> POST /api/v1/students + -> 解析 students 数组 + -> BEGIN 事务 + -> upsert students + -> 自动确保 classes 存在 + -> 将未出现在本次提交中的旧学生标记为 deleted + -> 解除 deleted 学生的人脸绑定 + -> COMMIT + -> sync_version + 1 +``` + +### 7.3 人脸照片上传流程 + +```text +K210 -> POST /api/v1/faces/photos/ + -> 保存 JPEG 到 faces/.jpg + -> REPLACE INTO face_photos + -> 确保 face_profiles 存在同 face_id 档案 + -> 取消该 face_id 旧 pending_delete 任务 + -> sync_version + 1 +``` + +### 7.4 人脸绑定保存流程 + +```text +Qt -> POST /api/v1/face_profiles + -> 解析 face_profiles 数组 + -> BEGIN 事务 + -> 校验 face_id 和 student_id + -> 若绑定学生,确保对应学生档案存在 + -> 更新 face_profiles.student_id + -> 未出现在本次提交中的旧 face_id 解除绑定 + -> COMMIT + -> sync_version + 1 + -> sync_request = true + -> K210 下次同步映射关系 +``` + +### 7.5 删除人脸流程 + +```text +Qt -> DELETE /api/v1/faces/ + -> 查找需要删除的 face_id + -> 删除服务端照片索引和照片文件 + -> 解除 face_profiles 绑定 + -> 写入 pending_delete + -> sync_version + 1 + -> sync_request = true + +K210 -> GET /api/v1/sync/state?client=k210&last_version=... + -> 获取 delete_tasks + -> 删除本地特征和照片 + -> POST /api/v1/sync/ack + -> 服务端将 pending_delete 标记为 acked +``` + +## 8. 数据一致性设计 + +### 8.1 事务使用 + +以下批量写入使用 SQLite 事务: + +| 操作 | 事务目的 | +| --- | --- | +| 保存学生档案 | 避免部分学生更新成功、部分失败 | +| 保存班级配置 | 避免班级删除和更新状态不一致 | +| 保存人脸绑定 | 避免人脸绑定关系半更新 | + +失败时执行 `ROLLBACK`,成功时执行 `COMMIT`。 + +### 8.2 软删除策略 + +学生使用软删除: + +```text +students.status = 'deleted' +``` + +优势: + +1. 保留历史考勤记录可解释性。 +2. 避免物理删除导致统计和导出数据断裂。 +3. 管理端读取学生列表时过滤 `deleted` 状态即可。 + +### 8.3 人脸删除的异步确认 + +人脸删除不是同步完成,而是拆成两步: + +1. 服务端删除照片和绑定,写入 `pending_delete`。 +2. K210 同步后删除本地特征,再 ACK 服务端。 + +这样设计可以避免服务端误以为 K210 已删除,但实际终端离线导致本地特征仍存在的问题。 + +### 8.4 WAL 模式 + +服务端初始化时执行: + +```sql +PRAGMA journal_mode=WAL; +``` + +WAL 模式可以让管理端读取、STM32 写入、K210 同步查询在并发情况下更平滑,减少数据库锁冲突。 + +## 9. 常用维护与检查 + +### 9.1 查看数据库表 + +```sql +.tables +``` + +### 9.2 查看表结构 + +```sql +.schema records +.schema students +.schema face_profiles +``` + +### 9.3 检查数据库完整性 + +```sql +PRAGMA quick_check; +``` + +服务端 `/api/v1/system/health` 和 `/api/v1/system/storage` 接口也会执行或返回类似检查结果。 + +### 9.4 查看当前同步版本 + +```sql +SELECT value FROM kv_store WHERE key='sync_version'; +``` + +也可以请求: + +```text +GET /api/v1/sync/version +``` + +### 9.5 查看未确认删除任务 + +```sql +SELECT task_id, face_id, student_id, created_at, status, ack_at +FROM pending_delete +WHERE status <> 'acked' +ORDER BY created_at, face_id; +``` + +### 9.6 查看设备同步进度 + +```sql +SELECT client, last_seen_version, last_ack_version, last_seen_at +FROM device_sync_state +ORDER BY client; +``` + +## 10. 数据库设计特点总结 + +本系统数据库结构围绕“考勤记录可追溯、人脸身份可重新绑定、端侧同步可确认”三个目标设计: + +1. `records` 保留每次签到的原始记录,支持统计、导出和重复签到检查。 +2. `students` 与 `classes` 维护管理端基础档案,学生删除采用软删除以保护历史数据。 +3. `face_profiles` 将 K210 原始 `face_id` 与真实学号解耦,避免学号变化影响 K210 本地特征库。 +4. `face_photos` 只保存照片元数据,真实 JPEG 文件保存在 `faces/` 目录,降低数据库体积。 +5. `pending_delete` 使用任务队列和 ACK 机制解决 K210 离线或延迟同步问题。 +6. `kv_store.sync_version` 将三端同步统一到同一版本流,减少各端重复设计同步状态。 +7. `device_sync_state` 提供同步可观测性,便于管理端或维护人员判断设备是否已经同步到最新版本。 +