Appearance
库存与拆单逻辑 - 完整设计
一、库存逻辑(按日期)
1.1 票务库的库存结构
票务基础信息:
┌────────────────────────────────────┐
│ 自我游SKU:ZWY001234 │
│ 票务名称:成人全日票-平日 │
│ 所属雪场:万龙滑雪场 │
│ 供应价:¥280 │
└────────────────────────────────────┘
日历库存(每日库存,实时同步):
┌────────┬────────┬────────┬────────┐
│日期 │库存 │已售 │剩余 │
├────────┼────────┼────────┼────────┤
│2025-01-15│300 │100 │200 │
├────────┼────────┼────────┼────────┤
│2025-01-16│300 │50 │250 │
├────────┼────────┼────────┼────────┤
│2025-01-17│200 │180 │20 ⚠ │
├────────┼────────┼────────┼────────┤
│2025-01-18│300 │300 │0 ✗ │
└────────┴────────┴────────┴────────┘
💡 库存存储在票务库,按日期实时同步1.2 产品的库存引用
产品信息:
┌────────────────────────────────────┐
│ 产品ID:P001 │
│ 产品名称:成人全日票-平日 │
│ 关联票务:ZWY001234 │
│ 销售价:¥320 │
└────────────────────────────────────┘
库存规则:
┌────────────────────────────────────┐
│ ● 直接引用票务库的日历库存 │
│ (产品本身不存储库存) │
│ │
│ 库存状态判断: │
│ • 剩余 >= 50:显示"库存充足" │
│ • 剩余 10-49:显示"仅剩XX张" │
│ • 剩余 < 10:显示"库存紧张" │
│ • 剩余 = 0:显示"已售罄" │
└────────────────────────────────────┘
💡 产品不设置库存,库存来自票务库1.3 住滑套餐的库存逻辑
住滑套餐产品:
┌────────────────────────────────────┐
│ 产品ID:P045 │
│ 产品名称:豪华双床房+双早+2日雪票 │
│ 关联票务: │
│ 住宿:ZWY002451(豪华双床房-1晚) │
│ 雪票:ZWY001236(成人全日票-2日) │
└────────────────────────────────────┘
库存规则:
┌────────────────────────────────────┐
│ ● 取最小值(住宿库存 vs 雪票库存) │
│ │
│ 示例:用户选择入住日期 2025-01-15 │
│ │
│ 住宿库存(1月15日1晚):45间 │
│ 雪票库存: │
│ 1月15日:200张 │
│ 1月16日:250张 │
│ │
│ 可售套餐数 = min(45, 200, 250) = 45套│
└────────────────────────────────────┘
💡 套餐库存 = 所有关联票务的最小日期库存1.4 后台库存监控
库存看板
[筛选] 日期:2025-01-15 ▼ | 雪场 ▼
产品库存明细:
┌──────────┬────────┬────────┬────────┬────────┐
│产品名称 │关联票务 │总库存 │剩余 │状态 │
├──────────┼────────┼────────┼────────┼────────┤
│成人全日票-│ZWY001234│300 │200 │充足 │
│平日 │ │ │ │ │
├──────────┼────────┼────────┼────────┼────────┤
│儿童全日票-│ZWY001237│200 │20 │⚠紧张 │
│平日 │ │ │ │ │
├──────────┼────────┼────────┼────────┼────────┤
│成人半日票-│ZWY001238│150 │0 │✗售罄 │
│周末 │ │ │ │ │
└──────────┴────────┴────────┴────────┴────────┘
库存预警:
• 儿童全日票-平日(1月15日):仅剩20张
• 成人半日票-周末(1月15日):已售罄
[手动同步库存] [查看同步日志]二、连续购买与拆单逻辑
2.1 C端购买流程(单次雪票)
产品详情页:成人全日票-平日
┌────────────────────────────────────┐
│ ¥320/张 │
│ │
│ 选择日期: │
│ ● 单日购买 │
│ ○ 连续多日购买 │
└────────────────────────────────────┘
---
场景1:单日购买
┌────────────────────────────────────┐
│ 选择日期:[2025-01-15] ▼ │
│ 库存状态:库存充足(剩余200张) │
│ │
│ 购买数量:[1] 张 │
│ │
│ 小计:¥320 │
│ │
│ [立即预订] │
└────────────────────────────────────┘
---
场景2:连续多日购买
┌────────────────────────────────────┐
│ 选择日期: │
│ 开始日期:[2025-01-15] ▼ │
│ 结束日期:[2025-01-19] ▼ │
│ 共 5 天 │
│ │
│ 库存检查: │
│ • 01-15:✓ 充足(200张) │
│ • 01-16:✓ 充足(250张) │
│ • 01-17:⚠ 紧张(20张) │
│ • 01-18:✗ 售罄(0张) │
│ • 01-19:✓ 充足(180张) │
│ │
│ ⚠ 01-18已售罄,无法购买连续5天 │
│ │
│ 建议: │
│ • 调整日期避开01-18 │
│ • 或分开购买可用日期 │
└────────────────────────────────────┘连续多日购买(库存充足的情况)
┌────────────────────────────────────┐
│ 选择日期: │
│ 开始:2025-01-15 │
│ 结束:2025-01-19 │
│ 共 5 天 │
│ │
│ 库存检查:全部充足 ✓ │
│ │
│ 购买数量:每天 [1] 张 │
│ │
│ 明细: │
│ • 01-15 成人全日票-平日 ¥320 │
│ • 01-16 成人全日票-平日 ¥320 │
│ • 01-17 成人全日票-平日 ¥320 │
│ • 01-18 成人全日票-平日 ¥320 │
│ • 01-19 成人全日票-平日 ¥320 │
│ │
│ 总计:5张 × ¥320 = ¥1,600 │
│ │
│ [立即预订] │
└────────────────────────────────────┘2.2 拆单逻辑(后台)
方案:1个订单 + N个子订单
用户下单:连续5天购买成人全日票
系统处理:
1. 创建主订单
2. 拆分为5个子订单
3. 锁定每天的库存
订单结构:
┌────────────────────────────────────┐
│ 主订单:#2025010700001 │
├────────────────────────────────────┤
│ 用户:张三(138****1234) │
│ 下单时间:2025-01-07 10:23 │
│ 订单总额:¥1,600 │
│ 支付状态:已支付 │
│ │
│ 子订单列表: │
│ │
│ ┌─ 子订单1:#2025010700001-1 │
│ │ 使用日期:2025-01-15 │
│ │ 产品:成人全日票-平日 │
│ │ 数量:1张 │
│ │ 金额:¥320 │
│ │ 关联票务:ZWY001234(01-15) │
│ │ 核销状态:未核销 │
│ │ 核销码:8756 4321 │
│ └─ │
│ │
│ ┌─ 子订单2:#2025010700001-2 │
│ │ 使用日期:2025-01-16 │
│ │ 产品:成人全日票-平日 │
│ │ 数量:1张 │
│ │ 金额:¥320 │
│ │ 关联票务:ZWY001234(01-16) │
│ │ 核销状态:未核销 │
│ │ 核销码:8756 4322 │
│ └─ │
│ │
│ ... (子订单3、4、5) │
└────────────────────────────────────┘
库存锁定:
• ZWY001234 @ 2025-01-15: -1
• ZWY001234 @ 2025-01-16: -1
• ZWY001234 @ 2025-01-17: -1
• ZWY001234 @ 2025-01-18: -1
• ZWY001234 @ 2025-01-19: -12.3 C端订单展示
我的订单
┌────────────────────────────────────┐
│ 订单号:#2025010700001 │
│ 下单时间:2025-01-07 10:23 │
│ │
│ 成人全日票-平日 × 5天 │
│ 万龙滑雪场 │
│ │
│ 订单总额:¥1,600 │
│ 支付状态:已支付 │
│ │
│ [查看详情] [申请退款] │
└────────────────────────────────────┘
点击【查看详情】:
订单详情:#2025010700001
订单信息:
• 下单时间:2025-01-07 10:23
• 订单总额:¥1,600
• 支付方式:微信支付
使用明细:
┌────────────────────────────────────┐
│ 2025-01-15(周三) │
│ 成人全日票-平日 × 1张 │
│ 核销状态:未核销 │
│ 核销码:8756 4321 │
│ [查看核销码] [申请退款] │
├────────────────────────────────────┤
│ 2025-01-16(周四) │
│ 成人全日票-平日 × 1张 │
│ 核销状态:未核销 │
│ 核销码:8756 4322 │
│ [查看核销码] [申请退款] │
├────────────────────────────────────┤
│ 2025-01-17(周五) │
│ 成人全日票-平日 × 1张 │
│ 核销状态:未核销 │
│ 核销码:8756 4323 │
│ [查看核销码] [申请退款] │
├────────────────────────────────────┤
│ ... (01-18、01-19) │
└────────────────────────────────────┘
💡 每天独立核销,可单独申请退款2.4 退款逻辑(部分退款)
用户申请退款:退01-17、01-18、01-19三天
退款计算:
┌────────────────────────────────────┐
│ 订单总额:¥1,600(5天) │
│ │
│ 申请退款: │
│ • 01-17(未核销)¥320 → 退¥320 │
│ • 01-18(未核销)¥320 → 退¥320 │
│ • 01-19(未核销)¥320 → 退¥320 │
│ │
│ 退改规则检查: │
│ • 01-17距今8天 → 符合免费退改 │
│ • 01-18距今9天 → 符合免费退改 │
│ • 01-19距今10天 → 符合免费退改 │
│ │
│ 应退金额:¥960 │
│ │
│ 保留订单: │
│ • 01-15 ¥320(已核销) │
│ • 01-16 ¥320(已核销) │
└────────────────────────────────────┘
系统处理:
1. 取消子订单3、4、5
2. 释放库存(01-17、01-18、01-19各+1)
3. 退款¥960到用户账户
4. 主订单保留,金额更新为¥6402.5 住滑套餐的连续购买
产品:豪华双床房+双早+2日雪票
用户选择:连住3晚(1月15-17日)+ 4日雪票(1月15-18日)
拆单逻辑:
┌────────────────────────────────────┐
│ 主订单:#2025010700002 │
├────────────────────────────────────┤
│ 套餐:豪华双床房+双早+2日雪票 │
│ 连住:3晚(1月15-17日) │
│ 雪票:4日(1月15-18日) │
│ 总额:¥3,840(¥1,280 × 3) │
│ │
│ 子订单: │
│ │
│ ┌─ 子订单1(1月15日入住) │
│ │ 住宿:1晚(01-15至01-16) │
│ │ 雪票:2日(01-15、01-16) │
│ │ 金额:¥1,280 │
│ │ 核销码:酒店订单号 + 2个雪票码 │
│ └─ │
│ │
│ ┌─ 子订单2(1月16日延住) │
│ │ 住宿:1晚(01-16至01-17) │
│ │ 雪票:2日(01-17、01-18) │
│ │ 金额:¥1,280 │
│ └─ │
│ │
│ ┌─ 子订单3(1月17日延住) │
│ │ 住宿:1晚(01-17至01-18) │
│ │ 雪票:0日(已用完) │
│ │ 金额:¥1,280 │
│ └─ │
└────────────────────────────────────┘
库存锁定:
住宿:
• 01-15: -1
• 01-16: -1
• 01-17: -1
雪票:
• 01-15: -1
• 01-16: -1
• 01-17: -1
• 01-18: -1
💡 套餐的拆单更复杂,需要合理分配住宿和雪票三、后台订单管理(拆单视图)
3.1 订单列表
订单管理
[筛选] 下单时间 ▼ | 使用日期 ▼ | 订单状态 ▼
订单列表:
┌──────────┬────────┬────────┬──────────┬────────┐
│订单号 │下单时间 │用户 │产品 │使用日期 │
├──────────┼────────┼────────┼──────────┼────────┤
│2025010700│01-07 │张三 │成人全日票-│01-15至 │
│001 │10:23 │138*1234│平日 ×5 │01-19 │
│ │ │ │ │(连续5天)│
└──────────┴────────┴────────┴──────────┴────────┘
│订单金额 │核销状态 │订单状态 │操作 │
├────────┼────────┼────────┼─────────────┤
│¥1,600 │未核销 │待使用 │[详情] [核销] │
│ │0/5 │ │[退款] │
└────────┴────────┴────────┴─────────────┘3.2 订单详情(拆单视图)
订单详情:#2025010700001
订单信息:
• 订单号:#2025010700001
• 下单时间:2025-01-07 10:23
• 用户:张三(138****1234)
• 订单总额:¥1,600
• 支付状态:已支付
子订单明细:
┌────────────────────────────────────┐
│ 子订单1:#2025010700001-1 │
├────────────────────────────────────┤
│ 使用日期:2025-01-15 │
│ 产品:成人全日票-平日 │
│ 数量:1张 │
│ 金额:¥320 │
│ 关联票务:ZWY001234 │
│ 自我游订单:ZWY-20250107-*** │
│ 核销码:8756 4321 │
│ 核销状态:未核销 │
│ │
│ [手动核销] [申请退款] │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 子订单2:#2025010700001-2 │
├────────────────────────────────────┤
│ 使用日期:2025-01-16 │
│ 产品:成人全日票-平日 │
│ 数量:1张 │
│ 金额:¥320 │
│ 核销状态:未核销 │
│ │
│ [手动核销] [申请退款] │
└────────────────────────────────────┘
... (子订单3、4、5)
[导出订单] [批量核销]3.3 核销(拆单核销)
核销订单:#2025010700001
子订单列表:
┌────────────────────────────────────┐
│ ☑ 子订单1(01-15)未核销 │
│ ☐ 子订单2(01-16)未核销 │
│ ☐ 子订单3(01-17)未核销 │
│ ☐ 子订单4(01-18)未核销 │
│ ☐ 子订单5(01-19)未核销 │
└────────────────────────────────────┘
核销方式:
● 核销单个子订单(01-15)
○ 批量核销全部
[确认核销]
---
核销成功:
✓ 子订单1(01-15)核销成功
核销时间:2025-01-15 09:23
核销员:运营001
主订单状态更新:
• 已核销:1/5
• 剩余未核销:4个四、库存同步机制
4.1 实时同步流程
自我游票务系统
↓
每10分钟全量同步日历库存
↓
票务库(日历库存表)
┌────────┬────────┬────────┐
│票务SKU │日期 │库存 │
├────────┼────────┼────────┤
│ZWY001234│01-15 │200 │
│ZWY001234│01-16 │250 │
│ZWY001234│01-17 │20 │
└────────┴────────┴────────┘
↓
产品实时查询
↓
C端展示库存状态4.2 下单时库存检查
用户下单流程:
1. 选择日期和数量
↓
2. 前端实时查询库存(AJAX)
↓
3. 显示库存状态
↓
4. 点击【立即预订】
↓
5. 后端再次检查库存(双重校验)
↓
6. 库存充足 → 创建订单,跳转支付
库存不足 → 提示"库存不足,请重新选择"
↓
7. 支付成功
↓
8. 锁定库存(向自我游下单)
↓
9. 本地库存 -1(异步)4.3 库存释放(退款/取消)
用户申请退款:
1. 提交退款申请
↓
2. 运营审核通过
↓
3. 取消自我游订单
↓
4. 释放库存(该日期 +1)
↓
5. 发起退款
↓
6. 更新订单状态五、关键技术要点
5.1 库存表设计
sql
-- 票务日历库存表
CREATE TABLE ticket_inventory (
id BIGINT PRIMARY KEY,
ticket_sku VARCHAR(50), -- 票务SKU
date DATE, -- 日期
total_stock INT, -- 总库存
sold_count INT DEFAULT 0, -- 已售数量
available INT AS (total_stock - sold_count), -- 剩余(计算列)
updated_at TIMESTAMP, -- 最后同步时间
UNIQUE(ticket_sku, date) -- 唯一索引
);
-- 索引
CREATE INDEX idx_ticket_date ON ticket_inventory(ticket_sku, date);5.2 订单表设计
sql
-- 主订单表
CREATE TABLE orders (
order_no VARCHAR(50) PRIMARY KEY,
user_id BIGINT,
product_id BIGINT,
total_amount DECIMAL(10,2),
pay_status TINYINT, -- 支付状态
created_at TIMESTAMP
);
-- 子订单表
CREATE TABLE order_items (
item_no VARCHAR(50) PRIMARY KEY, -- 子订单号
order_no VARCHAR(50), -- 主订单号
product_id BIGINT,
ticket_sku VARCHAR(50), -- 关联票务
use_date DATE, -- 使用日期
quantity INT,
amount DECIMAL(10,2),
verify_status TINYINT DEFAULT 0, -- 核销状态
verify_code VARCHAR(20), -- 核销码
zwt_order_no VARCHAR(50), -- 自我游订单号
created_at TIMESTAMP,
FOREIGN KEY (order_no) REFERENCES orders(order_no)
);
-- 索引
CREATE INDEX idx_order_items_date ON order_items(use_date);
CREATE INDEX idx_order_items_verify ON order_items(verify_code);5.3 库存检查逻辑(伪代码)
python
def check_inventory(ticket_sku, date_range):
"""
检查连续日期的库存是否充足
"""
for date in date_range:
inventory = get_inventory(ticket_sku, date)
if inventory.available < 1:
return False, f"{date}库存不足"
return True, "库存充足"
def create_order_with_items(user_id, product_id, date_range):
"""
创建主订单和子订单
"""
# 1. 再次检查库存
is_available, msg = check_inventory(ticket_sku, date_range)
if not is_available:
raise Exception(msg)
# 2. 创建主订单
order = create_main_order(user_id, product_id, total_amount)
# 3. 创建子订单
for date in date_range:
item = create_order_item(
order_no=order.order_no,
use_date=date,
ticket_sku=ticket_sku
)
# 4. 返回订单
return order六、总结
关键点:
库存按日期
- 票务库存储日历库存
- 产品不存储库存,实时引用
- 每10分钟同步
连续购买拆单
- 1个主订单 + N个子订单
- 每天独立核销
- 支持部分退款
库存检查
- 前端实时查询(用户体验)
- 后端双重校验(安全保证)
- 支付后锁定库存
退款释放库存
- 取消自我游订单
- 释放对应日期库存
- 更新订单状态
这个设计确保了: ✅ 库存准确(按日期) ✅ 连续购买流畅 ✅ 拆单逻辑清晰 ✅ 部分退款灵活 ✅ 核销管理方便
