SearchAdd Auto Quote Match

更新日期:2026-08-05

快速結論

這段負責一般 auto quote / narrow match 的同步配對與一般 auto quote 收費。current Legacy cfa660b0c(BAC-3685)後,多筆 preserve service 會逐筆參與 auto pool 判斷;未服務 category 的 preserve service 另依 active mute 決定是否允許額外插入候選。第 5 組已補 category hire/review/both 排序,以及實際建立成功的前 5 筆 auto bid 紀錄,程式 commit 為 81771a7c

current 入口:

SearchAddHandler::handle()
-> SpecialFlowDispatcher::handlePreAutoQuote()
-> AutoQuoteDispatcher::dispatch()
-> AutoQuoteMatcher::match()

如果 special flow 決定 skip_auto_quote = true,這段不應再跑一般 auto quote dispatch。

新舊程式對應

Current Legacy

主要入口:

/Users/mattsu/Documents/Site/get-lancer-docker-2/get-lancer/app/Plugin/Quotes/Model/QuoteService.php::handleNewQuoteRequestAutoQuote()
legacy 區段職責
_handleAutoQuoteMatch()建立 provider candidate pool。
_handleAutoQuoteMatch() filters保留或淘汰 provider,決定哪些能進 auto quote / narrow。
createAutoQuoteBid()建立 auto quote bid。
logConsumeUserWallet() / logQuoteBidTxn()一般 auto quote 同步扣款與 transaction。
QuoteRequestPreserveQuoteServiceId::getPreserveQuoteServiceIds()依 request id 讀完整 preserve service list。

Current PHP 8.2

current 位置職責
Lib/SearchAdd/SearchAddHandler.php:289-315special pre-flow 後,必要時 dispatch 一般 auto quote。
Lib/SearchAdd/AutoQuoteDispatcher.php:30-57載入 request、判斷是否可進 auto quote、套 matcher-only travel fallback、reload request / bids。
Lib/SearchAdd/AutoQuoteDispatcher.php:60-102qualification 與全 0 travel fallback/Legacy log。
Lib/SearchAdd/AutoQuoteMatcher.phpprovider candidate、auto quote、narrow、同步扣款主體。
Lib/Model/QuoteServiceCategoryPreserveMute.php查特定 service/category 是否有 active preserve mute。
Lib/SearchAdd/NarrowMatchBuilder.phpnarrow bid 建立邊界。

AutoQuoteDispatcher 現有 caller 有兩個:SearchAddHandler.php:317QuoteRequestCopy/CopyRequestHandler.php:465。Legacy search_add 與 copy 都會進 QuoteService::handleNewQuoteRequestAutoQuote(),因此 matcher-only fallback 保留在 dispatcher 可同時對齊兩條流程;沒有修改其他 QuoteBid::handleBidLimit() caller。

行號對照

這裡先放第一層對照。AutoQuoteMatcher 內部 filter / narrow 補位已拆到 06_auto_quote_filters.md,避免單頁過大。

