大家好,我是Java1234_小锋老师,分享一套锋哥原创的基于Java的微信小程序实验室预约管理系统(SpringBoot4+Vue3)

项目介绍
随着高校教学与科研规模的不断扩大,实验室作为重要的教学与实验资源,其预约与使用管理日益复杂。传统的实验室预约方式主要依靠人工登记、电话预约或纸质表单,存在信息不透明、预约冲突频繁、审核效率低下、使用情况难以追踪等诸多问题,已难以满足现代高校精细化管理的需求。为解决上述问题,本文设计并实现了一套微信小程序实验室预约管理系统。
本系统采用前后端分离的架构模式进行开发,包含三个终端:面向学生的微信小程序端、面向管理员的 Web 管理后台以及面向老师的审核端。学生通过微信小程序随时随地浏览实验室、提交预约申请、到场签到签退;管理员通过基于 Vue3 的管理后台维护实验室、设备、公告等信息并审核预约;老师端则可对本人相关的预约进行审核。服务端采用 SpringBoot4 框架搭建 RESTful 接口,使用 MyBatis 作为持久层框架,数据存储采用 MySQL 数据库,并通过 JWT 实现无状态的身份认证。
系统主要实现了学生端的登录注册、首页浏览、实验室查询、在线预约、我的预约(取消、签到、签退)、公告查看、个人中心等功能,以及管理后台的首页统计、学生管理、老师管理、管理员管理、实验室分类管理、实验室管理、设备管理、预约审核、使用记录、公告管理、轮播图管理等功能。在预约提交环节,系统对同一实验室、同一日期、同一时段进行冲突校验,有效避免了重复预约。经过功能测试,系统运行稳定、操作便捷,能够较好地满足高校实验室预约管理的实际需求。
本文详细阐述了系统的需求分析、总体设计、数据库设计、详细实现与系统测试的全过程,并给出了关键功能模块的核心代码与设计图示。
源码下载
链接: https://pan.baidu.com/s/1xIAd4fPzBMFQgcn9vRXa1Q?pwd=1234
提取码: 1234
系统展示











