接上回--------------->《预约优化方案全链路优化实践》

预约印章为例,讲解一下分时间段的预约代码具体实现

一、数据库设计

表格结构(Markdown 格式)

sys_holiday(节假日表)
字段名字段中文名类型注释约束
id主键 IDbigint唯一标识自增、非空、主键
date日期date预约相关日期(如 2024-10-01)非空、唯一索引(uk_date)
is_holiday是否节假日tinyint0 - 非节假日,1 - 节假日非空,默认 0
is_work是否调休上班tinyint0 - 不上班,1 - 调休上班非空,默认 0
seal_time_slot(印章时段配置表)
字段名字段中文名类型注释约束
id主键 IDbigint唯一标识自增、非空、主键
time_slot_start时段开始时间time如 09:00、10:00非空
time_slot_end时段结束时间time如 09:59、10:59非空
max_num最大可预约数int每个时段固定最多 5 人预约非空,默认 5
seal_stock(印章库存表)
字段名字段中文名类型注释约束
id主键 IDbigint唯一标识自增、非空、主键
date预约日期date关联的预约日期非空
time_slot_id时段 IDbigint关联 seal_time_slot.id非空,外键
remaining_num剩余可预约数int初始为 5,预约成功则减 1非空,默认 5
version乐观锁版本号int防并发更新冲突非空,默认 0

索引:联合索引 idx_date_slot(date, time_slot_id)

seal_appointment(印章预约订单表)
字段名字段中文名类型注释约束
id主键 IDbigint唯一标识自增、非空、主键
user_id用户 IDbigint预约用户的唯一标识非空
seal_id印章 IDbigint被预约印章的唯一标识非空
appointment_date预约日期date用户选择的预约日期非空
time_slot_id预约时段 IDbigint关联 seal_time_slot.id非空,外键
status订单状态tinyint0 - 待确认,1 - 已确认,2 - 已取消非空,默认 0
create_time创建时间datetime订单生成时间非空,默认当前时间

索引:唯一索引 uk_user_seal_date(user_id, seal_id, appointment_date)

二、前置准备:配置类 & 工具类

1. 核心依赖(pom.xml 关键部分)

<!-- SpringBoot核心 -->
  <dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <!-- 数据库 -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.32</version>
    </dependency>
    <!-- MyBatis -->
      <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>2.3.0</version>
      </dependency>
      <!-- Lombok(简化实体类) -->
        <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
        </dependency>
        <!-- Redis -->
          <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-redis</artifactId>
          </dependency>
          <!-- RabbitMQ -->
            <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
            </dependency>

2. 配置类

(1)Redis 配置(RedisConfig.java)

package com.seal.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis配置类:解决Redis序列化问题,方便存储对象
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
  RedisTemplate<String, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(factory);
    // 字符串序列化器(Key用String)
    StringRedisSerializer stringSerializer = new StringRedisSerializer();
    template.setKeySerializer(stringSerializer);
    template.setHashKeySerializer(stringSerializer);
    // JSON序列化器(Value用JSON)
    GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
    template.setValueSerializer(jsonSerializer);
    template.setHashValueSerializer(jsonSerializer);
    template.afterPropertiesSet();
    return template;
    }
    }

(2)RabbitMQ 配置(RabbitMQConfig.java)

package com.seal.config;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* RabbitMQ配置类:声明队列(库存更新+预约通知)
*/
@Configuration
public class RabbitMQConfig {
// 库存更新队列:处理Redis库存同步到MySQL的异步任务
public static final String QUEUE_STOCK_UPDATE = "seal.stock.update";
// 预约通知队列:处理预约成功后的通知+订单状态更新
public static final String QUEUE_APPOINT_NOTICE = "seal.notice";
// 声明库存更新队列(durable=true:队列持久化,重启MQ不丢失)
@Bean
public Queue stockUpdateQueue() {
return new Queue(QUEUE_STOCK_UPDATE, true);
}
// 声明预约通知队列
@Bean
public Queue appointNoticeQueue() {
return new Queue(QUEUE_APPOINT_NOTICE, true);
}
}

(3)MyBatis 配置(MyBatisConfig.java)

package com.seal.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
/**
* MyBatis配置类:扫描Mapper接口所在包
*/
@Configuration
@MapperScan("com.seal.mapper") // Mapper接口统一放在com.seal.mapper包下
public class MyBatisConfig {
}

3. 工具类

(1)日期工具类(DateUtils.java)

