酒店PMS系统设计:房型房价管理与动态价格优先级实现(Java/MySQL)

酒店PMS系统设计:房型房价管理与动态价格优先级实现

一、核心设计理念:房价跟着房型走

在酒店PMS(Property Management System)中,房价管理的核心原则是:房价绑定房型,而非绑定具体房间。

这样做的好处:

  • 房价维护成本低:改一个房型价格,所有该房型房间同步生效
  • 库存灵活:只要该房型有空房就能卖,不用逐间定价
  • 符合酒店运营习惯:前台/OTA上架的都是房型维度

二、价格动态性:每天的价格都不一样

酒店房价不是一成不变的,受多种因素影响:

影响因素 示例 典型场景
基础价 工作日 ¥400 / 周末 ¥500 常态化定价
特定时间段 暑假 ¥600/晚(7.1-8.31) 寒暑假、会展期
特定节假日 国庆 ¥800/晚 春节、五一、国庆
特定日期 12月31日 ¥1200/晚 跨年夜、店庆、本地大型活动

三、价格优先级规则设计

当多个价格规则在同一天同时命中时,必须有一个明确的优先级来决出"最终生效价格"。

优先级体系(从高到低)

复制代码
特定日期价格  >  特定节假日价格  >  特定时间段价格  >  基础价

设计逻辑

层级 说明
特定日期(最高优先级) 酒店偶尔会有特殊活动日或临时调价,需要最高优先级覆盖一切
特定节假日​ 法定节假日通常比普通时间段贵,但可能被某天的特殊活动覆盖
特定时间段​ 寒暑假、展会周等中期价格策略
基础价(最低优先级) 兜底价格,工作日/周末的常态价格

四、数据库表结构设计(MySQL)

4.1 房型表(room_type)