核心代码
package com.java1234.controller;
import com.java1234.common.Result;
import com.java1234.dto.PageResult;
import com.java1234.entity.Equipment;
import com.java1234.service.EquipmentService;
import com.java1234.util.WebAuth;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 设备管理(管理员)
* @author 小锋老师
*/
@RestController
@RequestMapping("/equipment")
@RequiredArgsConstructor
public class EquipmentManageController {
private final EquipmentService equipmentService;
@GetMapping("/page")
public Result<PageResult<Equipment>> page(HttpServletRequest request,
@RequestParam(required = false) Long labId,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String keyword) {
WebAuth.requireAdmin(request);
return Result.success(equipmentService.pageByLab(page, size, labId, keyword));
}
@PostMapping
public Result<Void> add(HttpServletRequest request, @RequestBody Equipment e) {
WebAuth.requireAdmin(request);
equipmentService.save(e);
return Result.success();
}
@PutMapping
public Result<Void> update(HttpServletRequest request, @RequestBody Equipment e) {
WebAuth.requireAdmin(request);
equipmentService.update(e);
return Result.success();
}
@DeleteMapping("/{id}")
public Result<Void> delete(HttpServletRequest request, @PathVariable Long id) {
WebAuth.requireAdmin(request);
equipmentService.delete(id);
return Result.success();
}
}
<template>
<div class="page-card">
<el-form inline class="admin-table-toolbar">
<el-button type="primary" @click="load">刷新</el-button>
<el-button type="success" @click="open()">新增轮播</el-button>
</el-form>
<el-table :data="rows" v-loading="loading" class="mt" border stripe style="width: 100%">
<el-table-column prop="id" width="64" label="ID" />
<el-table-column label="预览" min-width="160">
<template #default="{ row }">
<div v-if="row.image" class="preview-img-wrap">
<el-image :src="resolveImg(row.image)" fit="cover" class="preview-img" />
</div>
<div v-else class="preview-bg" :style="{ background: row.bgStyle || '#6366f1' }" />
</template>
</el-table-column>
<el-table-column prop="sort" width="72" label="排序" />
<el-table-column label="状态" width="88">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'">{{ row.status === 1 ? '启用' : '禁用' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" width="190">
<template #default="{ row }">{{ formatDateTimeDisplay(row.createTime) }}</template>
</el-table-column>
<el-table-column width="150" label="操作">
<template #default="{ row }">
<el-button link @click="open(row)">编辑</el-button>
<el-button link type="danger" @click="remove(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination class="mt" v-model:current-page="q.page" :total="total" :page-size="q.size" layout="total, prev, pager, next" @current-change="load" />
<el-dialog v-model="vis" :title="f.id ? '编辑轮播' : '新增轮播'" width="560px" destroy-on-close>
<el-form :model="f" label-width="88px">
<el-form-item label="轮播图片">
<el-input v-model="f.image" placeholder="可选,上传后自动填写;留空则使用渐变背景" />
<el-upload :show-file-list="false" :http-request="upImg" accept="image/*" class="up">
<el-button size="small">本地上传</el-button>
</el-upload>
</el-form-item>
<el-form-item label="渐变背景">
<el-input v-model="f.bgStyle" placeholder="无图时生效,如 linear-gradient(90deg,#6366f1,#8b5cf6)" />
</el-form-item>
<el-form-item label="跳转链接"><el-input v-model="f.linkUrl" placeholder="可选,小程序页面路径或外链" /></el-form-item>
<el-form-item label="排序"><el-input-number v-model="f.sort" :min="0" :max="9999" /></el-form-item>
<el-form-item label="状态"><el-switch v-model="f.status" :active-value="1" :inactive-value="0" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="vis = false">取消</el-button>
<el-button type="primary" @click="ok">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue'
import { adminApi } from '@/api/admin'
import { ElMessage, ElMessageBox } from 'element-plus'
import { formatDateTimeDisplay } from '@/utils/datetimeDisplay'
const q = reactive({ page: 1, size: 10 })
const rows = ref([])
const total = ref(0)
const loading = ref(false)
const vis = ref(false)
const f = reactive({ sort: 0, status: 1 })
function resolveImg(path) {
if (!path) return ''
if (path.startsWith('http://') || path.startsWith('https://')) return path
if (path.startsWith('/')) return path
return '/api/uploads/' + path
}
async function load() {
loading.value = true
try {
const r = await adminApi.bannerPage({ page: q.page, size: q.size })
rows.value = r.data.records
total.value = r.data.total
} finally {
loading.value = false
}
}
function open(row) {
if (row) {
Object.assign(f, { ...row, status: row.status ? 1 : 0 })
} else {
Object.assign(f, { id: null, image: '', bgStyle: 'linear-gradient(90deg,#6366f1,#8b5cf6)', linkUrl: '', sort: 0, status: 1 })
}
vis.value = true
}
async function upImg({ file }) {
const form = new FormData()
form.append('file', file)
const r = await adminApi.upload(form)
f.image = r.data.url
ElMessage.success('上传成功')
}
async function ok() {
if (!f.image?.trim() && !f.bgStyle?.trim()) {
ElMessage.warning('请上传轮播图片或填写渐变背景')
return
}
if (f.id) await adminApi.bannerUp(f)
else await adminApi.bannerAdd(f)
vis.value = false
ElMessage.success('已保存')
load()
}
function remove(row) {
ElMessageBox.confirm('确认删除该轮播?', { type: 'warning' })
.then(() => adminApi.bannerDel(row.id))
.then(() => {
load()
ElMessage.success('已删除')
})
.catch(() => {})
}
onMounted(load)
</script>
<style scoped>
.mt {
margin-top: 12px;
justify-content: flex-end;
}
.up {
margin-top: 8px;
}
.preview-img-wrap,
.preview-bg {
width: 160px;
height: 72px;
border-radius: 6px;
overflow: hidden;
}
.preview-img {
width: 160px;
height: 72px;
}
</style>
43万+

被折叠的 条评论
为什么被折叠?



