Skip to content

feat(tixcraft): add ticket_price_auto_select mode for multi-price rows#320

Closed
pengumina wants to merge 1 commit into
bouob:mainfrom
pengumina:feat/tixcraft-multi-price-row
Closed

feat(tixcraft): add ticket_price_auto_select mode for multi-price rows#320
pengumina wants to merge 1 commit into
bouob:mainfrom
pengumina:feat/tixcraft-multi-price-row

Conversation

@pengumina
Copy link
Copy Markdown

@pengumina pengumina commented May 13, 2026

變更摘要

拓元當「同一區域包含多個票價」時(例如 VIP 區分成「全票 6,800」與「全票+福利兌換券 6,880」),機器人會因為關鍵字未匹配而停擺。

設定頁新增「票種排序方式」下拉(位於「區域自動遞補」下方,僅 tixcraft 家族顯示),6 種模式:

  • from top to bottom(預設)/ from bottom to top / center / random
  • high price first / low price first — 解析票種名稱最後一段數字作為價格

獨立於 area_auto_fallback(訂購頁的用戶已經提交區域選擇,停擺不該是選項)。

變更類型

  • ✨ 新功能 (feat)
  • 🐛 Bug 修復 (fix)
  • ♻️ 重構 (refactor)
  • 📝 文件更新 (docs)
  • 🔧 維護 (chore)

影響平台

  • TixCraft
  • KKTIX
  • iBon
  • TicketPlus
  • 其他:___
  • 跨平台(共用功能)

檢查清單

  • 已測試功能正常(真實 DOM 驗證 + 設定頁 round-trip)
  • 無敏感資訊(密碼、Cookie、API key)
  • .py 檔案中沒有使用 emoji

相關 Issue

Closes #309
Related to #38 — 本 PR 解決「選哪個票種」的部分;#38 另外提到的「每個票種各買幾張」不在本 PR 範圍。

When an area has multiple ticket-price rows (e.g. "全票+福利兌換券 6,880"
vs "全票 6,800") and no area keyword matches, the bot previously stalled
and reloaded forever. Adds a deterministic fallback with 6 modes:
from top/bottom, center, random, high/low price first. Independent of
area_auto_fallback — ticket-page user has already committed to an area.

Also fixes a latent captcha pre-check bug exposed by the new picker:
querySelector('.mobile-select') only saw the FIRST row, so when the
picker filled the second row, pre-check thought qty was unset and reset
the first row too → 2× qty → "您共選擇了 N 張" alert. Three captcha
sites now use querySelectorAll.
@pengumina pengumina requested a review from bouob as a code owner May 13, 2026 15:17
@bouob
Copy link
Copy Markdown
Owner

bouob commented May 14, 2026

PR Review 報告

PR #320feat(tixcraft): add ticket_price_auto_select mode for multi-price rows
作者@pengumina
日期:2026-05-14


總結評語

這是一個設計思路清晰的功能 PR,解決了拓元訂購頁因多票價並列導致機器人卡住的真實問題。程式碼結構乾淨、helper function 切分合理、設定遷移完整、UI 說明文案詳盡。整體品質明顯高於一般 contributor PR 的水準。

但有 兩個需要確認或修正的問題,以及若干建議事項,請作者在合併前確認。


功能設計分析

6 種模式的合理性

6 種模式設計合理,涵蓋位置導向(from top to bottom / from bottom to top / center / random)與價格導向(high price first / low price first),能應對絕大多數使用情境。

center 的 floor(N/2) 語義(偶數偏後)已在 help-content.js FAQ 中說明,這個 FAQ 補充做得非常好。

價格解析的健壯性

_parse_ticket_price 函式使用 r"\d[\d,]*" 取最後一段連續數字串作為價格,並以 replace(",", "") 清除千分位逗號。此方法適用以下拓元常見格式:

票種名稱 解析結果
全票 6,800 6800
限18+ 5,800 5800(前綴 18 被正確排除)
已售出 10000 6,880 6880(取最後段)

