vr-shopxo-plugin/docs/api/VR_TICKET_TREE_API.md

843 lines
24 KiB
Markdown
Raw Normal View History

# VR Ticket Tree API 文档
> **状态**: ✅ 已完成
> **版本**: 1.2.0
> **更新日期**: 2026-05-18
---
## 目录
1. [概述](#一概述)
2. [快速开始](#二快速开始)
3. [接口详情](#三接口详情)
4. [请求参数](#四请求参数)
5. [响应结构](#五响应结构)
6. [新增字段详解](#六新增字段详解)
7. [数据字段说明](#七数据字段说明)
8. [前端使用指南](#八前端使用指南)
9. [使用示例](#九使用示例)
10. [最佳实践](#十最佳实践)
11. [错误处理](#十一错误处理)
12. [缓存策略](#十二缓存策略)
13. [字段校验与数据完整性约束](#十三字段校验与数据完整性约束)
14. [订单提交错误处理](#十四订单提交错误处理)
---
## 一、概述
### 1.1 什么是 Tree API
Tree API 是 VR Ticket 插件的核心接口,用于获取座位库存的层级树结构。该 API 将座位数据组织成动态层级树,前端可以根据 `group_by` 参数指定层级顺序,实现灵活的视图展示。
### 1.2 核心特性
| 特性 | 说明 |
|------|------|
| **动态层级** | 层级顺序由前端通过 `group_by` 参数控制 |
| **自底向上聚合** | 每个层级节点自动聚合子节点统计(库存、价格) |
| **模板去重** | 同一 venue+room+section 的模板只返回一份 |
| **座位嵌入** | 座位数据直接嵌入树的最深层,无需额外请求 |
| **多场次关联** | 通过 `peer_goods` 支持同演出多日期切换 |
| **停售控制** | 通过 `session_meta` 支持场次禁用与倒计时 |
| **字段校验** | Phase 1-3 校验体系,前后端多层拦截保障数据完整性 |
| **缓存机制** | 60秒缓存减少重复计算 |
### 1.3 与旧版对比
| 特性 | 旧版 API | Tree API v1.2 |
|------|----------|---------------|
| 数据结构 | 固定层级 | 动态层级 |
| 模板去重 | 无 | ✅ 原生支持 |
| 前端复杂度 | 低 | 低 |
| 数据冗余 | 高 | 低 |
| 座位查询 | 需额外计算 | 直接嵌入 |
| 多场次导航 | 无 | ✅ peer_goods |
| 停售倒计时 | 无 | ✅ session_meta |
| 字段校验 | 无 | ✅ 前端+保存+下单三层拦截 |
| 下单拦截 | 无 | ✅ BuyCheck 停售校验 |
---
## 二、快速开始
### 2.1 基本请求
```bash
curl "http://localhost:10000/api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=goods&pluginsaction=tree&goods_id=118&group_by=venue,session,room,section"
```
### 2.2 使用 Python 验证
```python
import requests
import json
response = requests.get("http://localhost:10000/api.php", params={
"s": "plugins/index",
"pluginsname": "vr_ticket",
"pluginscontrol": "goods",
"pluginsaction": "tree",
"goods_id": 118,
"group_by": "venue,session,room,section"
})
data = response.json()["data"]
print(f"场馆数量: {len(data['tree']['venues'])}")
print(f"模板数量: {data['meta']['template_count']}")
print(f"座位总数: {data['meta']['seat_count']}")
print(f"同场次商品: {data['peer_goods']}")
print(f"场次元数据: {data['session_meta']}")
```
---
## 三、接口详情
### 3.1 基本信息
| 属性 | 值 |
|------|-----|
| **URL** | `/api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=goods&pluginsaction=tree` |
| **方法** | GET |
| **认证** | 无需认证 |
| **缓存** | 60秒 |
### 3.2 支持的 group_by 维度
| 维度 | 说明 |
|------|------|
| `venue` | 场馆 |
| `session` | 场次 |
| `room` | 演播室/影厅 |
| `section` | 分区A区、B区等 |
### 3.3 group_by 组合示例
```bash
# 场馆优先Joery 场景)
group_by=venue,session,room,section
# 场次优先
group_by=session,venue,room,section
# 自定义顺序
group_by=section,venue,session,room
```
---
## 四、请求参数
### 4.1 参数列表
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `goods_id` | int | ✅ | - | 商品ID |
| `group_by` | string | ❌ | `venue,session,room,section` | 层级顺序,逗号分隔 |
| `cache_ttl` | int | ❌ | `60` | 缓存TTL仅开发环境使用 |
### 4.2 参数示例
```bash
# 基本请求
goods_id=118
# 场次优先
group_by=session,venue,room,section
# 开发环境禁用缓存
cache_ttl=0
```
---
## 五、响应结构
### 5.1 成功响应
```json
{
"code": 0,
"msg": "success",
"data": {
"goods_id": 118,
"group_by": ["venue", "session", "room", "section"],
"tree": { ... },
"seat_templates": { ... },
"session_meta": [ ... ],
"peer_goods": [ ... ],
"meta": { ... }
}
}
```
### 5.2 完整响应示例v1.1
```json
{
"code": 0,
"msg": "success",
"data": {
"goods_id": 119,
"group_by": ["venue", "session", "room", "section"],
"tree": {
"venues": {
"测试场馆": {
"name": "测试场馆",
"min_price": 0,
"max_price": 0,
"has_available": true,
"inventory": 16,
"sessions": {
"08:00-23:59": {
"name": "08:00-23:59",
"inventory": 16,
"rooms": {
"老展厅 1": {
"name": "老展厅 1",
"sections": {
"A": {
"name": "A",
"seats": { ... }
}
}
}
}
}
}
}
}
},
"seat_templates": {
"测试场馆_老展厅 1_A": { ... }
},
"session_meta": [
{
"session": "08:00-23:59",
"start": "08:00",
"end": "23:59",
"session_date": "2026-05-18",
"session_datetime": "2026-05-18 08:00:00",
"batch_expire_ts": 1746914400
}
],
"peer_goods": [
{
"id": 116,
"title": "测试3",
"date": ""
},
{
"id": 117,
"title": "测试4",
"date": "2026-05-19"
}
],
"meta": {
"seat_count": 30,
"template_count": 4,
"cache_hit": false,
"computed_at": 1779080970
}
}
}
```
### 5.3 错误响应
```json
{
"code": 400,
"msg": "goods_id 参数无效",
"data": []
}
```
---
## 六、新增字段详解
### 6.1 session_meta场次元数据
> **引入版本**: 1.1.0
> **来源**: 从 SKU 的 `GoodsSpecBase.extends` JSON 中提取,去重后按场次时间排序
**用途**
- 前端场次选择控件:判断场次是否已过期(禁用)
- 停售倒计时:计算 `batch_expire_ts - now()` 毫秒数
**字段说明**
| 字段 | 类型 | 说明 |
|------|------|------|
| `session` | string | 场次字符串,格式 `HH:MM-HH:MM`,如 `"19:30-21:30"` |
| `start` | string | 场次开始时间,如 `"19:30"` |
| `end` | string | 场次结束时间,如 `"21:30"` |
| `session_date` | string | 演出日期,格式 `YYYY-MM-DD`,从 `goods.batch_number_expire` 转换 |
| `session_datetime` | string | 完整演出时间,格式 `YYYY-MM-DD HH:MM:SS` |
| `batch_expire_ts` | int | **停售截止时间戳**Unix 秒),= 演出开始时间 - 5分钟。前端用此字段判断是否过期。 |
**业务规则**
-`batch_expire_ts <= 当前时间戳` 时,该场次已停止售票
- 前端应在场次选择 UI 中禁用对应选项
- `batch_expire_ts``SeatSkuService::BatchGenerate()` 在 SKU 生成时计算并写入
### 6.2 peer_goods同场次关联商品
> **引入版本**: 1.1.0
> **来源**: 查询 `goods` 表中 `coding` 字段相同的其他上架商品
**用途**
- 前端渲染"多日期切换"控件(如顶部日期标签栏)
- 同一演出(如"五月天演唱会")的不同日期场次商品通过 `coding` 关联
**字段说明**
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | int | 商品ID |
| `title` | string | 商品标题 |
| `date` | string | 演出日期,格式 `YYYY-MM-DD`,从 `batch_number_expire` 转换;未设置时为空字符串 |
**业务规则**
- 仅返回上架且未删除的同 `coding` 商品(排除自身)
-`batch_number_expire` 升序排列(最早的日期在最前)
-`batch_number_expire = 0`(未设置),`date` 返回空字符串,前端应提示用户完善日期
---
## 七、数据字段说明
### 7.1 tree 节点字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `name` | string | 节点名称 |
| `min_price` | float | 该层级及子节点的最低价格 |
| `max_price` | float | 该层级及子节点的最高价格 |
| `has_available` | bool | 是否有可用库存 |
| `inventory` | int | 可用库存数量 |
| `template_key` | string | 座位模板唯一标识(仅 section 节点) |
| `price` | float | 该分区的统一价格(仅 section 节点) |
| `seats` | object | 座位数据(仅最深层节点) |
### 7.2 seat_templates 字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `template_key` | string | 模板唯一标识venue_room_section |
| `name` | string | 场馆名称 |
| `room_name` | string | 演播室名称 |
| `section_name` | string | 分区名称 |
| `seat_map` | object | 座位图数据 |
| `layout_cols` | int | 列数 |
| `layout_rows` | int | 行数 |
### 7.3 meta 字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `seat_count` | int | 可用座位总数 |
| `template_count` | int | 模板数量 |
| `cache_hit` | bool | 是否命中缓存 |
| `computed_at` | int | 计算时间戳 |
### 7.4 seats 字段结构
| 字段 | 类型 | 说明 |
|------|------|------|
| `spec_key` | string | SKU 唯一标识 |
| `venue` | string | 场馆名称 |
| `session` | string | 场次时间 |
| `room` | string | 演播室 |
| `section` | string | 分区 |
| `seat` | string | 座位号 |
| `price` | float | 当前价格 |
| `inventory` | int | 库存0=已售) |
| `original_price` | float | 原价 |
---
## 八、前端使用指南
### 8.1 session_meta 使用:场次禁用判断 + 倒计时
```javascript
// 从 tree API 响应中获取
const { session_meta } = apiData;
// 判断场次是否可售
function isSessionAvailable(sessionKey, meta) {
const info = meta.find(s => s.session === sessionKey);
if (!info) return true; // 未找到按可用处理
return info.batch_expire_ts > Date.now() / 1000;
}
// 获取停售倒计时(秒)
function getCountdown(sessionKey, meta) {
const info = meta.find(s => s.session === sessionKey);
if (!info) return null;
const remaining = info.batch_expire_ts - Math.floor(Date.now() / 1000);
return remaining > 0 ? remaining : 0; // 已过期返回 0
}
// 渲染场次选择器
function renderSessionTabs(meta) {
const now = Math.floor(Date.now() / 1000);
return meta.map(session => {
const expired = session.batch_expire_ts <= now;
const countdown = getCountdown(session.session, meta);
return {
label: `${session.session_date} ${session.start}-${session.end}`,
value: session.session,
disabled: expired,
expired,
countdown, // 秒数,前端自行定时刷新
};
});
}
```
### 8.2 peer_goods 使用:多日期切换导航
```javascript
// 从 tree API 响应中获取
const { peer_goods } = apiData;
// 渲染日期切换栏
function renderDateSwitcher(currentGoodsId, peers) {
if (!peers || peers.length === 0) return null; // 无同场次商品则不展示
return peers.map(peer => ({
id: peer.id,
title: peer.title,
date: peer.date, // 格式 YYYY-MM-DD 或空字符串
active: peer.id === currentGoodsId,
}));
}
// 切换日期时,重新调用 tree API
function switchToSession(targetGoodsId) {
navigateTo(`/pages/goods-vr-ticket/goods-vr-ticket?goods_id=${targetGoodsId}`);
}
```
### 8.3 完整选座流程
```javascript
class VRTicketSelector {
constructor(apiData) {
this.tree = apiData.tree;
this.templates = apiData.seat_templates;
this.sessionMeta = apiData.session_meta;
this.peerGoods = apiData.peer_goods;
}
// 获取可用户选择的场次列表(过滤已过期)
getAvailableSessions() {
const now = Math.floor(Date.now() / 1000);
return this.sessionMeta
.filter(s => s.batch_expire_ts > now)
.map(s => ({
value: s.session,
label: `${s.session_date} ${s.start}~${s.end}`,
countdown: s.batch_expire_ts - now,
}));
}
// 切换场次时,过滤 tree 中的座位数据
filterBySession(sessionKey) {
// 根据 group_by 找到对应的 sessions 节点
const venues = Object.values(this.tree.venues);
// ... 过滤逻辑由前端 UI 框架实现
}
}
```
---
## 九、使用示例
### 9.1 获取场馆列表
```javascript
const response = await fetch(apiUrl);
const { tree, meta } = response.data;
// 获取所有场馆
const venues = Object.keys(tree.venues);
console.log(`共 ${venues.length} 个场馆`);
```
### 9.2 获取特定分区的座位
```javascript
// 获取 场馆=测试场馆 -> 场次=07:00-09:59 -> 演播室=老展厅 1 -> 分区=A 的座位
const section = tree.venues['测试场馆']
.sessions['07:00-09:59']
.rooms['老展厅 1']
.sections['A'];
const seats = section.seats;
console.log(`共 ${Object.keys(seats).length} 个座位`);
```
### 9.3 获取座位模板
```javascript
const { seat_templates } = response.data;
// 通过 template_key 获取模板
const templateKey = section.template_key; // "测试场馆_老展厅 1_A"
const template = seat_templates[templateKey];
// 渲染座位图
console.log(template.seat_map.rooms[0].map);
```
### 9.4 前端选座流程
```javascript
class SeatSelector {
constructor(apiData) {
this.tree = apiData.tree;
this.templates = apiData.seat_templates;
this.sessionMeta = apiData.session_meta;
this.peerGoods = apiData.peer_goods;
}
// 选择场馆
selectVenue(name) {
return this.tree.venues[name];
}
// 选择场次
selectSession(venueNode, session) {
return venueNode.sessions[session];
}
// 选择演播室
selectRoom(sessionNode, room) {
return sessionNode.rooms[room];
}
// 选择分区,获取座位和模板
selectSection(roomNode, section) {
const sectionNode = roomNode.sections[section];
const seats = sectionNode.seats;
const template = this.templates[sectionNode.template_key];
return { seats, template, ...sectionNode };
}
// 获取可用座位
getAvailableSeats(seats) {
return Object.entries(seats)
.filter(([_, seat]) => seat.inventory > 0)
.map(([name, data]) => ({ name, ...data }));
}
// 判断场次是否过期
isSessionExpired(sessionKey) {
const now = Math.floor(Date.now() / 1000);
const info = this.sessionMeta.find(s => s.session === sessionKey);
return info ? info.batch_expire_ts <= now : false;
}
}
// 使用
const selector = new SeatSelector(data);
const venue = selector.selectVenue('测试场馆');
const session = selector.selectSession(venue, '07:00-09:59');
const room = selector.selectRoom(session, '老展厅 1');
const { seats, template } = selector.selectSection(room, 'A');
const available = selector.getAvailableSeats(seats);
```
---
## 十、最佳实践
### 10.1 前端缓存策略
```javascript
// 本地缓存优化
const cache = new Map();
const CACHE_TTL = 60 * 1000; // 60秒
async function fetchTree(goodsId, groupBy) {
const key = `${goodsId}_${groupBy.join(',')}`;
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.data;
}
const response = await fetch(`/api.php?s=plugins/index&...&goods_id=${goodsId}&group_by=${groupBy}`);
const data = await response.json();
cache.set(key, { data, timestamp: Date.now() });
return data;
}
```
### 10.2 错误处理
```javascript
async function safeFetchTree(goodsId, groupBy) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const result = await response.json();
if (result.code !== 0) {
throw new Error(result.msg);
}
return result.data;
} catch (error) {
console.error('获取座位数据失败:', error);
// 降级处理或显示错误
return null;
}
}
```
### 10.3 座位图渲染
```javascript
function renderSeatMap(template, availableSeats) {
const { seat_map, layout_rows, layout_cols } = template;
const room = seat_map.rooms[0]; // 获取第一个演播室
// 生成座位矩阵
const seatMatrix = room.map.map((row, rowIndex) => {
return row.split('').map((char, colIndex) => {
const section = room.seats[char];
return {
row: rowIndex + 1,
col: colIndex + 1,
section: char,
available: availableSeats.some(s =>
s.row === rowIndex + 1 && s.col === colIndex + 1
),
price: section?.price || 0,
color: section?.color || '#ccc'
};
});
});
return seatMatrix;
}
```
### 10.4 性能优化
1. **避免频繁请求**: 使用本地缓存
2. **懒加载模板**: 仅在用户选择分区时加载模板
3. **虚拟列表**: 座位列表使用虚拟滚动
4. **预加载**: 进入页面时预加载最常用层级的数据
---
## 十一、错误处理
### 11.1 常见错误
| code | msg | 解决方案 |
|------|-----|----------|
| 400 | goods_id 参数无效 | 检查 goods_id 是否为有效整数 |
| 404 | 商品不存在 | 确认 goods_id 对应的商品存在 |
| -1 | 获取层级树失败: Column not found | 检查数据库字段名是否正确coding/batch_number_expire |
| -1 | 获取层级树失败: ... | 检查服务端日志,联系后端 |
### 11.2 服务端错误日志
```php
// Goods.php tree() 方法中的异常处理
catch (\Exception $e) {
return self::error('获取层级树失败: ' . $e->getMessage());
}
```
### 11.3 停售验证错误(订单提交时)
当用户尝试在开场前5分钟内下单后端 `Hook::BuyCheck` 返回:
```json
{
"code": -1,
"msg": "该场次19:30 21:30距开场已不足5分钟已停止售票请选择其他场次"
}
```
此错误由 `plugins_service_buy_order_insert_begin` 钩子触发,会导致订单事务回滚。
---
## 十二、缓存策略
### 12.1 缓存 Key 格式
```
vr_tree_v4_{goods_id}_{md5(group_by)}
```
示例:
- `vr_tree_v4_118_a1b2c3d4` (group_by = venue,session,room,section)
- `vr_tree_v4_118_e5f6g7h8` (group_by = session,venue,room,section)
### 12.2 缓存失效
当发生以下操作时,需要清除缓存:
| 操作 | 清除方法 |
|------|----------|
| 订单支付成功 | `SeatMapService::ClearCache()` |
| 座位图修改 | `SeatMapService::ClearCache()` |
| 商品配置修改 | `QueryManager::clearCache($goodsId)` |
### 12.3 手动清除缓存
```php
// 清除特定商品的 tree 缓存
QueryManager::clearCache(118);
// 清除所有 tree 缓存(不推荐)
\think\facade\Cache::tag('vr_tree_118', null);
```
---
## 十三、字段校验与数据完整性约束
> **引入版本**: 1.2.0
> **关联文档**: [16_VR_TICKET_FIELD_VALIDATION_PLAN.md](../16_VR_TICKET_FIELD_VALIDATION_PLAN.md)
### 13.1 受约束字段
Tree API 的返回数据依赖于 `goods` 表中两个字段的正确填写:
| 字段 | 数据库列 | 影响 API 输出 | 必填(票务商品) |
|------|----------|---------------|:---:|
| 商品编号 | `goods.coding` | `peer_goods`(同演出多日期关联) | ❌ 选填 |
| 演出日期 | `goods.batch_number_expire` | `session_meta`(场次元数据/停售倒计时) | ✅ 必填 |
### 13.2 字段缺失时的 API 行为
| 场景 | `batch_number_expire` | `coding` | API 响应表现 |
|------|----------------------|----------|-------------|
| 正常 | 有效时间戳 (>0) | 有值 | `session_meta` 返回完整场次数据;`peer_goods` 返回关联商品列表 |
| 缺日期 | 0 或 null | 有值 | `session_meta` 为空数组 `[]`;前端无法展示场次选择卡、无倒计时;`peer_goods` 中 `date` 为空串 |
| 缺编号 | 有效时间戳 (>0) | 空 | `session_meta` 正常返回;`peer_goods` 为空数组 `[]`,前端不展示多日期切换控件 |
| 双缺 | 0 或 null | 空 | `session_meta` 为空;`peer_goods` 为空;**保存时会被 AdminGoodsSaveHandle 拦截** |
### 13.3 校验体系架构
```
┌─────────────────────────────────────────────────────┐
│ Phase 1 — 前端 │
│ AdminGoodsSave.php · applyTicketRequired() │
│ 动态注入 required + 红色星号 + placeholder 提示 │
│ 校验时机:后台管理员填写表单时 │
├─────────────────────────────────────────────────────┤
│ Phase 2 — 保存钩子 │
│ AdminGoodsSaveHandle.php │
│ ① batch_number_expire > 0必填
│ ② (coding, batch_number_expire) 组合唯一 │
│ 校验时机plugins_service_goods_save_thing_end │
├─────────────────────────────────────────────────────┤
│ Phase 3 — 下单拦截 │
│ Hook.php · BuyCheck() │
│ ① batch_number_expire > 0数据完整性
│ ② now < batch_expire_ts
│ 校验时机plugins_service_buy_order_insert_begin │
└─────────────────────────────────────────────────────┘
```
---
## 十四、订单提交错误处理
### 14.1 票务专属错误
以下错误仅在商品为票务商品(`vr_goods_config` 非空)时触发。普通商品不受影响。
#### 错误码 -1演出日期未设置
**触发条件**: `BuyCheck` 发现票务商品的 `batch_number_expire <= 0`
**API 响应**:
```json
{
"code": -1,
"msg": "「{商品标题}」未设置演出日期,暂时无法购买"
}
```
**原因**: 管理员在保存商品时未填写演出日期字段(后台已强制必填,但可能存在历史遗留数据)。
**解决方案**: 前往商品编辑页,为票务商品设置有效的演出日期。
#### 错误码 -1场次停售
**触发条件**: `BuyCheck` 发现 `now >= batch_expire_ts`(距开场不足 5 分钟)
**API 响应**:
```json
{
"code": -1,
"msg": "该场次19:30 21:30距开场已不足5分钟已停止售票请选择其他场次"
}
```
**技术细节**: `batch_expire_ts = 演出开始时间戳 - 300秒`,由 `SeatSkuService::BatchGenerate()` 在 SKU 生成时写入 extends。
### 14.2 通用错误
| code | msg | 触发源 | 说明 |
|------|-----|--------|------|
| 400 | goods_id 参数无效 | Goods.php tree() | goods_id 非有效整数 |
| 404 | 商品不存在 | Goods.php tree() | 商品已删除或不存在 |
| -1 | 获取层级树失败: ... | Goods.php tree() | 服务端异常,检查日志 |
### 14.3 保存错误(后台管理)
以下错误由 `AdminGoodsSaveHandle` 在管理员保存商品时返回:
| 错误消息 | 触发条件 |
|----------|----------|
| 票务商品必须设置演出日期(批号有效期),请填写后重新保存 | `batch_number_expire <= 0` |
| 该商品编号「{coding}」在此演出日期已存在商品「{title}」,请检查是否重复创建或修改演出日期 | `(coding, batch_number_expire)` 组合重复 |
| 该演出日期已存在其他未设置编号的票务商品「{title}」,请先为已有商品设置商品编号或修改演出日期 | 空 coding + 相同日期冲突 |
---
## 附录 A相关文档
| 文档 | 说明 |
|------|------|
| [14_TREE_API_DESIGN.md](../14_TREE_API_DESIGN.md) | Tree API 设计文档 |
| [15_FLAT_INVENTORY_QUERY_MANAGER.md](../15_FLAT_INVENTORY_QUERY_MANAGER.md) | 查询管理器设计文档 |
| [16_VR_TICKET_FIELD_VALIDATION_PLAN.md](../16_VR_TICKET_FIELD_VALIDATION_PLAN.md) | 字段校验方案 |
| [VR_GOODS_CONFIG_SPEC.md](../VR_GOODS_CONFIG_SPEC.md) | 商品配置规范 |
| [DEVELOPMENT_LOG.md](../DEVELOPMENT_LOG.md) | 开发日志 |
## 附录 B变更历史
| 版本 | 日期 | 变更说明 |
|------|------|----------|
| 1.0.0 | 2026-05-15 | 初始版本,完成基础功能 |
| 1.1.0 | 2026-05-18 | 新增 session_meta场次元数据/停售控制、peer_goods同场次关联商品、BuyCheck 停售验证钩子修正数据库字段名coding/batch_number_expire |
| 1.2.0 | 2026-05-18 | 新增字段校验与数据完整性约束章节;新增订单提交错误处理章节;新增前端数据完整性检测示例;整合 Phase 1-3 校验体系文档 |
---
*文档由 Antigravity 生成*