new 行號對應 legacy職責
SearchAddHandler.php:298-324QuoteRequestsController.php:3328-3336, QuoteService.php:1117special pre-flow 後,一般 request 進 auto quote dispatch。
AutoQuoteDispatcher.php:30-37QuoteService.php:1099-1117載入處理中的 request;request 不存在時不 match。
AutoQuoteDispatcher.php:39-41, 60-70QuoteService.php:1101-1117qualification 通過後才進 matcher。
AutoQuoteDispatcher.php:72-102CommonHelper.php:342-351, QuoteService.php:1117三個 travel flag 都存在且合計為 0 時,只改 matcher local array 的 provider flag,並寫 handleBidLimit error log。
AutoQuoteDispatcher.php:43QuoteService.php:1128dispatch 後檢查 R1/R2 manual fallback。
AutoQuoteMatcher.php:68-87QuoteService.php:2557-2589取得 provider candidate pool,寫 match info r/sc/tp
AutoQuoteMatcher.php:89-106QuoteService.php:2590-2669auto quote 前置 filter:referral/blocked、blocked config、category limitation、travel preference、cert、booking、job preference、schedule。
AutoQuoteMatcher.php:107-118,627-641QuoteService.php:2678-2680先按 request id 讀關聯表;只有沒有關聯列時才退回單筆參數。
AutoQuoteMatcher.php:113-118,496-590QuoteService.php:2758-2810逐筆使用當前 preserve id;同 user service 排除,在 auto pool 且未 mute 就保留,否則塞入 narrow。
AutoQuoteMatcher.php:515-527,592-620QuoteServiceCategoryPreserveMute.php:11-17QuoteService.php:6167-6238QuoteServiceCategoryPreserveMute.php:13-24先確認 Legacy base joins,再依 category association/active mute 判斷,最後才做 referral;active mute 拒絕 preserve 額外插入並寫 Legacy 同名 error log。
AutoQuoteMatcher.php:124-185QuoteService.php:3022-3053read-segment、auto quote on、balance 等 gate;preserve exemption 使用完整 list。
AutoQuoteMatcher.php:187-203,820-946QuoteService.php:2674-2676,2758-2810,2887-2952initial sort 後把 preserve 移到 trim 前方,再去重/切前 10/balance sort/category sort。
AutoQuoteMatcher.php:205-291QuoteService.php:2960,3022-3057建 auto quote bid;完整 list 決定 preserve 欄位,並收集前 5 筆實際 bid id。
AutoQuoteMatcher.php:293-321,398-488QuoteService.php:3095-3174narrow fallback 與完整 list preserve flag/side effect。
AutoQuoteMatcher.php:323-337QuoteService.php:3088-3092,3288-3316寫排序紀錄、更新 auto_quote_sent_count,並寫 match result log。