需留意以下邊界案例:

  1. 含小數點格式1,200.50 會被 regex 拆為 1,20050,最後取 50(錯誤)。目前拓元不使用小數票價,但若未來格式有變,會靜默失敗(退回 DOM 順序)。可接受,但建議在 PR 說明中加一句說明。

  2. 尾部含備注數字:例如 6,800/限量100張,最後段為 100,會把限量數解析為票價。若拓元發生此格式,排序會出錯。Fallback 機制(退回 DOM 順序)能保底,但使用者看不到告警。


重要問題

問題一(討論點):area_auto_fallback=false 嚴格模式行為被靜默移除

這是本 PR 最需要與維護者確認的變更。

修改前的舊邏輯(nodriver_tixcraft_assign_ticket_number 函式中):

# 舊邏輯(約第 2227 行附近)
if not matched_ticket:
    if area_keyword_array and not area_auto_fallback:
        # 嚴格模式:有關鍵字但未匹配,且 fallback 關閉 → 回傳 False,機器人停擺
        return False, None, None

修改後(PR 新邏輯):

if not matched_ticket:
    # area_auto_fallback 完全被忽略
    matched_ticket = _pick_ticket_by_price_mode(valid_ticket_types, ticket_price_mode)

換言之,原本設定了關鍵字且關閉 area_auto_fallback 的使用者,現在會在訂購頁被自動選票,而非停擺。PR 的說明有提到「獨立於 area_auto_fallback」,PR description 也有說明理由(「訂購頁的用戶已提交區域,停擺不是選項」),邏輯上是合理的。

但這是無聲的行為破壞性變更,影響所有同時設定區域關鍵字且關閉 area_auto_fallback 的使用者。請確認:

  • 這是刻意設計,非疏漏?
  • 是否需要在 CHANGELOG 或 Release Notes 中明確告知此行為改變?

問題二(必修):area_auto_fallback 變數在函式中成為死變數

nodriver_tixcraft_assign_ticket_number 函式第 2068 行(本地對應行):

area_auto_fallback = config_dict.get('area_auto_fallback', False)

修改後,這個變數在整個函式中不再被任何地方讀取(舊的 if area_keyword_array and not area_auto_fallback: 分支已被移除)。這是一個懸置賦值,會讓後續維護者困惑。

建議:移除此行,或若要保留嚴格模式的門,在 _pick_ticket_by_price_mode 呼叫前加上條件判斷,讓 area_auto_fallback=false 仍可阻止自動選票(視設計意圖而定)。


程式碼品質

