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-315 | special 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-102 | qualification 與全 0 travel fallback/Legacy log。 |
Lib/SearchAdd/AutoQuoteMatcher.php | provider candidate、auto quote、narrow、同步扣款主體。 |
Lib/Model/QuoteServiceCategoryPreserveMute.php | 查特定 service/category 是否有 active preserve mute。 |
Lib/SearchAdd/NarrowMatchBuilder.php | narrow bid 建立邊界。 |
AutoQuoteDispatcher 現有 caller 有兩個:SearchAddHandler.php:317 與 QuoteRequestCopy/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-324 | QuoteRequestsController.php:3328-3336, QuoteService.php:1117 | special pre-flow 後,一般 request 進 auto quote dispatch。 |
AutoQuoteDispatcher.php:30-37 | QuoteService.php:1099-1117 | 載入處理中的 request;request 不存在時不 match。 |
AutoQuoteDispatcher.php:39-41, 60-70 | QuoteService.php:1101-1117 | qualification 通過後才進 matcher。 |
AutoQuoteDispatcher.php:72-102 | CommonHelper.php:342-351, QuoteService.php:1117 | 三個 travel flag 都存在且合計為 0 時,只改 matcher local array 的 provider flag,並寫 handleBidLimit error log。 |
AutoQuoteDispatcher.php:43 | QuoteService.php:1128 | dispatch 後檢查 R1/R2 manual fallback。 |
AutoQuoteMatcher.php:68-87 | QuoteService.php:2557-2589 | 取得 provider candidate pool,寫 match info r/sc/tp。 |
AutoQuoteMatcher.php:89-106 | QuoteService.php:2590-2669 | auto quote 前置 filter:referral/blocked、blocked config、category limitation、travel preference、cert、booking、job preference、schedule。 |
AutoQuoteMatcher.php:107-118,627-641 | QuoteService.php:2678-2680 | 先按 request id 讀關聯表;只有沒有關聯列時才退回單筆參數。 |
AutoQuoteMatcher.php:113-118,496-590 | QuoteService.php:2758-2810 | 逐筆使用當前 preserve id;同 user service 排除,在 auto pool 且未 mute 就保留,否則塞入 narrow。 |
AutoQuoteMatcher.php:515-527,592-620、QuoteServiceCategoryPreserveMute.php:11-17 | QuoteService.php:6167-6238、QuoteServiceCategoryPreserveMute.php:13-24 | 先確認 Legacy base joins,再依 category association/active mute 判斷,最後才做 referral;active mute 拒絕 preserve 額外插入並寫 Legacy 同名 error log。 |
AutoQuoteMatcher.php:124-185 | QuoteService.php:3022-3053 | read-segment、auto quote on、balance 等 gate;preserve exemption 使用完整 list。 |
AutoQuoteMatcher.php:187-203,820-946 | QuoteService.php:2674-2676,2758-2810,2887-2952 | initial sort 後把 preserve 移到 trim 前方,再去重/切前 10/balance sort/category sort。 |
AutoQuoteMatcher.php:205-291 | QuoteService.php:2960,3022-3057 | 建 auto quote bid;完整 list 決定 preserve 欄位,並收集前 5 筆實際 bid id。 |
AutoQuoteMatcher.php:293-321,398-488 | QuoteService.php:3095-3174 | narrow fallback 與完整 list preserve flag/side effect。 |
AutoQuoteMatcher.php:323-337 | QuoteService.php:3088-3092,3288-3316 | 寫排序紀錄、更新 auto_quote_sent_count,並寫 match result log。 |
第 5 組:category 排序與前 5 筆紀錄(已完成,commit 81771a7c)
Legacy 實際流程
| Legacy 行號 | 行為 |
|---|---|
QuoteService.php:2883-2887 | sort_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 | 只接受 hire、review、both;both 依 request id 奇偶轉成實際 sort type。 |
QuoteService.php:2927-2952 | 依 hire 或 review 重排,沒有 secondary tie-breaker。 |
QuoteService.php:2960,2963-3057 | final gate 通過、即將加入 auto bid dataset 時,依序收集最多 5 個 service id。 |
QuoteService.php:3204-3272 | auto 與 narrow bid bulk insert、後續處理及 transaction 完成。 |
QuoteService.php:3288-3316 | sort type 有效且至少有一筆 top service 時,寫 1 筆 sort type,再查 bid id 並依序寫最多 5 筆 display order。 |
精確排序規則:
hire:hired_count較大者在前。review:feedback_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;既有 reader46-52保持原 SQL 與 return shape。QuoteBidAutoQuoteSort.php:21-36:依輸入順序寫最多 5 筆 bid id;既有 reader43-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。 AutoQuoteDispatchercaller 是 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 值不是精確的
hire/review/both時回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.id | int | auto increment,不 explicit insert。 |
quote_request_auto_quote_sort_types.created | datetime NULL | Legacy Cake timestamp callback;new 明確寫當下時間。 |
quote_request_auto_quote_sort_types.quote_request_id | int NULL | 當前 quote_requests.id。 |
quote_request_auto_quote_sort_types.sort_type | varchar(30) NULL | selector 最終得到的 hire 或 review。 |
quote_bid_auto_quote_sorts.id | int | auto increment,不 explicit insert。 |
quote_bid_auto_quote_sorts.quote_request_id | int NOT NULL | 當前 quote_requests.id。 |
quote_bid_auto_quote_sorts.quote_bid_id | bigint NOT NULL | 實際成功建立的 auto quote_bids.id。 |
quote_bid_auto_quote_sorts.display_order | tinyint NOT NULL | 1 到 5,依建立順序。 |
實際 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:
tests/SearchAddStep5IntegrationTest.php:279:6 筆成功 auto bid 依hire排序,只記前 5 筆;同時驗證既有兩個 reader shape。tests/SearchAddStep5IntegrationTest.php:352:review覆蓋 zero-feedback 預設 4、雙方至少 4 分時比較 feedback count,以及低於 4 分時比較平均分數。tests/SearchAddStep5IntegrationTest.php:390:以 request id1/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_bidsquote_request_match_infosread_segment_9_matchesquote_service_categories.balancetransactionsusers.first_purchase_timequote_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.php2026-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、Other1 test / 10 assertions;Input 專用2 tests / 3 assertions。 SearchAddHandlerTest(3 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 順序。
- 精確驗證
getPreserveServiceDatachannel 與完整 Legacy message。 - PHP 8.2 結果:
1 test / 6 assertions;既有單筆 preserve1 / 11、多筆 preserve1 / 19、invalid multi other1 / 10均通過。
2026-08-05 category auto-quote sort:
- Core:
3 tests / 57 assertions;hire/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_quotequote_bids.is_narrow_matchquote_bids.total_site_feequote_service_categories.balancetransactionsquote_request_match_infosread_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 後才可切流。