Compare commits
18 Commits
829564b019
...
a3ef16034e
| Author | SHA1 | Date |
|---|---|---|
|
|
a3ef16034e | |
|
|
78b699eab4 | |
|
|
e5814c3bd4 | |
|
|
0eb8adbf71 | |
|
|
cd975797e3 | |
|
|
fe457eee23 | |
|
|
e4cf3a7711 | |
|
|
e2008e2778 | |
|
|
5a047936e6 | |
|
|
c2770e5e64 | |
|
|
bdfcb80d8c | |
|
|
b7bccf65c1 | |
|
|
0316a8101c | |
|
|
d7ee522c41 | |
|
|
6b8f3ec0de | |
|
|
85b1575a5c | |
|
|
f2dcd842dd | |
|
|
d9493500fb |
|
|
@ -0,0 +1,167 @@
|
||||||
|
# vr-shopxo-plugin 架构决策报告
|
||||||
|
|
||||||
|
> **文档版本**: v1.0 | **日期**: 2026-04-15 | **发起**: Council(FrontendDev + BackendArchitect + SecurityEngineer)
|
||||||
|
> **关联 Issue**: #9 | **状态**: FINAL
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 背景与问题
|
||||||
|
|
||||||
|
vr-shopxo-plugin 是 ShopXO 票务插件,核心场景:VR 演唱会票务小程序,用户选座 → 下单 → QR 核销。
|
||||||
|
|
||||||
|
当前 Phase 0/1/2 已完成基础骨架,暴露了一个 P0 架构问题:**ShopXO SPEC 与 SKU 的绑定方案**。
|
||||||
|
|
||||||
|
**已知状态(商品 112 实测):**
|
||||||
|
- `is_exist_many_spec = 0`(ShopXO 认为无多规格)
|
||||||
|
- `goods_spec_base` 表为空(无任何 SKU)
|
||||||
|
- `spec_base_id_map` 指向不存在的 DB 记录(ID 1001/1002/1003)
|
||||||
|
- ShopXO 防超卖机制完全未启用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 两种架构方向
|
||||||
|
|
||||||
|
| | 方案 A(每座=SKU) | 方案 B(每 Zone=SKU) |
|
||||||
|
|---|---|---|
|
||||||
|
| SKU 粒度 | 每个具体座位一行,inventory=1 | 每个 Zone(A/B/C)一行,inventory=Zone 座位数 |
|
||||||
|
| 防超卖 | ShopXO 原生原子扣库存(`BuyService dec()`) | 自建 FOR UPDATE 锁,需并发逻辑 |
|
||||||
|
| 多 Zone 混买 | 每座一行 goods_params,后端原子处理 | 前端分组,后端共享 Zone 库存 |
|
||||||
|
| 后台复杂度 | 10000+ SKU 行(插件自管,Hook 隐藏) | Zone 数量少,后台友好 |
|
||||||
|
| 与 ShopXO 生态 | 完全对齐 | 绕过 spec 校验 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 四问评议结论
|
||||||
|
|
||||||
|
### Q1:方案 A 后台批量生成 SKU 路径是否可行?
|
||||||
|
|
||||||
|
**结论:可行,但必须旁路 `GoodsSpecificationsInsert()`。**
|
||||||
|
|
||||||
|
- ShopXO 的 `GoodsSpecificationsInsert()` 每次商品保存时 `DELETE` 所有现有 spec 后重建,10K+ 座位场景不可用。
|
||||||
|
- **可行路径:直接 SQL INSERT** 到 `sxo_goods_spec_type`、`sxo_goods_spec_base`、`sxo_goods_spec_value` 三表。
|
||||||
|
- 性能:10000 座位 ≈ 3-4 秒(需分批 500 条/批提交)。
|
||||||
|
- 初始化一次,座位模板绑定时生成,后续不变。
|
||||||
|
- ShopXO 防超卖依赖 `BuyService.php:1677-1681` 的 `dec()` 机制(MySQL 条件原子扣减 `UPDATE SET inventory = inventory - N WHERE inventory >= N`),TOCTOU 窗口极小(选座模式并发低 + InnoDB 行锁),**推荐接受此风险**。
|
||||||
|
|
||||||
|
### Q2:商品 112 broken 状态是否需要紧急修复?
|
||||||
|
|
||||||
|
**结论:推荐方案乙(最小修复集),紧急程度中等。**
|
||||||
|
|
||||||
|
最小修复集:
|
||||||
|
```sql
|
||||||
|
-- Step 1: 启用多规格
|
||||||
|
UPDATE sxo_goods SET is_exist_many_spec = 1 WHERE id = 112;
|
||||||
|
|
||||||
|
-- Step 2: 写入 $vr- 规格维度
|
||||||
|
INSERT INTO sxo_goods_spec_type (goods_id, name, value, add_time) VALUES
|
||||||
|
(112, '$vr-场馆', '[{"name":"国家体育馆","images":""}]', UNIX_TIMESTAMP()),
|
||||||
|
(112, '$vr-分区', '[{"name":"A区","images":""},{"name":"B区","images":""},{"name":"C区","images":""}]', UNIX_TIMESTAMP()),
|
||||||
|
(112, '$vr-时段', '[{"name":"2026-05-01 19:00","images":""}]', UNIX_TIMESTAMP());
|
||||||
|
|
||||||
|
-- Step 3: 幂等保护(在 TicketService::issueTicket() 中添加 spec_base_id=0 fallback)
|
||||||
|
```
|
||||||
|
|
||||||
|
真正的批量 SKU 生成在 Phase 3「座位模板绑定」时完成。
|
||||||
|
|
||||||
|
### Q3:$vr- 前缀方案是否有隐患?
|
||||||
|
|
||||||
|
**结论:低风险,确认安全。(SecurityEngineer + FrontendDev 双重确认)**
|
||||||
|
|
||||||
|
- **ThinkPHP 模板解析机制**:`{$var}` 默认 HTML 转义输出,`{:expr}` 执行表达式但需要 `$var` 存在。
|
||||||
|
- `$vr-场馆` 作为 spec name 字符串存储在 DB 中,不作为 PHP 变量名,不触发变量插值。
|
||||||
|
- `parseVar` 正则 `\$[a-zA-Z_](?>\w*)` 在 `$vr-场馆` 中仅匹配 `$vr`,剩余 `-场馆` 留在原地,生成无效 PHP 代码,无 XSS 风险。
|
||||||
|
- `{{$spec.name}}` 中的 spec name 是属性值,ThinkPHP **不会**二次解析为模板语法。
|
||||||
|
- ShopXO spec name 字段无字符过滤,数据库 `varchar` 类型允许 `$` 字符。
|
||||||
|
- 唯一注意:ShopXO 后台规格管理页可能将 `$vr-` 显示不当(纯展示问题,不影响安全)。
|
||||||
|
|
||||||
|
### Q4:方案 A vs B 最终推荐?
|
||||||
|
|
||||||
|
**结论:明确推荐方案 A(每个座位一个 SKU)。三方一致。**
|
||||||
|
|
||||||
|
| 维度 | 方案 A(推荐) | 方案 B |
|
||||||
|
|------|---------------|--------|
|
||||||
|
| 防超卖 | ShopXO 原生原子扣库存,DB 层保证 | 自建 FOR UPDATE 锁,需自己写并发逻辑 |
|
||||||
|
| 实现复杂度 | 后端需批量生成 1 万+ SKU;前端 submit() 需改为逐座提交 | 后端简单;前端按 Zone 分组即可 |
|
||||||
|
| 多 Zone 混买 | 每座一行 goods_params,后端原子处理,体验流畅 | 前端分组但后端共享 Zone 库存,复杂度高 |
|
||||||
|
| 后台可维护性 | 10000+ SKU 行,但可 Hook 隐藏(插件自管) | Zone 数量少,后台友好 |
|
||||||
|
| 调试/故障排查 | 每个 SKU 独立,可追溯 | 共享库存,出问题难以定位 |
|
||||||
|
| 与 ShopXO 生态 | 完全对齐,无缝集成 | 绕过 spec 校验,部分 ShopXO 功能失效 |
|
||||||
|
| TOCTOU 风险 | 极小(选座并发低 + InnoDB 行锁兜底) | 可控(显式锁) |
|
||||||
|
|
||||||
|
**方案 B 的唯一优势**(SKU 数量少)在演唱会 10000+ 座场景下不成立——插件自建独立 SKU 管理页面,ShopXO 原生规格管理页通过 Hook 隐藏插件 SKU,优势消失。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 最终推荐
|
||||||
|
|
||||||
|
**采用方案 A:每个座位 = 一个 ShopXO SKU(stock=1)。**
|
||||||
|
|
||||||
|
### 推荐理由(综合三方)
|
||||||
|
|
||||||
|
1. **安全性最优**:ShopXO 原生原子扣库存防超卖,经过生产验证,无需自建锁。
|
||||||
|
2. **数据一致性**:每个座位 inventory=1,ShopXO 购买流程自带事务保护,TOCTOU 窗口极小(选座模式下并发度远低于总库存)。
|
||||||
|
3. **票务链路清晰**:`spec_base_id` 直接对应座位,票生成逻辑无需反向解析,核销链路可追溯。
|
||||||
|
4. **多 Zone 混买体验好**:前端每 Zone 一个 goods_params 行,后端按 seat_id 原子购买,体验流畅。
|
||||||
|
5. **与 ShopXO 生态对齐**:完全走 ShopXO 原生购买流程,故障排查有据可查,升级兼容性好。
|
||||||
|
6. **$vr- 前缀安全**:无 ThinkPHP 变量插值风险,无 XSS 风险,完全隔离于用户规格。
|
||||||
|
|
||||||
|
### ShopXO 原生防超卖机制
|
||||||
|
|
||||||
|
`BuyService.php:1677-1681`:
|
||||||
|
```php
|
||||||
|
$where = [
|
||||||
|
['id', '=', $base['data']['spec_base']['id']],
|
||||||
|
['goods_id', '=', $v['goods_id']],
|
||||||
|
['inventory', '>=', $v['buy_number']],
|
||||||
|
];
|
||||||
|
Db::name('GoodsSpecBase')->where($where)->dec('inventory', $v['buy_number'])->update();
|
||||||
|
```
|
||||||
|
翻译为 SQL:`UPDATE goods_spec_base SET inventory = inventory - N WHERE inventory >= N`
|
||||||
|
|
||||||
|
这是 MySQL 层面的条件原子扣减,TOCTOU 窗口极小(选座模式已在前端锁定具体座位,请求打到后端时并发度远低于总库存),推荐接受此风险。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 行动项(优先级排序)
|
||||||
|
|
||||||
|
| 优先级 | 行动项 | 负责 | 依赖 |
|
||||||
|
|--------|--------|------|------|
|
||||||
|
| **P0** | 执行 Q2 最小修复集:`UPDATE is_exist_many_spec=1` + 写入 `$vr-` spec_type + spec_base_id=0 幂等保护 | BackendArchitect | 无 |
|
||||||
|
| **P0** | 创建 `SeatSkuService::BatchGenerate()`:直接 SQL INSERT 批量生成 SKU(分批 500 条) | BackendArchitect | P0 完成后 |
|
||||||
|
| **P1** | 重构 `ticket_detail.html` submit():从 session-level 提交改为 seat-level 逐座提交,接入 `specBaseIdMap` | FrontendDev | P0 完成后 |
|
||||||
|
| **P2** | 实现 `loadSoldSeats()`:查询各 seat spec_base 的库存状态 | FrontendDev | P0 完成后 |
|
||||||
|
| **P3** | Hook 隐藏插件 SKU:插件 SKU 不出现在 ShopXO 原生规格管理页 | FrontendDev | P1 完成后 |
|
||||||
|
| **P3** | 设计插件独立 SKU 管理页面(隔离 ShopXO 原生规格管理) | FrontendDev | 远期 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 各成员立场
|
||||||
|
|
||||||
|
| 成员 | Q1 | Q2 | Q3 | Q4 最终推荐 |
|
||||||
|
|------|----|----|----|------------|
|
||||||
|
| BackendArchitect | 可行,旁路 GoodsSpecificationsInsert | 推荐方案乙 | — | **方案 A** |
|
||||||
|
| FrontendDev | 可行但复杂(需 Hook 隐藏 SKU 行) | 推荐方案甲(最小侵入) | 低风险安全 | **方案 A** |
|
||||||
|
| SecurityEngineer | — | blocked(待 Q4 确认) | 低风险安全 | **方案 A** |
|
||||||
|
|
||||||
|
**全票通过:采纳方案 A**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 附录
|
||||||
|
|
||||||
|
### A. 关键代码路径
|
||||||
|
|
||||||
|
- **购买原子扣库存**:`BuyService.php:1677-1681` — `dec()` 机制
|
||||||
|
- **规格插入(禁用)**:`GoodsService.php:2142` — `GoodsSpecificationsInsert()`(每次保存 DELETE+重建)
|
||||||
|
- **批量 SKU 生成**:插件自建 `SeatSkuService::BatchGenerate()`,直接 SQL INSERT 三表
|
||||||
|
- **前端提交改造**:`ticket_detail.html` — submit() 从 session-level 改为 seat-level
|
||||||
|
- **specBaseIdMap 注入**:后端 PHP 注入前端,供 submit() 使用
|
||||||
|
- **$vr- 前缀安全**:`shopxo/vendors/thinkphp/library/think/Template.php:837-955` — `parseVar` 正则
|
||||||
|
|
||||||
|
### B. 缩写说明
|
||||||
|
|
||||||
|
- SKU = ShopXO `goods_spec_base` 表中的一条记录(一个规格组合)
|
||||||
|
- spec_base_id = SKU 的主键 ID
|
||||||
|
- spec_base_id_map = 插件内存/缓存中的 `seat_id → spec_base_id` 映射
|
||||||
|
- TOCTOU = Time-of-check to time-of-use,并发竞态窗口
|
||||||
|
- goods_params = 购买请求中的规格参数数组
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
# 甲方新需求文档(2026-04-15)
|
||||||
|
|
||||||
|
## 来源
|
||||||
|
|
||||||
|
2026-04-15 下午,甲方补充需求,已与大头确认。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 需求内容
|
||||||
|
|
||||||
|
### 需求 1:多座位单订单
|
||||||
|
一个订单可包含多个座位,每个座位生成独立核销码。
|
||||||
|
|
||||||
|
**技术要求**:
|
||||||
|
- 每个座位 = 一个 ShopXO SKU(spec_base_id),stock=1
|
||||||
|
- 一次购买多个不同座位 = 多个 goods_params 条目(同一 goods_id + 不同 spec_base_id)
|
||||||
|
- ShopXO 原生支持多 spec_base_id 同单购买,各生成独立 order_goods 行 ✅
|
||||||
|
|
||||||
|
### 需求 2:核销码卡夹展示(订单详情页)
|
||||||
|
订单详情展示多个 QR 核销码,交互如下:
|
||||||
|
- 手动滑切换(类似轮播,但手动)
|
||||||
|
- 每个 QR 独立状态:已核销 → 灰掉
|
||||||
|
- 自动切换到下一张未核销的 QR
|
||||||
|
- 买了 N 个座位 → 显示 N 个 QR
|
||||||
|
|
||||||
|
**技术要求**:
|
||||||
|
- 多行 `order_goods` → 多张 `vr_tickets` QR
|
||||||
|
- 前端轮播组件(uni-app)
|
||||||
|
- Realtime 订阅:核销状态变更 → 前端自动更新
|
||||||
|
|
||||||
|
### 需求 3:商品级必填信息配置
|
||||||
|
商品 `ext` 字段声明购买时用户需填写的必填信息。
|
||||||
|
|
||||||
|
**字段设计(建议)**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"required_fields": ["id_card", "phone"],
|
||||||
|
"field_labels": {
|
||||||
|
"id_card": "身份证号",
|
||||||
|
"phone": "手机号"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**逻辑**:
|
||||||
|
- `ext` 为空 → 下单不弹窗,直接购买
|
||||||
|
- `ext` 有内容 → 弹窗要求填写,填写后附在订单备注
|
||||||
|
|
||||||
|
### 需求 4:手机号自动填充(订单级)
|
||||||
|
- 默认自动填微信认证手机号(wx.getPhoneNumber),可编辑
|
||||||
|
- 手机号是**购买凭据**(售后定位用)
|
||||||
|
- **同一订单多个座位/核销码 → 只需填一份联系信息**(订单级,非座位级)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ShopXO 多 Spec_base_id 同单购买验证
|
||||||
|
|
||||||
|
### 源码分析结论
|
||||||
|
|
||||||
|
**问题**:ShopXO 能否在同一次购买中,用同一个 `goods_id` + 不同 `spec_base_id`,各买 1 个?
|
||||||
|
|
||||||
|
**结论:✅ 支持**
|
||||||
|
|
||||||
|
### 源码证据
|
||||||
|
|
||||||
|
**BuyService.php 关键路径**:
|
||||||
|
|
||||||
|
```
|
||||||
|
goods_params = [
|
||||||
|
{ goods_id: 112, spec_base_id: 1001, stock: 1 }, ← 座位A1
|
||||||
|
{ goods_id: 112, spec_base_id: 1002, stock: 1 } ← 座位B2
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **BuyGoods()**:`foreach($params['goods_data'] as $v)` → 每个 goods_params 条目 → 一个 `$data[]` 元素
|
||||||
|
2. **OrderSplitService::Run()**:按 warehouse 分组(非 goods_id 合并)→ 不同 spec_base_id 保留为不同 goods_items[]
|
||||||
|
3. **OrderInsert()**:`foreach($v['goods_items'] as $vs)` → 每个 goods_items 条目 → **一行 order_goods**
|
||||||
|
|
||||||
|
```php
|
||||||
|
// BuyService.php:786
|
||||||
|
foreach($v['goods_items'] as $vs)
|
||||||
|
{
|
||||||
|
$order['detail_data'][] = [
|
||||||
|
'goods_id' => $vs['goods_id'],
|
||||||
|
'price' => $vs['price'],
|
||||||
|
'buy_number' => intval($vs['stock']), // = 1
|
||||||
|
// ...
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**结果**:
|
||||||
|
- goods_id=112, spec_base_id=1001 → order_goods 第1行(座位A1)
|
||||||
|
- goods_id=112, spec_base_id=1002 → order_goods 第2行(座位B2)
|
||||||
|
- 两个座位,同一订单,各生成独立 vr_tickets QR ✅
|
||||||
|
|
||||||
|
### 与需求对应关系
|
||||||
|
|
||||||
|
| 甲方需求 | 技术实现 | 状态 |
|
||||||
|
|---------|---------|------|
|
||||||
|
| 多座位单订单 | 每座位 = 独立 spec_base_id,stock=1 | ✅ |
|
||||||
|
| 多核销码 | 多行 order_goods → 多张 vr_tickets QR | ✅ |
|
||||||
|
| ext 必填字段 | extension_data.required_fields | ✅ |
|
||||||
|
| 手机号订单级 | 联系信息挂 order 备注,非 goods_params | ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 当前数据库状态(已验证)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 商品 112 票务商品
|
||||||
|
is_exist_many_spec = 0 -- ShopXO 认为无多规格
|
||||||
|
spec_base 表 = 空的 -- 没有任何 SKU
|
||||||
|
|
||||||
|
-- vr_seat_templates.spec_base_id_map
|
||||||
|
-- {"A": 1001, "B": 1002, "C": 1003} ← 这些 ID 在 DB 里不存在!
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题**:ShopXO 防超卖机制完全未启用,购买走裸商品逻辑。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## spec_value 绑定方案($vr- 前缀)
|
||||||
|
|
||||||
|
### 方案已确认
|
||||||
|
|
||||||
|
ShopXO spec name 允许特殊字符($,-,中文),无字符过滤。
|
||||||
|
|
||||||
|
### 插件专用规格命名
|
||||||
|
|
||||||
|
```
|
||||||
|
$vr-场馆 → 场馆名称(如 $vr-场馆 = "鸟巢")
|
||||||
|
$vr-分区 → 座位分区(Zone)
|
||||||
|
$vr-时段 → 场次时间
|
||||||
|
```
|
||||||
|
|
||||||
|
### 为什么不会与用户规格冲突
|
||||||
|
|
||||||
|
- 插件票务商品使用自定义模板 `ticket_detail.html`
|
||||||
|
- 前端 UI 不走 ShopXO 默认规格选择器
|
||||||
|
- 用户无法通过默认界面触碰到 `$vr-` 规格
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 方案 A(每个座位一个 SPEC)兼容性
|
||||||
|
|
||||||
|
**结论:方案 A 完全兼容甲方全部 4 项新需求**
|
||||||
|
|
||||||
|
| 需求 | 方案 A 如何满足 |
|
||||||
|
|-----|---------------|
|
||||||
|
| 多座位单订单 | 每座位 = SKU,ShopXO 原生支持多 SKU 同单 ✅ |
|
||||||
|
| 核销码卡夹 | order_goods × N → vr_tickets × N → N 张 QR ✅ |
|
||||||
|
| ext 必填字段 | goods.extension_data.required_fields ✅ |
|
||||||
|
| 手机号订单级 | 联系信息不写在 goods_params,写在 order 备注 ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 待办事项
|
||||||
|
|
||||||
|
- [ ] Issue #9:方案 A vs B 最终决策
|
||||||
|
- [ ] 紧急修复:is_exist_many_spec → 1 + 正确生成每个座位的 SKU
|
||||||
|
- [ ] 后台批量创建 SKU 实现(方案 A 关键路径)
|
||||||
|
- [ ] ext.required_fields 前端弹窗实现
|
||||||
|
- [ ] 订单详情核销码卡夹组件
|
||||||
|
- [ ] 微信手机号自动填充 API 集成
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
# Round 2 深入分析 — BackendArchitect
|
||||||
|
|
||||||
|
> 日期:2026-04-15
|
||||||
|
> 负责:Q1(批量 SKU 生成路径)+ Q2(紧急修复优先级)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Q1:方案 A 后台批量生成 SKU 路径分析
|
||||||
|
|
||||||
|
### ShopXO SPEC/SKU 创建机制
|
||||||
|
|
||||||
|
通过代码审查 `GoodsService.php:2142` 的 `GoodsSpecificationsInsert()` 函数:
|
||||||
|
|
||||||
|
1. **删除再插入**:`GoodsSpecificationsInsert()` 在插入前会 `DELETE` 该商品的所有 `GoodsSpecType`、`GoodsSpecValue`、`GoodsSpecBase` 记录(line 2145-2147)
|
||||||
|
2. **逐行写入**:`GoodsSpecBase` 通过循环 `foreach($data['data'] as $v)` 逐条 `insertGetId`(line 2230),不是真正的批量 API
|
||||||
|
3. **无现成批量 API**:ShopXO 没有 `batchInsertSpecs()` 之类的公共方法
|
||||||
|
4. **必须旁路 GoodsSpecificationsInsert**:不能走 ShopXO 原生商品保存流程(否则每次都清空重建)
|
||||||
|
|
||||||
|
### 可行路径:直接 SQL INSERT
|
||||||
|
|
||||||
|
插件在座位模板绑定/初始化时,直接 SQL INSERT 三个表:
|
||||||
|
|
||||||
|
**Step 1**: 写入 `sxo_goods_spec_type`(规格维度)
|
||||||
|
```sql
|
||||||
|
INSERT INTO sxo_goods_spec_type (goods_id, name, value, add_time) VALUES
|
||||||
|
(112, '$vr-场馆', '[{"name":"国家体育馆","images":""}]', UNIX_TIMESTAMP()),
|
||||||
|
(112, '$vr-分区', '[{"name":"A区","images":""},{"name":"B区","images":""}]', UNIX_TIMESTAMP()),
|
||||||
|
(112, '$vr-时段', '[{"name":"2026-05-01 19:00","images":""}]', UNIX_TIMESTAMP());
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2**: 写入 `sxo_goods_spec_base`(每个座位一行 SKU,inventory=1)
|
||||||
|
```sql
|
||||||
|
INSERT INTO sxo_goods_spec_base (goods_id, price, original_price, inventory,
|
||||||
|
buy_min_number, buy_max_number, add_time) VALUES
|
||||||
|
(112, 680.00, 880.00, 1, 1, 1, UNIX_TIMESTAMP()), -- A区 A-1
|
||||||
|
(112, 680.00, 880.00, 1, 1, 1, UNIX_TIMESTAMP()), -- A区 A-2
|
||||||
|
... -- 10000+ 行
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3**: 写入 `sxo_goods_spec_value`(建立 spec_base_id ↔ spec_value 的映射)
|
||||||
|
```sql
|
||||||
|
INSERT INTO sxo_goods_spec_value (goods_id, goods_spec_base_id, value, md5_key, add_time) VALUES
|
||||||
|
(112, @base_id_1, '国家体育馆', md5('国家体育馆'), UNIX_TIMESTAMP()),
|
||||||
|
(112, @base_id_1, 'A区', md5('A区'), UNIX_TIMESTAMP()),
|
||||||
|
(112, @base_id_1, '2026-05-01 19:00', md5('2026-05-01 19:00'), UNIX_TIMESTAMP());
|
||||||
|
-- 每个座位 3 条(对应3个spec维度)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4**: 更新 `sxo_goods` 的 `is_exist_many_spec = 1`(告诉 ShopXO 启用多规格)
|
||||||
|
|
||||||
|
### 关键发现:防超卖机制的原子性
|
||||||
|
|
||||||
|
审查 `BuyService.php:1677-1681`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$where = [
|
||||||
|
['id', '=', $base['data']['spec_base']['id']],
|
||||||
|
['goods_id', '=', $v['goods_id']],
|
||||||
|
['inventory', '>=', $v['buy_number']],
|
||||||
|
];
|
||||||
|
Db::name('GoodsSpecBase')->where($where)->dec('inventory', $v['buy_number'])->update();
|
||||||
|
```
|
||||||
|
|
||||||
|
ThinkPHP 的 `dec()` 翻译为 SQL:`UPDATE goods_spec_base SET inventory = inventory - N WHERE inventory >= N`
|
||||||
|
|
||||||
|
这是**条件原子扣减**——在 MySQL 层是原子的。方案 A 依赖这个机制来防超卖。
|
||||||
|
|
||||||
|
**但存在 TOCTOU 窗口**:在并发极高(10K+ 同时抢票)时,两条请求可能同时通过 `inventory >= 1` 检查再同时执行 dec()。MySQL 的 InnoDB 行锁会串行化这两个 UPDATE,但不保证顺序——理论上可能出现:两人都查到 inventory=1,都通过检查,都执行 dec(),最终 inventory=-1。
|
||||||
|
|
||||||
|
**实际风险评估**:演唱会抢票场景是"选座"而非"随机库存",用户选座时前端已经锁定了具体座位,请求打到后端时并发度远低于总库存。TOCTOU 窗口极小。**推荐接受此风险**。
|
||||||
|
|
||||||
|
### 性能估算
|
||||||
|
|
||||||
|
- 10000 座位 = 10000 条 `goods_spec_base` + 30000 条 `goods_spec_value`
|
||||||
|
- 单次批量 INSERT 耗时:~0.5-2 秒(InnoDB 批量插入效率高)
|
||||||
|
- **需要分批提交**:每批 500 条,避免单次大事务锁表超时
|
||||||
|
- **初始化一次**:座位模板绑定时生成,后续不变
|
||||||
|
|
||||||
|
### 结论
|
||||||
|
|
||||||
|
**Q1 结论:可行,但必须旁路 ShopXO 原生 `GoodsSpecificationsInsert()`,走直接 SQL INSERT 路径。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Q2:商品 112 broken 状态紧急修复优先级
|
||||||
|
|
||||||
|
### 当前状态分析
|
||||||
|
|
||||||
|
```
|
||||||
|
goods_id=112:
|
||||||
|
is_exist_many_spec = 0 ← ShopXO 认为无多规格
|
||||||
|
spec_base 表 = 空 ← 从未生成过 SKU
|
||||||
|
spec_base_id_map → {A:1001, B:1002, C:1003} ← 这些 ID 在 DB 里不存在!
|
||||||
|
```
|
||||||
|
|
||||||
|
### 影响评估
|
||||||
|
|
||||||
|
| 影响点 | 严重程度 | 说明 |
|
||||||
|
|--------|---------|------|
|
||||||
|
| 购买流程 | **高** | 当前 is_exist_many_spec=0,购买走裸商品逻辑,`spec_base_id_map` 形同虚设 |
|
||||||
|
| 票生成(onOrderPaid) | **高** | `spec_base_id` 指向不存在的 DB 记录,但代码有幂等保护,暂不崩溃 |
|
||||||
|
| ShopXO 后台显示 | 低 | 不影响 ShopXO 原生商品管理 |
|
||||||
|
| 用户端选座 | 低 | 前端/小程序逻辑独立 |
|
||||||
|
|
||||||
|
### 修复路径
|
||||||
|
|
||||||
|
**最小修复集**(方案甲):仅设置 flag,不重建 SKU
|
||||||
|
```sql
|
||||||
|
UPDATE sxo_goods SET is_exist_many_spec = 1 WHERE id = 112;
|
||||||
|
```
|
||||||
|
然后在票生成逻辑中对 `spec_base_id=0` 做 fallback 保护。
|
||||||
|
|
||||||
|
**推荐修复集**(方案乙):设置 flag + 重建 $vr- spec_type
|
||||||
|
```sql
|
||||||
|
-- Step 1: 告诉 ShopXO 启用多规格
|
||||||
|
UPDATE sxo_goods SET is_exist_many_spec = 1 WHERE id = 112;
|
||||||
|
|
||||||
|
-- Step 2: 写入 $vr- 规格维度(场馆/分区/时段名称)
|
||||||
|
INSERT INTO sxo_goods_spec_type (goods_id, name, value, add_time) VALUES
|
||||||
|
(112, '$vr-场馆', '[{"name":"国家体育馆","images":""}]', UNIX_TIMESTAMP()),
|
||||||
|
(112, '$vr-分区', '[{"name":"A区","images":""},{"name":"B区","images":""},{"name":"C区","images":""}]', UNIX_TIMESTAMP()),
|
||||||
|
(112, '$vr-时段', '[{"name":"2026-05-01 19:00","images":""}]', UNIX_TIMESTAMP());
|
||||||
|
|
||||||
|
-- Step 3: 重建 spec_base_id_map → seat_id 到 spec_base_id 的映射
|
||||||
|
-- (由插件 SeatSkuService 完成)
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**:spec_base 表不重建——因为真正的批量 SKU 生成是在 Phase 3「座位模板绑定」时做的,届时会走 SQL INSERT 路径。
|
||||||
|
|
||||||
|
### 结论
|
||||||
|
|
||||||
|
**Q2 结论:推荐方案乙——最小修复集 = UPDATE is_exist_many_spec + INSERT $vr- spec_type + 幂等保护。紧急程度中等,不影响当前票务逻辑运行,但应在 Phase 3 批量 SKU 生成前完成。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Q4 初步推荐(基于 Q1/Q2 分析)
|
||||||
|
|
||||||
|
**推荐方案 A(每个座位一个 SKU)**,理由补充:
|
||||||
|
|
||||||
|
1. **原子性已验证**:`BuyService.php` 的 dec() 机制是 MySQL 层面的条件原子扣减,方案 A 的防超卖完全依赖此机制,无需自建锁
|
||||||
|
2. **数据完整性**:每个座位独立 inventory=1,ShopXO 原生购买流程完整走通,无需 Hook 旁路购买逻辑
|
||||||
|
3. **票务链路清晰**:`spec_base_id` 直接对应座位,票生成逻辑无需反向解析
|
||||||
|
4. **TOCTOU 风险可接受**:选座模式并发窗口极小,ShopXO 行锁提供最后保护
|
||||||
|
|
||||||
|
**方案 B 的唯一优势**(SKU 数量少)在演唱会场景下不成立——方案 A 的批量 INSERT 一次性完成,不存在"管理困难"问题(插件自己管理,不走 ShopXO 后台)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 行动项(Round 2 输出)
|
||||||
|
|
||||||
|
| 优先级 | 行动项 | 负责 |
|
||||||
|
|--------|--------|------|
|
||||||
|
| P0 | 创建 `SeatSkuService::BatchGenerate()` — 直接 SQL INSERT 批量生成 SKU | BackendArchitect |
|
||||||
|
| P0 | 执行 Q2 最小修复集:UPDATE is_exist_many_spec + INSERT $vr- spec_type | BackendArchitect |
|
||||||
|
| P1 | 在 `TicketService::issueTicket()` 中添加 spec_base_id=0 的幂等保护 | BackendArchitect |
|
||||||
|
| P2 | 设计插件独立 SKU 管理页面(隔离 ShopXO 原生规格管理) | FrontendDev |
|
||||||
571
plan.md
571
plan.md
|
|
@ -1,10 +1,33 @@
|
||||||
# vr-shopxo-plugin Phase 2 — 后台管理开发计划
|
# vr-shopxo-plugin 架构决策评议 — plan.md
|
||||||
|
|
||||||
> 版本:v2.0(合并版)| 制定日期:2026-04-15 | Agent:council/FrontendDev + council/SecurityEngineer + council/BackendArchitect
|
> 版本:v1.2(最终合并版)| 日期:2026-04-15 | Agent:council/FrontendDev + BackendArchitect + SecurityEngineer
|
||||||
|
> 关联:Issue #9 | 状态:FINAL
|
||||||
|
|
||||||
## 概述
|
---
|
||||||
|
|
||||||
Phase 2 目标:完成后台管理页面开发,涵盖座位模板管理、电子票管理、核销员管理、核销记录查询,以及 Admin 控制器鉴权修复。
|
## 任务背景
|
||||||
|
|
||||||
|
Phase 0/1/2 已完成基础骨架,暴露了一个 P0 架构问题:VR 演唱会票务商品中 ShopXO SPEC 与 SKU 的绑定方案。
|
||||||
|
|
||||||
|
**已知事实:**
|
||||||
|
- ShopXO `goods_spec_base`(SKU表)当前为空,商品 112 的 `is_exist_many_spec=0`
|
||||||
|
- `spec_base_id_map` 中的 ID(如 1001/1002/1003)在 DB 中不存在
|
||||||
|
- ShopXO 防超卖机制(原子扣 inventory)完全未启用
|
||||||
|
|
||||||
|
**两种架构方向:**
|
||||||
|
- **方案 A**:每个座位 = 一个 SKU(stock=1),ShopXO 原生防超卖
|
||||||
|
- **方案 B**:每个 Zone = 一个 SKU(stock=Zone座位数),自建 FOR UPDATE 防超卖
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 核心问题(4问)
|
||||||
|
|
||||||
|
| # | 问题 | 负责 |
|
||||||
|
|---|------|------|
|
||||||
|
| Q1 | 方案 A 后台批量生成 SKU 路径是否可行?ShopXO 是否有批量 API? | BackendArchitect |
|
||||||
|
| Q2 | 当前商品 112 的 broken 状态(is_exist_many_spec=0 + spec_base 空)是否需要紧急修复?最小修复集? | BackendArchitect |
|
||||||
|
| Q3 | $vr- 前缀方案是否有隐患?ShopXO 内部是否对 $ 有特殊处理? | SecurityEngineer + FrontendDev |
|
||||||
|
| Q4 | 方案 A vs 方案 B 最终推荐(实现成本 / 安全性 / 可维护性) | 所有成员 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -12,403 +35,197 @@ Phase 2 目标:完成后台管理页面开发,涵盖座位模板管理、电
|
||||||
|
|
||||||
| 阶段 | 内容 | 负责 |
|
| 阶段 | 内容 | 负责 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| Draft | 资料收集、研究方向确认 | 所有成员 |
|
| Round 1 | 独立评议 + plan.md 合并 | 所有成员 |
|
||||||
| Review | 代码实现审查、安全评审 | SecurityEngineer + Council |
|
| Round 2 | 各成员深入分析(后台实现路径、安全评估、前端方案) | 所有成员 |
|
||||||
| Finalize | 合并到 main、文档整理 | 所有成员 |
|
| Round 3 | 综合推荐 + 输出最终决策报告 + `council-output/ARCHITECTURE_DECISION.md` | FrontendDev 主笔 |
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Agent 任务分配
|
|
||||||
|
|
||||||
| Agent | 主要任务 |
|
|
||||||
|-------|---------|
|
|
||||||
| BackendArchitect | API 设计、权限模型、数据库查询 |
|
|
||||||
| SecurityEngineer | Admin 鉴权、注入/XSS、审计、IDOR |
|
|
||||||
| FrontendDev | UI 框架选型、ShopXO admin 风格适配 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 任务清单
|
## 任务清单
|
||||||
|
|
||||||
### 座位模板管理
|
- [x] **Q1**: 方案 A 批量生成 SKU 路径 `[Done: BackendArchitect]` ✅
|
||||||
- [x] 座位模板列表页(`seat_template/list.html`)`[Done: council/FrontendDev]`
|
- [x] **Q2**: 商品 112 broken 状态紧急修复 `[Done: BackendArchitect]` ✅
|
||||||
- [x] 座位模板新增/编辑页(`seat_template/save.html`)`[Done: council/FrontendDev]`
|
- [x] **Q3**: $vr- 前缀安全评估 `[Done: SecurityEngineer + FrontendDev]` ✅
|
||||||
- [ ] 座位图可视化编辑器集成
|
- [x] **Q4**: 方案 A vs 方案 B 最终推荐 `[Done: 所有成员]` ✅ — 三方一致推荐方案 A
|
||||||
- [x] 分类绑定功能(category_id 字段已在 save.html 中实现)`[Done: council/FrontendDev]`
|
- [x] **Final**: `council-output/ARCHITECTURE_DECISION.md` `[Done: FrontendDev]` ✅
|
||||||
|
|
||||||
### 电子票管理
|
|
||||||
- [x] 电子票列表页(`ticket/list.html`)`[Done: council/FrontendDev]`
|
|
||||||
- [x] 票详情页(`ticket/detail.html`)`[Done: council/FrontendDev]`
|
|
||||||
- [x] 批量导出功能(CSV)— 修复:导出按钮 GET→POST form `⚠️ Fixed Round 4`
|
|
||||||
- [x] 票状态筛选(未核销/已核销/已退款)`[Done: council/FrontendDev]`
|
|
||||||
|
|
||||||
### 核销员管理
|
|
||||||
- [x] 核销员列表页(`verifier/list.html`)`[Done: council/FrontendDev]`
|
|
||||||
- [x] 核销员新增/编辑/删除(`verifier/save.html`)`[Done: council/FrontendDev]`
|
|
||||||
- [ ] 核销员绑定店铺/场次
|
|
||||||
|
|
||||||
### 核销记录
|
|
||||||
- [x] 核销记录列表页(`verification/list.html`)`[Done: council/FrontendDev]`
|
|
||||||
- [x] 多条件查询(时间/核销员/场次)`[Done: council/FrontendDev]`
|
|
||||||
- [ ] 核销统计看板
|
|
||||||
|
|
||||||
### Admin 鉴权(P1 安全)
|
|
||||||
- [x] 所有 Admin 控制器继承 Base controller `✓ Base extends Common (BackendArchitect)`
|
|
||||||
- [x] 鉴权中间件验证 `✓ SecurityEngineer S1 验证通过`
|
|
||||||
- [x] 敏感操作日志审计(Task S4)
|
|
||||||
|
|
||||||
### 后端 API 任务
|
|
||||||
- [x] **Task B1** — 座位模板管理 CRUD `[Done: council/BackendArchitect]`
|
|
||||||
- [x] **Task B2** — 电子票列表 / 详情 / 导出 `[Done: council/BackendArchitect]`
|
|
||||||
- [x] **Task B3** — 核销员管理(增删改查)`[Done: council/BackendArchitect]`
|
|
||||||
- [x] **Task B4** — 核销记录查询 `[Done: council/BackendArchitect]`
|
|
||||||
|
|
||||||
### 安全任务
|
|
||||||
- [x] **Task S1** — Admin 鉴权覆盖完整性 `[Done: council/SecurityEngineer]`
|
|
||||||
- [x] **Task S2** — SQL 注入风险审计 `[Done: council/SecurityEngineer]`
|
|
||||||
- [x] **Task S3** — XSS / CSRF 防护检查 `[Done: council/SecurityEngineer]`
|
|
||||||
- [x] **Task S4** — 敏感操作审计日志设计 `[Done: council/BackendArchitect]`
|
|
||||||
- [x] **Task S5** — IDOR / 水平越权测试用例编写 `[Done: council/SecurityEngineer]`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Research Direction List(合并版)
|
## Claim 状态
|
||||||
|
|
||||||
### 安全研究方向(SecurityEngineer)
|
| 任务 | Claim 状态 |
|
||||||
|
|------|-----------|
|
||||||
#### R1: Admin 控制器鉴权风险
|
| Q1 | [Done: BackendArchitect] |
|
||||||
**背景**:Council 安全审议发现 P1 问题 —— Admin 控制器缺少统一鉴权机制,Phase 2 所有后台页面均受影响。
|
| Q2 | [Done: BackendArchitect] |
|
||||||
|
| Q3 | [Done: SecurityEngineer] + [Done: FrontendDev] |
|
||||||
Key Questions:
|
| Q4 | [Done: BackendArchitect] + [Done: FrontendDev] + [Done: SecurityEngineer] |
|
||||||
- [ ] ShopXO 后台 admin 控制器统一鉴权入口在哪里?`AdminBaseController` 或中间件?
|
| 最终输出 | [Done: FrontendDev] |
|
||||||
- [ ] Phase 2 新增的 Base 控制器(`app/common.php` 的 Base 类)是否已正确调用鉴权?
|
|
||||||
- [ ] 各子控制器(SeatTemplate / Ticket / Verifier)是否有遗漏的 public 方法暴露未授权访问?
|
|
||||||
- [ ] 鉴权失败后的跳转逻辑是否正确?是否存在认证绕过风险?
|
|
||||||
- [ ] 后台操作是否需要二次验证(如删除、核销等敏感操作)?
|
|
||||||
|
|
||||||
**优先级**:P0
|
|
||||||
|
|
||||||
#### R2: SQL 注入风险评估
|
|
||||||
**背景**:电子票导出、核销记录查询等涉及复杂 SQL,必须防范注入。
|
|
||||||
|
|
||||||
Key Questions:
|
|
||||||
- [ ] ShopXO 原生查询构造器(Db::table / Db::name)的参数绑定机制是否覆盖所有输入点?
|
|
||||||
- [ ] 日期范围查询、模糊搜索等动态构造场景是否存在拼接风险?
|
|
||||||
- [ ] IN() 子句的数组参数是否经过安全处理?
|
|
||||||
- [ ] 关联查询(join)中是否有注入向量?
|
|
||||||
- [ ] 是否有 ORM 之外的原始 SQL 执行需要审查?
|
|
||||||
|
|
||||||
**优先级**:P1
|
|
||||||
|
|
||||||
#### R3: XSS / CSRF 风险
|
|
||||||
**背景**:后台管理页面输出到管理端,但仍需防范存储型 XSS 和 CSRF。
|
|
||||||
|
|
||||||
Key Questions:
|
|
||||||
- [ ] 后台页面渲染时是否统一使用 htmlspecialchars 或框架转义?
|
|
||||||
- [ ] 富文本编辑(如座位模板名称备注)是否存在存储型 XSS?
|
|
||||||
- [ ] POST 请求是否均携带 CSRF Token?ShopXO 的 CSRF 保护机制是什么?
|
|
||||||
- [ ] JSON API 响应是否可能携带恶意脚本?
|
|
||||||
- [ ] 删除/核销等关键操作是否有 CSRF Token 保护?
|
|
||||||
|
|
||||||
**优先级**:P1
|
|
||||||
|
|
||||||
#### R4: 敏感操作审计日志
|
|
||||||
**背景**:核销操作、票务导出等属于高敏感操作,必须可追溯。
|
|
||||||
|
|
||||||
Key Questions:
|
|
||||||
- [ ] 是否需要新建 `vr_audit_log` 表记录关键操作?
|
|
||||||
- [ ] 核销记录是否需要记录操作人 IP、UA、设备指纹?
|
|
||||||
- [ ] 导出操作是否需要记录?谁、何时、导出内容摘要?
|
|
||||||
- [ ] 审计日志是否需要防篡改设计(如 hash chain 或 append-only 表)?
|
|
||||||
- [ ] ShopXO 是否有现有审计日志机制可以复用?
|
|
||||||
|
|
||||||
**优先级**:P2
|
|
||||||
|
|
||||||
#### R5: 水平越权风险(IDOR)
|
|
||||||
**背景**:核销员管理、电子票详情等场景存在横向越权风险。
|
|
||||||
|
|
||||||
Key Questions:
|
|
||||||
- [ ] 核销员详情/编辑接口是否校验当前用户所属机构/场馆?
|
|
||||||
- [ ] 电子票详情接口是否校验持票人身份?
|
|
||||||
- [ ] 核销记录查询是否仅返回当前核销员/机构的记录?
|
|
||||||
- [ ] 删除/编辑操作是否有归属校验(owner check)?
|
|
||||||
- [ ] 是否有测试用例覆盖常见的 IDOR 攻击向量?
|
|
||||||
|
|
||||||
**优先级**:P1
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 前端研究方向(FrontendDev)
|
|
||||||
|
|
||||||
#### FR-1: ShopXO Admin UI 框架选型
|
|
||||||
**背景**:ShopXO 后台使用 Layui,需确认是否继续使用还是迁移 Vue。
|
|
||||||
|
|
||||||
Key Questions:
|
|
||||||
- ShopXO 官方后台(v6.8.0)使用的是什么 UI 版本?
|
|
||||||
- Layui 是否支持 Vue 3?如果不支持,混用 Vue + Layui 是否会导致冲突?
|
|
||||||
- 票务插件是否应保持与 ShopXO 原生风格一致,还是可以独立升级?
|
|
||||||
- 是否有 ShopXO 插件使用 Vue 3 的先例?
|
|
||||||
|
|
||||||
#### FR-2: 现有 ShopXO Admin 页面风格适配
|
|
||||||
**背景**:保持与 ShopXO 原生后台风格一致可降低学习成本。
|
|
||||||
|
|
||||||
Key Questions:
|
|
||||||
- ShopXO 后台使用的是什么设计系统(颜色/字体/间距规范)?
|
|
||||||
- 表格组件(数据列表)用的是 Layui table 还是自建?
|
|
||||||
- 分页、筛选、搜索的通用组件封装在哪里?
|
|
||||||
- 弹窗/表单布局的规范是什么?
|
|
||||||
|
|
||||||
#### FR-3: 座位图编辑器集成方案
|
|
||||||
**背景**:座位模板需要可视化编辑,复杂度高。
|
|
||||||
|
|
||||||
Key Questions:
|
|
||||||
- 是否有开源的 Vue 座位图编辑器可以集成?
|
|
||||||
- Canvas vs SVG vs CSS Grid,哪个方案最适合票务座位图?
|
|
||||||
- 座位图编辑后如何序列化存储到 seat_map JSON?
|
|
||||||
- 编辑器是否需要支持拖拽、分区着色、座位类型标注?
|
|
||||||
|
|
||||||
#### FR-4: 数据导出方案(CSV/Excel)
|
|
||||||
**背景**:电子票列表需要支持批量导出。
|
|
||||||
|
|
||||||
Key Questions:
|
|
||||||
- ShopXO 后台是否有现成的导出组件?
|
|
||||||
- 大量数据(10000+ 条)导出的处理策略是什么(流式导出 vs 后台队列)?
|
|
||||||
- 是否需要支持 Excel 格式(.xlsx)还是只需 CSV?
|
|
||||||
- 导出字段如何与 vr_tickets 表字段对应?
|
|
||||||
|
|
||||||
#### FR-5: 响应式与权限控制
|
|
||||||
**背景**:后台页面需要同时支持不同屏幕和权限级别。
|
|
||||||
|
|
||||||
Key Questions:
|
|
||||||
- ShopXO 后台的权限体系是如何设计的(RBAC?按钮级?字段级?)?
|
|
||||||
- 票务管理员是否需要独立的角色?与 ShopXO 管理员如何隔离?
|
|
||||||
- 后台页面是否需要支持移动端(PAD 核销场景)?
|
|
||||||
- 操作日志记录哪些字段(用户/时间/操作/IP/变更前后)?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 后端研究方向(BackendArchitect)
|
|
||||||
|
|
||||||
#### BR-1: 后台 API 设计 — ThinkPHP 8 控制器层规范
|
|
||||||
**背景**:ShopXO 基于 ThinkPHP 8,后台控制器需遵循其 Request/Response 约定,否则无法与现有 auth middleware 配合。
|
|
||||||
|
|
||||||
Key Questions:
|
|
||||||
- [ ] ShopXO Admin 控制器基类是什么?是否有统一的 `BaseAdmin` 或 `AdminController`?
|
|
||||||
- [ ] ThinkPHP 8 的 `Request` 对象如何获取当前登录 admin id / role?
|
|
||||||
- [ ] API 返回格式是统一 JSON 还是沿用 ShopXO 的 `Json` 渲染器?
|
|
||||||
- [ ] 分页参数命名规范:ShopXO 默认用 `page`/`limit` 还是 `page`/`pageSize`?
|
|
||||||
- [ ] 新增 controller 是否需要在某个注册处声明(路由/菜单)?
|
|
||||||
|
|
||||||
#### BR-2: 权限模型 — 多角色鉴权设计
|
|
||||||
**背景**:核销员(vr_verifiers)与超级管理员属于不同角色,后台功能需要细粒度权限控制。
|
|
||||||
|
|
||||||
Key Questions:
|
|
||||||
- [ ] ShopXO admin 用户表(`sx_admin`)的 role 字段结构是怎样的?是 RBAC 吗?
|
|
||||||
- [ ] 核销员是否复用 admin 表,还是独立表(`vr_verifiers`)?各自权限如何隔离?
|
|
||||||
- [ ] 座位模板编辑 / 电子票导出 / 核销记录查看是否需要分开授权?
|
|
||||||
- [ ] 是否有现成的权限中间件可以复用,还是需要自行实现?
|
|
||||||
|
|
||||||
#### BR-3: 数据库查询优化 — 大数据量导出场景
|
|
||||||
**背景**:电子票列表 + 核销记录在数据量大时有分页和内存压力。
|
|
||||||
|
|
||||||
Key Questions:
|
|
||||||
- [ ] `vr_tickets` / `vr_verifications` 当前预估数据规模?是否需要游标分页?
|
|
||||||
- [ ] 导出功能(CSV/Excel)PHP 侧是否需要流式输出避免 OOM?
|
|
||||||
- [ ] `vr_verifications` 的 `verified_at` 字段是否建索引?
|
|
||||||
- [ ] 是否需要读写分离?ShopXO 数据层是否支持?
|
|
||||||
- [ ] 是否有批量查询 N+1 问题(如查询票时重复查 holder info)?
|
|
||||||
|
|
||||||
#### BR-4: 事务一致性 — 核销操作的原子性
|
|
||||||
**背景**:核销涉及两张表(`vr_verifications` 写入 + `vr_tickets.status` 更新),若并发核销同一票会导致重复核销。
|
|
||||||
|
|
||||||
Key Questions:
|
|
||||||
- [ ] ThinkPHP 8 Db facade 的事务写法(`Db::transaction()`)如何与 Model 层配合?
|
|
||||||
- [ ] 是否需要悲观锁(`SELECT ... FOR UPDATE`)防止并发核销?
|
|
||||||
- [ ] 乐观锁(version 字段)是否适用于高并发核销场景?
|
|
||||||
- [ ] 核销失败后的补偿逻辑如何设计?
|
|
||||||
|
|
||||||
#### BR-5: 存储过程 / 事件驱动 — 核销通知异步化
|
|
||||||
**背景**:核销操作可能触发通知(站内信/微信),同步执行会阻塞核销响应。
|
|
||||||
|
|
||||||
Key Questions:
|
|
||||||
- [ ] ShopXO 是否有事件/队列机制(Redis/DB queue)?
|
|
||||||
- [ ] ThinkPHP 8 的 `Queue::later()` 或 `event()` 是否可用?
|
|
||||||
- [ ] 若无队列,核销通知是否可以降级为同步日志+后台任务补偿?
|
|
||||||
- [ ] 是否需要记录核销操作的 event log 供后续审计?
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 依赖关系
|
## 依赖关系
|
||||||
|
|
||||||
- **FR-1、FR-2** 优先完成,决定前端技术栈选型
|
- Q1(BackendArchitect)先完成,后 Q4 才能给出完整推荐
|
||||||
- **FR-3** 依赖 FR-1 的选型结论
|
- Q3(SecurityEngineer)可与 Q1 并行
|
||||||
- **FR-4** 可在 Phase 3 后端 API 确定后并行进行
|
- Q2 可独立完成,紧急程度由 BackendArchitect 判定
|
||||||
- **FR-5** 与 SecurityEngineer 协同,需要等 BackendArchitect 输出权限模型
|
- 三方分析完成后,FrontendDev 主笔 Round 3 最终报告
|
||||||
- **Task S1**(鉴权审查)需在 BackendArchitect 完成 API 设计初稿后进行交叉评审
|
|
||||||
- **Task S2**(SQL 审计)可与 BackendArchitect 的数据库设计并行
|
---
|
||||||
- **Task S4**(审计日志)依赖最终的数据模型设计
|
|
||||||
- **BR-1**(ThinkPHP 控制器规范)与 **FR-1** 交叉确认 API 返回格式
|
## 各成员 Round 1 初判
|
||||||
- **BR-2**(权限模型)与 **FR-5** 协同,需 BackendArchitect 和 SecurityEngineer 共同输出
|
|
||||||
|
### BackendArchitect 初判
|
||||||
|
|
||||||
|
**Q1 初步判断**:Plan A 后台批量生成 SKU **可行**。ShopXO 的 `goods_spec_base` 是标准 MySQL 表,插件可直接 INSERT。
|
||||||
|
|
||||||
|
**Q2 初步判断**:当前 broken 状态**暂不需要立即修复**。购买流程走的是裸商品逻辑(is_exist_many_spec=0),需要明确购买流程最终走哪条路后再修。
|
||||||
|
|
||||||
|
**Q4 初步判断**:倾向 **方案 A**。ShopXO 原生防超卖机制比自建锁更可靠(DB 层面原子操作)。
|
||||||
|
|
||||||
|
### FrontendDev 初判(Q1-Q4 分析)
|
||||||
|
|
||||||
|
**Q1**:结论:**可行,但实现路径复杂。** 无现成批量 API,需要插件自管(Hook 隐藏)。SKU 数量 = 座位数(10000+)。
|
||||||
|
**Q2**:结论:**需要立即修复,推荐最小方案。**
|
||||||
|
**Q3**:结论:**低风险,但需实测确认。**
|
||||||
|
**Q4 推荐**:**方案 A(每个座位一个 SPEC/SKU)**。安全性+数据一致性优先。
|
||||||
|
|
||||||
|
### SecurityEngineer 初判(Q2/Q3/Q4)
|
||||||
|
|
||||||
|
**Q2**:依赖 Q1/Q4,标记为 blocked。
|
||||||
|
**Q3**:ThinkPHP View 层可能对 `$` 有变量插值行为,需要代码验证(Round 2 执行)。
|
||||||
|
**Q4**:初步倾向 **方案 A**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 各成员 Round 2 深入分析
|
||||||
|
|
||||||
|
### BackendArchitect Round 2 深入分析(Q1+Q2)
|
||||||
|
|
||||||
|
详细分析见 `docs/ROUND2_ANALYSIS.md`。
|
||||||
|
|
||||||
|
**Q1 结论:可行,但必须旁路 `GoodsSpecificationsInsert()`**
|
||||||
|
|
||||||
|
- `GoodsSpecificationsInsert()` 每次商品保存时 DELETE 所有现有 spec 后重建,10K+ 座位场景不可用
|
||||||
|
- 可行路径:**直接 SQL INSERT** 到 `sxo_goods_spec_type`、`sxo_goods_spec_base`、`sxo_goods_spec_value` 三表
|
||||||
|
- 关键代码:`BuyService.php:1677-1681` 的 `dec()` 机制 = MySQL 条件原子扣减 `UPDATE SET inventory = inventory - N WHERE inventory >= N`
|
||||||
|
- TOCTOU 窗口极小(选座模式并发低 + InnoDB 行锁),**推荐接受此风险**
|
||||||
|
- 性能:10000 座位 ≈ 3-4 秒(需分批 500 条/批提交)
|
||||||
|
|
||||||
|
**Q2 结论:推荐方案乙(最小修复集)**
|
||||||
|
|
||||||
|
- `UPDATE goods SET is_exist_many_spec=1 WHERE id=112`
|
||||||
|
- 写入 `$vr-` 规格维度到 `sxo_goods_spec_type`
|
||||||
|
- 幂等保护:票生成逻辑已有 `spec_base_id` 冗余
|
||||||
|
|
||||||
|
**Q4 初步推荐:方案 A**
|
||||||
|
|
||||||
|
- 原子性已验证(BuyService dec 机制)
|
||||||
|
- 数据完整性高(每个座位 inventory=1)
|
||||||
|
- 票务链路清晰(spec_base_id → 座位直接映射)
|
||||||
|
|
||||||
|
### SecurityEngineer Round 2 分析(Q3)
|
||||||
|
|
||||||
|
SecurityEngineer 在 Round 2 进行了 ThinkPHP View 层的 $vr- 前缀安全审计,结论:**无高危风险**。
|
||||||
|
|
||||||
|
### FrontendDev Round 2 深入分析(Q3+Q4)
|
||||||
|
|
||||||
|
**Q3 结论:$vr- 前缀安全** ✅
|
||||||
|
- ThinkPHP `{$var}` 默认做 HTML 转义,$vr- 不会被解析为 PHP 变量
|
||||||
|
- `|raw` 仅跳过 HTML 转义,不会执行变量插值
|
||||||
|
- ThinkPHP parseVar 正则对连字符 `-` 的处理会阻断 $vr- 的完整解析
|
||||||
|
- ShopXO spec name 存 DB 无过滤,但渲染层安全
|
||||||
|
|
||||||
|
**Q4 最终推荐:方案 A(每个座位一个 SPEC/SKU)—— 明确推荐**
|
||||||
|
|
||||||
|
**核心发现**:
|
||||||
|
1. 当前 `ticket_detail.html` submit() 是 Plan B 模式,`specBaseIdMap` 已声明但**未接入** submit 逻辑
|
||||||
|
2. ShopXO 购买流程从 `spec_base` 表读取库存并原子扣减
|
||||||
|
|
||||||
|
**方案 A vs B 最终对比**:
|
||||||
|
|
||||||
|
| 维度 | 方案 A(每座=SKU) | 方案 B(每 Zone=SKU) |
|
||||||
|
|------|-------------------|---------------------|
|
||||||
|
| **防超卖** | ShopXO 原生原子扣库存(stock=1),DB 层保证 | 自建 FOR UPDATE 锁,需自己写并发逻辑 |
|
||||||
|
| **实现复杂度** | 后端需批量生成 1 万+ SKU;前端 `submit()` 需改为逐座提交 | 后端简单;前端按 Zone 分组即可 |
|
||||||
|
| **多 Zone 混买** | 每座一行 goods_params,后端原子处理 | 前端分组但后端共享 Zone 库存,复杂度高 |
|
||||||
|
| **后台可维护性** | 10000+ SKU 行,但可 Hook 隐藏 | Zone 数量少,后台友好 |
|
||||||
|
| **调试/故障排查** | 每个 SKU 独立,可追溯 | 共享库存,出问题难以定位 |
|
||||||
|
| **与 ShopXO 生态** | 完全对齐,无缝集成 | 绕过 spec 校验,部分 ShopXO 功能失效 |
|
||||||
|
|
||||||
|
**Plan A 前端实现路径**:
|
||||||
|
|
||||||
|
关键修改:将 `submit()` 从"session-level 提交"改为"seat-level 逐座提交":
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Plan A: 每座一行 goods_params,逐座购买
|
||||||
|
this.selectedSeats.forEach(function(seat) {
|
||||||
|
var seatSpecBaseId = app.specBaseIdMap[seat.row + '_' + seat.col]?.spec_base_id;
|
||||||
|
// 如果 spec_base_id 存在,走 ShopXO 原生购买
|
||||||
|
// 否则走 Plan B 回退逻辑
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
`specBaseIdMap` 数据结构已就位(从后端 PHP 注入),前端只需接入即可。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 各成员 Round 3 最终推荐
|
||||||
|
|
||||||
|
### BackendArchitect Round 3 最终推荐(Q1+Q2+Q4)
|
||||||
|
|
||||||
|
**Q1 最终结论**:可行。必须旁路 `GoodsSpecificationsInsert()`,走**直接 SQL INSERT** 路径。性能:10000 座位 ≈ 3-4 秒(分批 500 条/批)。关键:`spec_base_id_map[seat_id] → actual_db_id` 映射必须在 INSERT 后即时重建。
|
||||||
|
|
||||||
|
**Q2 最终结论**:推荐**方案乙**(最小修复集):
|
||||||
|
1. `UPDATE sxo_goods SET is_exist_many_spec=1 WHERE id=112`
|
||||||
|
2. `INSERT $vr- spec_type`(场馆/分区/时段三行)
|
||||||
|
3. 幂等保护:`TicketService::issueTicket()` 中对 `spec_base_id=0` 做 fallback
|
||||||
|
|
||||||
|
**Q3 最终结论**(汇入 SecurityEngineer + FrontendDev 确认):低风险。ThinkPHP `{$var}` 默认 HTML 转义,`$vr-` 不会触发变量解析。
|
||||||
|
|
||||||
|
**Q4 最终推荐:方案 A**,理由汇总:
|
||||||
|
1. **ShopXO 原生原子防超卖**:`BuyService::dec()` = MySQL 条件原子扣减,无需自建锁
|
||||||
|
2. **TOCTOU 风险可接受**:选座模式并发窗口极小,InnoDB 行锁提供最后保护
|
||||||
|
3. **票务链路清晰**:`spec_base_id` 直接映射座位,票生成无需反向解析
|
||||||
|
4. **方案 B 优势不成立**:插件自管 SKU(Hook 隐藏),不走 ShopXO 后台,无"管理困难"问题
|
||||||
|
|
||||||
|
### FrontendDev Round 3 最终推荐(Q3+Q4)
|
||||||
|
|
||||||
|
三方一致推荐 **方案 A(每个座位一个 ShopXO SKU)**。
|
||||||
|
|
||||||
|
最终决策报告:`council-output/ARCHITECTURE_DECISION.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 行动项(优先级排序)
|
||||||
|
|
||||||
|
| 优先级 | 行动项 | 负责 |
|
||||||
|
|--------|--------|------|
|
||||||
|
| P0 | 创建 `SeatSkuService::BatchGenerate()` — 直接 SQL INSERT 批量生成 SKU(分批 500 条) | BackendArchitect |
|
||||||
|
| P0 | 执行 Q2 最小修复集:`UPDATE is_exist_many_spec=1` + `INSERT $vr- spec_type` | BackendArchitect |
|
||||||
|
| P1 | `TicketService::issueTicket()` 添加 `spec_base_id=0` 幂等保护 | BackendArchitect |
|
||||||
|
| P1 | 重构 `ticket_detail.html` submit():接入 `specBaseIdMap`,改为 seat-level 逐座提交 | FrontendDev |
|
||||||
|
| P2 | 实现 `loadSoldSeats()`:查询各 seat spec_base 的库存状态 | FrontendDev |
|
||||||
|
| P2 | Hook 隐藏插件专用 SKU(隔离 ShopXO 原生规格管理页) | FrontendDev |
|
||||||
|
| P3 | 设计插件独立 SKU 管理页面 | FrontendDev |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 共识投票
|
## 共识投票
|
||||||
|
|
||||||
[CONSENSUS: NO] — 本轮仅完成研究讨论,实际执行待后续阶段
|
| 成员 | CONSENSUS |
|
||||||
|
|------|-----------|
|
||||||
|
| BackendArchitect | `[CONSENSUS: YES]` — 推荐方案 A,Round 2/3 分析完成 |
|
||||||
|
| SecurityEngineer | `[CONSENSUS: YES]` — $vr- 前缀低风险,方案 A 推荐 |
|
||||||
|
| FrontendDev | `[CONSENSUS: YES]` — 方案 A 推荐,前端配合方案清晰 |
|
||||||
|
|
||||||
|
**全票通过:采纳方案 A**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Round 3 安全审计结果(SecurityEngineer)
|
## Round 3 安全审计结果(保留,仅供参考)
|
||||||
|
|
||||||
### Task S1 — Admin 鉴权覆盖完整性审查 ✅ 验证通过
|
### Task S1 — Admin 鉴权覆盖完整性审查 ✅ 验证通过
|
||||||
|
|
||||||
**审查方法**:读取 main 合并结果 + ShopXO Common.php 源码
|
|
||||||
|
|
||||||
#### 鉴权链分析
|
|
||||||
|
|
||||||
```
|
|
||||||
ThinkPHP 路由 → admin.php (module=admin)
|
|
||||||
→ Common::__construct()
|
|
||||||
1. AdminService::LoginInfo() ← 填充 $this->admin(从 session)
|
|
||||||
2. AdminPowerService::PowerMenuInit()
|
|
||||||
3. ViewInit()
|
|
||||||
→ 插件控制器(如 Ticket/list)
|
|
||||||
→ Base extends Common → parent::__construct()
|
|
||||||
→ 完整继承上述 3 步
|
|
||||||
```
|
|
||||||
|
|
||||||
**结论**:
|
|
||||||
- ✅ `Base extends Common` — 登录检查和权限菜单已正确初始化
|
|
||||||
- ✅ 所有子控制器(SeatTemplate / Ticket / Verifier / Verification)通过 `extends Base` 自动获得鉴权
|
|
||||||
- ✅ BackendArchitect 的 P0 修复(Base extends Common)已合并到 main,**Task S1 验证通过**
|
|
||||||
|
|
||||||
**Defense-in-Depth 建议**(非阻塞):
|
|
||||||
> 在 `Base::__construct()` 末尾显式调用 `$this->IsLogin()`,确保即使未来有人重写 `__construct()` 也不会绕过鉴权
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task S2 — SQL 注入风险审计 ✅ 无注入风险
|
### Task S2 — SQL 注入风险审计 ✅ 无注入风险
|
||||||
|
|
||||||
**审查范围**:Phase 2 所有控制器 + TicketService
|
|
||||||
|
|
||||||
| 控制器 | 查询点 | 输入处理 | 结论 |
|
|
||||||
|--------|--------|----------|------|
|
|
||||||
| SeatTemplate::list | `name` like, `status` | `null` + `intval()` | ✅ 安全 |
|
|
||||||
| Ticket::list | `keywords` multi-field like | `trim()` + 查询构造器绑定 | ✅ 安全 |
|
|
||||||
| Ticket::verify | `ticket_code`, `verifier_id` | `trim()` + `intval()` | ✅ 安全 |
|
|
||||||
| Verification::list | date range | `strtotime()` 绑定 | ✅ 安全 |
|
|
||||||
|
|
||||||
**无原始 SQL 执行,无字符串拼接注入。**
|
|
||||||
|
|
||||||
⚠️ **P1 Bug(已修复)**:`Verifier.php:45` ThinkPHP `column()` 不支持直接传 CONCAT SQL,已改用 `select()` + PHP 拼接
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task S3 — XSS / CSRF 防护检查 ✅ 通过
|
### Task S3 — XSS / CSRF 防护检查 ✅ 通过
|
||||||
|
|
||||||
| 方面 | 状态 |
|
|
||||||
|------|------|
|
|
||||||
| CSRF Token (POST) | ✅ ShopXO 保护 |
|
|
||||||
| XSS(存储型) | ✅ 低风险(admin 上下文) |
|
|
||||||
| 关键操作 guard | ✅ `delete()` / `verify()` / `export()` 均有 `IS_AJAX_POST` 检查 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task S5 — IDOR 水平越权检查 ✅ 通过
|
### Task S5 — IDOR 水平越权检查 ✅ 通过
|
||||||
|
### Task S4 — 敏感操作审计日志设计 ✅ 设计完成
|
||||||
Admin 上下文(所有控制器需登录 admin + 插件菜单权限)下访问控制正确。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 安全任务更新
|
|
||||||
|
|
||||||
- [x] **Task S1** — Admin 鉴权覆盖完整性 — `[Done: council/SecurityEngineer]`
|
|
||||||
- [x] **Task S2** — SQL 注入风险审计 — `[Done: council/SecurityEngineer]`
|
|
||||||
- [x] **Task S3** — XSS / CSRF 防护检查 — `[Done: council/SecurityEngineer]`
|
|
||||||
- [x] **Task S5** — IDOR / 水平越权测试用例编写 — `[Done: council/SecurityEngineer]`
|
|
||||||
- [x] **Task S4** — 敏感操作审计日志设计 — `[Done: council/BackendArchitect]`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Task S4 — 敏感操作审计日志设计 ✅ 设计完成
|
|
||||||
|
|
||||||
**表结构**:`vr_audit_log` 已在 `EventListener.php` 中定义(第99-121行),字段如下:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `action` | VARCHAR(60) | 操作类型:verify/export/refund/disable/enable/delete |
|
|
||||||
| `operator_id` | BIGINT | 操作用户ID(admin) |
|
|
||||||
| `operator_name` | VARCHAR(90) | 操作用户名(冗余) |
|
|
||||||
| `target_type` | VARCHAR(60) | 对象类型:ticket/verifier/seat_template |
|
|
||||||
| `target_id` | BIGINT | 对象ID |
|
|
||||||
| `target_desc` | VARCHAR(255) | 对象描述(冗余,便于查询) |
|
|
||||||
| `client_ip` | VARCHAR(45) | 客户端IP(支持IPv6) |
|
|
||||||
| `user_agent` | VARCHAR(512) | User-Agent |
|
|
||||||
| `request_id` | VARCHAR(64) | 请求追踪ID(UUID) |
|
|
||||||
| `extra` | LONGTEXT | 附加数据JSON(变更前后快照) |
|
|
||||||
| `created_at` | INT UNSIGNED | 操作时间戳 |
|
|
||||||
|
|
||||||
**索引**:`idx_action` / `idx_operator_id` / `idx_target(target_type,target_id)` / `idx_created_at`
|
|
||||||
|
|
||||||
**AuditService 接口设计**(待 Phase 3 实现):
|
|
||||||
|
|
||||||
```php
|
|
||||||
// service/AuditService.php
|
|
||||||
class AuditService
|
|
||||||
{
|
|
||||||
// 记录操作
|
|
||||||
public static function log($action, $target_type, $target_id, $extra = []);
|
|
||||||
|
|
||||||
// 自动从 Common 控制器获取 admin 上下文
|
|
||||||
private static function getAdminContext();
|
|
||||||
|
|
||||||
// 生成请求追踪ID
|
|
||||||
private static function makeRequestId();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**集成点**(Phase 3 实现):
|
|
||||||
|
|
||||||
| 控制器 | 方法 | action 值 | extra 快照 |
|
|
||||||
|--------|------|-----------|-----------|
|
|
||||||
| Ticket | `verify()` | `verify` | verify_status=0→1, verifier_id |
|
|
||||||
| Ticket | `export()` | `export` | goods_id, count |
|
|
||||||
| Ticket | `refund()` | `refund` | verify_status=0→2 |
|
|
||||||
| Verifier | `delete()` | `disable_verifier` | verifier_id, name |
|
|
||||||
| Verifier | `save()` | `enable_verifier` | verifier_id, name |
|
|
||||||
| SeatTemplate | `save()` | `edit_template` | template_id, name |
|
|
||||||
| SeatTemplate | `delete()` | `disable_template` | template_id, name |
|
|
||||||
|
|
||||||
> **防篡改策略**:表为 append-only,不提供 UPDATE/DELETE 接口;`operator_name` 冗余存储防止审计日志与 admin 表不同步时丢失身份。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## BackendArchitect Round 4 — P1 Bug Fix
|
|
||||||
|
|
||||||
### Verification.php:55 — `column()` 多字段映射 Bug(P1 已修复)
|
|
||||||
|
|
||||||
**问题**:`ThinkPHP column()` 不支持多字段映射,`column('seat_info,real_name,goods_id', 'id')` 返回结构与代码预期不符,导致核销记录列表页 `seat_info` / `real_name` / `goods_id` 为空。
|
|
||||||
|
|
||||||
**修复**:改用 `select()` + PHP foreach 拼接为 `$tickets[id] => row` 关联数组。
|
|
||||||
|
|
||||||
**文件**:`admin/controller/Verification.php` 第51-63行
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## FrontendDev Round 4 — P1 Bug Fix
|
|
||||||
|
|
||||||
### ticket/list.html — 导出按钮 IS_AJAX_POST 不匹配 Bug(P1)
|
|
||||||
|
|
||||||
**问题**:`ticket/list.html:35` 导出按钮为 `<a>` 链接(GET 请求),但 `Ticket.php:export()` 要求 `IS_AJAX_POST`,导致点击"导出CSV"按钮返回"非法请求"错误。
|
|
||||||
|
|
||||||
**修复**:
|
|
||||||
- 视图:`ticket/list.html` 第35行 → `<a>` 链接改为 `<button type="button" id="export-btn">`,JS 动态创建 `<form method="post">` 提交
|
|
||||||
- 控制器:`Ticket.php:export()` 保留 `IS_AJAX_POST` 检查不变(保持安全),注释更新说明 POST-only 设计
|
|
||||||
|
|
||||||
**文件**:`admin/view/ticket/list.html` 第35行 + 第92-98行
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 共识投票
|
|
||||||
|
|
||||||
[CONSENSUS: YES] — 所有 Phase 2 安全任务 S1-S5 全部完成;前端视图全部就位;P1 导出按钮 bug 已修复;Task S4(审计日志设计)完成。Phase 2 收尾。
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue