程序员实战:SpringBoot + 微信小程序高校实验室管理系统全栈开发
高校实验室管理常面临设备预约混乱、耗材申领低效、使用记录零散等问题。基于 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>
更多推荐


所有评论(0)