QuoteBids Consumer Want Provider Quote
對應 legacy API POST /quote_bids/consumer_want_provider_quote/{quote_bid_id}.json。
注意:quote_bid_id 是 path segment;新專案也支援從 param/body fallback 讀 quote_bid_id,但正式 access log 與 staging 實測的公開 caller 都是 path id 形狀。
快速結論
- API 定位:consumer 從「新客源 / narrow match」卡片邀請指定 provider 報價。
- HTTP path:
POST /quote_bids/consumer_want_provider_quote/{quote_bid_id}.json - 舊專案入口:
get-lancer-php56/app/Plugin/Quotes/Controller/QuoteBidsController.php::consumer_want_provider_quote($quote_bid_id) - 舊專案核心方法:
get-lancer-php56/app/Plugin/Quotes/Model/QuoteBid.php::processConsumerWantProviderQuote() - 新專案 endpoint:
Endpoint/V1/QuoteBids.php::consumer_want_provider_quote() - 新專案核心方法:
Lib/Model/QuoteBid.php::processConsumerWantProviderQuote() - 新專案重用方法:
Lib/Model/QuoteBid.php::sendConsumerWantProviderQuoteMail() - 搬移狀態:已補 action 與 domain method,PHPUnit 已覆蓋主要流程;staging 已完成成功 / duplicate guard 實測
- 正式機 access log:2026-05-18 查詢
/quote_bids/consumer_want_provider_quote/合計9918筆,樣本包含POST200 與瀏覽器OPTIONS200
Legacy 主流程
- 驗 JSON request 與 API session。
- 從 session 解析 requestor
user_id。 - 讀取 input:
reasonpreserve_notebegin_dateend_date
reason必須存在於ConstPreserveActionKey,否則設成null。- transaction 包住
QuoteBid::processConsumerWantProviderQuote()。 - 遇到 deadlock / lock wait timeout,最多 retry 5 次。
- 成功回:
{
"status": "success",
"error": 0
}Request / Response 範例
一般邀請指定專家報價:
curl -X POST 'http://localhost:12351/quote_bids/consumer_want_provider_quote/<quote_bid_id>.json' \
-H 'X-PRO360-Rest-Api-Key: <api_key>' \
-H 'X-PRO360-User-Session-Token: <session_token>' \
-H 'Content-Type: application/json' \
-d '{
"reason": "SEND_REQUEST",
"preserve_note": "我想了解可服務時間"
}'帶預約時間時,會新增 quote_request_reserves 並把服務日期文字併入 quote_bids.requestor_note:
curl -X POST 'http://localhost:12351/quote_bids/consumer_want_provider_quote/<quote_bid_id>.json' \
-H 'X-PRO360-Rest-Api-Key: <api_key>' \
-H 'X-PRO360-User-Session-Token: <session_token>' \
-H 'Content-Type: application/json' \
-d '{
"reason": "DIRECT_RESERVE",
"preserve_note": "希望老師準時",
"begin_date": "2026-05-10 10:00:00",
"end_date": "2026-05-10 11:00:00"
}'成功 response:
{
"status": "success",
"error": 0
}legacy guard error response 格式:
{
"error": 7,
"status": "err_want_provider_quote_6"
}正式機 Access Log
2026-05-18 查詢紀錄:
機器:ip-10-5-2-242
查詢範圍:
/var/log/apache2/get-lancer_access.log
/var/log/apache2/get-lancer_access.log.1
/var/log/apache2/get-lancer_access.log.*.gz查詢條件:
LOG_DIR=/var/log/apache2
ACCESS_LOG=get-lancer_access.log
PATTERN='quote_bids/consumer_want_provider_quote/'查詢結果:
| API / 條件 | 筆數 |
|---|---|
/quote_bids/consumer_want_provider_quote/{quote_bid_id}.json | 9918 |
樣本觀察:
- 有實際
POST /quote_bids/consumer_want_provider_quote/{quote_bid_id}.json,HTTP status200。 - 同一批 log 也包含瀏覽器 CORS
OPTIONS,HTTP status200。 - 來源包含 Web mobile Safari / Chrome、consumer app iOS、Android okhttp。
Legacy Guard
QuoteBid::processConsumerWantProviderQuote() 會檢查:
| 規則 | 錯誤碼 | message key |
|---|---|---|
| request invalid / session invalid | 1 | err_want_provider_quote_1 |
| 找不到 bid | 2 | err_want_provider_quote_1 |
| 呼叫者不是 request owner | 3 | err_want_provider_quote_1 |
| request 已封存 / 關閉 / 過期 / 已 hired | 4 | err_want_provider_quote_4 |
| consumer 封鎖 provider | 256 | err_want_provider_quote_256 |
| provider 不可聯絡 | 690 | err_switch_to_manual_quote_690 |
| bid 不是 narrow match | 5 | err_want_provider_quote_1 |
| bid 已不是可邀請狀態 | 6 | err_want_provider_quote_6 |
| narrow 已經送過 invitation | 7 | err_want_provider_quote_6 |
| 聯繫人數達上限 | 8 | err_want_provider_quote_8 |
可邀請狀態:
quote_bids.is_narrow_match = 1
quote_bids.narrow_status_id = ConstNarrowMatch::NOT_SEND
quote_bids.quote_status_id IN (Send, NotInterested)資料表與狀態變化
成功後主要更新 quote_bids:
narrow_status_id = ConstNarrowMatch::WAITING
is_contact_charge = 0如果 provider 原本先選擇 not interested:
is_interested = 1
quote_status_id = Send
provider_status_id = Send如果有 reason:
quote_bids.reason = reason如果有 preserve_note:
quote_bids.requestor_note = preserve_note如果有 begin_date / end_date,會新增 reserve 資訊,並把服務日期文字併入 requestor_note。
Activity / Side Effect
成功後建立:
quote_activities.quote_activity_type_id = WantProviderQuote也會更新 provider my work unread count:
setMyWorkUnreadCount(provider_user_id, [quote_bid_id => ['t' => 'n', 'c' => 1]])非 direct reserve pro 時,會通知 provider:
event_queue.event_key = New_Leads_Pro其中包含:
- email template:
702 Consumer Want To Provider Quote - push / web push template:
push.content.new.leads - sms template:
sms.new.leads
新專案實作
新專案已補同名 endpoint:
Endpoint/V1/QuoteBids.php::consumer_want_provider_quote()核心流程放在:
Lib/Model/QuoteBid.php::processConsumerWantProviderQuote()原因是 switchToProSearchNewLeads() 會容忍部分已轉 new leads 的狀態,和 legacy consumer_want_provider_quote 對 narrow_status_id = NOT_SEND 的嚴格 guard 不完全一致,所以不直接共用它當主流程。
新舊程式對應
以下行號是本次 migration 實作後的定位,用來追 code review / 後續補差異;若檔案再改動,行號可能會漂移,仍以區塊職責為準。
Legacy PHP 5.6
controller:
/Users/mattsu/Documents/Site/get-lancer-php56/app/Plugin/Quotes/Controller/QuoteBidsController.php| legacy 行號 | 職責 |
|---|---|
2258-2266 | 入口 consumer_want_provider_quote();檢查 quote bid id、JSON request、API session,不符合回 error=1。 |
2268-2276 | 讀 requestor user、begin_date、end_date、preserve_note、reason;reason 不在 ConstPreserveActionKey 時改成 null。 |
2280-2299 | transaction + deadlock / lock wait retry,最多 5 次;呼叫 processConsumerWantProviderQuote()。 |
2302-2306 | 成功 response:{"status":"success","error":0}。 |
2307-2310 | ProException 用 getMessageKey() 回 legacy error response;其他 exception 走 consumer_want_provider_quote_error。 |
model:
/Users/mattsu/Documents/Site/get-lancer-php56/app/Plugin/Quotes/Model/QuoteBid.php| legacy 行號 | 職責 |
|---|---|
5151-5162 | 進入 processConsumerWantProviderQuote(),讀 direct reserve option,鎖定 quote_bids。 |
5163-5187 | 讀 bid / service / request / requestor;找不到 bid 回 error=2。 |
5189-5192 | actor 必須是 request owner;不符合回 error=3。 |
5194-5200 | request 已封存、關閉、過期或已 hired 時回 error=4。 |
5202-5206 | blocked chat guard;回 error=256。 |
5208-5212 | provider disabled guard;回 error=690。 |
5214-5229 | narrow / quote status / invitation status guard;分別回 error=5/6/7。 |
5231-5240 | 鎖 request,計算已聯繫 / narrow invitation 數量;達 10 人上限回 error=8。 |
5243-5280 | 若有 begin_date / end_date,新增 quote_request_reserves 並組服務日期文字。 |
5283-5303 | 更新 bid:narrow_status_id=WAITING、is_contact_charge=0、必要時恢復 Send 狀態,寫 reason / requestor_note。 |
5304 | 建立 WantProviderQuote activity。 |
5306 | 更新 provider my work unread count。 |
5308-5310 | 非 direct reserve pro 時送 provider notification。 |
5314-5452 | 組 email / push / web push / sms payload,寫 New_Leads_Pro event。 |
New PHP 8.2
route / endpoint:
| new 檔案行號 | 職責 |
|---|---|
Lib/Common/RouterV3.php:176-178 | routing 依序跑 Mapping、Origin、File。 |
Lib/Common/RouterRule/File.php:42-48 | legacy path 沒有 explicit mapping 時,自動補 v1 並載入 Endpoint/V1/QuoteBids.php。 |
Lib/Common/RouterRule/File.php:66-76 | 以 path segment 對應 action consumer_want_provider_quote,剩餘 quote bid id 進 rawParams。 |
Endpoint/V1/QuoteBids.php:2497-2517 | API action:驗 session、取 quote_bid_id;session / id invalid 回 legacy error=1。 |
Endpoint/V1/QuoteBids.php:2519-2526 | 讀 begin_date、end_date、preserve_note、reason;reason 不合法時改成 null。 |
Endpoint/V1/QuoteBids.php:2528-2559 | 呼叫 domain method,deadlock / lock wait retry 最多 5 次。 |
Endpoint/V1/QuoteBids.php:2562-2569 | 成功 response:{"status":"success","error":0}。 |
Endpoint/V1/QuoteBids.php:2570-2575 | PRO360Exception 用 getMessageKey() 回 legacy error response。 |
domain method:
Lib/Model/QuoteBid.php::processConsumerWantProviderQuote()| new 行號 | 對應 legacy | 職責 |
|---|---|---|
4081-4106 | 5160-5187 | transaction 內鎖 bid,讀 bid/request;找不到 bid 回 error=2。 |
4108-4116 | 5189-5192 | actor 必須是 request owner;不符合回 error=3。 |
4118-4127 | 5194-5200 | request 已封存、關閉、過期或已 hired 時回 error=4。 |
4129-4134 | 5202-5206 | blocked chat guard;回 error=256。 |
4136-4141 | 5208-5212 | provider disabled guard;回 error=690。 |
4143-4162 | 5214-5229 | narrow / quote status / invitation status guard;分別回 error=5/6/7。 |
4164-4181 | 5231-5240 | 鎖 request,計算已聯繫 / narrow invitation 數量;達 10 人上限回 error=8。 |
4183-4225 | 5243-5280 | 若有 begin_date / end_date,新增 quote_request_reserves 並組服務日期文字。 |
4227-4246 | 5283-5303 | 更新 bid:narrow_status_id=WAITING、is_contact_charge=0、必要時恢復 Send 狀態,寫 reason / requestor_note。 |
4248-4256 | 5304 | 建立 WantProviderQuote activity。 |
4258 | 5306 | 更新 provider my work unread count。 |
4260-4262 | 5308-5310 | 非 direct reserve pro 時送 provider notification。 |
4264-4271 | legacy controller transaction + model exception path | commit / rollback;非 PRO360Exception 寫 error_in_processConsumerWantProviderQuote log 後重拋。 |
notification payload 對照:
Lib/Model/QuoteBid.php::sendConsumerWantProviderQuoteMail()| new 行號 | 對應 legacy | 職責 |
|---|---|---|
5010-5052 | 5314-5357 | 讀 bid/request/provider/request title/address,建立 chat URL、security hash、dynamic short URL。 |
5053-5079 | 5359-5395 | email replace content、form summary、template 702 Consumer Want To Provider Quote。 |
5081-5092 | 5408-5419 | push template push.content.new.leads,type=goto_request_page。 |
5094-5102 | 5421-5429 | web push payload,target 指向 quote bid。 |
5104-5113 | 5431-5440 | sms template sms.new.leads 與短網址。 |
5115-5123 | 5442-5452 | 建立 EventTaskDO 並寫入 EventQueue.New_Leads_Pro。 |
tests:
| test 行號 | 職責 |
|---|---|
tests/QuoteBidsConsumerWantProviderQuoteTest.php:91-162 | 成功路徑:response、bid 狀態、reserve、activity、event payload。 |
tests/QuoteBidsConsumerWantProviderQuoteTest.php:164-188 | actor 必須是 request owner。 |
tests/QuoteBidsConsumerWantProviderQuoteTest.php:190-203 | 已送過 invitation 回 error=7。 |
tests/QuoteBidsConsumerWantProviderQuoteTest.php:205-224 | 非 narrow match 回 error=5。 |
tests/QuoteBidsConsumerWantProviderQuoteTest.php:226-245 | request closed 回 error=4。 |
tests/QuoteBidsConsumerWantProviderQuoteTest.php:247-265 | 聯繫人數達上限回 error=8。 |
新舊差異與實作注意
| 項目 | legacy | 新專案 |
|---|---|---|
| endpoint | 有 consumer_want_provider_quote() | 已補同名 action |
| session / requestor guard | controller + model 檢查 | 已補;session invalid 轉 legacy error 1 |
| request closed / archived / expired / hired guard | model 檢查 | 已補 |
| blocked chat user guard | BlockedChatUser::isBlocked() | 已補 |
| provider disabled guard | BlockedUserConfig::is_disable_match_user() | 已補 |
| narrow status guard | 嚴格要求 NOT_SEND | 已補 |
begin_date / end_date | 會寫 reserve 並合併 note | 已補 |
| retry | deadlock / timeout retry 5 次 | 已補 |
| provider notification | New_Leads_Pro | 已補,PHPUnit 已覆蓋 email / push / sms template |
| response | {"status":"success","error":0} | 已對齊 |
資料表關聯
| 規則 | 資料表 | 欄位 |
|---|---|---|
| 找 bid | quote_bids | id, quote_request_id, provider_user_id, quote_service_id |
| 確認 request owner | quote_requests | id, user_id |
| 判斷 request 是否可操作 | quote_requests | is_archived, is_closed, is_expired, hired_count |
| narrow invitation 狀態 | quote_bids | is_narrow_match, narrow_status_id, quote_status_id |
| 新客源狀態 | quote_bids | is_interested, is_contact_charge, narrow_status_id |
| 活動紀錄 | quote_activities | WantProviderQuote, secondary_foreign_id |
| 預約時間 | quote_request_reserves | quote_request_id, quote_service_id, begin_time, end_time |
LB Path
legacy 相容 path 應使用:
/quote_bids/consumer_want_provider_quote/*實際 URL:
/quote_bids/consumer_want_provider_quote/{quote_bid_id}.jsonStaging 實測紀錄
2026-05-19 已將 staging load balancer path 切到新專案並完成主要實測。
成功路徑
測試 bid:
quote_bids.id = 2147686294request:
POST /quote_bids/consumer_want_provider_quote/2147686294.json
reason = SEND_REQUEST
preserve_note = 請與我洽談response:
{
"status": "success",
"error": 0
}DB side effect:
| table | 確認結果 |
|---|---|
quote_bids | narrow_status_id = 1、is_interested = 1、reason = SEND_REQUEST、requestor_note = 請與我洽談 |
quote_activities | 建立 WantProviderQuote activity |
event_queue_log_4 | 寫入 New_Leads_Pro 初始 event |
Request closed / hired guard
測試 bid:
quote_bids.id = 2147686119response:
{
"error": 4,
"status": "err_want_provider_quote_4"
}DB side effect:
- 不應更新 bid invitation 狀態。
- 不應建立
WantProviderQuoteactivity。 - 不應建立初始
New_Leads_Proevent。
Duplicate guard
測試 bid:
quote_bids.id = 2147686295第一次 request:
POST /quote_bids/consumer_want_provider_quote/2147686295.json
reason = SEND_REQUEST
preserve_note = 請與我洽談第一次 response:
{
"status": "success",
"error": 0
}第二次同一筆重複 request response:
{
"error": 7,
"status": "err_want_provider_quote_6"
}第二次後 DB 確認:
| table | 結果 |
|---|---|
quote_bids | modified = 2026-05-19 04:29:44,仍停在第一次成功更新時間 |
quote_bids | narrow_status_id = 1、is_interested = 1、reason = SEND_REQUEST、requestor_note = 請與我洽談 |
quote_activities | 對 secondary_foreign_id = 2147686295 只有 1 筆 WantProviderQuote,id 404374 |
event_queue_log_4 | 對 quote_bid_id = 2147686295 只有 1 筆初始 New_Leads_Pro,id 56654 |
注意:staging worker 後續可能因 Every8d / SMS provider 回錯進入 retry;這屬於外部 SMS 發送設定 / 第三方服務問題,不列入本 API create / guard parity 的阻塞條件。
PHPUnit 覆蓋
目前已新增:
tests/QuoteBidsConsumerWantProviderQuoteTest.php已覆蓋:
- 成功 path
- 非 request owner
- 已送過 invitation
- 非 narrow match
- request closed
- contact limit 達上限
begin_date/end_date寫入quote_request_reservesquote_bids.requestor_note合併 preserve note 與服務日期文字quote_activities.WantProviderQuoteevent_queue.New_Leads_Pro的 email / push / sms template