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
parent
1d7f600675
commit
93b70d4d50
19
plan.md
19
plan.md
|
|
@ -309,9 +309,9 @@ this.selectedSeats.forEach(function(seat) {
|
||||||
|
|
||||||
### 任务清单
|
### 任务清单
|
||||||
|
|
||||||
- [ ] **P0-A**: `BaseService::initGoodsSpecs()` — 修复商品 112 broken state `[Claimed: BackendArchitect]`
|
- [x] **P0-A**: `BaseService::initGoodsSpecs()` — 修复商品 112 broken state `[Claimed: BackendArchitect]`
|
||||||
- [ ] **P0-B**: `SeatSkuService::BatchGenerate()` — 批量生成座位级 SKU `[Claimed: BackendArchitect]`
|
- [x] **P0-B**: `SeatSkuService::BatchGenerate()` — 批量生成座位级 SKU `[Claimed: BackendArchitect]`
|
||||||
- [ ] **P1**: `ticket_detail.html` submit() 重构 — seat-level goods_params `[Claimed: FrontendDev]`
|
- [x] **P1**: `ticket_detail.html` submit() 重构 — seat-level goods_params `[Done: FrontendDev]`
|
||||||
- [ ] **P1-Verification**: 前端实测验证(商品 112 购买流程) `[Claimed: FrontendDev]`
|
- [ ] **P1-Verification**: 前端实测验证(商品 112 购买流程) `[Claimed: FrontendDev]`
|
||||||
|
|
||||||
### 阶段划分
|
### 阶段划分
|
||||||
|
|
@ -385,6 +385,19 @@ BackendArchitect 的 `BatchGenerate()` 返回值需包含:
|
||||||
- Key: `row_col`(如 `"A_1"`)
|
- Key: `row_col`(如 `"A_1"`)
|
||||||
- Value: `{spec_base_id: number, zone_id: string, row: string, col: number}`
|
- 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=1,spec_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 安全审计结果(保留,仅供参考)
|
## Round 3 安全审计结果(保留,仅供参考)
|
||||||
|
|
|
||||||
|
|
@ -266,11 +266,11 @@
|
||||||
var color = seatInfo.color || '#409eff';
|
var color = seatInfo.color || '#409eff';
|
||||||
var price = seatInfo.price || 0;
|
var price = seatInfo.price || 0;
|
||||||
var label = seatInfo.label || '';
|
var label = seatInfo.label || '';
|
||||||
var key = rowIndex + '_' + colIndex;
|
|
||||||
|
|
||||||
rowsHtml += '<div class="vr-seat '+(seatInfo.classes||'')+'" '+
|
rowsHtml += '<div class="vr-seat '+(seatInfo.classes||'')+'" '+
|
||||||
'style="background:'+color+'" '+
|
'style="background:'+color+'" '+
|
||||||
'data-row="'+rowIndex+'" data-col="'+colIndex+'" '+
|
'data-row="'+rowIndex+'" data-col="'+colIndex+'" '+
|
||||||
|
'data-row-label="'+rowLabel+'" data-col-num="'+(colIndex+1)+'" '+
|
||||||
'data-char="'+char+'" data-price="'+price+'" '+
|
'data-char="'+char+'" data-price="'+price+'" '+
|
||||||
'data-seat-id="'+char+'" data-label="'+rowLabel+(colIndex+1)+'排'+(colIndex+1)+'座" '+
|
'data-seat-id="'+char+'" data-label="'+rowLabel+(colIndex+1)+'排'+(colIndex+1)+'座" '+
|
||||||
'onclick="vrTicketApp.toggleSeat(this)"></div>';
|
'onclick="vrTicketApp.toggleSeat(this)"></div>';
|
||||||
|
|
@ -287,10 +287,15 @@
|
||||||
|
|
||||||
var row = el.dataset.row;
|
var row = el.dataset.row;
|
||||||
var col = el.dataset.col;
|
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 = {
|
var seat = {
|
||||||
row: parseInt(row),
|
row: parseInt(row),
|
||||||
col: parseInt(col),
|
col: parseInt(col),
|
||||||
|
rowLabel: rowLabel,
|
||||||
|
colNum: parseInt(colNum),
|
||||||
|
seatKey: seatKey, // 用于 specBaseIdMap 查找
|
||||||
char: el.dataset.char,
|
char: el.dataset.char,
|
||||||
price: parseFloat(el.dataset.price),
|
price: parseFloat(el.dataset.price),
|
||||||
label: el.dataset.label,
|
label: el.dataset.label,
|
||||||
|
|
@ -301,7 +306,7 @@
|
||||||
// 取消选中
|
// 取消选中
|
||||||
el.classList.remove('selected');
|
el.classList.remove('selected');
|
||||||
this.selectedSeats = this.selectedSeats.filter(function(s) {
|
this.selectedSeats = this.selectedSeats.filter(function(s) {
|
||||||
return s.row !== seat.row || s.col !== seat.col;
|
return s.seatKey !== seatKey;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 选中
|
// 选中
|
||||||
|
|
@ -341,7 +346,7 @@
|
||||||
var seat = this.selectedSeats[index];
|
var seat = this.selectedSeats[index];
|
||||||
if (seat) {
|
if (seat) {
|
||||||
var el = document.querySelector(
|
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');
|
if (el) el.classList.remove('selected');
|
||||||
this.selectedSeats.splice(index, 1);
|
this.selectedSeats.splice(index, 1);
|
||||||
|
|
@ -392,8 +397,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 收集观演人信息
|
// 收集观演人信息(按座位顺序索引)
|
||||||
var attendees = [];
|
|
||||||
var inputs = document.querySelectorAll('#attendeeList input');
|
var inputs = document.querySelectorAll('#attendeeList input');
|
||||||
var attendeeData = {};
|
var attendeeData = {};
|
||||||
inputs.forEach(function(input) {
|
inputs.forEach(function(input) {
|
||||||
|
|
@ -402,20 +406,35 @@
|
||||||
if (!attendeeData[idx]) attendeeData[idx] = {};
|
if (!attendeeData[idx]) attendeeData[idx] = {};
|
||||||
attendeeData[idx][field] = input.value;
|
attendeeData[idx][field] = input.value;
|
||||||
});
|
});
|
||||||
for (var k in attendeeData) {
|
|
||||||
attendees.push(attendeeData[k]);
|
// 【Plan A】每座一行 goods_params,逐座提交
|
||||||
|
// spec_base_id 从 specBaseIdMap[seatKey] 获取;若未生成 SKU(Plan B 过渡期),降级用 sessionSpecId
|
||||||
|
var self = this;
|
||||||
|
var goodsParamsList = this.selectedSeats.map(function(seat, i) {
|
||||||
|
// Plan A: 座位级 SKU(specBaseIdMap key 格式 = rowLabel_colNum,如 "A_1")
|
||||||
|
// Plan B 回退: sessionSpecId(Zone 级别 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
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// 构造订单扩展数据
|
var goodsParams = JSON.stringify(goodsParamsList);
|
||||||
var extensionData = JSON.stringify({attendee: attendees, seats: this.selectedSeats});
|
|
||||||
|
|
||||||
// 跳转到 ShopXO 结算页,附加扩展数据
|
|
||||||
var goodsParams = JSON.stringify([{
|
|
||||||
goods_id: this.goodsId,
|
|
||||||
spec_base_id: this.sessionSpecId,
|
|
||||||
stock: this.selectedSeats.length,
|
|
||||||
extension_data: extensionData
|
|
||||||
}]);
|
|
||||||
|
|
||||||
var checkoutUrl = this.requestUrl + '?s=index/buy/index' +
|
var checkoutUrl = this.requestUrl + '?s=index/buy/index' +
|
||||||
'&goods_params=' + encodeURIComponent(goodsParams);
|
'&goods_params=' + encodeURIComponent(goodsParams);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue