背景调研
随着全民健身意识的提升,传统健身房的运营模式面临着信息传递滞后、预约流程繁琐、数据统计困难等挑战。本设计旨在开发一款基于微信小程序的健身房轻量化服务平台,实现线上线下服务闭环。系统采用前后端分离架构,前端基于微信小程序开发,后端采用Java SpringBoot框架,数据库选用MySQL。系统主要包含用户端和管理端两大模块:用户端提供栏目浏览、在线注册、私教/团课/场地预约、个人中心管理等功能;管理端提供内容维护、预约规则设置、扫码核销、数据导出及权限管理等能力。测试结果表明,该系统有效提升了会员健身体验,优化了场馆运营效率,实现了服务质量的量化管理
系统需求分析
用户端功能
- 栏目浏览模块: 小店动态(活动、促销)、健身干货(技巧、教学)、饮食科普、荣誉资质。 支持详情查看、分享至朋友圈、关键词检索。
- 用户注册模块: 微信一键登录,绑定基本信息(年龄、姓名、车辆情况等)。
- 服务预约模块: 支持私教预约、团课预约(瑜伽、动感单车等)、场地预约。 实时展示可预约时段及剩余名额。 生成核销二维码,支持到店扫码核销。
- 个人中心模块: 我的预约(查看状态、取消预约)、资料修改、浏览历史、我的收藏。
数据库设计
- 用户表:存储用户ID、微信OpenID、姓名、年龄、联系方式等。
- 栏目内容表:存储文章标题、内容、类型(动态/干货/饮食)、发布时间。
- 预约项目表:存储项目名称、类型(私教/团课/场地)、可预约时段、最大人数。
- 预约记录表:存储用户ID、项目ID、预约时间、状态(已预约/已完成/已取消/已核销)。
- 管理员表:存储账号、密码、角色权限。
- 预约流程设计:用户选择时段 -> 锁定名额 -> 生成订单 -> 到店出示二维码 -> 管理员扫码 -> 状态更新为“已核销”。 权限控制设计:基于角色的访问控制(RBAC),核销员仅拥有扫码权限,不可修改系统配置。
CREATE TABLE `sys_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`openid` varchar(64) NOT NULL COMMENT '微信OpenID',
`username` varchar(50) DEFAULT NULL COMMENT '用户昵称',
`phone` varchar(20) DEFAULT NULL COMMENT '联系电话',
`age` int(3) DEFAULT NULL COMMENT '年龄',
`gender` tinyint(1) DEFAULT 0 COMMENT '性别 0:未知 1:男 2:女',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间',
`status` tinyint(1) DEFAULT 1 COMMENT '状态 1:正常 0:禁用',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_openid` (`openid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';
CREATE TABLE `gym_project` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT '项目名称',
`type` varchar(20) NOT NULL COMMENT '类型:PRIVATE(私教), GROUP(团课), VENUE(场地)',
`coach_name` varchar(50) DEFAULT NULL COMMENT '教练姓名(私教/团课用)',
`max_capacity` int(11) NOT NULL DEFAULT 1 COMMENT '最大容纳人数/场次',
`price` decimal(10,2) DEFAULT 0.00 COMMENT '价格',
`description` text COMMENT '项目详情',
`is_available` tinyint(1) DEFAULT 1 COMMENT '是否上架',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预约项目表';
CREATE TABLE `gym_reservation` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`project_id` bigint(20) NOT NULL COMMENT '项目ID',
`reserve_date` date NOT NULL COMMENT '预约日期',
`reserve_time_slot` varchar(50) NOT NULL COMMENT '预约时段 如 14:00-15:00',
`verify_code` varchar(32) NOT NULL COMMENT '核销码/二维码内容',
`status` tinyint(2) NOT NULL DEFAULT 0 COMMENT '状态: 0-已预约 1-已核销 2-已取消 3-已过期',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`verify_time` datetime DEFAULT NULL COMMENT '核销时间',
`verify_admin_id` bigint(20) DEFAULT NULL COMMENT '核销管理员ID',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预约记录表';
CREATE TABLE `sys_admin` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '账号',
`password` varchar(100) NOT NULL COMMENT '加密密码',
`role` varchar(20) NOT NULL COMMENT '角色:SUPER_ADMIN, NORMAL_ADMIN, VERIFIER(核销员)',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='管理员表';
后端核心代码
@Service
public class ReservationService {
@Autowired
private ReservationMapper reservationMapper;
@Autowired
private ProjectMapper projectMapper;
/**
* 用户发起预约
* @param userId 用户ID
* @param dto 预约参数包含项目ID、日期、时段
* @return 预约结果
*/
@Transactional(rollbackFor = Exception.class)
public Result<Void> createReservation(Long userId, ReservationDTO dto) {
// 1. 查询项目信息
GymProject project = projectMapper.selectById(dto.getProjectId());
if (project == null || !project.getIsAvailable()) {
return Result.fail("该项目暂未开放预约");
}
// 2. 检查该时段剩余名额 (并发场景下建议使用数据库乐观锁或Redis锁,此处演示基础逻辑)
int bookedCount = reservationMapper.countByProjectAndSlot(
dto.getProjectId(), dto.getReserveDate(), dto.getReserveTimeSlot(), 0);
if (bookedCount >= project.getMaxCapacity()) {
return Result.fail("该时段名额已满,请更换时间");
}
// 3. 生成唯一核销码 (UUID + 时间戳)
String verifyCode = UUID.randomUUID().toString().replace("-", "");
// 4. 插入预约记录
GymReservation reservation = new GymReservation();
reservation.setUserId(userId);
reservation.setProjectId(dto.getProjectId());
reservation.setReserveDate(dto.getReserveDate());
reservation.setReserveTimeSlot(dto.getReserveTimeSlot());
reservation.setVerifyCode(verifyCode);
reservation.setStatus(0); // 0: 已预约
reservationMapper.insert(reservation);
return Result.success("预约成功,请前往个人中心查看核销码");
}
/**
* 管理员扫码核销
*/
@Transactional(rollbackFor = Exception.class)
public Result<Void> verifyReservation(String verifyCode, Long adminId) {
GymReservation reservation = reservationMapper.selectByVerifyCode(verifyCode);
if (reservation == null) {
return Result.fail("无效的核销码");
}
if (reservation.getStatus() != 0) {
return Result.fail("该预约状态不可核销 (当前状态:" + reservation.getStatus() + ")");
}
// 更新状态为已核销,记录核销人和时间
reservation.setStatus(1);
reservation.setVerifyAdminId(adminId);
reservation.setVerifyTime(new Date());
reservationMapper.updateById(reservation);
return Result.success("核销成功");
}
}
@RestController
@RequestMapping("/api/admin/export")
public class ExportController {
@Autowired
private ReservationService reservationService;
/**
* 导出预约数据 Excel
* @param startDate 开始日期
* @param endDate 结束日期
* @param response HttpServletResponse
*/
@GetMapping("/reservations")
public void exportReservations(@RequestParam String startDate,
@RequestParam String endDate,
HttpServletResponse response) throws IOException {
// 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = "健身预约数据_" + System.currentTimeMillis() + ".xlsx";
response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
// 查询数据
List<ReservationVO> data = reservationService.getReservationList(startDate, endDate);
// 使用 EasyExcel 写入流
EasyExcel.write(response.getOutputStream(), ReservationVO.class)
.sheet("预约明细")
.doWrite(data);
}
}
UI设计
管理系统设计