【原创唯一】基于微信小程序+uni-app+vue的课堂打卡签到考勤系统 课程设计/大作业/期末作业(源码+MySQL数据库+实验报告+PPT+远程部署)
系统摘要
随着移动互联网与微信小程序的普及,高校课堂考勤管理逐步从纸质点名、教室终端签到向移动端延伸。传统 Web 管理端虽能满足教师与管理员的操作需求,但学生参与签到往往需要借助 PC 浏览器,使用门槛较高、场景适配不足。本文以校园课堂考勤业务为背景,在既有 Spring Boot 后端与 Vue 3 管理前台基础上,采用 uni-app 3、Vue 3 Composition API 与 uView Plus 组件库,设计并实现了面向学生角色的微信小程序端。
小程序遵循「展示功能免登录、交互功能需登录」的产品策略:首页统计看板与签到场次浏览面向访客开放;课堂签到、个人考勤查询、资料维护等操作须以学生身份完成 JWT 认证后方可执行。系统后端基于 MySQL 存储用户、班级、签到场次与考勤明细,通过 RESTful API 与小程序通信;前端采用页面自治架构,公共能力沉淀于 utils 工具层,底部 TabBar 配置语义化图标以提升导航辨识度。
本文按照软件工程方法,从需求分析、总体设计、数据库设计、系统实现到系统测试完整论述了小程序的工程化落地过程。测试结果表明,各功能模块运行稳定,能够满足学生移动端签到与考勤查询的实际需求。
技术栈:Spring Boot3+uni-app+Vue3+uViewPlus+Vite+MybatsiPlus+Echarts+微信小程序
数据库表:6张
🍅文末获取联系🍅
🍅文末获取联系🍅
作者介绍:专注于计算机课设、毕设辅导,本人开发,原创代码,一题一稿,绝不撞题,坚持原创,个人创作,非工作室,源码全网唯一。
✅ 原创唯一:个人原创开发,独立设计数据库与业务逻辑,拒绝工作室代码改造
✅ 技术主流:SpringBoot+Vue+uni-app前后端分离,MySQL,Echarts,可本地运行
✅ 配套资料:源码 + 数据库 + 实验报告/论文 + 答辩 PPT+部署演示+远程调试+问题解答

技术范围:SpringBoot、Vue、数据可视化、小程序、HLMT、Nodejs、uni-app、MySQL数据库、ElementUi等设计与开发。
适用范围:软件工程、软件技术、数据库课程设计、计算机科学与技术、数据库系统原理、JavaWeb开发、JavaEE、Java、Web应用开发、动态网页设计的课程设计、课设、大作业、课程实验、期末作业
实验报告参考内容
实验报告可供大家参考使用



用户端功能(小程序)



教师端功能




管理员端功能



数据库设计
系统数据库设计为:

