← 返回文档中心

库存管理

库存管理

优先级: P1(业务增长模块)
开发周期: 2 周

目标

实时管理各供应商的房态和房量,支持库存预占、同步和释放,避免超售。

核心功能

功能 描述
实时库存同步 定时/实时从供应商拉取库存,或接收供应商推送
库存预占 下单时预占库存(带 TTL,超时自动释放)
库存释放 取消订单时释放预占库存
关停房管理 管理酒店的关停房日期(Stop Sell)
房量分配 按渠道/代理商分配房量配额
库存告警 库存低于阈值时告警
超售保护 本地库存计数 + 供应商实时校验双重保护

连接模式与库存管理范围

库存管理模块主要适用于 direct_procure 供应商,但直采供应商也可能"有价即有房"(available_rooms=NULL),不一定要管理库存数量。不同连接模式的库存处理差异:

注意: available_rooms 允许 NULL。NULL 表示有价即有房、不限量;有值表示真实库存数、售完为止。

维度 direct_connect(直连) direct_procure(直采)
库存管理 ❌ 不涉及 ✅ 完整生命周期管理
库存概念 有价即有房,无库存数量 可能有明确房量数字(available_rooms 有值),也可能有价即有房(available_rooms=NULL)
数据写入 不写入库存表 inventory_snapshots 定期同步
下单操作 无需预占/扣减库存 Redis DECRBY 预占 → 确认转占用
库存同步 不适用 定时拉取(pull)/ 供应商推送(push)
关停房管理 不适用 stop_sells 表管理
房量分配 不适用 inventory_allocations 按渠道/代理商分配

设计原则:库存模块在查询供应商数据时,应先检查 connection_mode 字段。对于 direct_connect 供应商,跳过所有库存相关逻辑。对于 direct_procure 供应商,还需检查 available_rooms 是否为 NULL(NULL 表示有价即有房,无需校验库存数量)。

库存预占机制

下单时:
1. Redis DECRBY 预占库存 (原子操作)
2. 若预占成功 → 创建订单
3. 若预占失败 → 返回库存不足

供应商确认后:
1. 将预占库存转为确认占用
2. Redis 更新实际可用库存

供应商拒绝/取消/超时:
1. Redis INCRBY 释放预占库存
2. 通知代理商

定时任务(每5分钟):
1. 清理超时未确认的预占库存
2. 同步供应商最新库存

数据模型

-- 实时库存表(热数据,频繁更新)
CREATE TABLE inventory_snapshots (
    id                  BIGSERIAL PRIMARY KEY,
    supplier_code       VARCHAR(32) NOT NULL,
    supplier_hotel_id   VARCHAR(64) NOT NULL,
    room_type_code      VARCHAR(32) NOT NULL,

    stay_date           DATE NOT NULL,             # 入住日期

    total_rooms         INT DEFAULT 0,             # 总房量
    sold_rooms          INT DEFAULT 0,             # 已售
    blocked_rooms       INT DEFAULT 0,             # 关停房
    available_rooms     INT DEFAULT 0,             # 可用房量

    last_synced_at      TIMESTAMPTZ,
    sync_source         VARCHAR(32) DEFAULT 'pull', # pull/push/manual

    created_at          TIMESTAMPTZ DEFAULT NOW(),
    updated_at          TIMESTAMPTZ DEFAULT NOW(),

    UNIQUE(supplier_code, supplier_hotel_id, room_type_code, stay_date)
);

CREATE INDEX idx_inventory_date ON inventory_snapshots (supplier_code, stay_date);

