作为一名经历过多次企业信息化系统开发的Java全栈工程师,最近刚完成了一个基于SSM框架的会议室预约微信小程序项目。这个系统不仅包含了微信小程序前端,还配套开发了基于Java+Vue的后台管理系统,完整实现了从会议室管理到预约审批的全流程数字化。
在传统办公场景中,会议室管理常常面临这些痛点:
我们开发的这套系统正是为了解决这些实际问题。采用B/S架构设计,前端使用微信小程序(uniapp框架)保证移动端体验,后端采用成熟的SSM(Spring+SpringMVC+MyBatis)技术栈,数据库选用MySQL 5.7版本。系统上线后,企业会议室使用效率提升了60%,行政事务处理时间缩短了75%。
系统采用前后端分离架构,分为三个主要部分:
微信小程序端:
后台管理系统:
数据层:
技术选型心得:选择uniapp而非原生小程序开发,主要考虑后续可能扩展其他移动端平台。SSM框架虽然不如Spring Boot新潮,但在学校教学和企业旧系统中应用广泛,更符合毕设项目的技术栈要求。
数据库设计遵循第三范式,核心表包括:
会议室表(meeting_room):
sql复制CREATE TABLE `meeting_room` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`room_name` varchar(50) NOT NULL COMMENT '会议室名称',
`capacity` int(11) NOT NULL COMMENT '容纳人数',
`equipment` varchar(255) DEFAULT NULL COMMENT '设备清单',
`status` tinyint(4) DEFAULT '0' COMMENT '0-可用 1-维修中',
`location` varchar(100) DEFAULT NULL COMMENT '位置信息',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
预约记录表(reservation):
sql复制CREATE TABLE `reservation` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`room_id` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
`start_time` datetime NOT NULL,
`end_time` datetime NOT NULL,
`purpose` varchar(255) DEFAULT NULL COMMENT '会议用途',
`status` tinyint(4) DEFAULT '0' COMMENT '0-待审核 1-已通过 2-已拒绝',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_room_time` (`room_id`,`start_time`,`end_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
设计经验:在reservation表上创建了联合索引(idx_room_time),大幅提升了时间段冲突检查的查询性能。时间字段使用datetime而非timestamp,避免时区转换问题。
首页使用swiper组件实现会议室推荐轮播:
html复制<swiper
indicator-dots="{{true}}"
autoplay="{{true}}"
interval="5000"
circular="{{true}}">
<block wx:for="{{bannerList}}" wx:key="id">
<swiper-item>
<image src="{{item.imageUrl}}" mode="aspectFill"
bindtap="navToDetail" data-id="{{item.roomId}}"/>
</swiper-item>
</block>
</swiper>
后台接口采用Redis缓存热门会议室数据,减轻数据库压力:
java复制@GetMapping("/banner")
public Result getBannerRooms() {
String cacheKey = "banner:rooms";
// 先查缓存
String cache = redisTemplate.opsForValue().get(cacheKey);
if (cache != null) {
return Result.success(JSON.parseArray(cache, RoomVO.class));
}
// 缓存不存在则查数据库
List<RoomVO> rooms = roomService.getPopularRooms(5);
redisTemplate.opsForValue().set(cacheKey,
JSON.toJSONString(rooms), 1, TimeUnit.HOURS);
return Result.success(rooms);
}
核心冲突检测SQL:
sql复制SELECT COUNT(*) FROM reservation
WHERE room_id = #{roomId}
AND status = 1 -- 只考虑已通过的预约
AND (
(start_time < #{endTime} AND end_time > #{startTime})
OR (start_time >= #{startTime} AND start_time < #{endTime})
OR (end_time > #{startTime} AND end_time <= #{endTime})
)
在Java服务层进行二次校验:
java复制public boolean checkTimeConflict(Integer roomId,
LocalDateTime newStart, LocalDateTime newEnd) {
// 基础校验:结束时间不能早于开始时间
if (newEnd.isBefore(newStart)) {
throw new BusinessException("结束时间必须晚于开始时间");
}
// 最小预约时长30分钟
if (Duration.between(newStart, newEnd).toMinutes() < 30) {
throw new BusinessException("最短预约时长为30分钟");
}
// 查询已有预约
List<Reservation> exists = reservationMapper
.selectByRoomAndTime(roomId, newStart, newEnd);
return !exists.isEmpty();
}
采用状态机模式管理预约状态流转:
java复制public enum ReservationStatus {
PENDING(0, "待审核") {
@Override
public boolean canChangeTo(ReservationStatus newStatus) {
return newStatus == APPROVED || newStatus == REJECTED;
}
},
APPROVED(1, "已通过") {
@Override
public boolean canChangeTo(ReservationStatus newStatus) {
return newStatus == CANCELLED;
}
},
// 其他状态...
public abstract boolean canChangeTo(ReservationStatus newStatus);
}
审批接口实现:
java复制@PostMapping("/approve")
public Result approve(@RequestBody ApproveDTO dto) {
Reservation reservation = reservationMapper.selectById(dto.getId());
if (reservation == null) {
throw new BusinessException("预约记录不存在");
}
if (!reservation.getStatus().canChangeTo(dto.getNewStatus())) {
throw new BusinessException("当前状态不允许此操作");
}
// 更新状态并发送微信通知
reservation.setStatus(dto.getNewStatus());
reservation.setApprover(SecurityUtils.getCurrentUserId());
reservationMapper.updateById(reservation);
wxNoticeService.sendApproveResult(
reservation.getUserId(),
reservation.getId(),
dto.getNewStatus(),
dto.getRemark());
return Result.success();
}
使用ECharts实现会议室使用率统计:
javascript复制// Vue组件中
async loadUsageData() {
const res = await getRoomUsageStats({
roomId: this.roomId,
dateRange: this.dateRange
});
this.chart = echarts.init(this.$refs.chart);
this.chart.setOption({
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: res.data.dates },
yAxis: { type: 'value', max: 100 },
series: [{
name: '使用率',
type: 'line',
data: res.data.rates,
markLine: {
data: [{ type: 'average', name: '平均值' }]
}
}]
});
}
图片上传限制:
javascript复制wx.uploadFile({
url: 'https://yourdomain.com/upload',
filePath: tempFilePath,
name: 'file',
formData: { 'type': 'repair' },
success(res) {
const data = JSON.parse(res.data)
if (data.code !== 0) {
wx.showToast({ title: '上传失败', icon: 'error' });
}
}
})
用户登录态维护:
java复制// 登录接口
@PostMapping("/login")
public Result login(@RequestBody LoginDTO dto) {
String url = "https://api.weixin.qq.com/sns/jscode2session";
Map<String, String> params = new HashMap<>();
params.put("appid", appId);
params.put("secret", appSecret);
params.put("js_code", dto.getCode());
params.put("grant_type", "authorization_code");
String response = restTemplate.getForObject(
url + "?appid={appid}&secret={secret}&js_code={code}&grant_type={type}",
String.class, params);
JSONObject json = JSON.parseObject(response);
String openid = json.getString("openid");
String sessionKey = json.getString("session_key");
// 生成自定义token返回给小程序
String token = JwtUtil.generateToken(openid);
return Result.success(token);
}
接口幂等性设计:
java复制public boolean tryLock(String key, long expireSeconds) {
String value = UUID.randomUUID().toString();
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(key, value, expireSeconds, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@PostMapping("/reserve")
public Result createReservation(@RequestBody ReserveDTO dto) {
String lockKey = "reserve:lock:" + dto.getUserId();
try {
if (!redisLock.tryLock(lockKey, 30)) {
throw new BusinessException("操作太频繁,请稍后再试");
}
// 业务处理...
return Result.success();
} finally {
redisLock.unlock(lockKey);
}
}
事务管理:
java复制@Transactional(rollbackFor = Exception.class)
public void reportRepair(RepairDTO dto) {
// 1. 创建报修记录
Repair repair = new Repair();
BeanUtils.copyProperties(dto, repair);
repairMapper.insert(repair);
// 2. 更新设备状态
equipmentMapper.updateStatus(dto.getEquipmentId(), 2); // 2-维修中
// 3. 记录操作日志
logService.addRepairLog(dto);
}
最低配置:
推荐配置:
使用Docker Compose编排服务:
yaml复制version: '3'
services:
mysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: yourpassword
MYSQL_DATABASE: meeting_room
ports:
- "3306:3306"
volumes:
- ./mysql/data:/var/lib/mysql
redis:
image: redis:6
ports:
- "6379:6379"
volumes:
- ./redis/data:/data
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
数据库层面:
ANALYZE TABLE更新统计信息接口层面:
前端层面:
这个项目从技术选型到最终上线历时3个月,期间遇到了不少挑战,比如微信小程序端的性能优化、预约冲突的精确检测等。通过这个项目,我深刻体会到良好的系统设计和规范的编码习惯的重要性。特别是在高并发场景下,一个小小的索引缺失就可能导致系统响应变慢。建议开发类似系统的同学,一定要在前期做好充分的技术调研和设计评审。