package com.seal.utils;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
/**
* 日期工具类:处理日期格式化、获取未来日期等
*/
public class DateUtils {
// 标准日期格式(yyyy-MM-dd)
public static final SimpleDateFormat SDF_DATE = new SimpleDateFormat("yyyy-MM-dd");
/**
* 日期转字符串(yyyy-MM-dd)
*/
public static String formatDate(Date date) {
if (date == null) return null;
return SDF_DATE.format(date);
}
/**
* 字符串转日期(yyyy-MM-dd)
*/
public static Date parseDate(String dateStr) throws ParseException {
if (dateStr == null || dateStr.isEmpty()) return null;
return SDF_DATE.parse(dateStr);
}
/**
* 获取未来N天的日期列表(从明天开始)
*/
public static List<Date> getNextNDays(int n) {
  List<Date> dateList = new ArrayList<>();
    Calendar calendar = Calendar.getInstance();
    for (int i = 1; i <= n; i++) {
    calendar.add(Calendar.DAY_OF_YEAR, 1); // 每天加1天
    dateList.add(calendar.getTime());
    }
    return dateList;
    }
    }

(2)接口统一返回结果(Result.java)

package com.seal.utils;
import lombok.Data;
/**
* 统一接口返回结果:前后端交互用,避免返回格式混乱
*/
@Data
public class Result<T> {
  // 状态码:200-成功,500-失败
  private Integer code;
  // 提示信息
  private String msg;
  // 返回数据(成功时携带)
  private T data;
  // 成功(无数据)
  public static <T> Result<T> success() {
    Result<T> result = new Result<>();
      result.setCode(200);
      result.setMsg("操作成功");
      return result;
      }
      // 成功(有数据)
      public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
          result.setCode(200);
          result.setMsg("操作成功");
          result.setData(data);
          return result;
          }
          // 失败(自定义消息)
          public static <T> Result<T> fail(String msg) {
            Result<T> result = new Result<>();
              result.setCode(500);
              result.setMsg(msg);
              return result;
              }
              }

三、实体类(Entity,用 Lombok 简化)

1. 节假日实体(SysHoliday.java)

package com.seal.entity;
import lombok.Data;
import java.util.Date;
/**
* 节假日实体:对应sys_holiday表
*/
@Data // Lombok注解:自动生成getter/setter/toString等
public class SysHoliday {
private Long id;             // 主键ID
private Date date;           // 日期
private Integer isHoliday;   // 是否节假日(0-否,1-是)
private Integer isWork;      // 是否调休上班(0-否,1-是)
}

2. 时段配置实体(SealTimeSlot.java)

package com.seal.entity;
import lombok.Data;
import java.time.LocalTime;
/**
* 时段配置实体:对应seal_time_slot表(每个时段最多5人)
*/
@Data
public class SealTimeSlot {
private Long id;                 // 主键ID
private LocalTime timeSlotStart; // 时段开始时间(如09:00)
private LocalTime timeSlotEnd;   // 时段结束时间(如09:59)
private Integer maxNum;          // 最大可预约数(固定5)
}

3. 库存实体(SealStock.java)

package com.seal.entity;
import lombok.Data;
import java.util.Date;
/**
* 库存实体:对应seal_stock表(乐观锁控制并发)
*/
@Data
public class SealStock {
private Long id;             // 主键ID
private Date date;           // 预约日期
private Long timeSlotId;     // 关联时段ID
private Integer remainingNum;// 剩余可预约数(初始5)
private Integer version;     // 乐观锁版本号
}

4. 预约订单实体(SealAppointment.java)

package com.seal.entity;
import lombok.Data;
import java.util.Date;
/**
* 预约订单实体:对应seal_appointment表
*/
@Data
public class SealAppointment {
private Long id;                 // 主键ID
private Long userId;             // 用户ID
private Long sealId;             // 印章ID
private Date appointmentDate;    // 预约日期
private Long timeSlotId;         // 预约时段ID
private Integer status;          // 订单状态(0-待确认,1-已确认,2-已取消)
private Date createTime;         // 创建时间
}

四、Mapper 层(数据访问层,MyBatis)

1. 节假日 Mapper(SysHolidayMapper.java)

package com.seal.mapper;
import com.seal.entity.SysHoliday;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
/**
* 节假日Mapper:操作sys_holiday表
*/
@Mapper // MyBatis注解:标识为Mapper接口,自动扫描
public interface SysHolidayMapper {
/**
* 批量插入/更新节假日(同步阿里云API时用)
* 存在则更新(基于date唯一索引),不存在则插入
*/
void batchUpsert(@Param("list") List<SysHoliday> holidayList);
  /**
  * 查询所有节假日(筛选可预约日期时用)
  */
  List<SysHoliday> selectAll();
    /**
    * 根据日期查询单个节假日(缓存未命中时用)
    */
    SysHoliday selectByDate(@Param("date") Date date);
    }

对应的 XML(SysHolidayMapper.xml)