-- 库存预占记录表
CREATE TABLE inventory_holds (
    id                  BIGSERIAL PRIMARY KEY,
    order_id            BIGINT NOT NULL REFERENCES orders(id),
    supplier_code       VARCHAR(32) NOT NULL,
    supplier_hotel_id   VARCHAR(64) NOT NULL,
    room_type_code      VARCHAR(32) NOT NULL,

    stay_date           DATE NOT NULL,
    room_count          SMALLINT NOT NULL DEFAULT 1,

    status              VARCHAR(20) DEFAULT 'active', -- active/confirmed/released/expired
    expires_at          TIMESTAMPTZ NOT NULL,          # 预占过期时间
    released_at         TIMESTAMPTZ,

    created_at          TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_inventory_holds_order ON inventory_holds (order_id);
CREATE INDEX idx_inventory_holds_status ON inventory_holds (status, expires_at);
CREATE INDEX idx_inventory_holds_expire ON inventory_holds (status, expires_at)
    WHERE status = 'active';

-- 关停房日期表
CREATE TABLE stop_sells (
    id                  BIGSERIAL PRIMARY KEY,
    supplier_code       VARCHAR(32) NOT NULL,
    supplier_hotel_id   VARCHAR(64) NOT NULL,
    room_type_code      VARCHAR(32),                # 空=整个酒店
    start_date          DATE NOT NULL,
    end_date            DATE NOT NULL,
    reason              VARCHAR(128),
    source              VARCHAR(32) DEFAULT 'supplier', -- supplier/manual
    created_at          TIMESTAMPTZ DEFAULT NOW(),
    created_by          VARCHAR(64)
);

-- 房量配额分配表
CREATE TABLE inventory_allocations (
    id                  BIGSERIAL PRIMARY KEY,
    supplier_code       VARCHAR(32) NOT NULL,
    supplier_hotel_id   VARCHAR(64) NOT NULL,
    room_type_code      VARCHAR(32) NOT NULL,
    stay_date           DATE NOT NULL,

    agent_code          VARCHAR(32),                 # 分配给特定代理商
    channel_code        VARCHAR(32),                 # 或分配给渠道

    allocated_rooms     INT NOT NULL DEFAULT 0,      # 分配房量
    used_rooms          INT NOT NULL DEFAULT 0,      # 已用房量

    created_at          TIMESTAMPTZ DEFAULT NOW(),
    updated_at          TIMESTAMPTZ DEFAULT NOW(),

    UNIQUE(supplier_code, supplier_hotel_id, room_type_code, stay_date, 
           COALESCE(agent_code, ''), COALESCE(channel_code, ''))
);

-- Redis 库存缓存 Key 设计
# inventory:{supplier_code}:{hotel_id}:{room_code}:{date} → INT (可用房量)
# hold:{order_id} → JSON (预占信息, TTL = 30min)

接口设计

# 库存管理 API (内部)
POST   /api/v1/inventory/hold                 # 预占库存
# 请求: { "order_id": 123, "supplier_code": "HB", "hotel_id": "X", "room_code": "DBL", "date": "2025-03-01", "count": 1 }
DELETE /api/v1/inventory/hold/:order_id       # 释放库存
GET    /api/v1/inventory/status               # 查询库存状态
POST   /api/v1/inventory/sync/:supplier_code  # 触发库存同步

# 关停房 API (管理后台)
GET    /api/v1/stop-sells                     # 关停房列表
POST   /api/v1/stop-sells                     # 创建关停房
DELETE /api/v1/stop-sells/:id                 # 删除关停房

# 配额管理 API
GET    /api/v1/inventory/allocations          # 配额列表
POST   /api/v1/inventory/allocations          # 创建配额
PUT    /api/v1/inventory/allocations/:id      # 更新配额

6.8.1 可售日期范围(售卖日期控制)

功能描述

供应商可设置酒店的可售日期范围(sell_from, sell_to),不同渠道/代理商可以有不同的可售日期范围。超出范围的查询直接返回无房,不穿透供应商,减少无效请求。

数据模型

-- 可售日期范围表
CREATE TABLE sell_date_range (
    id              BIGSERIAL PRIMARY KEY,
    supplier_id     BIGINT NOT NULL REFERENCES suppliers(id),
    hotel_id        BIGINT NOT NULL,
    room_type_id    BIGINT,                         -- NULL = 该酒店所有房型
    channel_id      BIGINT REFERENCES channels(id),  -- NULL = 全局默认
    agent_id        BIGINT REFERENCES agents(id),    -- NULL = 不限代理商(配合 channel_id 使用)
    sell_from       DATE NOT NULL,                   -- 可售开始日期
    sell_to         DATE NOT NULL,                   -- 可售结束日期
    overwrite_global BOOLEAN DEFAULT FALSE,          -- true = 覆盖全局设置
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_sdr_supplier_hotel ON sell_date_range (supplier_id, hotel_id, room_type_id);
CREATE INDEX idx_sdr_channel ON sell_date_range (channel_id, sell_from, sell_to);

API 接口

GET    /api/admin/sell-date-ranges                          # 可售日期范围列表
POST   /api/admin/sell-date-ranges                          # 创建可售日期范围
PUT    /api/admin/sell-date-ranges/:id                      # 更新
DELETE /api/admin/sell-date-ranges/:id                      # 删除
GET    /api/internal/sell-date-range/:supplier_id/:hotel_id # 内部:查询可售日期范围

业务逻辑

  1. 查价时,先根据代理商/渠道查询对应的可售日期范围配置
  2. 如果查询的入住日期(check_in / check_out)不在 sell_from ~ sell_to 范围内,直接返回无房
  3. 优先级:渠道级别 > 代理商级别 > 全局默认
  4. overwrite_global = true 的规则覆盖全局默认设置
  5. 配置缓存到 Redis(sdr:{supplier_id}:{hotel_id}:{channel_id}),TTL 10min

6.8.2 预定窗口(Booking Window)

功能描述

控制预订的时间窗口:最少提前几天预订(min_advance_days),最远可以订到哪天(max_advance_days)。不同渠道/代理商可配置不同窗口。超出窗口的查询直接返回无房。

数据模型

-- 预定窗口表
CREATE TABLE booking_window (
    id              BIGSERIAL PRIMARY KEY,
    supplier_id     BIGINT NOT NULL REFERENCES suppliers(id),
    hotel_id        BIGINT NOT NULL,
    room_type_id    BIGINT,                         -- NULL = 该酒店所有房型
    channel_id      BIGINT REFERENCES channels(id),  -- NULL = 全局默认
    agent_id        BIGINT REFERENCES agents(id),    -- NULL = 不限代理商
    min_advance     SMALLINT NOT NULL DEFAULT 1,     -- 最少提前天数(如 1 = 当天不能订)
    max_advance     SMALLINT NOT NULL DEFAULT 365,   -- 最远可订天数
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_bw_supplier_hotel ON booking_window (supplier_id, hotel_id, room_type_id);
CREATE INDEX idx_bw_channel ON booking_window (channel_id);

API 接口

GET    /api/admin/booking-windows                          # 预定窗口列表
POST   /api/admin/booking-windows                          # 创建预定窗口
PUT    /api/admin/booking-windows/:id                      # 更新
DELETE /api/admin/booking-windows/:id                      # 删除
GET    /api/internal/booking-window/:supplier_id/:hotel_id # 内部:查询预定窗口

业务逻辑

  1. 查价时检查入住日期是否在预定窗口内:
  2. check_in >= TODAY + min_advancecheck_in <= TODAY + max_advance
  3. 不满足条件直接返回无房,不穿透供应商
  4. 优先级:渠道/代理商级别 > 全局默认
  5. 缓存到 Redis(bw:{supplier_id}:{hotel_id}:{channel_id}),TTL 10min

6.8.3 连住规则(Length of Stay)

功能描述

控制最少/最多入住天数(min_los / max_los),并支持连住折扣(住 N 晚以上给折扣)。入住天数不在范围内直接返回无房。

数据模型

-- 连住规则表
CREATE TABLE los_rule (
    id              BIGSERIAL PRIMARY KEY,
    supplier_id     BIGINT NOT NULL REFERENCES suppliers(id),
    hotel_id        BIGINT NOT NULL,
    room_type_id    BIGINT,                         -- NULL = 该酒店所有房型
    channel_id      BIGINT REFERENCES channels(id),  -- NULL = 全局默认
    agent_id        BIGINT REFERENCES agents(id),    -- NULL = 不限代理商
    min_los         SMALLINT NOT NULL DEFAULT 1,     -- 最少入住天数
    max_los         SMALLINT,                        -- NULL = 不限最多天数
    discount_type   VARCHAR(16),                     -- percentage/fixed/none
    discount_value  DECIMAL(8, 2) DEFAULT 0,        -- 折扣值(百分比或固定金额)
    effective_from  DATE NOT NULL DEFAULT CURRENT_DATE,
    effective_to    DATE,                            -- NULL = 长期有效
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_los_supplier_hotel ON los_rule (supplier_id, hotel_id, room_type_id);
CREATE INDEX idx_los_channel ON los_rule (channel_id, effective_from, effective_to);

API 接口

GET    /api/admin/los-rules                          # 连住规则列表
POST   /api/admin/los-rules                          # 创建连住规则
PUT    /api/admin/los-rules/:id                      # 更新
DELETE /api/admin/los-rules/:id                      # 删除
GET    /api/internal/los-rule/:supplier_id/:hotel_id # 内部:查询连住规则

业务逻辑

  1. 查价时计算入住天数 nights = check_out - check_in
  2. nights < min_losmax_los 不为空且 nights > max_los,直接返回无房
  3. nights >= min_los 且设置了折扣,查价引擎在价格计算阶段应用连住折扣
  4. 折扣类型:percentage(如 9 折 = 10% off)或 fixed(每晚减固定金额)
  5. 优先级:渠道/代理商级别 > 全局默认,有效期内的规则优先

6.8.4 房量分配(Allocation)

功能描述

供应商可将某日期的某房型库存分配给不同渠道,各渠道独立计数。某渠道售完后自动 stop_sell。支持共用 overbooking 池。

数据模型

-- 房量分配表
CREATE TABLE allocation (
    id              BIGSERIAL PRIMARY KEY,
    supplier_id     BIGINT NOT NULL REFERENCES suppliers(id),
    hotel_id        BIGINT NOT NULL,
    room_type_id    BIGINT NOT NULL,
    date            DATE NOT NULL,
    channel_id      BIGINT,                         -- NULL = 共用池(overbooking)
    agent_id        BIGINT REFERENCES agents(id),    -- NULL = 不限代理商(配合 channel_id)
    allocated_qty   INT NOT NULL DEFAULT 0,         -- 分配房量
    sold_qty        INT NOT NULL DEFAULT 0,         -- 已售房量
    released_at     TIMESTAMPTZ,                     -- 释放时间(售罄时间)
    status          VARCHAR(16) DEFAULT 'active',   -- active/exhausted/released
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ DEFAULT NOW(),

    UNIQUE(supplier_id, hotel_id, room_type_id, date, COALESCE(channel_id, 0))
);

CREATE INDEX idx_alloc_date ON allocation (supplier_id, hotel_id, room_type_id, date);
CREATE INDEX idx_alloc_channel ON allocation (channel_id, date, status);

API 接口

GET    /api/admin/allocations                                    # 房量分配列表
POST   /api/admin/allocations                                    # 创建分配(支持批量)
PUT    /api/admin/allocations/:id                                # 更新分配数量
DELETE /api/admin/allocations/:id                                # 删除分配
POST   /api/admin/allocations/batch                              # 批量设置分配(日期范围)
GET    /api/internal/allocation/:supplier_id/:hotel_id/:date     # 内部:查询某日分配情况

业务逻辑

  1. 供应商为某日期某房型设置分配:A 渠道 10 间、B 渠道 5 间、共用池 3 间
  2. 查价时根据代理商所属渠道查询对应分配的剩余房量
  3. 若渠道分配已售完(sold_qty >= allocated_qty),自动标记为 exhausted,返回无房
  4. 共用池(channel_id = NULL)在所有渠道分配用完后兜底使用
  5. 订单取消时 sold_qty 回退,exhausted 状态自动恢复为 active
  6. 支持 Redis 缓存剩余房量(alloc:{supplier_id}:{hotel_id}:{room_type_id}:{date}:{channel_id}

6.8.5 关停房管理(Stop Sell / Close Out)

功能描述

供应商可以关闭某日期某房型(维护、满房、活动等原因),支持批量关停(日期范围)。关停后所有渠道立即生效,查价时直接返回无房。

注:此功能与 6.8 原有的 stop_sells 表功能重叠,此处定义增强版的关停管理,支持渠道级别关停和更完善的状态管理。

数据模型

-- 增强版关停房表(替代原 stop_sells 表或作为补充)
CREATE TABLE stop_sell_v2 (
    id              BIGSERIAL PRIMARY KEY,
    supplier_id     BIGINT NOT NULL REFERENCES suppliers(id),
    hotel_id        BIGINT NOT NULL,
    room_type_id    BIGINT,                         -- NULL = 整个酒店关停
    channel_id      BIGINT REFERENCES channels(id),  -- NULL = 所有渠道关停
    stop_from       DATE NOT NULL,                   -- 关停开始日期
    stop_to         DATE NOT NULL,                   -- 关停结束日期
    reason          VARCHAR(128) NOT NULL,           -- maintenance/full/event/seasonal/other
    reason_detail   TEXT,                            -- 详细说明
    status          VARCHAR(16) DEFAULT 'active',   -- active/cancelled/expired
    created_by      VARCHAR(64) NOT NULL,            -- 创建人
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_ss_v2_hotel_date ON stop_sell_v2 (supplier_id, hotel_id, room_type_id, stop_from, stop_to);
CREATE INDEX idx_ss_v2_channel ON stop_sell_v2 (channel_id, status, stop_from, stop_to);
CREATE INDEX idx_ss_v2_status ON stop_sell_v2 (status, stop_from, stop_to);

API 接口

GET    /api/admin/stop-sells                         # 关停房列表
POST   /api/admin/stop-sells                         # 创建关停(支持日期范围批量)
PUT    /api/admin/stop-sells/:id                     # 更新关停
DELETE /api/admin/stop-sells/:id                     # 取消关停
POST   /api/admin/stop-sells/batch                   # 批量关停(多酒店/多房型)
GET    /api/internal/stop-sell/:supplier_id/:hotel_id # 内部:查询关停状态

业务逻辑

  1. 供应商创建关停记录,支持设置日期范围(stop_from ~ stop_to),批量关停多天
  2. 关停支持三种粒度:酒店级别(room_type_id = NULL)、房型级别、渠道级别
  3. channel_id = NULL 表示所有渠道关停,指定 channel_id 则仅对该渠道关停
  4. 查价时检查入住日期是否在关停范围内,若在范围内直接返回无房
  5. 关停支持取消(status 变为 cancelled),立即恢复可售
  6. 过期关停自动标记为 expired(定时任务处理)
  7. 关停数据缓存到 Redis(ss:{supplier_id}:{hotel_id}:{room_type_id}),TTL 5min,变更时主动刷新

与其他模块的关系

关联模块 关系
查价引擎 提供实时库存数据
订单管理 下单预占、取消释放
匹配管理 供应商编码映射
管理后台 库存监控和关停房管理
供应商API 拉取/接收库存变更推送

技术选型建议