文章目录
- 一、项目演示
- 二、项目介绍
- 三、运行截图
- 四、主要代码
一、项目演示
项目演示地址: 视频地址
二、项目介绍
项目描述:这是一个基于SpringBoot+Vue框架开发的体育场馆预约管理系统。首先,这是一个前后端分离的项目,代码简洁规范,注释说明详细,易于理解和学习。其次,这项目功能丰富,具有一个体育场馆预约管理系统该有的所有功能。
项目功能:此项目分为两个角色:普通用户和管理员。普通用户有登录注册、管理个人信息、浏览或租借体育器材、浏览或预约体育场馆信息、管理个人租借体育器材信息、管理个人预约体育场馆信息、浏览公告信息等等功能。管理员有管理所有用户新息、管理所有体育器材信息、管理所有体育场馆信息、管理所有租借体育器材信息、管理所有预约体育场馆信息、管理所有公告信息等等功能。
应用技术:SpringBoot + Vue + MySQL + MyBatis + Redis + ElementUI
运行环境:IntelliJ IDEA2019.3.5 + MySQL5.7(项目压缩包中自带) + Redis5.0.5(项目压缩包中自带) + JDK1.8 + Maven3.6.3(项目压缩包中自带)+ Node14.16.1(项目压缩包中自带)
三、运行截图
四、主要代码
1.租借体育器材代码:
/**
* 保存租借数据(添加、修改)
* @param rentalDTO
* @return
*/
@Override
public ResponseDTO<Boolean> saveRental(RentalDTO rentalDTO) {
// 进行统一表单验证
CodeMsg validate = ValidateEntityUtil.validate(rentalDTO);
if(!validate.getCode().equals(CodeMsg.SUCCESS.getCode())){
return ResponseDTO.errorByMsg(validate);
}
Rental rental = CopyUtil.copy(rentalDTO, Rental.class);
ResponseDTO<Boolean> responseDTO = ResponseDTO.successByMsg(true, "保存成功!");
if(CommonUtil.isEmpty(rental.getId())){
// id为空 说明是添加数据
// 生成8位id
rental.setId(UuidUtil.getShortUuid());
rental.setCreateTime(new Date());
rental.setState(RentalStateEnum.WAIT.getCode());
String redissonKey = String.format(EQUIPMENT_REDIS_KEY_TEMPLATE, rental.getEquipmentId());
RLock lock = redissonClient.getLock(redissonKey);
//1.加锁 阻塞获取锁:获取不到一直循环尝试获取
lock.lock();
try {
// @Transactional 事务执行完后 再unlock释放锁
// 为了避免锁在事务提交前释放,我们应该在事务外层使用锁。
responseDTO = createRental(rental);
}catch (Exception e){
logger.error(e.getMessage());
}finally {
//解锁
lock.unlock();
}
} else {
// id不为空 说明是修改数据
// 修改数据库中数据
responseDTO = updateRental(rental);
}
return responseDTO;
}
/**
* 更新租借信息
* @param rental
* @return
*/
@Transactional
public ResponseDTO<Boolean> updateRental(Rental rental) {
if(RentalStateEnum.FAIL.getCode().equals(rental.getState()) || RentalStateEnum.CANCEL.getCode().equals(rental.getState())) {
myEquipmentMapper.addRentalNum(rental.getNum(), rental.getEquipmentId());
}
if(rentalMapper.updateByPrimaryKeySelective(rental) == 0){
throw new RuntimeException(CodeMsg.RENTAL_EDIT_ERROR.getMsg());
}
return ResponseDTO.successByMsg(true, "保存成功!");
}
/**
* 创建租赁信息
* @param rental
* @return
*/
@Transactional
public ResponseDTO<Boolean> createRental(Rental rental) {
// 根据体育器材id和租借数量判断体育器材剩余库存
Equipment equipment = equipmentMapper.selectByPrimaryKey(rental.getEquipmentId());
if(EquipmentStateEnum.OFF.getCode().equals(equipment.getState())) {
return ResponseDTO.errorByMsg(CodeMsg.EQUIPMENT_ALREADY_OFF);
}
if(equipment.getNum() < rental.getNum()) {
return ResponseDTO.errorByMsg(CodeMsg.EQUIPMENT_STOCK_ERROR);
}
// 数据落地
if(rentalMapper.insertSelective(rental) == 0) {
return ResponseDTO.errorByMsg(CodeMsg.RENTAL_ADD_ERROR);
}
// 减少体育器材数量
myEquipmentMapper.decreaseRentalNum(rental.getNum(), rental.getEquipmentId());
return ResponseDTO.successByMsg(true, "保存成功!");
}
2.用户登录代码:
/**
* 用户登录操作
* @param userDTO
* @return
*/
@Override
public ResponseDTO<UserDTO> login(UserDTO userDTO) {
// 进行是否为空判断
if(CommonUtil.isEmpty(userDTO.getUsername())){
return ResponseDTO.errorByMsg(CodeMsg.USERNAME_EMPTY);
}
if(CommonUtil.isEmpty(userDTO.getPassword())){
return ResponseDTO.errorByMsg(CodeMsg.PASSWORD_EMPTY);
}
if(CommonUtil.isEmpty(userDTO.getCaptcha())){
return ResponseDTO.errorByMsg(CodeMsg.CAPTCHA_EMPTY);
}
if(CommonUtil.isEmpty(userDTO.getCorrectCaptcha())){
return ResponseDTO.errorByMsg(CodeMsg.CAPTCHA_EXPIRED);
}
// 比对验证码是否正确
String value = stringRedisTemplate.opsForValue().get((userDTO.getCorrectCaptcha()));
if(CommonUtil.isEmpty(value)){
return ResponseDTO.errorByMsg(CodeMsg.CAPTCHA_EXPIRED);
}
if(!value.toLowerCase().equals(userDTO.getCaptcha().toLowerCase())){
return ResponseDTO.errorByMsg(CodeMsg.CAPTCHA_ERROR);
}
// 对比昵称和密码是否正确
UserExample userExample = new UserExample();
// select * from user where username = ? and password = ?
userExample.createCriteria().andUsernameEqualTo(userDTO.getUsername()).andPasswordEqualTo(userDTO.getPassword());
List<User> userList = userMapper.selectByExample(userExample);
if(userList == null || userList.size() != 1){
return ResponseDTO.errorByMsg(CodeMsg.USERNAME_PASSWORD_ERROR);
}
// 生成登录token并存入Redis中
UserDTO selectedUserDTO = CopyUtil.copy(userList.get(0), UserDTO.class);
String token = UuidUtil.getShortUuid();
selectedUserDTO.setToken(token);
//把token存入redis中 有效期1小时
stringRedisTemplate.opsForValue().set("USER_" + token, JSON.toJSONString(selectedUserDTO), 3600, TimeUnit.SECONDS);
return ResponseDTO.successByMsg(selectedUserDTO, "登录成功!");
}
3.Redis中stream消息队列读取预约数据代码:
private static final ExecutorService APPOINTMENT_UPDATE_EXECUTOR = Executors.newSingleThreadExecutor();
@PostConstruct
private void init() {
APPOINTMENT_UPDATE_EXECUTOR.submit(new AppointmentHandler());
}
private class AppointmentHandler implements Runnable {
@Override
public void run() {
while (true) {
try {
// 1.获取消息队列中的预约信息 XREAD GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS stream.orders >
// ReadOffset.lastConsumed() 获取下一个未消费的预约数据
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
StreamOffset.create(APPOINTMENT_REDIS_KEY_TEMPLATE, ReadOffset.lastConsumed())
);
// 2.判断预约信息是否为空
if (list == null || list.isEmpty()) {
// 如果为null,说明没有消息,继续下一次循环
continue;
}
// 解析数据 获取一条数据 因为上面count(1)指定获取一条
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> value = record.getValue();
String jsonAppointmentString = (String) value.get("appointmentData");
Appointment appointment = JSON.parseObject(jsonAppointmentString, Appointment.class);
// 3.更新预约数据
logger.info("接收到消息队列数据,准备更新...");
appointmentMapper.updateByPrimaryKeySelective(appointment);
Hall hall = hallMapper.selectByPrimaryKey(appointment.getHallId());
if(AppointmentStateEnum.FINISH.getCode().equals(appointment.getState())
&& HallStateEnum.APPOINT.getCode().equals(hall.getState())) {
hall.setState(HallStateEnum.FREE.getCode());
hallMapper.updateByPrimaryKeySelective(hall);
}
logger.info("预约数据更新完成...");
// 4.确认消息 XACK
stringRedisTemplate.opsForStream().acknowledge(APPOINTMENT_REDIS_KEY_TEMPLATE, "g1", record.getId());
} catch (Exception e) {
// logger.error("处理预约数据异常", e);
handlePendingList();
}
}
}
// 确认异常的预约数据再次处理
private void handlePendingList() {
while (true) {
try {
// 1.获取pending-list中的预约信息 XREAD GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS stream.orders 0
// ReadOffset.from("0") 从第一个消息开始
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1),
StreamOffset.create(APPOINTMENT_REDIS_KEY_TEMPLATE, ReadOffset.from("0"))
);
// 2.判断预约信息是否为空
if (list == null || list.isEmpty()) {
// 如果为null,说明没有异常消息,结束循环
break;
}
// 解析数据
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> value = record.getValue();
String jsonAppointmentString = (String) value.get("appointmentData");
Appointment appointment = JSON.parseObject(jsonAppointmentString, Appointment.class);
// 3.更新预约数据
logger.info("接收到消息队列数据,准备更新...");
appointmentMapper.updateByPrimaryKeySelective(appointment);
Hall hall = hallMapper.selectByPrimaryKey(appointment.getHallId());
if(AppointmentStateEnum.FINISH.getCode().equals(appointment.getState())
&& HallStateEnum.APPOINT.getCode().equals(hall.getState())) {
hall.setState(HallStateEnum.FREE.getCode());
hallMapper.updateByPrimaryKeySelective(hall);
}
logger.info("预约数据更新完成...");
// 4.确认消息 XACK
stringRedisTemplate.opsForStream().acknowledge(APPOINTMENT_REDIS_KEY_TEMPLATE, "g1", record.getId());
} catch (Exception e) {
// logger.error("处理预约数据异常", e);
try {
Thread.sleep(100);
} catch (InterruptedException interruptedException) {
// interruptedException.printStackTrace();
}
}
}
}
}