符合規範之處

  • DebugLogger 使用正確:所有新增除錯輸出皆透過 debug.log(...) 輸出,無任何 print() 呼叫。
  • 命名慣例_parse_ticket_price_pick_ticket_by_price_mode 使用 Python 私有函式慣例(前置底線),符合 code-boundaries.md 的精神。
  • 函式大小_parse_ticket_price(15 行)、_pick_ticket_by_price_mode(18 行),完全符合 platforms/*.py 的 50 行建議上限。
  • util.py 正確複用_pick_ticket_by_price_mode 對 4 種非價格模式呼叫 util.get_target_item_from_matched_list(),沒有另行重新實作,符合「先搜尋 util.py」的規範。
  • re 模組:tixcraft.py 檔案頂端已有 import re(第 13 行),PR 不需要新增。
  • 設定遷移migrate_config 已更新,ticket_price_auto_select 加入了遷移清單。
  • captcha/form_ready 的 JS 修正:將 querySelector 改為 querySelectorAll 對多票種區域是正確且必要的修正,做得好。

需要改進之處

可觀測性問題:_pick_ticket_by_price_mode 的 fallback 缺少 debug 輸出

當任意票種名稱無法解析出數字時,目前的程式碼:

if price is None:
    # Any unparseable row → can't sort by price; fall back to DOM order.
    return valid_ticket_types[0]

此分支靜默地退回 DOM 順序,但使用者明確選擇了 high price firstlow price first。若沒有 log,使用者會不知道為什麼沒有按預期的價格排序選票。

建議:在呼叫端(fallback 分支選票後)補一行 debug log,例如:

debug.log(f"[TICKET SELECT] Price parse failed for '{t.get('name', '')}', falling back to DOM order")

由於 _pick_ticket_by_price_mode 是純輔助函式,不持有 debug 物件,建議在呼叫端(nodriver_tixcraft_assign_ticket_number 中)記錄,或讓 _pick_ticket_by_price_mode 回傳一個旗標讓呼叫端決定是否 log。


設定 UI

符合規範之處

  • settings.html 的下拉使用 data-under="tixcraft" 屬性,符合既有 UI 僅在 tixcraft 家族顯示的模式。
  • help-content.jsticket_price_select_mode 的說明文案結構(title / short / detail / faq / link)與既有條目(如 area_auto_fallback)格式完全一致。
  • settings.js 的讀取(load_settins_to_form)和儲存(save_changes_to_dict)邏輯對稱,處理了 settings.ticket_price_auto_select 不存在時的預設值。

小問題

settings.py 的預設值與既有設定不一致

ticket_price_auto_select.mode 的預設值為 CONST_FROM_TOP_TO_BOTTOM"from top to bottom"),但專案中同性質的 area_auto_select.modedate_auto_select.mode 皆預設為 CONST_SELECT_ORDER_DEFAULT(等於 CONST_RANDOM):

# settings.py,第 121、125 行
config_dict["date_auto_select"]["mode"] = CONST_SELECT_ORDER_DEFAULT   # RANDOM
config_dict["area_auto_select"]["mode"] = CONST_SELECT_ORDER_DEFAULT    # RANDOM
config_dict["ticket_price_auto_select"]["mode"] = CONST_FROM_TOP_TO_BOTTOM  # 不一致

預設確定性選擇(from top to bottom)對票種選擇來說是合理的(比隨機選更可預期),但這個不一致值得一個明確說明。若刻意如此,請在 PR 說明或 commit message 中備注。


平台影響範圍確認

PR 說明只勾選了 TixCraft,但 nodriver_tixcraft_assign_ticket_number 函式的頂部說明(第 2031 行)包含 indievox,函式也被 nodriver_tixcraft_ticket_main 呼叫,而後者服務 tixcraft / ticketmaster / teamear / indievox 整個家族。

請確認:ticket_price_auto_select.mode 的行為在 ticketmaster 與 indievox 頁面上是否也如預期?(Ticketmaster 的 captcha 確認頁有特別處理,見現有 isTicketmaster 判斷。)


建議改進(摘要)

優先序 類別 建議
必須確認 行為變更 area_auto_fallback=false 嚴格模式移除是否刻意,是否需要 CHANGELOG 告知
必修 程式碼 移除 area_auto_fallback 死變數賦值
建議 可觀測性 在 price fallback 分支補 debug log
建議 設定 說明 ticket_price_auto_select.mode 預設值為何不跟隨 CONST_SELECT_ORDER_DEFAULT
建議(後續) 測試 依 testing.md 規範,_parse_ticket_price 是純函式,適合在 tests/unit/test_tixcraft_price.py 加入單元測試,至少涵蓋 FAQ 中的三個範例與 fallback 案例

潛在 Edge Case 彙整

  1. 任意票種無數字時 fallback 無 log(上文已述)
  2. 小數點票價1,200.50 → regex 取 50(錯誤);實際影響低,但若未來拓元格式調整則靜默出錯
  3. 票種名稱含尾部備注數字:如 6,800/限量100張 → 取 100(錯誤票價)
  4. 票種名稱為空字串時_parse_ticket_price("") 直接回傳 None,觸發 DOM 順序 fallback,行為正確
  5. 全部票種均無數字時:每一個都觸發 return valid_ticket_types[0],最終取第一個票種,行為合理,但無 log

結語

感謝 @pengumina 提交這個功能。這個 PR 補上了拓元多票價場景下的一個真實缺口,程式碼品質整體令人滿意,help-content 的 FAQ 撰寫尤為用心。主要請確認 area_auto_fallback 嚴格模式移除的設計意圖,以及清理死變數,其餘修改建議可視情況採納。期待後續的更新!

@bouob
Copy link
Copy Markdown
Owner

bouob commented Jun 4, 2026

@pengumina 感謝提供PR,評估後暫時不合併
如果有福利等關鍵字可以透過排除關鍵字把它過濾掉;
若改以數字排序配對,會有6000 or 6,000 等情況,會增加程式碼複雜度,目前認為非必要功能

@bouob bouob closed this Jun 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]拓元遇到有2種票價時會停擺無動作

2 participants