第 5 組:category 排序與前 5 筆紀錄(已完成,commit 81771a7c

Legacy 實際流程

Legacy 行號行為
QuoteService.php:2883-2887sort_type 預設為 null;authorized mode 不進 selector。
QuoteService.php:2891-2916先排除同 provider 的重複 service,再切到 quote.auto_quote_max_quote(預設 10)筆。
QuoteService.php:2918-2926先依 balance 由高到低,再查 category 的 sort type。
QuoteRequestAutoQuoteSortType.php:14-23只接受 hirereviewbothboth 依 request id 奇偶轉成實際 sort type。
QuoteService.php:2927-2952hirereview 重排,沒有 secondary tie-breaker。
QuoteService.php:2960,2963-3057final gate 通過、即將加入 auto bid dataset 時,依序收集最多 5 個 service id。
QuoteService.php:3204-3272auto 與 narrow bid bulk insert、後續處理及 transaction 完成。
QuoteService.php:3288-3316sort type 有效且至少有一筆 top service 時,寫 1 筆 sort type,再查 bid id 並依序寫最多 5 筆 display order。

精確排序規則:

  • hirehired_count 較大者在前。
  • reviewfeedback_count = 0 時平均分數不是 0,而是預設 4
  • 比較的兩位平均分數都 >= 4 時,feedback_count 較大者在前;任一位低於 4 時,平均分數較高者在前。
  • Legacy callback 相同值時仍回 1,不回 0;因此不新增 service id、balance 或原順序作 tie-breaker。
  • both:奇數 request id 實際記錄 hire;偶數記錄 review。資料表不記錄 both
  • top 5 不含 self、重複 provider、ibon 非 authorized tier、read segment 轉存或 narrow bid;只記真正建立 auto bid 的順序。

PHP 8 實作與 shared caller

  • QuoteRequestAutoQuoteSortType.php:17-32:查 quote_categories.auto_quote_sort_type,只接受 hire/review/both,並把 both 解析成實際 type。
  • QuoteRequestAutoQuoteSortType.php:34-44:寫一筆 request sort type;既有 reader 46-52 保持原 SQL 與 return shape。
  • QuoteBidAutoQuoteSort.php:21-36:依輸入順序寫最多 5 筆 bid id;既有 reader 43-87 保持原 SQL 與 return shape。
  • AutoQuoteMatcher.php:128-130,187-203:authorized mode 以外,在既有 trim/balance sort 後解析並套 category sort。
  • AutoQuoteMatcher.php:257-260,323-326:只收集 DBFactory::create($quoteBid) 後取得的實際 auto bid id;有效 type 且至少一筆 auto bid 才寫兩張表。
  • AutoQuoteMatcher.php:916-946:實作 Legacy 原 comparator,沒有新增 secondary tie-breaker。
  • candidate source SpecificUserUtilAbstract.php:48-55,73-162 已含三個排序欄位,沒有新增 join 或 guard。
  • AutoQuoteDispatcher caller 是 search_add 與 quote request copy;Legacy 兩條流程都共用 QuoteService::handleNewQuoteRequestAutoQuote(),所以 matcher scope 一致。
  • response reader caller 仍是 Endpoint/V1/QuoteRequests.php:648-656,本次 writer 沒有改其契約。

PHP 8 程式流程

eligible auto candidates
-> 現有 provider 去重/前 10 trim/balance sort
-> 依 quote_categories.auto_quote_sort_type 解析 hire 或 review
-> 使用 Legacy comparator 重排
-> final gate 後逐筆建立 auto bid
-> 收集實際成功的前 5 個 quote_bid id
-> matcher 後段寫 sort type 與 display order

已實作邊界:

  • 新增 scoped selector/writer method;不改既有 response reader。
  • category 值不是精確的 hirereviewboth 時回 null,不排序、不寫表。
  • authorized mode 不查 category sort type,也不寫表。
  • 沒有成功 auto bid 時不寫任何排序 row。
  • 不記 narrow bid;不因重複執行新增 upsert/unique guard。
  • DB write 不自行 catch/忽略;Legacy 寫表處也沒有 swallow exception 的保護。
  • 不改既有 bid、balance、transaction、activity、notification 或 response mapping。

DB 欄位分類與 SQL

已先用 PHP 8.2 容器 SHOW COLUMNS 核對本地 schema。

table.field型別來源/規則
quote_request_auto_quote_sort_types.idintauto increment,不 explicit insert。
quote_request_auto_quote_sort_types.createddatetime NULLLegacy Cake timestamp callback;new 明確寫當下時間。
quote_request_auto_quote_sort_types.quote_request_idint NULL當前 quote_requests.id
quote_request_auto_quote_sort_types.sort_typevarchar(30) NULLselector 最終得到的 hirereview
quote_bid_auto_quote_sorts.idintauto increment,不 explicit insert。
quote_bid_auto_quote_sorts.quote_request_idint NOT NULL當前 quote_requests.id
quote_bid_auto_quote_sorts.quote_bid_idbigint NOT NULL實際成功建立的 auto quote_bids.id
quote_bid_auto_quote_sorts.display_ordertinyint NOT NULL15,依建立順序。

實際 SQL shape:

SELECT auto_quote_sort_type
FROM quote_categories
WHERE id = ?;
 
INSERT INTO quote_request_auto_quote_sort_types
    (created, quote_request_id, sort_type)
VALUES (NOW(), ?, ?);
 
INSERT INTO quote_bid_auto_quote_sorts
    (quote_request_id, quote_bid_id, display_order)
VALUES (?, ?, ?);

Negative contract:不寫 modified、category id、service id、narrow bid id 或 Legacy 沒有的狀態欄位。

測試結果

Core 3 tests / 57 assertions

  1. tests/SearchAddStep5IntegrationTest.php:279:6 筆成功 auto bid 依 hire 排序,只記前 5 筆;同時驗證既有兩個 reader shape。
  2. tests/SearchAddStep5IntegrationTest.php:352review 覆蓋 zero-feedback 預設 4、雙方至少 4 分時比較 feedback count,以及低於 4 分時比較平均分數。
  3. tests/SearchAddStep5IntegrationTest.php:390:以 request id 1/2 明確驗證 both 兩個奇偶分支,再確認實際 request 記錄解析後 type。

Other 3 tests / 31 assertions

  • tests/SearchAddStep5IntegrationTest.php:422:未知 sort type 仍可建立 auto bid,但兩張排序表皆為 0 筆。
  • tests/SearchAddStep5IntegrationTest.php:444:有效 sort type 但沒有成功 auto bid,不寫任何排序 row。
  • tests/SearchAddStep5IntegrationTest.php:466:authorized mode 可建立 1 筆 auto bid,但不套 category sort、不寫兩張排序表。

另重跑既有 multi preserve/category mute regression:2 tests / 25 assertions。所有修改檔案皆通過 host 與容器 PHP 8.2 syntax;測後 category 280 的 sort type 已還原,兩張排序表沒有測試 orphan row。

Side Effects

同步可能寫入:

  • quote_bids
  • quote_request_match_infos
  • read_segment_9_matches
  • quote_service_categories.balance
  • transactions
  • users.first_purchase_time
  • quote_request_auto_quote_sort_types(僅有效 sort type 且有 auto bid)
  • quote_bid_auto_quote_sorts(最多 5 筆實際 auto bid)

同步讀取:

  • quote_request_preserve_quote_service_ids.quote_service_id(不加 active、status 或其他 Legacy 沒有的 guard)
  • quote_categories_quote_services.id(只做 service/category association 存在性檢查)
  • quote_service_category_preserve_mutes.id(只接受同 service/category 且 is_active=1

Preserve category mute log:

  • channel:getPreserveServiceData
  • level:error
  • message:preserve_service_id 未服務該項目 svc_id:{service_id} c_id:{category_id}
  • 沒有新增 context、DB write 或 response 欄位。

不應直接用 response 判斷的 downstream:

  • manual / related fallback
  • worker 搬移後的 notification logs
  • request sent count 重算

Travel fallback:

  • 不新增 DB write;quote_requests 三個 travel flag 保持原值。
  • 不改 API response;dispatcher matcher 後重新讀 DB,因此 response 不會出現暫時補的 provider flag。
  • log channel:handleBidLimit;message:三個 flag 合計為 0 加 request id;context 為空。
  • PDO log 各環境保留;Slack error 只在 production 啟用,與 Legacy LogHelper.php:22-30 一致。

不可改變的契約

  • candidate pool 與 filter 邊界必須以 legacy _handleAutoQuoteMatch() 為準。
  • multi preserve 的每一筆都必須使用自己的 service id;不可再使用單筆變數代替 loop id。
  • category association 存在時不可套 preserve mute;association 不存在時也只能用 is_active=1 拒絕,inactive row 必須視為允許。
  • category mute 必須在 referral 前判斷;service/user/profile base data 不存在時不可誤寫 mute log。
  • preserve mute 只阻止 preserve 額外插入/優先處理,不可擴大成一般 candidate pool 的全域排除。
  • preserve list 必須在 auto trim 前取得優先位置,否則超過前 10 筆時會與 Legacy 不同。
  • read-segment exemption、auto/narrow is_preserve、reason、note 與 narrow side effect 都使用完整 list。
  • quote_requests.has_preserve/preserve_quote_service_id 不因 multi 改變。
  • 不是所有被 auto quote filter 掉的 provider 都能進 narrow。
  • 一般 auto quote 的 transactions.transaction_type_id 應是 quote bid payment 類型。
  • 一般 auto quote 成功時,bid、balance、transaction 要同步成立。
  • AutoQuoteDispatcher 只是入口與 reload,不應把 matcher 細節寫回 dispatcher。
  • 全 0 travel fallback 是 dispatcher 送 matcher 前的 input compatibility;不可移到 shared QuoteBid::handleBidLimit(),避免改變其他 PHP 8 caller,也不可回寫 DB。

Verification

主要測試:

tests/SearchAddStep5IntegrationTest.php
tests/SearchAddStep6IntegrationTest.php
tests/SearchAddTravelPreferenceTest.php
tests/SearchAddInputTest.php
tests/SearchAddStep7IntegrationTest.php

2026-08-04:Core 1 test / 5 assertions,覆蓋全 0 時 matcher copy provider=1、原 request 仍為 0、精確 log message;Other 1 test / 3 assertions,覆蓋 non-zero 與缺欄位時不 fallback/不寫 log。

2026-08-05 multi preserve:

  • Core:單筆仍寫關聯表且留在 auto;multi-only 兩筆都寫表,兩位都建立 is_auto_quote=1/is_narrow_match=0/is_preserve=1,reason/note 一致。
  • Other:multi 含一個 invalid 值時整組不寫關聯表;一般 auto bid 的 is_preserve/reason/requestor_note 都是 NULL
  • 執行結果:Core 2 tests / 30 assertions、Other 1 test / 10 assertions;Input 專用 2 tests / 3 assertions
  • SearchAddHandlerTest3 tests / 9 assertions)與 SearchAddInputTest 全檔(7 tests / 18 assertions)通過。
  • 兩個既有 Step 5 narrow reflection tests 在進 matcher 前由 request validation 回 error 11(Email 格式),因此不列為本次通過結果。
  • 另以 PHP 8.2 空資料直接呼叫舊四參數 reflection 介面,確認 optional preserve list 沒有破壞既有 caller,回傳 0
  • 8/4 staging request 22824 的三筆 narrow bid 是 BAC-3685 前證據,不再當成最新預期。

2026-08-05 preserve category mute:

  • 程式 commit:4b3e0af3
  • Core matrix:未服務+active mute 排除;未服務+inactive mute 允許;已有 category association+active mute 仍允許。active mute fixture 同時是 referral,用來驗證 Legacy guard 順序。
  • 精確驗證 getPreserveServiceData channel 與完整 Legacy message。
  • PHP 8.2 結果:1 test / 6 assertions;既有單筆 preserve 1 / 11、多筆 preserve 1 / 19、invalid multi other 1 / 10 均通過。

2026-08-05 category auto-quote sort:

  • Core:3 tests / 57 assertionshire/review/both 兩個奇偶分支、前 5 筆與既有 reader shape 全部通過。
  • Other:3 tests / 31 assertions;未知 type、無成功 auto bid、authorized mode 都不寫排序表。
  • multi preserve/category mute regression:2 tests / 25 assertions
  • host 與 PHP 8.2 容器 syntax 全部通過;git diff --check 通過。
  • 測後 category 280 已還原;測試 request 對應兩張排序表 orphan row 皆為 0。

baseline 比對優先看:

  • provider 名單
  • quote_bids.is_auto_quote
  • quote_bids.is_narrow_match
  • quote_bids.total_site_fee
  • quote_service_categories.balance
  • transactions
  • quote_request_match_infos
  • read_segment_9_matches

待確認 / 風險

  • checkR1ManualMatchStartNoAutoQuote() 同時出現在 dispatcher 與 handler 後段,屬於 async fallback 邊界,補 09_async_fallbacks.md 時要確認是否只是 idempotent 保護,避免誤判為同步 match 差異。
  • filterLocationTravelPreference() 同時承接 travel preference 與部分 provider 基本資格 gate;helper-level 對照已拆到 06_auto_quote_filters.md,後續要用 baseline 確認 provider 名單是否完全一致。
  • legacy additional narrow 不足 15 人時會再補 related category;current 已在 NarrowMatchBuilder 補齊同等補位,細節見 06_auto_quote_filters.md
  • PHP 8 NewQuoteRequestHandler.php:178-211,1160-1174 已用 request row id 讀 multi list;建立 manual bid 的 is_preserve 仍只比較單筆變數,與 current Legacy 一致。詳細測試見 09_async_fallbacks.md
  • 本次第 5 組新增 category sort type read 與兩張排序表 write;沒有新增 config、queue、payment、notification、log 或 response 欄位。
  • 本地 PHP 8.2 DB 已核對兩張表欄位;staging/production schema 尚待部署前確認。
  • 第 5 組不讀 deployment config、不修改 ALB;staging/production 仍需核對 auto_quote_sort_type 欄位與兩張排序表 schema 後才可切流。