<?xml version="1.0" encoding="UTF-8"?>
  <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
      <mapper namespace="com.seal.mapper.SysHolidayMapper">
      <!-- 批量插入/更新节假日 -->
          <insert id="batchUpsert">
          INSERT INTO sys_holiday (date, is_holiday, is_work)
          VALUES
            <foreach collection="list" item="item" separator=",">
            (#{item.date}, #{item.isHoliday}, #{item.isWork})
          </foreach>
          ON DUPLICATE KEY UPDATE
          is_holiday = VALUES(is_holiday),  -- 冲突时更新is_holiday
          is_work = VALUES(is_work)        -- 冲突时更新is_work
        </insert>
        <!-- 查询所有节假日 -->
            <select id="selectAll" resultType="com.seal.entity.SysHoliday">
            SELECT id, date, is_holiday, is_work
            FROM sys_holiday
            ORDER BY date ASC
          </select>
          <!-- 根据日期查询单个节假日 -->
              <select id="selectByDate" parameterType="java.util.Date" resultType="com.seal.entity.SysHoliday">
              SELECT id, date, is_holiday, is_work
              FROM sys_holiday
              WHERE date = #{date}
            </select>
          </mapper>

2. 时段配置 Mapper(SealTimeSlotMapper.java)

package com.seal.mapper;
import com.seal.entity.SealTimeSlot;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 时段配置Mapper:操作seal_time_slot表
*/
@Mapper
public interface SealTimeSlotMapper {
/**
* 查询所有可预约时段(初始化库存时用)
*/
List<SealTimeSlot> selectAll();
  /**
  * 根据ID查询时段(获取最大预约数时用)
  */
  SealTimeSlot selectById(Long id);
  }

对应的 XML(SealTimeSlotMapper.xml)

<?xml version="1.0" encoding="UTF-8"?>
  <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
      <mapper namespace="com.seal.mapper.SealTimeSlotMapper">
      <!-- 查询所有时段 -->
          <select id="selectAll" resultType="com.seal.entity.SealTimeSlot">
          SELECT id, time_slot_start, time_slot_end, max_num
          FROM seal_time_slot
          ORDER BY time_slot_start ASC
        </select>
        <!-- 根据ID查询时段 -->
            <select id="selectById" parameterType="java.lang.Long" resultType="com.seal.entity.SealTimeSlot">
            SELECT id, time_slot_start, time_slot_end, max_num
            FROM seal_time_slot
            WHERE id = #{id}
          </select>
        </mapper>

3. 库存 Mapper(SealStockMapper.java)

package com.seal.mapper;
import com.seal.entity.SealStock;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
/**
* 库存Mapper:操作seal_stock表(乐观锁更新)
*/
@Mapper
public interface SealStockMapper {
/**
* 批量插入库存(每日20:00初始化时用)
*/
void batchInsert(@Param("list") List<SealStock> stockList);
  /**
  * 根据日期+时段ID查询库存(乐观锁更新前查版本号)
  */
  SealStock selectByDateAndSlot(@Param("date") Date date, @Param("timeSlotId") Long timeSlotId);
  /**
  * 乐观锁扣减库存:仅版本号匹配时更新(防并发冲突)
  * @return 受影响行数:1-成功,0-失败(版本号不匹配)
  */
  int decreaseWithVersion(
  @Param("date") Date date,
  @Param("timeSlotId") Long timeSlotId,
  @Param("version") Integer version
  );
  }

对应的 XML(SealStockMapper.xml)

<?xml version="1.0" encoding="UTF-8"?>
  <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
      <mapper namespace="com.seal.mapper.SealStockMapper">
      <!-- 批量插入库存 -->
          <insert id="batchInsert">
          INSERT INTO seal_stock (date, time_slot_id, remaining_num, version)
          VALUES
            <foreach collection="list" item="item" separator=",">
            (#{item.date}, #{item.timeSlotId}, #{item.remainingNum}, #{item.version})
          </foreach>
        </insert>
        <!-- 根据日期+时段ID查询库存 -->
            <select id="selectByDateAndSlot" resultType="com.seal.entity.SealStock">
            SELECT id, date, time_slot_id, remaining_num, version
            FROM seal_stock
            WHERE date = #{date}
            AND time_slot_id = #{timeSlotId}
          </select>
          <!-- 乐观锁扣减库存:version匹配才更新 -->
              <update id="decreaseWithVersion">
              UPDATE seal_stock
              SET
              remaining_num = remaining_num - 1,  -- 剩余数减1
              version = version + 1              -- 版本号加1
              WHERE
              date = #{date}
              AND time_slot_id = #{timeSlotId}
              AND version = #{version}             -- 关键:仅版本号匹配时更新
            </update>
          </mapper>

4. 预约订单 Mapper(SealAppointmentMapper.java)

package com.seal.mapper;
import com.seal.entity.SealAppointment;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 预约订单Mapper:操作seal_appointment表
*/
@Mapper
public interface SealAppointmentMapper {
/**
* 插入预约订单(用户提交预约时用)
*/
void insert(SealAppointment appointment);
/**
* 根据ID查询订单(通知时更新状态用)
*/
SealAppointment selectById(@Param("id") Long id);
/**
* 更新订单状态(如待确认→已确认)
*/
void updateStatus(@Param("id") Long id, @Param("status") Integer status);
}

对应的 XML(SealAppointmentMapper.xml)

<?xml version="1.0" encoding="UTF-8"?>
  <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
      <mapper namespace="com.seal.mapper.SealAppointmentMapper">
      <!-- 插入预约订单 -->
          <insert id="insert" parameterType="com.seal.entity.SealAppointment">
          INSERT INTO seal_appointment (
          user_id, seal_id, appointment_date, time_slot_id, status, create_time
          ) VALUES (
          #{userId}, #{sealId}, #{appointmentDate}, #{timeSlotId}, #{status}, #{createTime}
          )
        </insert>
        <!-- 根据ID查询订单 -->
            <select id="selectById" parameterType="java.lang.Long" resultType="com.seal.entity.SealAppointment">
            SELECT
            id, user_id, seal_id, appointment_date, time_slot_id, status
            FROM
            seal_appointment
            WHERE
            id = #{id}
          </select>
          <!-- 更新订单状态 -->
              <update id="updateStatus">
              UPDATE seal_appointment
              SET status = #{status}
              WHERE id = #{id}
            </update>
          </mapper>

五、Service 层(业务逻辑层,调用 Mapper)

1. 定时任务服务(SealScheduleService.java)

package com.seal.service;
import com.seal.entity.SealStock;
import com.seal.entity.SealTimeSlot;
import com.seal.mapper.SealStockMapper;
import com.seal.mapper.SealTimeSlotMapper;
import com.seal.utils.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* 定时任务服务:每日20:00同步节假日+初始化未来7天库存
*/
@Service // Spring注解:标识为业务层服务
public class SealScheduleService {
@Autowired
private SysHolidayService holidayService; // 注入节假日服务
@Autowired
private SealTimeSlotMapper timeSlotMapper; // 注入时段Mapper
@Autowired
private SealStockMapper stockMapper;       // 注入库存Mapper
@Autowired
private RedisTemplate<String, Object> redisTemplate; // 注入Redis工具
  // Redis库存缓存Key:seal:stock:日期:时段ID(如seal:stock:2024-10-01:1)
  private static final String STOCK_CACHE_KEY = "seal:stock:%s:%s";
  /**
  * 每日20:00执行定时任务
  * cron表达式:分 时 日 月 周 → 0 0 20 * * ? 表示每天20点整
  */
  @Scheduled(cron = "0 0 20 * * ?")
  public void dailyTask() {
  // 1. 同步未来3个月节假日(调用阿里云API,由SysHolidayService实现)
  holidayService.syncHolidayFromAliCloud();
  // 2. 初始化未来7天库存(分批异步处理,避免阻塞主线程)
  List<Date> next7Days = DateUtils.getNextNDays(7); // 获取未来7天日期
    for (Date date : next7Days) {
    // 异步处理单天库存初始化(CompletableFuture:不阻塞当前线程)
    CompletableFuture.runAsync(() -> initStockForOneDay(date));
    }
    }
    /**
    * 初始化单天库存:为当天所有时段设置初始库存(每个时段5人)
    */
    @Transactional // 事务注解:保证单天库存要么全成功,要么全回滚
    public void initStockForOneDay(Date date) {
    // ① 查询所有可预约时段(从数据库查,时段配置不常变)
    List<SealTimeSlot> timeSlots = timeSlotMapper.selectAll();
      if (timeSlots.isEmpty()) {
      System.err.println("时段配置为空,跳过日期:" + DateUtils.formatDate(date) + "的库存初始化");
      return;
      }
      // ② 构造单天库存列表(每个时段初始库存=max_num=5)
      List<SealStock> stockList = new ArrayList<>();
        for (SealTimeSlot slot : timeSlots) {
        SealStock stock = new SealStock();
        stock.setDate(date);
        stock.setTimeSlotId(slot.getId());
        stock.setRemainingNum(slot.getMaxNum()); // 初始库存=5
        stock.setVersion(0); // 初始版本号=0
        stockList.add(stock);
        }
        // ③ 批量插入库存到数据库
        stockMapper.batchInsert(stockList);
        System.out.println("库存初始化成功:日期=" + DateUtils.formatDate(date) + ",共" + stockList.size() + "个时段");
        // ④ 同步库存到Redis(供前端快速查询,减轻数据库压力)
        for (SealStock stock : stockList) {
        String dateStr = DateUtils.formatDate(stock.getDate());
        String cacheKey = String.format(STOCK_CACHE_KEY, dateStr, stock.getTimeSlotId());
        redisTemplate.opsForValue().set(cacheKey, stock.getRemainingNum());
        }
        }
        }

2. 节假日服务(SysHolidayService.java)

package com.seal.service;
import com.seal.entity.SysHoliday;
import com.seal.mapper.SysHolidayMapper;
import com.seal.utils.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
* 节假日服务:同步阿里云API+筛选可预约日期
*/
@Service
public class SysHolidayService {
@Autowired
private SysHolidayMapper holidayMapper; // 注入节假日Mapper
@Autowired
private RedisTemplate<String, Object> redisTemplate; // 注入Redis工具
  // Redis节假日缓存Key:seal:holiday:日期(如seal:holiday:2024-10-01)
  private static final String HOLIDAY_CACHE_KEY = "seal:holiday:%s";
  /**
  * 同步阿里云万年历API数据到数据库和Redis
  * 实际开发中替换为真实API调用(这里用模拟数据演示)
  */
  @Transactional
  public void syncHolidayFromAliCloud() {
  // ① 调用阿里云API获取未来3个月节假日数据(模拟)
  List<SysHoliday> holidayList = mockAliCloudHolidayData();
    if (holidayList.isEmpty()) {
    System.err.println("阿里云API返回空数据,同步失败");
    return;
    }
    // ② 批量插入/更新到数据库(基于date唯一索引,避免重复)
    holidayMapper.batchUpsert(holidayList);
    System.out.println("节假日同步成功,共" + holidayList.size() + "条数据");
    // ③ 同步数据到Redis(方便后续查询,减少数据库访问)
    for (SysHoliday holiday : holidayList) {
    String dateStr = DateUtils.formatDate(holiday.getDate());
    String cacheKey = String.format(HOLIDAY_CACHE_KEY, dateStr);
    redisTemplate.opsForValue().set(cacheKey, holiday);
    }
    }
    /**
    * 筛选可预约日期:非节假日 或 调休上班的日期
    * @return 可预约日期列表
    */
    public List<String> listAvailableDates() {
      // ① 查询所有节假日数据(数据量小,全查效率高)
      List<SysHoliday> allHolidays = holidayMapper.selectAll();
        // ② 筛选规则:可预约 = 非节假日(is_holiday=0) OR 调休上班(is_work=1)
        return allHolidays.stream()
        .filter(holiday -> holiday.getIsHoliday() == 0 || holiday.getIsWork() == 1)
        .map(holiday -> DateUtils.formatDate(holiday.getDate())) // 转成字符串格式返回
        .collect(Collectors.toList());
        }
        /**
        * 模拟阿里云API返回数据(实际开发替换为真实HTTP调用)
        */
        private List<SysHoliday> mockAliCloudHolidayData() {
          List<SysHoliday> list = new ArrayList<>();
            try {
            // 示例1:2024-10-01(国庆节,节假日,不上班)
            SysHoliday h1 = new SysHoliday();
            h1.setDate(DateUtils.parseDate("2024-10-01"));
            h1.setIsHoliday(1);
            h1.setIsWork(0);
            list.add(h1);
            // 示例2:2024-10-06(调休上班,非节假日)
            SysHoliday h2 = new SysHoliday();
            h2.setDate(DateUtils.parseDate("2024-10-06"));
            h2.setIsHoliday(0);
            h2.setIsWork(1);
            list.add(h2);
            // 示例3:2024-10-08(正常工作日)
            SysHoliday h3 = new SysHoliday();
            h3.setDate(DateUtils.parseDate("2024-10-08"));
            h3.setIsHoliday(0);
            h3.setIsWork(0);
            list.add(h3);
            } catch (Exception e) {
            e.printStackTrace();
            }
            return list;
            }
            }

3. 库存服务(SealStockService.java)

package com.seal.service;
import com.seal.config.RabbitMQConfig;
import com.seal.entity.SealStock;
import com.seal.mapper.SealStockMapper;
import com.seal.utils.DateUtils;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Date;
/**
* 库存服务:查询库存+扣减库存(Redis原子操作+MQ异步落库)
*/
@Service
public class SealStockService {
@Autowired
private RedisTemplate<String, Object> redisTemplate; // 注入Redis工具
  @Autowired
  private RabbitTemplate rabbitTemplate;               // 注入RabbitMQ工具
  @Autowired
  private SealStockMapper stockMapper;                 // 注入库存Mapper
  // Redis库存缓存Key
  private static final String STOCK_CACHE_KEY = "seal:stock:%s:%s";
  /**
  * 查询某日期+时段的剩余库存(优先查Redis,缓存未命中查数据库)
  * @param date 预约日期
  * @param timeSlotId 时段ID
  * @return 剩余可预约数(0表示约满)
  */
  public Integer getRemainingStock(Date date, Long timeSlotId) {
  String dateStr = DateUtils.formatDate(date);
  String cacheKey = String.format(STOCK_CACHE_KEY, dateStr, timeSlotId);
  // ① 先查Redis缓存(快速,毫秒级响应)
  Object cacheValue = redisTemplate.opsForValue().get(cacheKey);
  if (cacheValue != null) {
  return (Integer) cacheValue;
  }
  // ② 缓存未命中,查数据库(并同步到Redis,下次直接查缓存)
  SealStock stock = stockMapper.selectByDateAndSlot(date, timeSlotId);
  int remaining = stock != null ? stock.getRemainingNum() : 0;
  redisTemplate.opsForValue().set(cacheKey, remaining); // 同步缓存
  return remaining;
  }
  /**
  * 扣减库存:Redis原子操作保证高并发安全,MQ异步更新数据库
  * @return true-扣减成功,false-库存不足
  */
  public boolean decreaseStock(Date date, Long timeSlotId) {
  String dateStr = DateUtils.formatDate(date);
  String cacheKey = String.format(STOCK_CACHE_KEY, dateStr, timeSlotId);
  // ① Redis原子扣减(DECR命令:线程安全,避免超卖)
  Long remainAfterDecrease = redisTemplate.opsForValue().decrement(cacheKey);
  if (remainAfterDecrease == null || remainAfterDecrease < 0) {
  // 库存不足:回滚Redis(把刚才减的1加回来)
  redisTemplate.opsForValue().increment(cacheKey);
  return false;
  }
  // ② 发送MQ消息,异步更新数据库(不阻塞当前接口)
  StockUpdateMsg msg = new StockUpdateMsg(date, timeSlotId);
  rabbitTemplate.convertAndSend(RabbitMQConfig.QUEUE_STOCK_UPDATE, msg);
  return true;
  }
  /**
  * 库存更新消息体:用于MQ传递日期和时段ID(内部静态类,避免单独建类)
  */
  public static class StockUpdateMsg {
  private Date date;         // 预约日期
  private Long timeSlotId;   // 时段ID
  public StockUpdateMsg(Date date, Long timeSlotId) {
  this.date = date;
  this.timeSlotId = timeSlotId;
  }
  // Getter(MQ序列化需要)
  public Date getDate() { return date; }
  public Long getTimeSlotId() { return timeSlotId; }
  }
  }

4. 预约订单服务(SealAppointmentService.java)

package com.seal.service;
import com.seal.config.RabbitMQConfig;
import com.seal.entity.SealAppointment;
import com.seal.mapper.SealAppointmentMapper;
import com.seal.utils.DateUtils;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
/**
* 预约订单服务:处理用户预约提交(controller→service→mapper)
*/
@Service
public class SealAppointmentService {
@Autowired
private SealStockService stockService;           // 注入库存服务
@Autowired
private SealAppointmentMapper appointmentMapper; // 注入订单Mapper
@Autowired
private RabbitTemplate rabbitTemplate;           // 注入RabbitMQ工具
/**
* 提交印章预约
* @param userId 用户ID(登录后获取)
* @param sealId 印章ID(用户选择的印章)
* @param dateStr 预约日期(yyyy-MM-dd格式)
* @param timeSlotId 时段ID(用户选择的时段)
* @return 预约订单ID(成功时返回)
* @throws Exception 日期格式错误/库存不足等异常
*/
@Transactional // 事务注解:保证订单生成和库存扣减的一致性
public Long submitAppointment(
Long userId,
Long sealId,
String dateStr,
Long timeSlotId
) throws Exception {
// 1. 日期格式转换(前端传字符串,转成Date对象)
Date appointmentDate = DateUtils.parseDate(dateStr);
if (appointmentDate == null) {
throw new Exception("预约日期格式错误,需为yyyy-MM-dd");
}
// 2. 检查库存(调用库存服务,优先查Redis)
Integer remainingStock = stockService.getRemainingStock(appointmentDate, timeSlotId);
if (remainingStock <= 0) {
throw new Exception("该时段库存不足(最多5人),请选择其他时段");
}
// 3. 扣减库存(Redis原子操作+MQ异步落库)
boolean decreaseSuccess = stockService.decreaseStock(appointmentDate, timeSlotId);
if (!decreaseSuccess) {
throw new Exception("库存扣减失败,请重试");
}
// 4. 生成预约订单(同步写入数据库,保证订单实时性)
SealAppointment appointment = new SealAppointment();
appointment.setUserId(userId);
appointment.setSealId(sealId);
appointment.setAppointmentDate(appointmentDate);
appointment.setTimeSlotId(timeSlotId);
appointment.setStatus(0); // 0-待确认(后续通知后更新为1)
appointment.setCreateTime(new Date()); // 订单生成时间=当前时间
// 调用Mapper插入订单
appointmentMapper.insert(appointment);
System.out.println("预约订单生成成功:ID=" + appointment.getId());
// 5. 发送MQ消息,异步处理通知(不阻塞当前接口)
rabbitTemplate.convertAndSend(RabbitMQConfig.QUEUE_APPOINT_NOTICE, appointment.getId());
return appointment.getId(); // 返回订单ID给前端
}
}

六、Controller 层(接口层,调用 Service)

package com.seal.controller;
import com.seal.service.SealAppointmentService;
import com.seal.service.SysHolidayService;
import com.seal.utils.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 印章预约Controller:前端调用的接口入口(遵循REST风格)
*/
@RestController // 组合注解:@Controller + @ResponseBody(返回JSON)
@RequestMapping("/api/seal/appointment") // 接口统一前缀,避免URL冲突
public class SealAppointmentController {
@Autowired
private SysHolidayService holidayService;         // 注入节假日服务
@Autowired
private SealAppointmentService appointmentService; // 注入预约服务
/**
* 接口1:查询可预约日期(前端日期选择器用)
* 请求方式:GET
* 示例URL:http://localhost:8080/api/seal/appointment/available-dates
*/
@GetMapping("/available-dates")
public Result<List<String>> getAvailableDates() {
  try {
  // 调用Service查询可预约日期
  List<String> availableDates = holidayService.listAvailableDates();
    return Result.success(availableDates); // 返回成功结果(带数据)
    } catch (Exception e) {
    return Result.fail("查询可预约日期失败:" + e.getMessage()); // 返回失败结果
    }
    }
    /**
    * 接口2:提交预约(用户确认预约时调用)
    * 请求方式:POST
    * 示例URL:http://localhost:8080/api/seal/appointment/submit
    * 示例参数:userId=1&sealId=2&dateStr=2024-10-08&timeSlotId=1
    */
    @PostMapping("/submit")
    public Result<Long> submitAppointment(
      @RequestParam Long userId,         // 用户ID(前端传参)
      @RequestParam Long sealId,         // 印章ID(前端传参)
      @RequestParam String dateStr,      // 预约日期(yyyy-MM-dd,前端传参)
      @RequestParam Long timeSlotId      // 时段ID(前端传参)
      ) {
      try {
      // 调用Service提交预约,返回订单ID
      Long orderId = appointmentService.submitAppointment(userId, sealId, dateStr, timeSlotId);
      return Result.success(orderId); // 返回成功结果(带订单ID)
      } catch (Exception e) {
      return Result.fail("预约失败:" + e.getMessage()); // 返回失败结果
      }
      }
      }

七、RabbitMQ 消费者(异步处理,解耦业务)

1. 库存更新消费者(StockUpdateConsumer.java)

package com.seal.consumer;
import com.seal.config.RabbitMQConfig;
import com.seal.entity.SealStock;
import com.seal.mapper.SealStockMapper;
import com.seal.service.SealStockService;
import com.seal.utils.DateUtils;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 库存更新消费者:监听MQ队列,异步更新MySQL库存(解耦业务)
*/
@Component // Spring注解:标识为组件,自动扫描
public class StockUpdateConsumer {
@Autowired
private SealStockMapper stockMapper; // 注入库存Mapper
/**
* 监听库存更新队列(QUEUE_STOCK_UPDATE)
* 收到消息后,用乐观锁更新数据库库存
*/
@RabbitListener(queues = RabbitMQConfig.QUEUE_STOCK_UPDATE)
public void handleStockUpdate(SealStockService.StockUpdateMsg msg) {
try {
// ① 根据日期+时段ID查询当前库存(获取版本号)
SealStock stock = stockMapper.selectByDateAndSlot(msg.getDate(), msg.getTimeSlotId());
if (stock == null) {
System.err.println("库存不存在:日期=" + DateUtils.formatDate(msg.getDate()) + ", 时段ID=" + msg.getTimeSlotId());
return;
}
// ② 乐观锁更新库存(仅版本号匹配时成功,防并发冲突)
int affectedRows = stockMapper.decreaseWithVersion(
msg.getDate(),
msg.getTimeSlotId(),
stock.getVersion()
);
if (affectedRows == 1) {
System.out.println("数据库库存更新成功:日期=" + DateUtils.formatDate(msg.getDate()) + ", 时段ID=" + msg.getTimeSlotId());
} else {
// 更新失败(版本号不匹配,说明被其他线程修改)
System.err.println("数据库库存更新失败(乐观锁冲突):日期=" + DateUtils.formatDate(msg.getDate()) + ", 时段ID=" + msg.getTimeSlotId());
}
} catch (Exception e) {
e.printStackTrace();
System.err.println("库存更新异常:" + e.getMessage());
}
}
}

2. 预约通知消费者(NoticeConsumer.java)

package com.seal.consumer;
import com.seal.config.RabbitMQConfig;
import com.seal.entity.SealAppointment;
import com.seal.mapper.SealAppointmentMapper;
import com.seal.utils.DateUtils;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 预约通知消费者:监听MQ队列,异步发送通知+更新订单状态
*/
@Component
public class NoticeConsumer {
@Autowired
private SealAppointmentMapper appointmentMapper; // 注入订单Mapper
/**
* 监听预约通知队列(QUEUE_APPOINT_NOTICE)
* 收到消息后,发送通知+更新订单状态为“已确认”
*/
@RabbitListener(queues = RabbitMQConfig.QUEUE_APPOINT_NOTICE)
public void handleAppointNotice(Long appointmentId) {
try {
// ① 根据订单ID查询订单信息
SealAppointment appointment = appointmentMapper.selectById(appointmentId);
if (appointment == null) {
System.err.println("预约订单不存在:ID=" + appointmentId);
return;
}
// ② 模拟发送通知(实际项目替换为短信/站内信/邮件)
String noticeMsg = String.format(
"用户%d,您已成功预约印章%d,日期:%s,时段ID:%d,请按时办理",
appointment.getUserId(),
appointment.getSealId(),
DateUtils.formatDate(appointment.getAppointmentDate()),
appointment.getTimeSlotId()
);
System.out.println("发送预约通知:" + noticeMsg);
// ③ 更新订单状态为“已确认”(0→1)
appointmentMapper.updateStatus(appointmentId, 1);
System.out.println("订单状态更新成功:ID=" + appointmentId + ",状态:0→1");
} catch (Exception e) {
e.printStackTrace();
System.err.println("预约通知异常:" + e.getMessage());
}
}
}

八、配置文件(application.yml)

spring:
# 数据库配置
datasource:
url: jdbc:mysql://localhost:3306/seal_db?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root          # 你的MySQL用户名
password: 123456        # 你的MySQL密码
driver-class-name: com.mysql.cj.jdbc.Driver
# Redis配置
redis:
host: localhost         # Redis地址(本地)
port: 6379              # Redis端口(默认)
password:               # Redis密码(无则留空)
database: 0             # Redis数据库编号(默认0)
# RabbitMQ配置
rabbitmq:
host: localhost         # RabbitMQ地址(本地)
port: 5672              # RabbitMQ端口(默认)
username: guest         # RabbitMQ默认用户名
password: guest         # RabbitMQ默认密码
listener:
simple:
acknowledge-mode: manual # 手动确认消息(避免消息丢失)
# MyBatis配置
mybatis:
mapper-locations: classpath:mapper/*.xml  # Mapper XML文件路径
type-aliases-package: com.seal.entity     # 实体类包(简化XML中的resultType)
configuration:
map-underscore-to-camel-case: true      # 下划线转驼峰(如time_slot_start→timeSlotStart)
# 阿里云api
holiday:
ali-api:
url: "https://alidayu.market.aliyuncs.com/api/dayu/getholiday" # 阿里云API真实地址(需替换为你申请的接口地址)
app-code: "你的阿里云APPCODE" # 从阿里云控制台获取(API开通后在「我的API」中查看)
date-range: 90 # 同步未来90天(3个月)的节假日数据
# 服务端口
server:
port: 8080

核心流程总结

  • 定时任务初始化:每日 20:00 同步节假日 + 初始化未来 7 天库存(每个时段 5 人),同步到 Redis;
  • 前端查询可预约日期:调用/api/seal/appointment/available-dates,后端返回非节假日 / 调休上班日期;
  • 用户提交预约:调用/api/seal/appointment/submit,后端流程:
    Controller 接收参数→Service 检查库存→Redis 原子扣减库存→生成订单(Mapper 插入)→发送 MQ 通知;
  • 异步处理
    库存消费者:监听 MQ,用乐观锁更新 MySQL 库存;
    通知消费者:监听 MQ,发送通知 + 更新订单状态为 “已确认”。