表4-1 admins(管理员表)
表说明:后台管理员账号,小程序不涉及
|
字段名 |
类型 |
约束 |
说明 |
|
id |
BIGINT |
PK, AUTO_INCREMENT |
主键 |
|
username |
VARCHAR(50) |
NOT NULL, UNIQUE |
登录账号 |
|
password |
VARCHAR(100) |
NOT NULL |
BCrypt 加密密码 |
|
real_name |
VARCHAR(50) |
NULL |
姓名 |
|
phone |
VARCHAR(20) |
NULL |
手机号 |
|
enabled |
TINYINT |
DEFAULT 1 |
启用状态 |
|
created_at |
DATETIME |
NULL |
创建时间 |
表4-2 teachers(教师表)
表说明:授课教师账号
|
字段名 |
类型 |
约束 |
说明 |
|
id |
BIGINT |
PK, AUTO_INCREMENT |
主键 |
|
username |
VARCHAR(50) |
NOT NULL, UNIQUE |
登录账号 |
|
password |
VARCHAR(100) |
NOT NULL |
BCrypt 密码 |
|
real_name |
VARCHAR(50) |
NULL |
姓名 |
|
phone |
VARCHAR(20) |
NULL |
手机号 |
|
employee_no |
VARCHAR(30) |
NULL |
工号 |
|
enabled |
TINYINT |
DEFAULT 1 |
启用状态 |
|
created_at |
DATETIME |
NULL |
创建时间 |
表4-3 classes(班级表)
表说明:教学班级基础数据
|
字段名 |
类型 |
约束 |
说明 |
|
id |
BIGINT |
PK, AUTO_INCREMENT |
主键 |
|
class_code |
VARCHAR(30) |
NOT NULL, UNIQUE |
班级编码 |
|
name |
VARCHAR(100) |
NOT NULL |
班级名称 |
|
grade |
VARCHAR(30) |
NULL |
年级 |
|
major |
VARCHAR(100) |
NULL |
专业 |
|
teacher_id |
BIGINT |
FK→teachers |
负责教师 |
|
created_at |
DATETIME |
NULL |
创建时间 |
表4-4 students(学生表)
表说明:学生账号,小程序登录主体
|
字段名 |
类型 |
约束 |
说明 |
|
id |
BIGINT |
PK, AUTO_INCREMENT |
主键 |
|
username |
VARCHAR(50) |
NOT NULL, UNIQUE |
登录账号 |
|
password |
VARCHAR(100) |
NOT NULL |
BCrypt 密码 |
|
real_name |
VARCHAR(50) |
NULL |
姓名 |
|
phone |
VARCHAR(20) |
NULL |
手机号 |
|
student_no |
VARCHAR(30) |
NULL |
学号 |
|
class_id |
BIGINT |
FK→classes |
所属班级 |
|
enabled |
TINYINT |
DEFAULT 1 |
启用状态 |
|
created_at |
DATETIME |
NULL |
创建时间 |
学生注册接口写入本表,class_id 可由管理员后续分配。
表4-5 attendance_sessions(签到场次表)
表说明:教师发起的课堂签到任务
|
字段名 |
类型 |
约束 |
说明 |
|
id |
BIGINT |
PK, AUTO_INCREMENT |
主键 |
|
title |
VARCHAR(200) |
NOT NULL |
签到标题 |
|
class_id |
BIGINT |
FK→classes |
目标班级 |
|
teacher_id |
BIGINT |
FK→teachers |
发起教师 |
|
session_date |
DATE |
NOT NULL |
签到日期 |
|
start_time |
TIME |
NULL |
开始时间 |
|
end_time |
TIME |
NULL |
截止时间 |
|
status |
VARCHAR(20) |
DEFAULT 'OPEN' |
OPEN 进行中 / CLOSED 已结束 |
|
created_at |
DATETIME |
NULL |
创建时间 |
表4-6 attendance_records(考勤明细表)
表说明:学生在各场次的考勤状态
|
字段名 |
类型 |
约束 |
说明 |
|
id |
BIGINT |
PK, AUTO_INCREMENT |
主键 |
|
session_id |
BIGINT |
FK→attendance_sessions |
签到场次 |
|
student_id |
BIGINT |
FK→students |
学生 |
|
status |
VARCHAR(20) |
DEFAULT 'PENDING' |
PENDING/PRESENT/LATE/ABSENT/MAKEUP |
|
sign_time |
DATETIME |
NULL |
签到登记时间 |
|
remark |
VARCHAR(200) |
NULL |
备注 |
|
created_at |
DATETIME |
NULL |
创建时间 |
系统架构

