diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..af62a96
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,58 @@
+# ============================
+# Java / Maven
+# ============================
+target/
+*.class
+*.jar
+*.war
+*.log
+*.logs
+
+# ============================
+# IDE
+# ============================
+.idea/
+*.iml
+.vscode/
+.settings/
+.project
+.classpath
+.factorypath
+*.swp
+*.swo
+*~
+
+# ============================
+# OS
+# ============================
+.DS_Store
+Thumbs.db
+Desktop.ini
+
+# ============================
+# Logs
+# ============================
+logs/
+*.log
+*.log.*
+
+# ============================
+# Temp / Cache
+# ============================
+*.tmp
+*.bak
+*.cache
+
+# ============================
+# Environment / Sensitive
+# ============================
+.env
+.env.local
+application-local.yml
+application-dev.yml
+
+# ============================
+# Upload / Data
+# ============================
+upload/
+data/
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..27ef4be
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,126 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.7.18
+
+
+
+ com.attendance
+ attendance-system-server
+ 1.0.0
+ jar
+ attendance-system-server
+ 教室智能人脸考勤系统后端服务
+
+
+ 1.8
+ 3.5.5
+ 4.4.0
+ 4.4.0
+ 2.0.49
+ 8.5.7
+
+
+
+
+
+
+ cn.hutool
+ hutool-all
+ 5.8.16
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.springframework.boot
+ spring-boot-starter-aop
+
+
+
+
+ mysql
+ mysql-connector-java
+ 8.0.33
+ runtime
+
+
+ com.baomidou
+ mybatis-plus-boot-starter
+ ${mybatis-plus.version}
+
+
+
+
+ com.github.xiaoymin
+ knife4j-openapi3-spring-boot-starter
+ ${knife4j.version}
+
+
+
+
+ com.auth0
+ java-jwt
+ ${jwt.version}
+
+
+
+
+ com.alibaba.fastjson2
+ fastjson2
+ ${fastjson2.version}
+
+
+
+
+ io.minio
+ minio
+ ${minio.version}
+
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+
diff --git a/sql/attendance_system.sql b/sql/attendance_system.sql
new file mode 100644
index 0000000..4eb4c48
--- /dev/null
+++ b/sql/attendance_system.sql
@@ -0,0 +1,756 @@
+/*
+ Navicat Premium Dump SQL
+
+ Source Server : JavaProjects
+ Source Server Type : MySQL
+ Source Server Version : 80040 (8.0.40)
+ Source Host : localhost:3306
+ Source Schema : attendance_system
+
+ Target Server Type : MySQL
+ Target Server Version : 80040 (8.0.40)
+ File Encoding : 65001
+
+ Date: 04/06/2026 11:38:13
+*/
+
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- ----------------------------
+-- Table structure for att_detail
+-- ----------------------------
+DROP TABLE IF EXISTS `att_detail`;
+CREATE TABLE `att_detail` (
+ `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `task_id` bigint UNSIGNED NOT NULL COMMENT '考勤任务ID',
+ `record_id` bigint UNSIGNED NOT NULL COMMENT '考勤记录ID',
+ `student_id` bigint UNSIGNED NOT NULL COMMENT '学生ID',
+ `student_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '学号',
+ `student_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '学生姓名',
+ `course_id` bigint UNSIGNED NOT NULL COMMENT '课程ID',
+ `classroom_id` bigint UNSIGNED NOT NULL COMMENT '教室ID',
+ `att_date` date NOT NULL COMMENT '考勤日期',
+ `check_in_time` datetime NULL DEFAULT NULL COMMENT '签到时间',
+ `check_out_time` datetime NULL DEFAULT NULL COMMENT '签退时间',
+ `att_status` tinyint NOT NULL DEFAULT 0 COMMENT '考勤状态:0-未签到 1-正常 2-迟到 3-缺勤 4-早退 5-请假',
+ `check_type` tinyint NOT NULL DEFAULT 1 COMMENT '签到方式:1-人脸识别 2-手动补签',
+ `face_similarity` decimal(5, 2) NULL DEFAULT NULL COMMENT '人脸相似度',
+ `face_image` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '抓拍人脸图片URL',
+ `device_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '签到设备ID',
+ `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
+ `school_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '所属学校ID',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ `deleted` tinyint NULL DEFAULT 0,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_task_student`(`task_id` ASC, `student_id` ASC) USING BTREE,
+ INDEX `idx_record_id`(`record_id` ASC) USING BTREE,
+ INDEX `idx_student_id`(`student_id` ASC) USING BTREE,
+ INDEX `idx_att_status`(`att_status` ASC) USING BTREE,
+ INDEX `idx_att_date`(`att_date` ASC) USING BTREE,
+ INDEX `idx_course_id`(`course_id` ASC) USING BTREE,
+ INDEX `idx_school_id`(`school_id` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '考勤明细表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of att_detail
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for att_record
+-- ----------------------------
+DROP TABLE IF EXISTS `att_record`;
+CREATE TABLE `att_record` (
+ `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `task_id` bigint UNSIGNED NOT NULL COMMENT '考勤任务ID',
+ `course_id` bigint UNSIGNED NOT NULL COMMENT '课程ID',
+ `classroom_id` bigint UNSIGNED NOT NULL COMMENT '教室ID',
+ `teacher_id` bigint UNSIGNED NOT NULL COMMENT '教师ID',
+ `att_date` date NOT NULL COMMENT '考勤日期',
+ `start_time` datetime NOT NULL COMMENT '开始时间',
+ `end_time` datetime NOT NULL COMMENT '结束时间',
+ `total_count` int NOT NULL DEFAULT 0 COMMENT '应到人数',
+ `actual_count` int NOT NULL DEFAULT 0 COMMENT '实到人数',
+ `absent_count` int NOT NULL DEFAULT 0 COMMENT '缺勤人数',
+ `late_count` int NOT NULL DEFAULT 0 COMMENT '迟到人数',
+ `leave_early_count` int NOT NULL DEFAULT 0 COMMENT '早退人数',
+ `attendance_rate` decimal(5, 2) NULL DEFAULT NULL COMMENT '出勤率%',
+ `absent_rate` decimal(5, 2) NULL DEFAULT NULL COMMENT '缺勤率%',
+ `record_status` tinyint NOT NULL DEFAULT 0 COMMENT '记录状态:0-正常 1-异常',
+ `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
+ `school_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '所属学校ID',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ `deleted` tinyint NULL DEFAULT 0,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_task_id`(`task_id` ASC) USING BTREE,
+ INDEX `idx_course_id`(`course_id` ASC) USING BTREE,
+ INDEX `idx_classroom_id`(`classroom_id` ASC) USING BTREE,
+ INDEX `idx_att_date`(`att_date` ASC) USING BTREE,
+ INDEX `idx_school_id`(`school_id` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '考勤记录汇总表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of att_record
+-- ----------------------------
+INSERT INTO `att_record` VALUES (1, 1, 1, 1, 4, '2024-06-01', '2024-06-01 08:00:00', '2024-06-01 09:40:00', 45, 44, 1, 0, 0, 97.78, 2.22, 0, NULL, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `att_record` VALUES (2, 2, 2, 2, 2, '2024-06-01', '2024-06-01 10:00:00', '2024-06-01 11:40:00', 38, 38, 0, 0, 0, 100.00, 0.00, 0, NULL, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `att_record` VALUES (3, 3, 3, 3, 3, '2024-06-01', '2024-06-01 14:00:00', '2024-06-01 15:40:00', 52, 49, 3, 0, 0, 94.23, 5.77, 0, NULL, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `att_record` VALUES (4, 4, 4, 4, 4, '2024-05-31', '2024-05-31 08:00:00', '2024-05-31 09:40:00', 40, 38, 2, 0, 0, 95.00, 5.00, 0, NULL, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `att_record` VALUES (5, 5, 5, 5, 5, '2024-05-31', '2024-05-31 10:00:00', '2024-05-31 11:40:00', 120, 108, 12, 0, 0, 90.00, 10.00, 0, NULL, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+
+-- ----------------------------
+-- Table structure for att_rule
+-- ----------------------------
+DROP TABLE IF EXISTS `att_rule`;
+CREATE TABLE `att_rule` (
+ `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `rule_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '规则名称',
+ `rule_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '规则类型:course-课程/room-教室/global-全局',
+ `target_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '关联目标ID',
+ `check_in_start` time NOT NULL COMMENT '签到开始时间(课前N分钟)',
+ `check_in_end` time NOT NULL COMMENT '签到截止时间(课后N分钟)',
+ `late_threshold` int NOT NULL DEFAULT 15 COMMENT '迟到阈值(分钟)',
+ `absent_threshold` int NOT NULL DEFAULT 30 COMMENT '缺勤阈值(分钟)',
+ `early_leave_threshold` int NOT NULL DEFAULT 15 COMMENT '早退阈值(分钟)',
+ `allow_make_up` tinyint NOT NULL DEFAULT 0 COMMENT '是否允许补签:0-否 1-是',
+ `make_up_limit` int NULL DEFAULT NULL COMMENT '补签时限(小时)',
+ `school_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '所属学校ID',
+ `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ `deleted` tinyint NULL DEFAULT 0,
+ PRIMARY KEY (`id`) USING BTREE,
+ INDEX `idx_rule_type`(`rule_type` ASC) USING BTREE,
+ INDEX `idx_target_id`(`target_id` ASC) USING BTREE,
+ INDEX `idx_school_id`(`school_id` ASC) USING BTREE,
+ INDEX `idx_status`(`status` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '考勤规则表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of att_rule
+-- ----------------------------
+INSERT INTO `att_rule` VALUES (1, '默认考勤规则', 'global', NULL, '00:10:00', '00:15:00', 15, 30, 15, 1, 24, 1, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `att_rule` VALUES (2, '严格考勤规则', 'global', NULL, '00:05:00', '00:10:00', 5, 15, 5, 0, NULL, 1, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+
+-- ----------------------------
+-- Table structure for att_task
+-- ----------------------------
+DROP TABLE IF EXISTS `att_task`;
+CREATE TABLE `att_task` (
+ `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `task_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '任务编号',
+ `schedule_id` bigint UNSIGNED NOT NULL COMMENT '课程安排ID',
+ `course_id` bigint UNSIGNED NOT NULL COMMENT '课程ID',
+ `course_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '课程名称',
+ `classroom_id` bigint UNSIGNED NOT NULL COMMENT '教室ID',
+ `classroom_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '教室名称',
+ `teacher_id` bigint UNSIGNED NOT NULL COMMENT '教师ID',
+ `teacher_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '教师姓名',
+ `class_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '班级ID',
+ `att_date` date NOT NULL COMMENT '考勤日期',
+ `start_time` datetime NOT NULL COMMENT '上课开始时间',
+ `end_time` datetime NOT NULL COMMENT '上课结束时间',
+ `total_count` int NOT NULL DEFAULT 0 COMMENT '应到人数',
+ `actual_count` int NOT NULL DEFAULT 0 COMMENT '实到人数',
+ `absent_count` int NOT NULL DEFAULT 0 COMMENT '缺勤人数',
+ `late_count` int NOT NULL DEFAULT 0 COMMENT '迟到人数',
+ `leave_early_count` int NOT NULL DEFAULT 0 COMMENT '早退人数',
+ `attendance_rate` decimal(5, 2) NULL DEFAULT NULL COMMENT '出勤率%',
+ `task_status` tinyint NOT NULL DEFAULT 0 COMMENT '任务状态:0-未开始 1-进行中 2-已结束 3-已取消',
+ `school_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '所属学校ID',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ `deleted` tinyint NULL DEFAULT 0,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_task_no`(`task_no` ASC) USING BTREE,
+ INDEX `idx_schedule_id`(`schedule_id` ASC) USING BTREE,
+ INDEX `idx_course_id`(`course_id` ASC) USING BTREE,
+ INDEX `idx_classroom_id`(`classroom_id` ASC) USING BTREE,
+ INDEX `idx_teacher_id`(`teacher_id` ASC) USING BTREE,
+ INDEX `idx_att_date`(`att_date` ASC) USING BTREE,
+ INDEX `idx_task_status`(`task_status` ASC) USING BTREE,
+ INDEX `idx_school_id`(`school_id` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '考勤任务表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of att_task
+-- ----------------------------
+INSERT INTO `att_task` VALUES (1, 'ATT202406010001', 1, 1, '高等数学A', 1, '301教室', 4, '赵教授', 3, '2026-06-01', '2026-06-01 08:00:00', '2024-06-01 09:40:00', 45, 44, 1, 0, 0, 97.78, 2, 1, '2026-06-01 17:11:42', '2026-06-03 14:35:40', 0);
+INSERT INTO `att_task` VALUES (2, 'ATT202406010002', 2, 2, '大学英语B', 2, '205教室', 2, '李老师', 4, '2026-06-01', '2026-06-01 10:00:00', '2024-06-01 11:40:00', 38, 38, 0, 0, 0, 100.00, 2, 1, '2026-06-01 17:11:42', '2026-06-03 14:35:48', 0);
+INSERT INTO `att_task` VALUES (3, 'ATT202406010003', 3, 3, '计算机导论', 3, '102实验室', 3, '张教授', 1, '2026-06-01', '2026-06-01 14:00:00', '2024-06-01 15:40:00', 52, 49, 3, 0, 0, 94.23, 2, 1, '2026-06-01 17:11:42', '2026-06-03 14:35:55', 0);
+INSERT INTO `att_task` VALUES (4, 'ATT202405310001', 4, 4, '线性代数', 4, '408教室', 4, '赵教授', 3, '2026-06-01', '2026-06-01 08:00:00', '2024-05-31 09:40:00', 40, 38, 2, 0, 0, 95.00, 2, 1, '2026-06-01 17:11:42', '2026-06-03 14:38:03', 0);
+INSERT INTO `att_task` VALUES (5, 'ATT202405310002', 5, 5, '马克思原理', 5, '大阶梯教室', 5, '刘老师', 5, '2026-06-01', '2026-06-01 10:00:00', '2024-05-31 11:40:00', 120, 108, 12, 0, 0, 90.00, 2, 1, '2026-06-01 17:11:42', '2026-06-03 14:38:10', 0);
+
+-- ----------------------------
+-- Table structure for behavior_record
+-- ----------------------------
+DROP TABLE IF EXISTS `behavior_record`;
+CREATE TABLE `behavior_record` (
+ `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `task_id` bigint UNSIGNED NOT NULL COMMENT '考勤任务ID',
+ `course_id` bigint UNSIGNED NOT NULL COMMENT '课程ID',
+ `classroom_id` bigint UNSIGNED NOT NULL COMMENT '教室ID',
+ `student_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '学生ID(可为空,表示整体统计)',
+ `behavior_type_id` bigint UNSIGNED NOT NULL COMMENT '行为类型ID',
+ `behavior_time` datetime NOT NULL COMMENT '行为发生时间',
+ `duration` int NULL DEFAULT NULL COMMENT '持续时间(秒)',
+ `confidence` decimal(5, 2) NULL DEFAULT NULL COMMENT 'AI识别置信度',
+ `snapshot_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '行为抓拍图片',
+ `is_warning` tinyint NOT NULL DEFAULT 0 COMMENT '是否预警:0-否 1-是',
+ `warning_level` tinyint NULL DEFAULT NULL COMMENT '预警级别:1-低 2-中 3-高',
+ `handled` tinyint NOT NULL DEFAULT 0 COMMENT '是否已处理:0-否 1-是',
+ `handler_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '处理人ID',
+ `handle_time` datetime NULL DEFAULT NULL COMMENT '处理时间',
+ `handle_remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '处理备注',
+ `school_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '所属学校ID',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `deleted` tinyint NULL DEFAULT 0,
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`) USING BTREE,
+ INDEX `idx_task_id`(`task_id` ASC) USING BTREE,
+ INDEX `idx_course_id`(`course_id` ASC) USING BTREE,
+ INDEX `idx_student_id`(`student_id` ASC) USING BTREE,
+ INDEX `idx_behavior_type_id`(`behavior_type_id` ASC) USING BTREE,
+ INDEX `idx_behavior_time`(`behavior_time` ASC) USING BTREE,
+ INDEX `idx_is_warning`(`is_warning` ASC) USING BTREE,
+ INDEX `idx_school_id`(`school_id` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 13 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '课堂行为记录表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of behavior_record
+-- ----------------------------
+INSERT INTO `behavior_record` VALUES (1, 1, 1, 1, NULL, 1, '2026-06-01 08:15:00', 600, 0.95, NULL, 0, NULL, 0, NULL, NULL, NULL, 1, '2026-06-01 17:11:42', 0, '2026-06-03 11:33:04');
+INSERT INTO `behavior_record` VALUES (2, 1, 1, 1, NULL, 1, '2026-06-01 08:30:00', 900, 0.92, NULL, 0, NULL, 0, NULL, NULL, NULL, 1, '2026-06-01 17:11:42', 0, '2026-06-03 11:33:08');
+INSERT INTO `behavior_record` VALUES (3, 1, 1, 1, NULL, 2, '2026-06-01 08:45:00', 120, 0.88, NULL, 0, NULL, 0, NULL, NULL, NULL, 1, '2026-06-01 17:11:42', 0, '2026-06-03 11:33:16');
+INSERT INTO `behavior_record` VALUES (4, 1, 1, 1, NULL, 3, '2026-06-01 09:00:00', 300, 0.85, NULL, 0, NULL, 0, NULL, NULL, NULL, 1, '2026-06-01 17:11:42', 0, '2026-06-03 11:33:20');
+INSERT INTO `behavior_record` VALUES (5, 1, 1, 1, NULL, 4, '2026-06-01 09:10:00', 180, 0.78, NULL, 1, 1, 0, NULL, NULL, NULL, 1, '2026-06-01 17:11:42', 0, '2026-06-03 11:33:23');
+INSERT INTO `behavior_record` VALUES (6, 1, 1, 1, NULL, 7, '2026-06-01 09:20:00', 60, 0.65, NULL, 0, NULL, 0, NULL, NULL, NULL, 1, '2026-06-01 17:11:42', 0, '2026-06-03 11:33:27');
+INSERT INTO `behavior_record` VALUES (7, 2, 2, 2, NULL, 1, '2026-06-01 10:15:00', 1200, 0.96, NULL, 0, NULL, 0, NULL, NULL, NULL, 1, '2026-06-01 17:11:42', 0, '2026-06-03 11:33:31');
+INSERT INTO `behavior_record` VALUES (8, 2, 2, 2, NULL, 2, '2026-06-01 10:30:00', 180, 0.90, NULL, 0, NULL, 0, NULL, NULL, NULL, 1, '2026-06-01 17:11:42', 0, '2026-06-03 11:33:33');
+INSERT INTO `behavior_record` VALUES (9, 2, 2, 2, NULL, 3, '2026-06-01 11:00:00', 600, 0.82, NULL, 0, NULL, 0, NULL, NULL, NULL, 1, '2026-06-01 17:11:42', 0, '2026-06-03 11:33:37');
+INSERT INTO `behavior_record` VALUES (10, 3, 3, 3, NULL, 1, '2026-06-01 14:10:00', 800, 0.94, NULL, 0, NULL, 0, NULL, NULL, NULL, 1, '2026-06-01 17:11:42', 0, '2026-06-03 11:33:40');
+INSERT INTO `behavior_record` VALUES (11, 3, 3, 3, NULL, 4, '2026-06-01 14:30:00', 240, 0.80, NULL, 1, 1, 0, NULL, NULL, NULL, 1, '2026-06-01 17:11:42', 0, '2026-06-03 11:33:43');
+INSERT INTO `behavior_record` VALUES (12, 3, 3, 3, NULL, 5, '2026-06-01 14:50:00', 300, 0.75, NULL, 1, 2, 0, NULL, NULL, NULL, 1, '2026-06-01 17:11:42', 0, '2026-06-03 11:33:47');
+
+-- ----------------------------
+-- Table structure for behavior_type
+-- ----------------------------
+DROP TABLE IF EXISTS `behavior_type`;
+CREATE TABLE `behavior_type` (
+ `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `type_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '类型编码',
+ `type_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '类型名称',
+ `category` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '分类:positive-积极/neutral-中性/negative-消极',
+ `color` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '图表颜色',
+ `sort_order` int NOT NULL DEFAULT 0 COMMENT '排序',
+ `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '描述',
+ `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `deleted` tinyint NULL DEFAULT 0,
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_type_code`(`type_code` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '行为类型表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of behavior_type
+-- ----------------------------
+INSERT INTO `behavior_type` VALUES (1, 'focus', '专注听讲', 'positive', '#52c41a', 1, '学生专注听课', 1, '2026-06-01 17:11:42', 0, '2026-06-02 17:16:38');
+INSERT INTO `behavior_type` VALUES (2, 'raise_hand', '举手互动', 'positive', '#1890ff', 2, '学生举手发言或提问', 1, '2026-06-01 17:11:42', 0, '2026-06-02 17:16:38');
+INSERT INTO `behavior_type` VALUES (3, 'writing', '低头书写', 'neutral', '#722ed1', 3, '学生低头做笔记或写作业', 1, '2026-06-01 17:11:42', 0, '2026-06-02 17:16:38');
+INSERT INTO `behavior_type` VALUES (4, 'talking', '交谈讨论', 'neutral', '#faad14', 4, '学生与旁边同学交谈', 1, '2026-06-01 17:11:42', 0, '2026-06-02 17:16:38');
+INSERT INTO `behavior_type` VALUES (5, 'sleeping', '趴桌睡觉', 'negative', '#f5222d', 5, '学生趴在桌上睡觉', 1, '2026-06-01 17:11:42', 0, '2026-06-02 17:16:38');
+INSERT INTO `behavior_type` VALUES (6, 'phone', '使用手机', 'negative', '#ff4d4f', 6, '学生在课堂上使用手机', 1, '2026-06-01 17:11:42', 0, '2026-06-02 17:16:38');
+INSERT INTO `behavior_type` VALUES (7, 'other', '其他', 'neutral', '#bfbfbf', 99, '其他未分类行为', 1, '2026-06-01 17:11:42', 0, '2026-06-02 17:16:38');
+
+-- ----------------------------
+-- Table structure for device
+-- ----------------------------
+DROP TABLE IF EXISTS `device`;
+CREATE TABLE `device` (
+ `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `device_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '设备编号',
+ `device_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '设备名称',
+ `device_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '设备类型:camera-摄像头/face_recognition-人脸识别终端/nvr-NVR',
+ `brand` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '品牌',
+ `model` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '型号',
+ `serial_no` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '序列号',
+ `ip_address` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'IP地址',
+ `port` int NULL DEFAULT NULL COMMENT '端口',
+ `mac_address` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'MAC地址',
+ `classroom_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '安装教室ID',
+ `location` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '安装位置描述',
+ `firmware_version` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '固件版本',
+ `last_heartbeat` datetime NULL DEFAULT NULL COMMENT '最后心跳时间',
+ `online_status` tinyint NOT NULL DEFAULT 0 COMMENT '在线状态:0-离线 1-在线',
+ `status` tinyint NOT NULL DEFAULT 1 COMMENT '使用状态:0-停用 1-正常 2-维修中',
+ `school_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '所属学校ID',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ `deleted` tinyint NULL DEFAULT 0,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_device_no`(`device_no` ASC) USING BTREE,
+ INDEX `idx_classroom_id`(`classroom_id` ASC) USING BTREE,
+ INDEX `idx_device_type`(`device_type` ASC) USING BTREE,
+ INDEX `idx_online_status`(`online_status` ASC) USING BTREE,
+ INDEX `idx_school_id`(`school_id` ASC) USING BTREE,
+ INDEX `idx_status`(`status` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '设备表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of device
+-- ----------------------------
+INSERT INTO `device` VALUES (1, 'CAM001', '301教室摄像头', 'camera', '海康威视', 'DS-2CD3T86FWDV2', NULL, '192.168.1.101', 80, NULL, 1, '301教室前方', NULL, NULL, 1, 1, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `device` VALUES (2, 'CAM002', '205教室摄像头', 'camera', '海康威视', 'DS-2CD3T86FWDV2', NULL, '192.168.1.102', 80, NULL, 2, '205教室前方', NULL, NULL, 1, 1, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `device` VALUES (3, 'CAM003', '102实验室摄像头', 'camera', '大华', 'DH-IPC-HFW4631M', NULL, '192.168.1.103', 80, NULL, 3, '102实验室后方', NULL, NULL, 1, 1, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `device` VALUES (4, 'CAM004', '408教室摄像头', 'camera', '海康威视', 'DS-2CD3T86FWDV2', NULL, '192.168.1.104', 80, NULL, 4, '408教室前方', NULL, NULL, 0, 1, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `device` VALUES (5, 'CAM005', '大阶梯教室摄像头', 'camera', '大华', 'DH-IPC-HFW4631M', NULL, '192.168.1.105', 80, NULL, 5, '大阶梯教室中央', NULL, NULL, 1, 1, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `device` VALUES (6, 'FACE001', '301人脸识别终端', 'face_recognition', '旷视', 'FaceID-Pro', NULL, '192.168.1.201', 8080, NULL, 1, '301教室门口', NULL, NULL, 1, 1, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `device` VALUES (7, 'FACE002', '205人脸识别终端', 'face_recognition', '旷视', 'FaceID-Pro', NULL, '192.168.1.202', 8080, NULL, 2, '205教室门口', NULL, NULL, 1, 1, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+
+-- ----------------------------
+-- Table structure for edu_class
+-- ----------------------------
+DROP TABLE IF EXISTS `edu_class`;
+CREATE TABLE `edu_class` (
+ `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `class_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '班级名称',
+ `class_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '班级编码',
+ `grade` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '年级',
+ `major` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '专业',
+ `school_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '所属学校ID',
+ `student_count` int NOT NULL DEFAULT 0 COMMENT '学生人数',
+ `headteacher_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '班主任ID(教师)',
+ `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ `deleted` tinyint NULL DEFAULT 0,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_class_code`(`class_code` ASC) USING BTREE,
+ INDEX `idx_school_id`(`school_id` ASC) USING BTREE,
+ INDEX `idx_status`(`status` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '班级表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of edu_class
+-- ----------------------------
+INSERT INTO `edu_class` VALUES (1, '计算机科学与技术1班', 'CS202401', '2024级', '计算机科学与技术', 1, 52, NULL, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `edu_class` VALUES (2, '软件工程2班', 'SE202402', '2024级', '软件工程', 1, 45, NULL, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `edu_class` VALUES (3, '数学与应用数学1班', 'MA202401', '2024级', '数学与应用数学', 1, 40, NULL, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `edu_class` VALUES (4, '英语师范1班', 'EN202401', '2024级', '英语', 1, 38, NULL, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `edu_class` VALUES (5, '思想政治教育1班', 'PE202401', '2024级', '思想政治教育', 1, 120, NULL, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+
+-- ----------------------------
+-- Table structure for edu_building
+-- ----------------------------
+DROP TABLE IF EXISTS `edu_building`;
+CREATE TABLE `edu_building` (
+ `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `building_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '教学楼名称',
+ `building_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '教学楼编码',
+ `floors` int NULL DEFAULT NULL COMMENT '楼层数',
+ `school_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '所属学校ID',
+ `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:0-停用 1-正常',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ `deleted` tinyint NULL DEFAULT 0,
+ PRIMARY KEY (`id`) USING BTREE,
+ INDEX `idx_school_id`(`school_id` ASC) USING BTREE,
+ INDEX `idx_status`(`status` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '教学楼表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of edu_building
+-- ----------------------------
+INSERT INTO `edu_building` VALUES (1, '第一教学楼', 'BLD-A', 5, 1, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `edu_building` VALUES (2, '第二教学楼', 'BLD-B', 6, 1, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `edu_building` VALUES (3, '实验楼', 'BLD-C', 3, 1, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+
+-- ----------------------------
+-- Table structure for edu_classroom
+-- ----------------------------
+DROP TABLE IF EXISTS `edu_classroom`;
+CREATE TABLE `edu_classroom` (
+ `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `room_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '教室编号',
+ `room_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '教室名称',
+ `building_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '所属教学楼ID',
+ `floor` int NULL DEFAULT NULL COMMENT '楼层',
+ `capacity` int NULL DEFAULT NULL COMMENT '容纳人数',
+ `room_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '普通教室' COMMENT '教室类型',
+ `camera_device_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '关联摄像头设备ID',
+ `school_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '所属学校ID',
+ `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:0-停用 1-正常',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ `deleted` tinyint NULL DEFAULT 0,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_room_no`(`room_no` ASC) USING BTREE,
+ INDEX `idx_building_id`(`building_id` ASC) USING BTREE,
+ INDEX `idx_school_id`(`school_id` ASC) USING BTREE,
+ INDEX `idx_status`(`status` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '教室表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of edu_classroom
+-- ----------------------------
+INSERT INTO `edu_classroom` VALUES (1, '301', '301教室', 1, 3, 50, '普通教室', NULL, 1, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `edu_classroom` VALUES (2, '205', '205教室', 1, 2, 45, '普通教室', NULL, 1, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `edu_classroom` VALUES (3, '102', '102实验室', 3, 1, 55, '实验室', NULL, 1, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `edu_classroom` VALUES (4, '408', '408教室', 2, 4, 48, '普通教室', NULL, 1, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `edu_classroom` VALUES (5, 'DJT', '大阶梯教室', 1, 1, 150, '阶梯教室', NULL, 1, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `edu_classroom` VALUES (6, '302', '302教室', 1, 3, 50, '普通教室', NULL, 1, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `edu_classroom` VALUES (7, '303', '303教室', 1, 3, 50, '普通教室', NULL, 1, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `edu_classroom` VALUES (8, '304', '304教室', 1, 3, 50, '普通教室', NULL, 1, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+
+-- ----------------------------
+-- Table structure for edu_course
+-- ----------------------------
+DROP TABLE IF EXISTS `edu_course`;
+CREATE TABLE `edu_course` (
+ `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `course_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '课程编码',
+ `course_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '课程名称',
+ `course_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '课程类型',
+ `credit` decimal(3, 1) NULL DEFAULT NULL COMMENT '学分',
+ `teacher_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '授课教师ID',
+ `school_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '所属学校ID',
+ `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '课程描述',
+ `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ `deleted` tinyint NULL DEFAULT 0,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_course_code`(`course_code` ASC) USING BTREE,
+ INDEX `idx_teacher_id`(`teacher_id` ASC) USING BTREE,
+ INDEX `idx_school_id`(`school_id` ASC) USING BTREE,
+ INDEX `idx_status`(`status` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '课程表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of edu_course
+-- ----------------------------
+INSERT INTO `edu_course` VALUES (1, 'CS101', '高等数学A', '必修课', 4.0, 4, 1, '高等数学基础课程', 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `edu_course` VALUES (2, 'EN101', '大学英语B', '必修课', 3.0, 2, 1, '大学英语基础课程', 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `edu_course` VALUES (3, 'CS102', '计算机导论', '必修课', 2.0, 3, 1, '计算机专业入门课程', 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `edu_course` VALUES (4, 'MA101', '线性代数', '必修课', 3.0, 4, 1, '线性代数基础课程', 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `edu_course` VALUES (5, 'PE101', '马克思原理', '必修课', 3.0, 5, 1, '马克思主义基本原理', 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+
+-- ----------------------------
+-- Table structure for edu_course_schedule
+-- ----------------------------
+DROP TABLE IF EXISTS `edu_course_schedule`;
+CREATE TABLE `edu_course_schedule` (
+ `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `course_id` bigint UNSIGNED NOT NULL COMMENT '课程ID',
+ `classroom_id` bigint UNSIGNED NOT NULL COMMENT '教室ID',
+ `class_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '上课班级ID',
+ `teacher_id` bigint UNSIGNED NOT NULL COMMENT '授课教师ID',
+ `week_day` tinyint NOT NULL COMMENT '星期几:1-7',
+ `start_section` tinyint NOT NULL COMMENT '开始节次',
+ `end_section` tinyint NOT NULL COMMENT '结束节次',
+ `start_week` int NOT NULL DEFAULT 1 COMMENT '开始周',
+ `end_week` int NOT NULL DEFAULT 16 COMMENT '结束周',
+ `semester` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '学期,如:2024-2025-1',
+ `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ `deleted` tinyint NULL DEFAULT 0,
+ PRIMARY KEY (`id`) USING BTREE,
+ INDEX `idx_course_id`(`course_id` ASC) USING BTREE,
+ INDEX `idx_classroom_id`(`classroom_id` ASC) USING BTREE,
+ INDEX `idx_teacher_id`(`teacher_id` ASC) USING BTREE,
+ INDEX `idx_class_id`(`class_id` ASC) USING BTREE,
+ INDEX `idx_week_day`(`week_day` ASC) USING BTREE,
+ INDEX `idx_semester`(`semester` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '课程安排表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of edu_course_schedule
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for edu_student
+-- ----------------------------
+DROP TABLE IF EXISTS `edu_student`;
+CREATE TABLE `edu_student` (
+ `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `student_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '学号',
+ `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '姓名',
+ `gender` tinyint NULL DEFAULT NULL COMMENT '性别:0-女 1-男',
+ `avatar` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '头像URL',
+ `face_feature` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '人脸特征值(Base64)',
+ `face_image` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '人脸照片URL',
+ `class_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '所属班级ID',
+ `school_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '所属学校ID',
+ `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '手机号',
+ `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '邮箱',
+ `user_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '关联系统用户ID',
+ `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:0-离校 1-在读',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ `deleted` tinyint NULL DEFAULT 0,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_student_no`(`student_no` ASC) USING BTREE,
+ INDEX `idx_class_id`(`class_id` ASC) USING BTREE,
+ INDEX `idx_school_id`(`school_id` ASC) USING BTREE,
+ INDEX `idx_user_id`(`user_id` ASC) USING BTREE,
+ INDEX `idx_status`(`status` ASC) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '学生表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of edu_student
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for edu_teacher
+-- ----------------------------
+DROP TABLE IF EXISTS `edu_teacher`;
+CREATE TABLE `edu_teacher` (
+ `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `teacher_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '工号',
+ `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '姓名',
+ `gender` tinyint NULL DEFAULT NULL COMMENT '性别:0-女 1-男',
+ `avatar` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '头像URL',
+ `title` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '职称',
+ `department` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '所属院系',
+ `school_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '所属学校ID',
+ `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '手机号',
+ `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '邮箱',
+ `user_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '关联系统用户ID',
+ `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ `deleted` tinyint NULL DEFAULT 0,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_teacher_no`(`teacher_no` ASC) USING BTREE,
+ INDEX `idx_school_id`(`school_id` ASC) USING BTREE,
+ INDEX `idx_user_id`(`user_id` ASC) USING BTREE,
+ INDEX `idx_status`(`status` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '教师表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of edu_teacher
+-- ----------------------------
+INSERT INTO `edu_teacher` VALUES (1, 'T001', '王教授', 1, NULL, '教授', '计算机学院', 1, '13900139001', 'wang@school.edu', NULL, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `edu_teacher` VALUES (2, 'T002', '李老师', 0, NULL, '讲师', '外国语学院', 1, '13900139002', 'li@school.edu', NULL, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `edu_teacher` VALUES (3, 'T003', '张教授', 1, NULL, '教授', '计算机学院', 1, '13900139003', 'zhang@school.edu', NULL, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `edu_teacher` VALUES (4, 'T004', '赵教授', 1, NULL, '副教授', '数学学院', 1, '13900139004', 'zhao@school.edu', NULL, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `edu_teacher` VALUES (5, 'T005', '刘老师', 0, NULL, '讲师', '马克思主义学院', 1, '13900139005', 'liu@school.edu', NULL, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+
+-- ----------------------------
+-- Table structure for monitor_record
+-- ----------------------------
+DROP TABLE IF EXISTS `monitor_record`;
+CREATE TABLE `monitor_record` (
+ `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `classroom_id` bigint UNSIGNED NOT NULL COMMENT '教室ID',
+ `device_id` bigint UNSIGNED NOT NULL COMMENT '监控设备ID',
+ `record_date` date NOT NULL COMMENT '记录日期',
+ `start_time` datetime NOT NULL COMMENT '开始时间',
+ `end_time` datetime NULL DEFAULT NULL COMMENT '结束时间',
+ `stream_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '直播流地址',
+ `record_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '录像地址',
+ `file_size` bigint NULL DEFAULT NULL COMMENT '录像文件大小(字节)',
+ `record_status` tinyint NOT NULL DEFAULT 1 COMMENT '记录状态:0-失败 1-成功',
+ `school_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '所属学校ID',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `deleted` tinyint NULL DEFAULT 0,
+ PRIMARY KEY (`id`) USING BTREE,
+ INDEX `idx_classroom_id`(`classroom_id` ASC) USING BTREE,
+ INDEX `idx_device_id`(`device_id` ASC) USING BTREE,
+ INDEX `idx_record_date`(`record_date` ASC) USING BTREE,
+ INDEX `idx_school_id`(`school_id` ASC) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '教室监控记录表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of monitor_record
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for sys_operation_log
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_operation_log`;
+CREATE TABLE `sys_operation_log` (
+ `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `user_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '操作用户ID',
+ `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '操作用户名',
+ `module` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '操作模块',
+ `action` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '操作动作',
+ `method` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '请求方法',
+ `request_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '请求URL',
+ `request_params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '请求参数',
+ `response_data` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '响应数据',
+ `ip_address` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'IP地址',
+ `user_agent` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'User-Agent',
+ `duration` int NULL DEFAULT NULL COMMENT '执行时长(毫秒)',
+ `status` tinyint NULL DEFAULT NULL COMMENT '状态:0-失败 1-成功',
+ `error_msg` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '错误信息',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `deleted` tinyint NULL DEFAULT 0,
+ PRIMARY KEY (`id`) USING BTREE,
+ INDEX `idx_user_id`(`user_id` ASC) USING BTREE,
+ INDEX `idx_module`(`module` ASC) USING BTREE,
+ INDEX `idx_created_at`(`created_at` ASC) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统操作日志表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of sys_operation_log
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for sys_permission
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_permission`;
+CREATE TABLE `sys_permission` (
+ `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `perm_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '权限编码',
+ `perm_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '权限名称',
+ `perm_type` tinyint NOT NULL DEFAULT 1 COMMENT '权限类型:1-菜单 2-按钮 3-接口',
+ `parent_id` bigint UNSIGNED NULL DEFAULT 0 COMMENT '父权限ID',
+ `path` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '前端路由路径',
+ `icon` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '图标',
+ `sort_order` int NOT NULL DEFAULT 0 COMMENT '排序',
+ `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `deleted` tinyint NULL DEFAULT 0,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_perm_code`(`perm_code` ASC) USING BTREE,
+ INDEX `idx_parent_id`(`parent_id` ASC) USING BTREE,
+ INDEX `idx_status`(`status` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '权限表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of sys_permission
+-- ----------------------------
+INSERT INTO `sys_permission` VALUES (1, 'dashboard', '首页', 1, 0, '/dashboard', 'HomeFilled', 1, 1, '2026-06-01 17:11:42', 0);
+INSERT INTO `sys_permission` VALUES (2, 'behavior', '课堂行为分析', 1, 0, '/behavior', 'TrendCharts', 2, 1, '2026-06-01 17:11:42', 0);
+INSERT INTO `sys_permission` VALUES (3, 'history', '历史记录查询', 1, 0, '/history', 'Search', 3, 1, '2026-06-01 17:11:42', 0);
+INSERT INTO `sys_permission` VALUES (4, 'bigscreen', '数据展示大屏', 1, 0, '/bigscreen', 'DataAnalysis', 4, 1, '2026-06-01 17:11:42', 0);
+INSERT INTO `sys_permission` VALUES (5, 'settings', '系统设置', 1, 0, '/settings', 'Setting', 5, 1, '2026-06-01 17:11:42', 0);
+INSERT INTO `sys_permission` VALUES (6, 'settings.personnel', '人员管理', 1, 5, '/settings/personnel', 'User', 1, 1, '2026-06-01 17:11:42', 0);
+INSERT INTO `sys_permission` VALUES (7, 'settings.device', '设备管理', 1, 5, '/settings/device', 'Monitor', 2, 1, '2026-06-01 17:11:42', 0);
+INSERT INTO `sys_permission` VALUES (8, 'settings.rules', '考勤规则设置', 1, 5, '/settings/rules', 'Notebook', 3, 1, '2026-06-01 17:11:42', 0);
+INSERT INTO `sys_permission` VALUES (9, 'settings.permissions', '权限管理', 1, 5, '/settings/permissions', 'Lock', 4, 1, '2026-06-01 17:11:42', 0);
+
+-- ----------------------------
+-- Table structure for sys_role
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_role`;
+CREATE TABLE `sys_role` (
+ `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `role_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色编码:admin/teacher/staff',
+ `role_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色名称',
+ `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '角色描述',
+ `sort_order` int NOT NULL DEFAULT 0 COMMENT '排序',
+ `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:0-禁用 1-启用',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ `deleted` tinyint NULL DEFAULT 0,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_role_code`(`role_code` ASC) USING BTREE,
+ INDEX `idx_status`(`status` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of sys_role
+-- ----------------------------
+INSERT INTO `sys_role` VALUES (1, 'admin', '管理员', '系统管理员,拥有全部权限', 1, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `sys_role` VALUES (2, 'teacher', '教师', '授课教师,可查看本班/本课程考勤', 2, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+INSERT INTO `sys_role` VALUES (3, 'staff', '教务员', '教务管理人员,可查看和管理考勤数据', 3, 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+
+-- ----------------------------
+-- Table structure for sys_role_permission
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_role_permission`;
+CREATE TABLE `sys_role_permission` (
+ `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
+ `role_id` bigint UNSIGNED NOT NULL COMMENT '角色ID',
+ `permission_id` bigint UNSIGNED NOT NULL COMMENT '权限ID',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `deleted` tinyint NULL DEFAULT 0,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_role_perm`(`role_id` ASC, `permission_id` ASC) USING BTREE,
+ INDEX `idx_permission_id`(`permission_id` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 24 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色权限关联表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of sys_role_permission
+-- ----------------------------
+INSERT INTO `sys_role_permission` VALUES (1, 1, 1, '2026-06-01 17:11:42', 0);
+INSERT INTO `sys_role_permission` VALUES (2, 1, 2, '2026-06-01 17:11:42', 0);
+INSERT INTO `sys_role_permission` VALUES (3, 1, 3, '2026-06-01 17:11:42', 0);
+INSERT INTO `sys_role_permission` VALUES (4, 1, 4, '2026-06-01 17:11:42', 0);
+INSERT INTO `sys_role_permission` VALUES (5, 1, 5, '2026-06-01 17:11:42', 0);
+INSERT INTO `sys_role_permission` VALUES (6, 1, 6, '2026-06-01 17:11:42', 0);
+INSERT INTO `sys_role_permission` VALUES (7, 1, 7, '2026-06-01 17:11:42', 0);
+INSERT INTO `sys_role_permission` VALUES (8, 1, 8, '2026-06-01 17:11:42', 0);
+INSERT INTO `sys_role_permission` VALUES (9, 1, 9, '2026-06-01 17:11:42', 0);
+INSERT INTO `sys_role_permission` VALUES (16, 3, 1, '2026-06-01 17:11:42', 0);
+INSERT INTO `sys_role_permission` VALUES (17, 3, 2, '2026-06-01 17:11:42', 0);
+INSERT INTO `sys_role_permission` VALUES (18, 3, 3, '2026-06-01 17:11:42', 0);
+INSERT INTO `sys_role_permission` VALUES (19, 3, 6, '2026-06-01 17:11:42', 0);
+INSERT INTO `sys_role_permission` VALUES (20, 3, 8, '2026-06-01 17:11:42', 0);
+INSERT INTO `sys_role_permission` VALUES (21, 2, 1, '2026-06-01 17:11:42', 0);
+INSERT INTO `sys_role_permission` VALUES (22, 2, 2, '2026-06-01 17:11:42', 0);
+INSERT INTO `sys_role_permission` VALUES (23, 2, 3, '2026-06-01 17:11:42', 0);
+
+-- ----------------------------
+-- Table structure for sys_school
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_school`;
+CREATE TABLE `sys_school` (
+ `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '学校名称',
+ `code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '学校编码',
+ `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '地址',
+ `contact_phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '联系电话',
+ `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:0-禁用 1-启用',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ `deleted` tinyint NULL DEFAULT 0,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_school_code`(`code` ASC) USING BTREE,
+ INDEX `idx_status`(`status` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '学校/机构表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of sys_school
+-- ----------------------------
+INSERT INTO `sys_school` VALUES (1, '阳光实验学校', 'SCHOOL_001', '阳光大道88号', '010-88888888', 1, '2026-06-01 17:11:42', '2026-06-01 17:11:42', 0);
+
+-- ----------------------------
+-- Table structure for sys_user
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_user`;
+CREATE TABLE `sys_user` (
+ `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '登录账号(工号/学号)',
+ `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '登录密码(BCrypt加密)',
+ `real_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '真实姓名',
+ `avatar` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '头像URL',
+ `role_id` bigint UNSIGNED NOT NULL COMMENT '角色ID',
+ `school_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '所属学校ID',
+ `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '手机号',
+ `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '邮箱',
+ `last_login_time` datetime NULL DEFAULT NULL COMMENT '最后登录时间',
+ `last_login_ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '最后登录IP',
+ `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:0-禁用 1-启用',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ `deleted` tinyint NULL DEFAULT 0,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_username`(`username` ASC) USING BTREE,
+ INDEX `idx_role_id`(`role_id` ASC) USING BTREE,
+ INDEX `idx_school_id`(`school_id` ASC) USING BTREE,
+ INDEX `idx_status`(`status` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统用户表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of sys_user
+-- ----------------------------
+INSERT INTO `sys_user` VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', '管理员', NULL, 1, 1, '13800138001', 'admin@school.edu', '2026-06-03 10:13:06', '10.23.22.43', 1, '2026-06-01 17:11:42', '2026-06-02 17:26:20', 0);
+
+SET FOREIGN_KEY_CHECKS = 1;
diff --git a/src/main/java/com/attendance/AttendanceApplication.java b/src/main/java/com/attendance/AttendanceApplication.java
new file mode 100644
index 0000000..0d31f45
--- /dev/null
+++ b/src/main/java/com/attendance/AttendanceApplication.java
@@ -0,0 +1,15 @@
+package com.attendance;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableAsync;
+
+@SpringBootApplication
+@EnableAsync
+@MapperScan("com.attendance.mapper")
+public class AttendanceApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(AttendanceApplication.class, args);
+ }
+}
diff --git a/src/main/java/com/attendance/annotation/OperationLog.java b/src/main/java/com/attendance/annotation/OperationLog.java
new file mode 100644
index 0000000..2b56d2f
--- /dev/null
+++ b/src/main/java/com/attendance/annotation/OperationLog.java
@@ -0,0 +1,18 @@
+package com.attendance.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 操作日志注解,标注在 Controller 方法上自动记录操作日志
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface OperationLog {
+
+ /** 操作模块,如:考勤管理、用户管理 */
+ String module() default "";
+
+ /** 操作动作,如:新增用户、删除考勤任务 */
+ String action() default "";
+}
diff --git a/src/main/java/com/attendance/aspect/OperationLogAspect.java b/src/main/java/com/attendance/aspect/OperationLogAspect.java
new file mode 100644
index 0000000..b32d64b
--- /dev/null
+++ b/src/main/java/com/attendance/aspect/OperationLogAspect.java
@@ -0,0 +1,103 @@
+package com.attendance.aspect;
+
+import com.alibaba.fastjson2.JSON;
+import com.attendance.annotation.OperationLog;
+import com.attendance.common.UserContext;
+import com.attendance.entity.SysOperationLog;
+import com.attendance.service.SysOperationLogService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Objects;
+
+@Slf4j
+@Aspect
+@Component
+@RequiredArgsConstructor
+public class OperationLogAspect {
+
+ private final SysOperationLogService sysOperationLogService;
+
+ @Around("@annotation(operationLog)")
+ public Object around(ProceedingJoinPoint joinPoint, OperationLog operationLog) throws Throwable {
+ long startTime = System.currentTimeMillis();
+
+ SysOperationLog logEntity = new SysOperationLog();
+ logEntity.setUserId(UserContext.getUserId());
+ logEntity.setUsername(UserContext.getUsername());
+ logEntity.setModule(operationLog.module());
+ logEntity.setAction(operationLog.action());
+
+ // 填充请求相关字段
+ ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+ if (attributes != null) {
+ HttpServletRequest request = attributes.getRequest();
+ logEntity.setMethod(request.getMethod());
+ logEntity.setRequestUrl(request.getRequestURI());
+ logEntity.setIpAddress(getClientIp(request));
+ logEntity.setUserAgent(request.getHeader("User-Agent"));
+
+ // 记录请求参数(排除文件上传等二进制参数)
+ Object[] args = joinPoint.getArgs();
+ try {
+ logEntity.setRequestParams(JSON.toJSONString(args));
+ } catch (Exception e) {
+ logEntity.setRequestParams("[无法序列化的参数]");
+ }
+ }
+
+ Object result;
+ try {
+ result = joinPoint.proceed();
+ logEntity.setStatus(1);
+ try {
+ logEntity.setResponseData(JSON.toJSONString(result));
+ } catch (Exception e) {
+ logEntity.setResponseData("[无法序列化的响应]");
+ }
+ } catch (Throwable e) {
+ logEntity.setStatus(0);
+ logEntity.setErrorMsg(e.getMessage());
+ throw e;
+ } finally {
+ logEntity.setDuration((int) (System.currentTimeMillis() - startTime));
+ try {
+ sysOperationLogService.asyncSave(logEntity);
+ } catch (Exception e) {
+ log.error("保存操作日志失败", e);
+ }
+ }
+
+ return result;
+ }
+
+ /** 获取客户端真实IP */
+ private String getClientIp(HttpServletRequest request) {
+ String ip = request.getHeader("X-Forwarded-For");
+ if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+ ip = request.getHeader("X-Real-IP");
+ }
+ if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+ ip = request.getHeader("Proxy-Client-IP");
+ }
+ if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+ ip = request.getHeader("WL-Proxy-Client-IP");
+ }
+ if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+ ip = request.getRemoteAddr();
+ }
+ // 多级代理取第一个IP
+ if (ip != null && ip.contains(",")) {
+ ip = ip.split(",")[0].trim();
+ }
+ return ip;
+ }
+}
diff --git a/src/main/java/com/attendance/common/BusinessException.java b/src/main/java/com/attendance/common/BusinessException.java
new file mode 100644
index 0000000..24d7d2c
--- /dev/null
+++ b/src/main/java/com/attendance/common/BusinessException.java
@@ -0,0 +1,24 @@
+package com.attendance.common;
+
+import lombok.Getter;
+
+@Getter
+public class BusinessException extends RuntimeException {
+
+ private final Integer code;
+
+ public BusinessException(String message) {
+ super(message);
+ this.code = ResultCode.ERROR.getCode();
+ }
+
+ public BusinessException(ResultCode resultCode) {
+ super(resultCode.getMessage());
+ this.code = resultCode.getCode();
+ }
+
+ public BusinessException(Integer code, String message) {
+ super(message);
+ this.code = code;
+ }
+}
diff --git a/src/main/java/com/attendance/common/GlobalExceptionHandler.java b/src/main/java/com/attendance/common/GlobalExceptionHandler.java
new file mode 100644
index 0000000..8de8b10
--- /dev/null
+++ b/src/main/java/com/attendance/common/GlobalExceptionHandler.java
@@ -0,0 +1,36 @@
+package com.attendance.common;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.validation.BindException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@Slf4j
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ @ExceptionHandler(BusinessException.class)
+ public Result handleBusinessException(BusinessException e) {
+ log.warn("业务异常: {}", e.getMessage());
+ return Result.error(e.getCode(), e.getMessage());
+ }
+
+ @ExceptionHandler(BindException.class)
+ public Result handleBindException(BindException e) {
+ String message = e.getAllErrors().get(0).getDefaultMessage();
+ log.warn("参数校验失败: {}", message);
+ return Result.error(ResultCode.PARAM_VALIDATE_FAILED.getCode(), message);
+ }
+
+ @ExceptionHandler(IllegalArgumentException.class)
+ public Result handleIllegalArgumentException(IllegalArgumentException e) {
+ log.warn("参数异常: {}", e.getMessage());
+ return Result.error(ResultCode.PARAM_ERROR.getCode(), e.getMessage());
+ }
+
+ @ExceptionHandler(Exception.class)
+ public Result handleException(Exception e) {
+ log.error("系统异常", e);
+ return Result.error("服务器异常,请稍后重试");
+ }
+}
diff --git a/src/main/java/com/attendance/common/Result.java b/src/main/java/com/attendance/common/Result.java
new file mode 100644
index 0000000..2ebdb93
--- /dev/null
+++ b/src/main/java/com/attendance/common/Result.java
@@ -0,0 +1,67 @@
+package com.attendance.common;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Data
+@Schema(description = "统一响应结果")
+public class Result {
+
+ @Schema(description = "状态码", example = "200")
+ private Integer code;
+
+ @Schema(description = "提示消息", example = "操作成功")
+ private String message;
+
+ @Schema(description = "响应数据")
+ private T data;
+
+ @Schema(description = "时间戳")
+ private Long timestamp;
+
+ public Result() {
+ this.timestamp = System.currentTimeMillis();
+ }
+
+ public static Result success() {
+ return success(null);
+ }
+
+ public static Result success(T data) {
+ Result result = new Result<>();
+ result.setCode(ResultCode.SUCCESS.getCode());
+ result.setMessage(ResultCode.SUCCESS.getMessage());
+ result.setData(data);
+ return result;
+ }
+
+ public static Result success(String message, T data) {
+ Result result = new Result<>();
+ result.setCode(ResultCode.SUCCESS.getCode());
+ result.setMessage(message);
+ result.setData(data);
+ return result;
+ }
+
+ public static Result ok(String message) {
+ Result result = new Result<>();
+ result.setCode(ResultCode.SUCCESS.getCode());
+ result.setMessage(message);
+ return result;
+ }
+
+ public static Result error(String message) {
+ return error(ResultCode.ERROR.getCode(), message);
+ }
+
+ public static Result error(ResultCode resultCode) {
+ return error(resultCode.getCode(), resultCode.getMessage());
+ }
+
+ public static Result error(Integer code, String message) {
+ Result result = new Result<>();
+ result.setCode(code);
+ result.setMessage(message);
+ return result;
+ }
+}
diff --git a/src/main/java/com/attendance/common/ResultCode.java b/src/main/java/com/attendance/common/ResultCode.java
new file mode 100644
index 0000000..12171d0
--- /dev/null
+++ b/src/main/java/com/attendance/common/ResultCode.java
@@ -0,0 +1,32 @@
+package com.attendance.common;
+
+import lombok.Getter;
+
+@Getter
+public enum ResultCode {
+
+ SUCCESS(200, "操作成功"),
+ ERROR(500, "操作失败"),
+ PARAM_ERROR(400, "参数错误"),
+ UNAUTHORIZED(401, "未登录或登录已过期"),
+ FORBIDDEN(403, "没有访问权限"),
+ NOT_FOUND(404, "资源不存在"),
+ METHOD_NOT_ALLOWED(405, "请求方式不允许"),
+ PARAM_VALIDATE_FAILED(422, "参数校验失败"),
+
+ USER_NOT_FOUND(1001, "用户不存在"),
+ USER_PASSWORD_ERROR(1002, "密码错误"),
+ USER_DISABLED(1003, "账号已被禁用"),
+ USERNAME_EXISTS(1004, "用户名已存在"),
+
+ TOKEN_INVALID(2001, "Token无效"),
+ TOKEN_EXPIRED(2002, "Token已过期");
+
+ private final Integer code;
+ private final String message;
+
+ ResultCode(Integer code, String message) {
+ this.code = code;
+ this.message = message;
+ }
+}
diff --git a/src/main/java/com/attendance/common/UserContext.java b/src/main/java/com/attendance/common/UserContext.java
new file mode 100644
index 0000000..c6929ed
--- /dev/null
+++ b/src/main/java/com/attendance/common/UserContext.java
@@ -0,0 +1,45 @@
+package com.attendance.common;
+
+/**
+ * 用户上下文(ThreadLocal),用于在请求链路中传递当前用户信息
+ */
+public class UserContext {
+
+ private static final ThreadLocal THREAD_LOCAL = new ThreadLocal<>();
+
+ public static void set(Long userId, String username) {
+ THREAD_LOCAL.set(new UserInfo(userId, username));
+ }
+
+ public static Long getUserId() {
+ UserInfo info = THREAD_LOCAL.get();
+ return info != null ? info.getUserId() : null;
+ }
+
+ public static String getUsername() {
+ UserInfo info = THREAD_LOCAL.get();
+ return info != null ? info.getUsername() : null;
+ }
+
+ public static void clear() {
+ THREAD_LOCAL.remove();
+ }
+
+ private static class UserInfo {
+ private final Long userId;
+ private final String username;
+
+ UserInfo(Long userId, String username) {
+ this.userId = userId;
+ this.username = username;
+ }
+
+ Long getUserId() {
+ return userId;
+ }
+
+ String getUsername() {
+ return username;
+ }
+ }
+}
diff --git a/src/main/java/com/attendance/config/JwtInterceptor.java b/src/main/java/com/attendance/config/JwtInterceptor.java
new file mode 100644
index 0000000..91c39bf
--- /dev/null
+++ b/src/main/java/com/attendance/config/JwtInterceptor.java
@@ -0,0 +1,61 @@
+package com.attendance.config;
+
+import com.alibaba.fastjson2.JSON;
+import com.attendance.common.Result;
+import com.attendance.common.ResultCode;
+import com.attendance.common.UserContext;
+import com.attendance.utils.JwtUtil;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import java.io.IOException;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class JwtInterceptor implements HandlerInterceptor {
+
+ private final JwtUtil jwtUtil;
+
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+ // 放行 CORS 预检请求
+ if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
+ return true;
+ }
+
+ String token = request.getHeader("Authorization");
+ if (token == null || !token.startsWith("Bearer ")) {
+ writeError(response, ResultCode.UNAUTHORIZED);
+ return false;
+ }
+
+ token = token.substring(7);
+ try {
+ jwtUtil.verifyToken(token);
+ // 将用户信息存入上下文,供操作日志等下游使用
+ UserContext.set(jwtUtil.getUserId(token), jwtUtil.getUsername(token));
+ return true;
+ } catch (Exception e) {
+ log.warn("Token验证失败: {}", e.getMessage());
+ writeError(response, ResultCode.UNAUTHORIZED);
+ return false;
+ }
+ }
+
+ @Override
+ public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
+ // 请求结束后清理 ThreadLocal,避免内存泄漏
+ UserContext.clear();
+ }
+
+ private void writeError(HttpServletResponse response, ResultCode resultCode) throws IOException {
+ response.setContentType("application/json;charset=UTF-8");
+ response.setStatus(401);
+ response.getWriter().write(JSON.toJSONString(Result.error(resultCode)));
+ }
+}
diff --git a/src/main/java/com/attendance/config/Knife4jConfig.java b/src/main/java/com/attendance/config/Knife4jConfig.java
new file mode 100644
index 0000000..3ca3687
--- /dev/null
+++ b/src/main/java/com/attendance/config/Knife4jConfig.java
@@ -0,0 +1,27 @@
+package com.attendance.config;
+
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Contact;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.info.License;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class Knife4jConfig {
+
+ @Bean
+ public OpenAPI customOpenAPI() {
+ return new OpenAPI()
+ .info(new Info()
+ .title("教室智能人脸考勤系统 API")
+ .description("教室智能人脸考勤系统后端接口文档")
+ .version("v1.0.0")
+ .contact(new Contact()
+ .name("SmartCampus")
+ .email("admin@school.edu"))
+ .license(new License()
+ .name("Apache 2.0")
+ .url("https://www.apache.org/licenses/LICENSE-2.0")));
+ }
+}
diff --git a/src/main/java/com/attendance/config/MyBatisPlusConfig.java b/src/main/java/com/attendance/config/MyBatisPlusConfig.java
new file mode 100644
index 0000000..e429207
--- /dev/null
+++ b/src/main/java/com/attendance/config/MyBatisPlusConfig.java
@@ -0,0 +1,18 @@
+package com.attendance.config;
+
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class MyBatisPlusConfig {
+
+ @Bean
+ public MybatisPlusInterceptor mybatisPlusInterceptor() {
+ MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+ interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
+ return interceptor;
+ }
+}
diff --git a/src/main/java/com/attendance/config/MyMetaObjectHandler.java b/src/main/java/com/attendance/config/MyMetaObjectHandler.java
new file mode 100644
index 0000000..5674571
--- /dev/null
+++ b/src/main/java/com/attendance/config/MyMetaObjectHandler.java
@@ -0,0 +1,25 @@
+package com.attendance.config;
+
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.ibatis.reflection.MetaObject;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+
+@Slf4j
+@Component
+public class MyMetaObjectHandler implements MetaObjectHandler {
+
+ @Override
+ public void insertFill(MetaObject metaObject) {
+ this.strictInsertFill(metaObject, "createdAt", LocalDateTime.class, LocalDateTime.now());
+ this.strictInsertFill(metaObject, "updatedAt", LocalDateTime.class, LocalDateTime.now());
+ this.strictInsertFill(metaObject, "deleted", Integer.class, 0);
+ }
+
+ @Override
+ public void updateFill(MetaObject metaObject) {
+ this.strictUpdateFill(metaObject, "updatedAt", LocalDateTime.class, LocalDateTime.now());
+ }
+}
diff --git a/src/main/java/com/attendance/config/WebMvcConfig.java b/src/main/java/com/attendance/config/WebMvcConfig.java
new file mode 100644
index 0000000..d92e486
--- /dev/null
+++ b/src/main/java/com/attendance/config/WebMvcConfig.java
@@ -0,0 +1,40 @@
+package com.attendance.config;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+@RequiredArgsConstructor
+public class WebMvcConfig implements WebMvcConfigurer {
+
+ private final JwtInterceptor jwtInterceptor;
+
+ @Override
+ public void addCorsMappings(CorsRegistry registry) {
+ registry.addMapping("/**")
+ .allowedOriginPatterns("*")
+ .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
+ .allowedHeaders("*")
+ .allowCredentials(true)
+ .maxAge(3600);
+ }
+
+ @Override
+ public void addInterceptors(InterceptorRegistry registry) {
+ registry.addInterceptor(jwtInterceptor)
+ .addPathPatterns("/**")
+ .excludePathPatterns(
+ "/auth/login",
+ "/auth/register",
+ "/v3/api-docs/**",
+ "/swagger-ui/**",
+ "/swagger-ui.html",
+ "/doc.html",
+ "/webjars/**",
+ "/favicon.ico"
+ );
+ }
+}
diff --git a/src/main/java/com/attendance/controller/AttRuleController.java b/src/main/java/com/attendance/controller/AttRuleController.java
new file mode 100644
index 0000000..5f2605d
--- /dev/null
+++ b/src/main/java/com/attendance/controller/AttRuleController.java
@@ -0,0 +1,73 @@
+package com.attendance.controller;
+
+import com.attendance.annotation.OperationLog;
+import com.attendance.common.Result;
+import com.attendance.entity.AttRule;
+import com.attendance.service.AttRuleService;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@Tag(name = "考勤规则", description = "考勤规则设置接口")
+@RestController
+@RequestMapping("/att-rule")
+public class AttRuleController {
+
+ private final AttRuleService attRuleService;
+
+ public AttRuleController(AttRuleService attRuleService) {
+ this.attRuleService = attRuleService;
+ }
+
+ @Operation(summary = "分页查询考勤规则")
+ @GetMapping("/page")
+ public Result> page(
+ @RequestParam(defaultValue = "1") Long current,
+ @RequestParam(defaultValue = "10") Long size) {
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ wrapper.orderByDesc(AttRule::getCreatedAt);
+ return Result.success(attRuleService.page(new Page<>(current, size), wrapper));
+ }
+
+ @Operation(summary = "获取规则详情")
+ @GetMapping("/{id}")
+ public Result getById(@PathVariable Long id) {
+ return Result.success(attRuleService.getById(id));
+ }
+
+ @OperationLog(module = "考勤规则", action = "新增考勤规则")
+ @Operation(summary = "新增考勤规则")
+ @PostMapping
+ public Result save(@RequestBody AttRule rule) {
+ attRuleService.save(rule);
+ return Result.ok("新增成功");
+ }
+
+ @OperationLog(module = "考勤规则", action = "修改考勤规则")
+ @Operation(summary = "修改考勤规则")
+ @PutMapping("/{id}")
+ public Result update(@PathVariable Long id, @RequestBody AttRule rule) {
+ rule.setId(id);
+ attRuleService.updateById(rule);
+ return Result.ok("修改成功");
+ }
+
+ @OperationLog(module = "考勤规则", action = "删除考勤规则")
+ @Operation(summary = "删除考勤规则")
+ @DeleteMapping
+ public Result remove(@Parameter(description = "规则ID列表") @RequestBody List ids) {
+ attRuleService.removeByIds(ids);
+ return Result.ok("删除成功");
+ }
+
+ @Operation(summary = "获取全部规则")
+ @GetMapping("/list")
+ public Result> list() {
+ return Result.success(attRuleService.list());
+ }
+}
diff --git a/src/main/java/com/attendance/controller/AttendanceController.java b/src/main/java/com/attendance/controller/AttendanceController.java
new file mode 100644
index 0000000..5ef8a50
--- /dev/null
+++ b/src/main/java/com/attendance/controller/AttendanceController.java
@@ -0,0 +1,151 @@
+package com.attendance.controller;
+
+import com.attendance.annotation.OperationLog;
+import com.attendance.common.Result;
+import com.attendance.entity.AttDetail;
+import com.attendance.entity.AttRecord;
+import com.attendance.entity.AttTask;
+import com.attendance.entity.Student;
+import com.attendance.service.AttDetailService;
+import com.attendance.service.AttRecordService;
+import com.attendance.service.AttTaskService;
+import com.attendance.vo.AttRecordVO;
+import com.attendance.vo.AttTaskVO;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDate;
+import java.util.List;
+
+@Tag(name = "考勤管理", description = "考勤任务、记录、明细及历史查询接口")
+@RestController
+@RequestMapping("/attendance")
+public class AttendanceController {
+
+ private final AttTaskService attTaskService;
+ private final AttRecordService attRecordService;
+ private final AttDetailService attDetailService;
+
+ public AttendanceController(AttTaskService attTaskService, AttRecordService attRecordService, AttDetailService attDetailService) {
+ this.attTaskService = attTaskService;
+ this.attRecordService = attRecordService;
+ this.attDetailService = attDetailService;
+ }
+
+ @Operation(summary = "分页查询考勤任务")
+ @GetMapping("/task/page")
+ public Result> taskPage(
+ @Parameter(description = "页码") @RequestParam(defaultValue = "1") Long current,
+ @Parameter(description = "每页条数") @RequestParam(defaultValue = "10") Long size,
+ @Parameter(description = "考勤日期(yyyy-MM-dd)") @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate attDate,
+ @Parameter(description = "关键字(课程名/教室名/教师名)") @RequestParam(required = false) String keyword,
+ @Parameter(description = "任务状态:0-未开始 1-进行中 2-已结束 3-已取消") @RequestParam(required = false) Integer taskStatus) {
+ Page page = new Page<>(current, size);
+ return Result.success(attTaskService.getTaskPage(page, attDate, keyword, taskStatus));
+ }
+
+ @Operation(summary = "获取考勤任务详情")
+ @GetMapping("/task/{id}")
+ public Result getTaskById(@PathVariable Long id) {
+ return Result.success(attTaskService.getById(id));
+ }
+
+ @OperationLog(module = "考勤管理", action = "新增考勤任务")
+ @Operation(summary = "新增考勤任务")
+ @PostMapping("/task")
+ public Result saveTask(@RequestBody AttTask task) {
+ attTaskService.save(task);
+ return Result.ok("新增成功");
+ }
+
+ @OperationLog(module = "考勤管理", action = "修改考勤任务")
+ @Operation(summary = "修改考勤任务")
+ @PutMapping("/task/{id}")
+ public Result updateTask(@PathVariable Long id, @RequestBody AttTask task) {
+ task.setId(id);
+ attTaskService.updateById(task);
+ return Result.ok("修改成功");
+ }
+
+ @OperationLog(module = "考勤管理", action = "删除考勤任务")
+ @Operation(summary = "删除考勤任务")
+ @DeleteMapping("/task")
+ public Result removeTask(@Parameter(description = "考勤任务ID列表") @RequestBody List ids) {
+ attTaskService.removeByIds(ids);
+ return Result.ok("删除成功");
+ }
+
+ @Operation(summary = "分页查询考勤记录汇总")
+ @GetMapping("/record/page")
+ public Result> recordPage(
+ @RequestParam(defaultValue = "1") Long current,
+ @RequestParam(defaultValue = "10") Long size,
+ @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate attDate) {
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ if (attDate != null) {
+ wrapper.eq(AttRecord::getAttDate, attDate);
+ }
+ wrapper.orderByDesc(AttRecord::getAttDate);
+ return Result.success(attRecordService.page(new Page<>(current, size), wrapper));
+ }
+
+ @Operation(summary = "获取考勤记录详情")
+ @GetMapping("/record/{id}")
+ public Result getRecordById(@PathVariable Long id) {
+ return Result.success(attRecordService.getById(id));
+ }
+
+ @Operation(summary = "根据任务ID查询考勤明细")
+ @GetMapping("/detail/{taskId}")
+ public Result> getDetailsByTaskId(@PathVariable Long taskId) {
+ return Result.success(attDetailService.getDetailsByTaskId(taskId));
+ }
+
+ @OperationLog(module = "考勤管理", action = "修改考勤明细")
+ @Operation(summary = "修改考勤明细")
+ @PutMapping("/detail/{id}")
+ public Result updateDetail(@PathVariable Long id, @RequestBody AttDetail detail) {
+ detail.setId(id);
+ attDetailService.updateById(detail);
+ return Result.ok("修改成功");
+ }
+
+ @Operation(summary = "分页查询考勤明细")
+ @GetMapping("/detail/page")
+ public Result> detailPage(
+ @RequestParam(defaultValue = "1") Long current,
+ @RequestParam(defaultValue = "10") Long size,
+ @RequestParam(required = false) Long taskId,
+ @RequestParam(required = false) String studentName,
+ @RequestParam(required = false) Integer attStatus) {
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ if (studentName != null && !studentName.isEmpty()) {
+ wrapper.like(AttDetail::getStudentName, studentName);
+ }
+ if (attStatus != null) {
+ wrapper.eq(AttDetail::getAttStatus, attStatus);
+ }
+ if (taskId != null) {
+ wrapper.eq(AttDetail::getTaskId, taskId);
+ }
+ return Result.success(attDetailService.page(new Page<>(current, size), wrapper));
+ }
+
+ @Operation(summary = "历史考勤查询")
+ @GetMapping("/history")
+ public Result> history(
+ @RequestParam(defaultValue = "1") Long current,
+ @RequestParam(defaultValue = "10") Long size,
+ @Parameter(description = "课程名称") @RequestParam(required = false) String courseName,
+ @Parameter(description = "考勤日期(yyyy-MM-dd)") @RequestParam(required = false) String attDate) {
+ Page page = new Page<>(current, size);
+ return Result.success(attRecordService.getHistoryPage(page, courseName, attDate));
+ }
+
+}
diff --git a/src/main/java/com/attendance/controller/AuthController.java b/src/main/java/com/attendance/controller/AuthController.java
new file mode 100644
index 0000000..e239ccc
--- /dev/null
+++ b/src/main/java/com/attendance/controller/AuthController.java
@@ -0,0 +1,35 @@
+package com.attendance.controller;
+
+import com.attendance.common.Result;
+import com.attendance.dto.LoginDTO;
+import com.attendance.dto.LoginResultDTO;
+import com.attendance.service.SysUserService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+
+@Tag(name = "认证管理", description = "用户登录相关接口")
+@RestController
+@RequestMapping("/auth")
+public class AuthController {
+
+ private final SysUserService sysUserService;
+
+ public AuthController(SysUserService sysUserService) {
+ this.sysUserService = sysUserService;
+ }
+
+ @Operation(summary = "用户登录")
+ @PostMapping("/login")
+ public Result login(@Valid @RequestBody LoginDTO loginDTO) {
+ return Result.success(sysUserService.login(loginDTO));
+ }
+
+ @Operation(summary = "用户登出")
+ @PostMapping("/logout")
+ public Result logout() {
+ return Result.success();
+ }
+}
diff --git a/src/main/java/com/attendance/controller/BehaviorController.java b/src/main/java/com/attendance/controller/BehaviorController.java
new file mode 100644
index 0000000..6a28c2c
--- /dev/null
+++ b/src/main/java/com/attendance/controller/BehaviorController.java
@@ -0,0 +1,140 @@
+package com.attendance.controller;
+
+import com.attendance.annotation.OperationLog;
+import com.attendance.common.Result;
+import com.attendance.vo.BehaviorRecordVO;
+import com.attendance.entity.BehaviorRecord;
+import com.attendance.entity.BehaviorType;
+import com.attendance.service.BehaviorRecordService;
+import com.attendance.service.BehaviorTypeService;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDate;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Tag(name = "课堂行为分析", description = "课堂行为识别与统计接口")
+@RestController
+@RequestMapping("/behavior")
+public class BehaviorController {
+
+ private final BehaviorRecordService behaviorRecordService;
+ private final BehaviorTypeService behaviorTypeService;
+
+ public BehaviorController(BehaviorRecordService behaviorRecordService, BehaviorTypeService behaviorTypeService) {
+ this.behaviorRecordService = behaviorRecordService;
+ this.behaviorTypeService = behaviorTypeService;
+ }
+
+ @Operation(summary = "分页查询行为记录")
+ @GetMapping("/page")
+ public Result> page(
+ @RequestParam(defaultValue = "1") Long current,
+ @RequestParam(defaultValue = "10") Long size,
+ @RequestParam(required = false) Long taskId,
+ @RequestParam(required = false) Long behaviorTypeId) {
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ if (taskId != null) {
+ wrapper.eq(BehaviorRecord::getTaskId, taskId);
+ }
+ if (behaviorTypeId != null) {
+ wrapper.eq(BehaviorRecord::getBehaviorTypeId, behaviorTypeId);
+ }
+ wrapper.orderByDesc(BehaviorRecord::getBehaviorTime);
+ return Result.success(behaviorRecordService.page(new Page<>(current, size), wrapper));
+ }
+
+ @Operation(summary = "获取行为类型列表")
+ @GetMapping("/types")
+ public Result> getTypes() {
+ return Result.success(behaviorTypeService.list(
+ new LambdaQueryWrapper().eq(BehaviorType::getStatus, 1).orderByAsc(BehaviorType::getSortOrder)));
+ }
+
+ @Operation(summary = "获取行为类型列表(含统计次数,支持筛选)")
+ @GetMapping("/types-with-stats")
+ public Result> getTypesWithStats(
+ @RequestParam(required = false) @Parameter(description = "课程ID") Long courseId,
+ @RequestParam(required = false) @Parameter(description = "教师ID") Long teacherId,
+ @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") @Parameter(description = "考勤日期,格式:yyyy-MM-dd") LocalDate attDate) {
+ List types = behaviorTypeService.list(
+ new LambdaQueryWrapper().eq(BehaviorType::getStatus, 1).orderByAsc(BehaviorType::getSortOrder));
+ List