高校实验室管理常面临设备预约混乱、耗材申领低效、使用记录零散等问题。基于 SpringBoot + 微信小程序的实验室管理系统,能实现 “设备预约 - 耗材申领 - 使用登记 - 数据统计” 全流程线上化,是 Java Web 方向兼具实用性与技术完整性的开发项目。本文从程序员视角,结合完整代码示例,拆解系统架构设计、核心功能实现及部署交付全流程,附带可直接复用的源码与部署方案。

系统架构设计与技术选型

实验室管理系统采用 “前后端分离” 架构,后端以 SpringBoot 为核心搭建 RESTful 接口服务,前端基于微信小程序原生框架开发,通过 MySQL 存储业务数据,MyBatis 实现数据交互。合理的技术选型与架构设计,是系统稳定性与可扩展性的基础。

1. 技术栈选型

  • 后端技术:SpringBoot 2.7.x(快速开发框架)、MyBatis-Plus 3.5.x(数据访问增强)、Shiro 1.10.x(权限控制)、MySQL 8.0(关系型数据库)、Redis 6.2.x(缓存热点数据);
  • 前端技术:微信小程序原生框架(适配移动端)、Vant Weapp 1.10.x(UI 组件库)、Axios(网络请求封装);
  • 开发工具:IntelliJ IDEA(后端开发)、微信开发者工具(前端开发)、Navicat(数据库管理)。

2. 系统架构与项目结构

后端遵循 “Controller-Service-Mapper” 三层架构,按业务模块划分包结构,确保代码解耦与可维护性:


// 后端项目包结构

com.lab.management

├── config // 配置类(Shiro、Redis、跨域等)

├── controller // 接口层(接收请求、返回响应)

├── service // 业务逻辑层(核心业务处理)

│ └── impl // 业务实现类

├── mapper // 数据访问层(数据库操作接口)

├── entity // 数据库实体类(与表结构映射)

├── dto // 数据传输对象(前后端交互)

├── vo // 视图对象(接口返回格式化数据)

├── util // 工具类(日期、加密、缓存等)

└── LabManagementApplication.java // 项目启动类

前端按功能模块划分页面与组件,核心结构如下:


// 微信小程序前端项目结构

/pages

├── device // 设备管理相关页面

│ ├── list // 设备列表

│ ├── detail // 设备详情

│ └── reserve // 设备预约

├── material // 耗材管理相关页面

│ ├── apply // 耗材申领

│ └── record // 申领记录

├── lab // 实验室管理相关页面

│ ├── info // 实验室信息

│ └── usage // 使用登记

├── user // 用户相关页面

│ ├── login // 登录授权

│ └── center // 个人中心

├── admin // 管理员页面

│ ├── device // 设备管理(增删改查)

│ └── statistics // 数据统计

/utils // 工具类(请求封装、权限判断等)

/components // 自定义组件(设备卡片、预约日历等)

3. 核心配置实现

数据库配置(application.yml)


spring:

datasource:

driver-class-name: com.mysql.cj.jdbc.Driver

url: jdbc:mysql://localhost:3306/lab_management?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&allowMultiQueries=true

username: root

password: root

redis: # Redis缓存配置(用于设备预约状态缓存)

host: localhost

port: 6379

password:

database: 0

# MyBatis-Plus配置

mybatis-plus:

mapper-locations: classpath:mapper/*.xml # Mapper XML文件路径

type-aliases-package: com.lab.management.entity # 实体类别名包

configuration:

log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL日志(开发环境)

global-config:

db-config:

id-type: auto # 主键自增策略

# 自定义配置(文件上传路径,用于设备图片、耗材图片存储)

lab:

upload:

base-path: /var/lab/upload/

access-url: http://localhost:8080/upload/

Shiro 权限配置(核心权限控制)


@Configuration

public class ShiroConfig {

// 1. 配置SecurityManager

@Bean

public SecurityManager securityManager(UserRealm userRealm, RedisCacheManager cacheManager) {

DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

securityManager.setRealm(userRealm);

// 配置Redis缓存(存储用户权限、会话信息)

securityManager.setCacheManager(cacheManager);

return securityManager;

}

// 2. 配置Shiro过滤器(URL权限控制)

@Bean

public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {

ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean();

filterFactory.setSecurityManager(securityManager);

// 未登录跳转接口(返回JSON,而非页面)

filterFactory.setLoginUrl("/api/user/unauth");

// 权限规则配置

Map<String, String> filterMap = new LinkedHashMap<>();

// 匿名访问接口(无需登录)

filterMap.put("/api/user/login", "anon");

filterMap.put("/api/lab/info/*", "anon");

// 管理员权限接口

filterMap.put("/api/admin/**", "roles[admin]");

// 其他接口需登录

filterMap.put("/api/**", "authc");

filterFactory.setFilterChainDefinitionMap(filterMap);

return filterFactory;

}

// 3. 配置Redis缓存管理器(缓存用户权限)

@Bean

public RedisCacheManager redisCacheManager(RedisManager redisManager) {

RedisCacheManager cacheManager = new RedisCacheManager();

cacheManager.setRedisManager(redisManager);

cacheManager.setCacheKeyPrefix("lab_shiro:"); // 缓存键前缀

return cacheManager;

}

}

核心功能模块实现

系统核心功能涵盖 “设备管理”“预约管理”“耗材申领” 三大模块,以下结合代码示例详解各模块的核心业务逻辑与前端实现。

1. 设备管理模块(后端实现)

设备管理模块包含 “设备信息 CRUD”“设备状态监控”“设备维修登记” 功能,以设备列表查询与详情接口为例:

Controller 层(接口定义)


@RestController

@RequestMapping("/api/device")

@Api(tags = "设备管理接口")

public class DeviceController {

@Autowired

private DeviceService deviceService;

@GetMapping("/list")

@ApiOperation("分页查询设备列表")

public ResultDTO<IPage<DeviceVO>> getDeviceList(

@RequestParam(required = false) String deviceName, // 设备名称模糊查询

@RequestParam(required = false) Integer status, // 设备状态(1-可用 2-占用 3-维修)

@RequestParam(defaultValue = "1") Integer pageNum,

@RequestParam(defaultValue = "10") Integer pageSize) {

Page<DeviceVO> page = new Page<>(pageNum, pageSize);

IPage<DeviceVO> devicePage = deviceService.queryDevicePage(page, deviceName, status);

return ResultDTO.success(devicePage);

}

@GetMapping("/{id}")

@ApiOperation("获取设备详情")

public ResultDTO<DeviceDetailVO> getDeviceDetail(@PathVariable Integer id) {

DeviceDetailVO detailVO = deviceService.getDeviceDetail(id);

return ResultDTO.success(detailVO);

}

@PostMapping

@ApiOperation("新增设备(仅管理员)")

@RequiresRoles("admin") // 仅管理员可调用

public ResultDTO addDevice(@Valid @RequestBody DeviceDTO deviceDTO) {

deviceService.createDevice(deviceDTO);

return ResultDTO.success("设备新增成功");

}

@PutMapping("/{id}/status")

@ApiOperation("更新设备状态(维修/启用)")

@RequiresRoles("admin")

public ResultDTO updateDeviceStatus(@PathVariable Integer id,

@RequestParam Integer status) {

deviceService.updateDeviceStatus(id, status);

return ResultDTO.success("设备状态更新成功");

}

}

Service 层(业务逻辑实现)


@Service

public class DeviceServiceImpl implements DeviceService {

@Autowired

private DeviceMapper deviceMapper;

@Autowired

private DeviceTypeMapper deviceTypeMapper;

@Autowired

private FileUploadUtil fileUploadUtil;

@Autowired

private StringRedisTemplate redisTemplate;

@Override

@Transactional

public void createDevice(DeviceDTO dto) {

// 1. 验证设备类型是否存在

DeviceType deviceType = deviceTypeMapper.selectById(dto.getTypeId());

if (deviceType == null) {

throw new BusinessException("设备类型不存在,请选择有效类型");

}

// 2. 处理设备图片上传(Base64转文件)

String deviceImg = null;

if (StringUtils.isNotBlank(dto.getDeviceImgBase64())) {

deviceImg = fileUploadUtil.uploadBase64Image(dto.getDeviceImgBase64(), "device/");

}

// 3. 转换DTO为实体类

Device device = new Device();

BeanUtils.copyProperties(dto, device);

device.setDeviceImg(deviceImg);

device.setStatus(1); // 初始状态:可用

device.setCreateTime(new Date());

device.setUpdateTime(new Date());

// 4. 保存到数据库

deviceMapper.insert(device);

// 5. 缓存设备状态(可用状态)

redisTemplate.opsForValue().set(

"device:status:" + device.getId(),

"1",

1,

TimeUnit.DAYS

);

}

@Override

public DeviceDetailVO getDeviceDetail(Integer id) {

// 1. 查询设备基本信息

Device device = deviceMapper.selectById(id);

if (device == null) {

throw new BusinessException("设备不存在或已删除");

}

// 2. 关联查询设备类型名称

DeviceType deviceType = deviceTypeMapper.selectById(device.getTypeId());

String typeName = deviceType != null ? deviceType.getTypeName() : "未知类型";

// 3. 查询最近预约记录(近7天)

List<DeviceReserveVO> recentReserves = deviceMapper.queryRecentReserves(id, 7);

// 4. 封装详情VO

DeviceDetailVO detailVO = new DeviceDetailVO();

BeanUtils.copyProperties(device, detailVO);

detailVO.setTypeName(typeName);

detailVO.setRecentReserves(recentReserves);

// 格式化时间

detailVO.setCreateTimeStr(DateUtil.format(device.getCreateTime(), "yyyy-MM-dd"));

detailVO.setBuyTimeStr(DateUtil.format(device.getBuyTime(), "yyyy-MM-dd"));

return detailVO;

}

}

2. 设备预约模块(前端实现)

设备预约模块是系统核心交互功能,用户可通过小程序查询设备可用时间、提交预约申请,前端需实现 “日历选择 - 时段筛选 - 预约提交” 流程:

预约页面逻辑(pages/device/reserve/reserve.js)


import { get, post } from '../../../utils/request';

import { formatDate, getCurrentDate } from '../../../utils/dateUtil';

Page({

data: {

deviceId: '', // 设备ID(从上个页面传递)

deviceName: '', // 设备名称

selectedDate: '', // 选中的预约日期

availableTimeSlots: [],// 选中日期的可用时段

selectedTimeSlot: '', // 选中的时段

reason: '', // 预约理由

loading: false // 加载状态

},

// 页面加载时初始化(接收设备ID与名称)

onLoad(options) {

const { deviceId, deviceName } = options;

this.setData({

deviceId,

deviceName,

selectedDate: getCurrentDate() // 默认选中今天

});

// 加载选中日期的可用时段

this.loadAvailableTimeSlots();

},

// 加载选中日期的可用时段

loadAvailableTimeSlots() {

const { deviceId, selectedDate } = this.data;

this.setData({ loading: true });

// 调用后端接口获取可用时段

get('/api/device/reserve/available', {

deviceId,

reserveDate: selectedDate

}).then(res => {

this.setData({

availableTimeSlots: res.timeSlots, // 格式:[{slot: '09:00-11:00', available: true}, ...]

loading: false

});

}).catch(err => {

console.error('加载可用时段失败:', err);

wx.showToast({ title: '加载失败,请重试', icon: 'none' });

this.setData({ loading: false });

});

},

// 日期选择器变化

handleDateChange(e) {

this.setData({

selectedDate: e.detail.value,

selectedTimeSlot: '' // 重置选中时段

});

// 重新加载选中日期的可用时段

this.loadAvailableTimeSlots();

},

// 选择时段

handleTimeSlotSelect(e) {

this.setData({ selectedTimeSlot: e.currentTarget.dataset.slot });

},

// 提交预约申请

handleSubmit() {

const { deviceId, selectedDate, selectedTimeSlot, reason } = this.data;

// 表单验证

if (!selectedTimeSlot) {

wx.showToast({ title: '请选择预约时段', icon: 'none' });

return;

}

if (!reason || reason.length < 10) {

wx.showToast({ title: '预约理由需至少10个字', icon: 'none' });

return;

}

this.setData({ loading: true });

// 调用预约接口

post('/api/device/reserve', {

deviceId,

reserveDate: selectedDate,

timeSlot: selectedTimeSlot,

reason

}).then(() => {

wx.showToast({ title: '预约申请提交成功' });

// 跳转回设备详情页

setTimeout(() => {

wx.navigateBack();

}, 1500);

}).catch(err => {

wx.showToast({ title: err.message || '预约失败', icon: 'none' });

this.setData({ loading: false });

});

}

});

预约页面 UI(pages/device/reserve/reserve.wxml)


<view class="container">

<!-- 设备信息 -->

<view class="device-info">

<view class="title">预约设备:{{deviceName}}</view>

<view class="tips">请选择预约日期与时段</view>

</view>

<!-- 日期选择器 -->

<view class="date-picker">

<van-datetime-picker

type="date"

value="{{selectedDate}}"

bind:change="handleDateChange"

min-date="{{getCurrentDate()}}"

max-date="{{getMaxDate()}}"

/>

</view>

<!-- 时段选择 -->

<view class="time-slot-container">

<view class="section-title">可用时段</view>

<view class="time-slot-list">

<block wx:for="{{availableTimeSlots}}" wx:key="index">

<view

class="time-slot-item {{item.available ? (selectedTimeSlot === item.slot ? 'selected' : 'available') : 'unavailable'}}"

bindtap="{{item.available ? 'handleTimeSlotSelect' : ''}}"

data-slot="{{item.slot}}"

>

<view class="slot-text">{{item.slot}}</view>

<view class="status-tag {{item.available ? 'available-tag' : 'unavailable-tag'}}">

{{item.available ? '可预约' : '已预约'}}

</view>

</view>

</block>

</view>

<van-loading wx:if="{{loading}}" size="24" class="loading" />

</view>

<!-- 预约理由 -->

<view class="reason-input">

<view class="section-title">预约理由</view>

<textarea

placeholder="请输入预约理由(至少1</doubaocanvas>

更多推荐