Controller及Service层核心代码写法:
@Service
@RequiredArgsConstructor
public class AttendanceService {
private static final DateTimeFormatter DT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private final AttendanceSessionMapper sessionMapper;
private final AttendanceRecordMapper recordMapper;
private final StudentMapper studentMapper;
private final RelationFillHelper relationFillHelper;
/** 公开浏览签到场次(小程序未登录可查看) */
public PageResult<AttendanceSession> browseSessions(String keyword, int page, int size) {
Page<AttendanceSession> result = sessionMapper.selectPage(new Page<>(page, size),
Wrappers.<AttendanceSession>lambdaQuery()
.and(StringUtils.hasText(keyword), w -> w
.like(AttendanceSession::getTitle, keyword))
.orderByDesc(AttendanceSession::getSession_date)
.orderByDesc(AttendanceSession::getId));
result.getRecords().forEach(this::finalizeExpiredSession);
relationFillHelper.fillSessions(result.getRecords());
return PageResult.of(result);
}
public PageResult<AttendanceSession> listSessions(String keyword, Long classId, int page, int size) {
Long teacherId = AuthContext.getRole() == UserRole.TEACHER ? AuthContext.getUserId() : null;
Page<AttendanceSession> result = sessionMapper.selectPage(new Page<>(page, size),
Wrappers.<AttendanceSession>lambdaQuery()
.eq(teacherId != null, AttendanceSession::getTeacher_id, teacherId)
.eq(classId != null, AttendanceSession::getClass_id, classId)
.and(StringUtils.hasText(keyword), w -> w
.like(AttendanceSession::getTitle, keyword))
.orderByDesc(AttendanceSession::getSession_date)
.orderByDesc(AttendanceSession::getId));
result.getRecords().forEach(this::finalizeExpiredSession);
relationFillHelper.fillSessions(result.getRecords());
return PageResult.of(result);
}
public AttendanceSession getSession(Long id) {
AttendanceSession session = sessionMapper.selectById(id);
if (session == null) {
throw new RuntimeException("签到场次不存在");
}
finalizeExpiredSession(session);
assertCanManageSession(session);
relationFillHelper.fillSession(session);
return session;
}
@Transactional
public AttendanceSession createSession(AttendanceSessionDTO dto) {
if (AuthContext.getRole() != UserRole.TEACHER) {
throw new RuntimeException("仅教师可创建签到");
}
AttendanceSession session = new AttendanceSession();
session.setTitle(dto.getTitle());
session.setClass_id(dto.getClass_id());
session.setTeacher_id(AuthContext.getUserId());
session.setSession_date(dto.getSession_date());
session.setStart_time(dto.getStart_time());
session.setEnd_time(dto.getEnd_time());
session.setStatus("OPEN");
sessionMapper.insert(session);
List<Student> students = studentMapper.selectList(
Wrappers.<Student>lambdaQuery().eq(Student::getClass_id, dto.getClass_id()));
for (Student s : students) {
AttendanceRecord record = new AttendanceRecord();
record.setSession_id(session.getId());
record.setStudent_id(s.getId());
record.setStatus("PENDING");
recordMapper.insert(record);
}
relationFillHelper.fillSession(session);
return session;
}
@Transactional
public AttendanceSession closeSession(Long id) {
AttendanceSession session = sessionMapper.selectById(id);
if (session == null) {
throw new RuntimeException("签到场次不存在");
}
assertCanManageSession(session);
markPendingAsAbsent(session.getId());
session.setStatus("CLOSED");
sessionMapper.updateById(session);
relationFillHelper.fillSession(session);
return session;
}
public PageResult<AttendanceRecord> listRecords(Long sessionId, String status, int page, int size) {
AttendanceSession session = sessionMapper.selectById(sessionId);
if (session == null) {
throw new RuntimeException("签到场次不存在");
}
finalizeExpiredSession(session);
assertCanViewSession(session);
Page<AttendanceRecord> result = recordMapper.selectPage(new Page<>(page, size),
Wrappers.<AttendanceRecord>lambdaQuery()
.eq(AttendanceRecord::getSession_id, sessionId)
.eq(StringUtils.hasText(status), AttendanceRecord::getStatus, status)
.orderByAsc(AttendanceRecord::getId));
relationFillHelper.fillRecords(result.getRecords());
return PageResult.of(result);
}
@Transactional
public AttendanceRecord updateRecord(Long id, AttendanceRecordDTO dto) {
AttendanceRecord record = recordMapper.selectById(id);
if (record == null) {
throw new RuntimeException("考勤记录不存在");
}
AttendanceSession session = sessionMapper.selectById(record.getSession_id());
assertCanManageSession(session);
validateStatus(dto.getStatus());
record.setStatus(dto.getStatus());
record.setRemark(dto.getRemark());
if ("PRESENT".equals(dto.getStatus()) || "LATE".equals(dto.getStatus()) || "MAKEUP".equals(dto.getStatus())) {
if (record.getSign_time() == null) {
record.setSign_time(LocalDateTime.now());
}
}
recordMapper.updateById(record);
relationFillHelper.fillRecord(record);
return record;
}
@Transactional
public AttendanceRecord makeup(Long id, String remark) {
AttendanceRecordDTO dto = new AttendanceRecordDTO();
dto.setStatus("MAKEUP");
dto.setRemark(remark);
return updateRecord(id, dto);
}
public List<AttendanceRecord> listOpenForStudent() {
if (AuthContext.getRole() != UserRole.STUDENT) {
throw new RuntimeException("仅学生可查看待签到场次");
}
Student student = studentMapper.selectById(AuthContext.getUserId());
if (student == null || student.getClass_id() == null) {
return List.of();
}
List<AttendanceSession> sessions = sessionMapper.selectList(
Wrappers.<AttendanceSession>lambdaQuery()
.eq(AttendanceSession::getClass_id, student.getClass_id())
.eq(AttendanceSession::getStatus, "OPEN")
.orderByDesc(AttendanceSession::getSession_date)
.orderByDesc(AttendanceSession::getId));
List<AttendanceRecord> result = new ArrayList<>();
for (AttendanceSession session : sessions) {
finalizeExpiredSession(session);
if (!"OPEN".equals(session.getStatus())) {
continue;
}
AttendanceRecord record = recordMapper.selectOne(
Wrappers.<AttendanceRecord>lambdaQuery()
.eq(AttendanceRecord::getSession_id, session.getId())
.eq(AttendanceRecord::getStudent_id, student.getId()));
if (record != null && "PENDING".equals(record.getStatus())) {
relationFillHelper.fillSession(session);
record.setSession(session);
result.add(record);
}
}
return result;
}
@Transactional
public AttendanceRecord checkIn(Long sessionId) {
if (AuthContext.getRole() != UserRole.STUDENT) {
throw new RuntimeException("仅学生可签到");
}
Student student = studentMapper.selectById(AuthContext.getUserId());
if (student == null || student.getClass_id() == null) {
throw new RuntimeException("未分配班级,无法签到");
}
AttendanceSession session = sessionMapper.selectById(sessionId);
if (session == null) {
throw new RuntimeException("签到场次不存在");
}
finalizeExpiredSession(session);
if (!"OPEN".equals(session.getStatus())) {
throw new RuntimeException("签到已结束");
}
if (!student.getClass_id().equals(session.getClass_id())) {
throw new RuntimeException("不属于该班级签到");
}
LocalDateTime now = LocalDateTime.now();
LocalDateTime deadline = sessionDeadline(session);
if (deadline != null && now.isAfter(deadline)) {
markPendingAsAbsent(session.getId());
session.setStatus("CLOSED");
sessionMapper.updateById(session);
throw new RuntimeException("已过签到截止时间");
}
AttendanceRecord record = recordMapper.selectOne(
Wrappers.<AttendanceRecord>lambdaQuery()
.eq(AttendanceRecord::getSession_id, sessionId)
.eq(AttendanceRecord::getStudent_id, student.getId()));
if (record == null) {
throw new RuntimeException("考勤记录不存在");
}
if (!"PENDING".equals(record.getStatus())) {
throw new RuntimeException("当前状态不可签到");
}
// 签到时段内主动签到记为出勤;迟到由教师/管理员手动标记
record.setStatus("PRESENT");
record.setSign_time(now);
recordMapper.updateById(record);
relationFillHelper.fillRecord(record);
return record;
}
}
博主本身从事软件开发、有丰富的编程能力和水平,累积给上千名同学进行辅导,论文纯手写查重低于10%,全都顺利通过答辩!
擅长:功能设计、开题报告、任务书、中期检查PPT、系统功能实现、代码编写、论文编写和辅导、论文降重、长期答辩答疑辅导、腾讯会议一对一专业讲解辅导答辩、模拟答辩演练、和理解代码逻辑思路等。
更多个人原创作品👇🏻
获取联系
项目功能完整,可在本地运行,并可远程调试,确保运行顺利!
查看👇🏻👇🏻获取联系方式👇🏻👇🏻
更多推荐


所有评论(0)