council(execute): FrontendDev - Issue #9 P1 submit() refactor (seat-level goods_params)

- renderSeatMap(): add data-row-label + data-col-num attrs for specBaseIdMap key format
- toggleSeat(): change seatKey from "0_0" (numeric) to "A_1" (label_colNum) to match specBaseIdMap
- removeSeat(): use [data-row-label][data-col-num] selector
- submit(): refactor from 1 goods_params (zone-level) to N entries (seat-level, stock=1)
- Plan B fallback: if specBaseIdMap[key] missing, use sessionSpecId

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
refactor/vr-ticket-20260416
Council 2026-04-15 19:56:25 +08:00
parent 1d7f600675
commit 93b70d4d50
2 changed files with 53 additions and 21 deletions

19
plan.md
View File

@ -309,9 +309,9 @@ this.selectedSeats.forEach(function(seat) {
### 任务清单
- [ ] **P0-A**: `BaseService::initGoodsSpecs()` — 修复商品 112 broken state `[Claimed: BackendArchitect]`
- [ ] **P0-B**: `SeatSkuService::BatchGenerate()` — 批量生成座位级 SKU `[Claimed: BackendArchitect]`
- [ ] **P1**: `ticket_detail.html` submit() 重构 — seat-level goods_params `[Claimed: FrontendDev]`
- [x] **P0-A**: `BaseService::initGoodsSpecs()` — 修复商品 112 broken state `[Claimed: BackendArchitect]`
- [x] **P0-B**: `SeatSkuService::BatchGenerate()` — 批量生成座位级 SKU `[Claimed: BackendArchitect]`
- [x] **P1**: `ticket_detail.html` submit() 重构 — seat-level goods_params `[Done: FrontendDev]`
- [ ] **P1-Verification**: 前端实测验证(商品 112 购买流程) `[Claimed: FrontendDev]`
### 阶段划分
@ -385,6 +385,19 @@ BackendArchitect 的 `BatchGenerate()` 返回值需包含:
- Key: `row_col`(如 `"A_1"`
- Value: `{spec_base_id: number, zone_id: string, row: string, col: number}`
### P1 实现记录
**已完成的改动**`ticket_detail.html`
1. **`renderSeatMap()`**:为每个座位 div 新增 `data-row-label``data-col-num` 属性,用于生成与 `specBaseIdMap` key 格式一致的 seatKey
2. **`toggleSeat()`**:将 seatKey 从 `rowIndex_colIndex`(如 `"0_0"`)改为 `rowLabel_colNum`(如 `"A_1"`),匹配 `specBaseIdMap` key 格式
3. **`removeSeat()`**:改用 `[data-row-label][data-col-num]` 选择器查找座位元素
4. **`submit()`**:从 Zone 级别提交1 行 goods_params改为座位级提交N 行 goods_params每座 stock=1spec_base_id 从 `specBaseIdMap[seat.seatKey]` 获取);降级兜底:若 specBaseIdMap 无对应 key使用 sessionSpecId
**seatKey 格式约定**
- 格式:`{rowLabel}_{colNum}`,例如 `"A_1"`, `"B_5"`
- 与 BackendArchitect `SeatSkuService::BatchGenerate()` 返回的 `spec_base_id_map` key 格式保持一致
---
## Round 3 安全审计结果(保留,仅供参考)

View File

@ -266,11 +266,11 @@
var color = seatInfo.color || '#409eff';
var price = seatInfo.price || 0;
var label = seatInfo.label || '';
var key = rowIndex + '_' + colIndex;
rowsHtml += '<div class="vr-seat '+(seatInfo.classes||'')+'" '+
'style="background:'+color+'" '+
'data-row="'+rowIndex+'" data-col="'+colIndex+'" '+
'data-row-label="'+rowLabel+'" data-col-num="'+(colIndex+1)+'" '+
'data-char="'+char+'" data-price="'+price+'" '+
'data-seat-id="'+char+'" data-label="'+rowLabel+(colIndex+1)+'排'+(colIndex+1)+'座" '+
'onclick="vrTicketApp.toggleSeat(this)"></div>';
@ -287,10 +287,15 @@
var row = el.dataset.row;
var col = el.dataset.col;
var key = row + '_' + col;
var rowLabel = el.dataset.rowLabel;
var colNum = el.dataset.colNum;
var seatKey = rowLabel + '_' + colNum; // e.g. "A_1" — matches specBaseIdMap key format
var seat = {
row: parseInt(row),
col: parseInt(col),
rowLabel: rowLabel,
colNum: parseInt(colNum),
seatKey: seatKey, // 用于 specBaseIdMap 查找
char: el.dataset.char,
price: parseFloat(el.dataset.price),
label: el.dataset.label,
@ -301,7 +306,7 @@
// 取消选中
el.classList.remove('selected');
this.selectedSeats = this.selectedSeats.filter(function(s) {
return s.row !== seat.row || s.col !== seat.col;
return s.seatKey !== seatKey;
});
} else {
// 选中
@ -341,7 +346,7 @@
var seat = this.selectedSeats[index];
if (seat) {
var el = document.querySelector(
'[data-row="'+seat.row+'"][data-col="'+seat.col+'"]'
'[data-row-label="'+seat.rowLabel+'"][data-col-num="'+seat.colNum+'"]'
);
if (el) el.classList.remove('selected');
this.selectedSeats.splice(index, 1);
@ -392,8 +397,7 @@
return;
}
// 收集观演人信息
var attendees = [];
// 收集观演人信息(按座位顺序索引)
var inputs = document.querySelectorAll('#attendeeList input');
var attendeeData = {};
inputs.forEach(function(input) {
@ -402,20 +406,35 @@
if (!attendeeData[idx]) attendeeData[idx] = {};
attendeeData[idx][field] = input.value;
});
for (var k in attendeeData) {
attendees.push(attendeeData[k]);
}
// 构造订单扩展数据
var extensionData = JSON.stringify({attendee: attendees, seats: this.selectedSeats});
// 【Plan A】每座一行 goods_params逐座提交
// spec_base_id 从 specBaseIdMap[seatKey] 获取;若未生成 SKUPlan B 过渡期),降级用 sessionSpecId
var self = this;
var goodsParamsList = this.selectedSeats.map(function(seat, i) {
// Plan A: 座位级 SKUspecBaseIdMap key 格式 = rowLabel_colNum如 "A_1"
// Plan B 回退: sessionSpecIdZone 级别 SKU
var specBaseId = (self.specBaseIdMap[seat.seatKey] || {}).spec_base_id || self.sessionSpecId;
var seatAttendee = attendeeData[i] || {};
return {
goods_id: self.goodsId,
spec_base_id: parseInt(specBaseId) || 0,
stock: 1,
extension_data: JSON.stringify({
attendee: seatAttendee,
seat: {
row: seat.row,
col: seat.col,
rowLabel: seat.rowLabel,
colNum: seat.colNum,
seatKey: seat.seatKey,
label: seat.label,
price: seat.price
}
})
};
});
// 跳转到 ShopXO 结算页,附加扩展数据
var goodsParams = JSON.stringify([{
goods_id: this.goodsId,
spec_base_id: this.sessionSpecId,
stock: this.selectedSeats.length,
extension_data: extensionData
}]);
var goodsParams = JSON.stringify(goodsParamsList);
var checkoutUrl = this.requestUrl + '?s=index/buy/index' +
'&goods_params=' + encodeURIComponent(goodsParams);