系统摘要

随着移动互联网与微信小程序的普及,高校课堂考勤管理逐步从纸质点名、教室终端签到向移动端延伸。传统 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、系统功能实现、代码编写、论文编写和辅导、论文降重、长期答辩答疑辅导、腾讯会议一对一专业讲解辅导答辩、模拟答辩演练、和理解代码逻辑思路等。

更多个人原创作品👇🏻

原创课程设计大全✅

原创毕业设计集合✅

获取联系

项目功能完整,可在本地运行,并可远程调试,确保运行顺利!

查看👇🏻👇🏻获取联系方式👇🏻👇🏻

更多推荐