复制代码
CREATE TABLE room_type (
    id             BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '房型ID',
    type_code      VARCHAR(50)  NOT NULL COMMENT '房型编码,如 DELUXE_DBL',
    type_name      VARCHAR(100) NOT NULL COMMENT '房型名称,如 豪华大床房',
    bed_type       VARCHAR(30)           DEFAULT NULL COMMENT '床型',
    max_occupancy  INT          NOT NULL DEFAULT 2 COMMENT '最大入住人数',
    status         TINYINT      NOT NULL DEFAULT 1 COMMENT '1-有效 0-无效',
    created_at     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_type_code (type_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='房型表';

4.2 基础价表(rate_base)

存储工作日/周末的常态化价格:

复制代码
CREATE TABLE rate_base (
    id             BIGINT PRIMARY KEY AUTO_INCREMENT,
    room_type_id   BIGINT       NOT NULL COMMENT '房型ID',
    weekday_type   TINYINT      NOT NULL COMMENT '1-工作日(周一至周五) 2-周末(周六周日) 3-全天统一',
    base_price     DECIMAL(10,2) NOT NULL COMMENT '基础价格',
    effective_from DATE                  DEFAULT NULL COMMENT '生效开始日期,NULL表示不限制',
    effective_to   DATE                  DEFAULT NULL COMMENT '生效结束日期,NULL表示不限制',
    status         TINYINT      NOT NULL DEFAULT 1 COMMENT '1-启用 0-停用',
    created_at     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_roomtype_weekday (room_type_id, weekday_type),
    CONSTRAINT fk_rate_base_roomtype FOREIGN KEY (room_type_id) REFERENCES room_type(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='基础房价表(工作日/周末)';

4.3 特定时间段价格表(rate_period)

复制代码
CREATE TABLE rate_period (
    id             BIGINT PRIMARY KEY AUTO_INCREMENT,
    room_type_id   BIGINT       NOT NULL COMMENT '房型ID',
    period_name    VARCHAR(100) NOT NULL COMMENT '时间段名称,如 暑假/寒假/展会期',
    start_date     DATE         NOT NULL COMMENT '开始日期',
    end_date       DATE         NOT NULL COMMENT '结束日期',
    price          DECIMAL(10,2) NOT NULL COMMENT '该时间段价格',
    priority       INT          NOT NULL DEFAULT 20 COMMENT '优先级数值,越小越高(预留扩展)',
    status         TINYINT      NOT NULL DEFAULT 1 COMMENT '1-启用 0-停用',
    created_at     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_daterange (start_date, end_date),
    INDEX idx_roomtype (room_type_id),
    CONSTRAINT fk_rate_period_roomtype FOREIGN KEY (room_type_id) REFERENCES room_type(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='特定时间段房价表(寒暑假/展会等)';

4.4 特定节假日价格表(rate_holiday)

复制代码
CREATE TABLE rate_holiday (
    id             BIGINT PRIMARY KEY AUTO_INCREMENT,
    room_type_id   BIGINT       NOT NULL COMMENT '房型ID',
    holiday_name   VARCHAR(50)  NOT NULL COMMENT '节假日名称,如 春节/国庆节/端午节',
    holiday_date   DATE         NOT NULL COMMENT '节假日日期(精确到天)',
    price          DECIMAL(10,2) NOT NULL COMMENT '该节假日价格',
    status         TINYINT      NOT NULL DEFAULT 1 COMMENT '1-启用 0-停用',
    created_at     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_roomtype_holiday (room_type_id, holiday_date),
    INDEX idx_holiday_date (holiday_date),
    CONSTRAINT fk_rate_holiday_roomtype FOREIGN KEY (room_type_id) REFERENCES room_type(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='特定节假日房价表';

4.5 特定日期价格表(rate_specific_date)

最高优先级的价格规则:

复制代码
CREATE TABLE rate_specific_date (
    id             BIGINT PRIMARY KEY AUTO_INCREMENT,
    room_type_id   BIGINT       NOT NULL COMMENT '房型ID',
    specific_date  DATE         NOT NULL COMMENT '特定日期',
    price          DECIMAL(10,2) NOT NULL COMMENT '该日价格',
    reason         VARCHAR(200)          DEFAULT NULL COMMENT '调价原因,如 跨年夜/店庆/大型活动',
    status         TINYINT      NOT NULL DEFAULT 1 COMMENT '1-启用 0-停用',
    created_at     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_roomtype_date (room_type_id, specific_date),
    CONSTRAINT fk_rate_specific_roomtype FOREIGN KEY (room_type_id) REFERENCES room_type(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='特定日期房价表(最高优先级)';

4.6 每日房型最终价格表(room_type_daily_price)

最终生效价格落地表,通过定时任务或实时计算后写入,供预订、下单时直接读取:

复制代码
CREATE TABLE room_type_daily_price (
    id             BIGINT PRIMARY KEY AUTO_INCREMENT,
    room_type_id   BIGINT       NOT NULL COMMENT '房型ID',
    price_date     DATE         NOT NULL COMMENT '日期',
    final_price    DECIMAL(10,2) NOT NULL COMMENT '最终生效价格',
    source_type    TINYINT               DEFAULT NULL COMMENT '1-基础价 2-时间段 3-节假日 4-特定日期',
    source_id      BIGINT                DEFAULT NULL COMMENT '来源规则ID',
    calculated_at   DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '计算时间',
    UNIQUE KEY uk_roomtype_date (room_type_id, price_date),
    INDEX idx_price_date (price_date),
    INDEX idx_roomtype (room_type_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='每日房型最终价格表(房价日历)';

五、Java 实体类

复制代码
// ========== 房型 ==========
@Data
@TableName("room_type")
public class RoomType {
    private Long id;
    private String typeCode;
    private String typeName;
    private String bedType;
    private Integer maxOccupancy;
    private Integer status;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

// ========== 基础价 ==========
@Data
@TableName("rate_base")
public class RateBase {
    private Long id;
    private Long roomTypeId;
    /** 1-工作日 2-周末 3-全天统一 */
    private Integer weekdayType;
    private BigDecimal basePrice;
    private LocalDate effectiveFrom;
    private LocalDate effectiveTo;
    private Integer status;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

// ========== 时间段价格 ==========
@Data
@TableName("rate_period")
public class RatePeriod {
    private Long id;
    private Long roomTypeId;
    private String periodName;
    private LocalDate startDate;
    private LocalDate endDate;
    private BigDecimal price;
    private Integer priority;
    private Integer status;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

// ========== 节假日价格 ==========
@Data
@TableName("rate_holiday")
public class RateHoliday {
    private Long id;
    private Long roomTypeId;
    private String holidayName;
    private LocalDate holidayDate;
    private BigDecimal price;
    private Integer status;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

// ========== 特定日期价格 ==========
@Data
@TableName("rate_specific_date")
public class RateSpecificDate {
    private Long id;
    private Long roomTypeId;
    private LocalDate specificDate;
    private BigDecimal price;
    private String reason;
    private Integer status;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

// ========== 每日最终价格 ==========
@Data
@TableName("room_type_daily_price")
public class RoomTypeDailyPrice {
    private Long id;
    private Long roomTypeId;
    private LocalDate priceDate;
    private BigDecimal finalPrice;
    /** 1-基础价 2-时间段 3-节假日 4-特定日期 */
    private Integer sourceType;
    private Long sourceId;
    private LocalDateTime calculatedAt;
}

// ========== 价格计算结果DTO ==========
@Data
@AllArgsConstructor
public class PriceResultDTO {
    /** 最终价格 */
    private BigDecimal finalPrice;
    /** 来源类型 */
    private Integer sourceType;
    /** 来源规则ID */
    private Long sourceId;
}

六、Mapper 接口(MyBatis-Plus)

复制代码
@Mapper
public interface RoomTypeDailyPriceMapper extends BaseMapper<RoomTypeDailyPrice> {

    /**
     * 批量插入或更新每日价格
     */
    int batchInsertOrUpdate(@Param("list") List<RoomTypeDailyPrice> list);
}

@Mapper
public interface RateSpecificDateMapper extends BaseMapper<RateSpecificDate> {
    RateSpecificDate selectByRoomTypeAndDate(@Param("roomTypeId") Long roomTypeId,
                                              @Param("date") LocalDate date);
}

@Mapper
public interface RateHolidayMapper extends BaseMapper<RateHoliday> {
    RateHoliday selectByRoomTypeAndDate(@Param("roomTypeId") Long roomTypeId,
                                         @Param("date") LocalDate date);
}

@Mapper
public interface RatePeriodMapper extends BaseMapper<RatePeriod> {
    RatePeriod selectByRoomTypeAndDate(@Param("roomTypeId") Long roomTypeId,
                                        @Param("date") LocalDate date);
}

@Mapper
public interface RateBaseMapper extends BaseMapper<RateBase> {
    RateBase selectByRoomTypeAndWeekday(@Param("roomTypeId") Long roomTypeId,
                                         @Param("weekdayType") Integer weekdayType);
}

七、价格计算核心逻辑

7.1 单日价格计算服务

复制代码
@Service
@RequiredArgsConstructor
public class PriceCalculationService {

    private final RateSpecificDateMapper specificDateMapper;
    private final RateHolidayMapper holidayMapper;
    private final RatePeriodMapper periodMapper;
    private final RateBaseMapper baseMapper;

    /**
     * 计算指定房型在指定日期的最终价格
     * 优先级:特定日期(4) > 节假日(3) > 时间段(2) > 基础价(1)
     */
    public PriceResultDTO calculateDailyPrice(Long roomTypeId, LocalDate targetDate) {

        // 优先级4:特定日期价格(最高)
        RateSpecificDate specific = specificDateMapper.selectByRoomTypeAndDate(roomTypeId, targetDate);
        if (specific != null && specific.getStatus() == 1) {
            return new PriceResultDTO(specific.getPrice(), 4, specific.getId());
        }

        // 优先级3:特定节假日价格
        RateHoliday holiday = holidayMapper.selectByRoomTypeAndDate(roomTypeId, targetDate);
        if (holiday != null && holiday.getStatus() == 1) {
            return new PriceResultDTO(holiday.getPrice(), 3, holiday.getId());
        }

        // 优先级2:特定时间段价格
        RatePeriod period = periodMapper.selectByRoomTypeAndDate(roomTypeId, targetDate);
        if (period != null && period.getStatus() == 1) {
            return new PriceResultDTO(period.getPrice(), 2, period.getId());
        }

        // 优先级1:基础价(区分工作日/周末)
        int dayOfWeek = targetDate.getDayOfWeek().getValue(); // 1=周一 ... 7=周日
        Integer weekdayType = (dayOfWeek <= 5) ? 1 : 2;

        RateBase base = baseMapper.selectByRoomTypeAndWeekday(roomTypeId, weekdayType);
        if (base != null && base.getStatus() == 1) {
            return new PriceResultDTO(base.getBasePrice(), 1, base.getId());
        }

        // 兜底:尝试全天统一的基础价
        RateBase unified = baseMapper.selectByRoomTypeAndWeekday(roomTypeId, 3);
        if (unified != null && unified.getStatus() == 1) {
            return new PriceResultDTO(unified.getBasePrice(), 1, unified.getId());
        }

        throw new PriceNotFoundException(
            String.format("未找到房型[%d]在日期[%s]的价格配置", roomTypeId, targetDate));
    }
}

7.2 批量生成房价日历服务

复制代码
@Service
@RequiredArgsConstructor
public class DailyPriceGenerateService {

    private final PriceCalculationService priceCalculationService;
    private final RoomTypeDailyPriceMapper dailyPriceMapper;
    private final RoomTypeMapper roomTypeMapper;

    /**
     * 为指定房型生成未来N天的每日价格
     */
    @Transactional
    public int generateDailyPrices(Long roomTypeId, int daysAhead) {
        LocalDate startDate = LocalDate.now();
        List<RoomTypeDailyPrice> batchList = new ArrayList<>();

        for (int i = 0; i < daysAhead; i++) {
            LocalDate targetDate = startDate.plusDays(i);
            PriceResultDTO result = priceCalculationService.calculateDailyPrice(roomTypeId, targetDate);

            RoomTypeDailyPrice dailyPrice = new RoomTypeDailyPrice();
            dailyPrice.setRoomTypeId(roomTypeId);
            dailyPrice.setPriceDate(targetDate);
            dailyPrice.setFinalPrice(result.getFinalPrice());
            dailyPrice.setSourceType(result.getSourceType());
            dailyPrice.setSourceId(result.getSourceId());
            dailyPrice.setCalculatedAt(LocalDateTime.now());

            batchList.add(dailyPrice);
        }

        return dailyPriceMapper.batchInsertOrUpdate(batchList);
    }

    /**
     * 为所有有效房型生成每日价格
     */
    @Transactional
    public void generateAllRoomTypeDailyPrices(int daysAhead) {
        List<RoomType> roomTypes = roomTypeMapper.selectList(
            Wrappers.<RoomType>lambdaQuery().eq(RoomType::getStatus, 1)
        );
        for (RoomType rt : roomTypes) {
            generateDailyPrices(rt.getId(), daysAhead);
        }
    }
}

7.3 批量生成SQL(纯SQL方案,可选)

复制代码
INSERT INTO room_type_daily_price
    (room_type_id, price_date, final_price, source_type, source_id, calculated_at)
SELECT
    v_room_type_id,
    date_series.dt,
    CASE
        WHEN rsd.price IS NOT NULL THEN rsd.price
        WHEN rh.price IS NOT NULL THEN rh.price
        WHEN rp.price IS NOT NULL THEN rp.price
        WHEN rb.price IS NOT NULL THEN rb.price
        ELSE v_default_price
    END AS final_price,
    CASE
        WHEN rsd.price IS NOT NULL THEN 4
        WHEN rh.price IS NOT NULL THEN 3
        WHEN rp.price IS NOT NULL THEN 2
        WHEN rb.price IS NOT NULL THEN 1
        ELSE 0
    END AS source_type,
    CASE
        WHEN rsd.price IS NOT NULL THEN rsd.id
        WHEN rh.price IS NOT NULL THEN rh.id
        WHEN rp.price IS NOT NULL THEN rp.id
        WHEN rb.price IS NOT NULL THEN rb.id
        ELSE NULL
    END AS source_id,
    NOW()
FROM (
    SELECT DATE_ADD(CURDATE(), INTERVAL seq DAY) AS dt
    FROM seq_0_to_89
) date_series
LEFT JOIN rate_specific_date rsd
    ON rsd.room_type_id = v_room_type_id
    AND rsd.specific_date = date_series.dt
    AND rsd.status = 1
LEFT JOIN rate_holiday rh
    ON rh.room_type_id = v_room_type_id
    AND rh.holiday_date = date_series.dt
    AND rh.status = 1
LEFT JOIN rate_period rp
    ON rp.room_type_id = v_room_type_id
    AND date_series.dt BETWEEN rp.start_date AND rp.end_date
    AND rp.status = 1
LEFT JOIN rate_base rb
    ON rb.room_type_id = v_room_type_id
    AND rb.status = 1
    AND (
        (DAYOFWEEK(date_series.dt) IN (2,3,4,5,6) AND rb.weekday_type = 1)
        OR (DAYOFWEEK(date_series.dt) IN (1,7) AND rb.weekday_type = 2)
        OR rb.weekday_type = 3
    )
ON DUPLICATE KEY UPDATE
    final_price   = VALUES(final_price),
    source_type   = VALUES(source_type),
    source_id     = VALUES(source_id),
    calculated_at = VALUES(calculated_at);

八、Mapper XML(批量插入/更新)

复制代码
<!-- RoomTypeDailyPriceMapper.xml -->
<mapper namespace="com.example.pms.mapper.RoomTypeDailyPriceMapper">

    <insert id="batchInsertOrUpdate" parameterType="java.util.List">
        INSERT INTO room_type_daily_price
            (room_type_id, price_date, final_price, source_type, source_id, calculated_at)
        VALUES
        <foreach collection="list" item="item" separator=",">
            (#{item.roomTypeId}, #{item.priceDate}, #{item.finalPrice},
             #{item.sourceType}, #{item.sourceId}, #{item.calculatedAt})
        </foreach>
        ON DUPLICATE KEY UPDATE
            final_price   = VALUES(final_price),
            source_type   = VALUES(source_type),
            source_id     = VALUES(source_id),
            calculated_at = VALUES(calculated_at)
    </insert>

</mapper>

九、定时任务触发(可选)

复制代码
@Component
@RequiredArgsConstructor
public class PriceGenerateScheduler {

    private final DailyPriceGenerateService generateService;

    /**
     * 每天凌晨2点生成未来90天的价格日历
     */
    @Scheduled(cron = "0 0 2 * * ?")
    public void scheduledGenerate() {
        generateService.generateAllRoomTypeDailyPrices(90);
    }
}

十、设计要点回顾

要点 说明
价格与房间解耦​ 房价绑定房型,所有同房型房间共享价格
优先级链​ 特定日期 > 节假日 > 时间段 > 基础价,通过代码判断顺序实现
规则与落地分离​ 规则表负责配置,daily_price表负责落地查询,避免实时计算
可追溯​ source_type + source_id 记录价格来源,方便对账
MySQL适配​ 使用 ON DUPLICATE KEY UPDATE 实现幂等写入,适合定时任务反复执行
扩展性强​ 新增价格类型(如渠道价、会员等级价)只需新增规则表并在计算逻辑中插入对应层级

声明:本文为个人技术总结,表结构、代码均为通用行业示例,非任何真实生产系统。


相关推荐
howard20051 小时前
任务 2-3 图形化管理工具Navicat
数据库·mysql·navicat
一碗白开水一1 小时前
Day 5 · 弹性 | 微观
java·前端·数据库
迪飞特科技1 小时前
大模型接入业务系统:API、本地部署、RAG、智能体四种路线怎么选
java·数据库·人工智能·知识图谱·deepseek·本地化大模型
星轨zb1 小时前
[专题5]支付发放物品的一致性保障:事务、幂等与状态流转
数据库·后端·mongodb·支付架构学习
保密资质顾问说1 小时前
保密培训三大误区纠正:从形式主义到持续合规
数据库
泡海椒1 小时前
JQuick-SQL GROUP BY 聚合:COUNT、SUM、AVG、MIN 与 MAX
数据库·sql
小冯不疯1 小时前
销售订单同步实战:从聚水潭到金蝶云星空的线上出库单落地
大数据·数据库
vipxieliang1 小时前
ValidX 验证器缓存机制:如何提升验证性能
java·spring boot
qq_401700411 小时前
Qt 实时大数据量存储:自己动手实现 TDMS 读写
数据库·qt