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 筆,樣本包含 POST 200 與瀏覽器 OPTIONS 200

Legacy 主流程

  1. 驗 JSON request 與 API session。
  2. 從 session 解析 requestor user_id
  3. 讀取 input:
    • reason
    • preserve_note
    • begin_date
    • end_date
  4. reason 必須存在於 ConstPreserveActionKey,否則設成 null
  5. transaction 包住 QuoteBid::processConsumerWantProviderQuote()
  6. 遇到 deadlock / lock wait timeout,最多 retry 5 次。
  7. 成功回:
{
  "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}.json9918

樣本觀察:

  • 有實際 POST /quote_bids/consumer_want_provider_quote/{quote_bid_id}.json,HTTP status 200
  • 同一批 log 也包含瀏覽器 CORS OPTIONS,HTTP status 200
  • 來源包含 Web mobile Safari / Chrome、consumer app iOS、Android okhttp。

Legacy Guard

QuoteBid::processConsumerWantProviderQuote() 會檢查:

規則錯誤碼message key
request invalid / session invalid1err_want_provider_quote_1
找不到 bid2err_want_provider_quote_1
呼叫者不是 request owner3err_want_provider_quote_1
request 已封存 / 關閉 / 過期 / 已 hired4err_want_provider_quote_4
consumer 封鎖 provider256err_want_provider_quote_256
provider 不可聯絡690err_switch_to_manual_quote_690
bid 不是 narrow match5err_want_provider_quote_1
bid 已不是可邀請狀態6err_want_provider_quote_6
narrow 已經送過 invitation7err_want_provider_quote_6
聯繫人數達上限8err_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_quotenarrow_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_dateend_datepreserve_notereasonreason 不在 ConstPreserveActionKey 時改成 null
2280-2299transaction + deadlock / lock wait retry,最多 5 次;呼叫 processConsumerWantProviderQuote()
2302-2306成功 response:{"status":"success","error":0}
2307-2310ProExceptiongetMessageKey() 回 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-5192actor 必須是 request owner;不符合回 error=3
5194-5200request 已封存、關閉、過期或已 hired 時回 error=4
5202-5206blocked chat guard;回 error=256
5208-5212provider disabled guard;回 error=690
5214-5229narrow / 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=WAITINGis_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-178routing 依序跑 MappingOriginFile
Lib/Common/RouterRule/File.php:42-48legacy 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-2517API action:驗 session、取 quote_bid_id;session / id invalid 回 legacy error=1
Endpoint/V1/QuoteBids.php:2519-2526begin_dateend_datepreserve_notereasonreason 不合法時改成 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-2575PRO360ExceptiongetMessageKey() 回 legacy error response。

domain method:

Lib/Model/QuoteBid.php::processConsumerWantProviderQuote()
new 行號對應 legacy職責
4081-41065160-5187transaction 內鎖 bid,讀 bid/request;找不到 bid 回 error=2
4108-41165189-5192actor 必須是 request owner;不符合回 error=3
4118-41275194-5200request 已封存、關閉、過期或已 hired 時回 error=4
4129-41345202-5206blocked chat guard;回 error=256
4136-41415208-5212provider disabled guard;回 error=690
4143-41625214-5229narrow / quote status / invitation status guard;分別回 error=5/6/7
4164-41815231-5240鎖 request,計算已聯繫 / narrow invitation 數量;達 10 人上限回 error=8
4183-42255243-5280若有 begin_date / end_date,新增 quote_request_reserves 並組服務日期文字。
4227-42465283-5303更新 bid:narrow_status_id=WAITINGis_contact_charge=0、必要時恢復 Send 狀態,寫 reason / requestor_note
4248-42565304建立 WantProviderQuote activity。
42585306更新 provider my work unread count。
4260-42625308-5310非 direct reserve pro 時送 provider notification。
4264-4271legacy controller transaction + model exception pathcommit / rollback;非 PRO360Exceptionerror_in_processConsumerWantProviderQuote log 後重拋。

notification payload 對照:

Lib/Model/QuoteBid.php::sendConsumerWantProviderQuoteMail()
new 行號對應 legacy職責
5010-50525314-5357讀 bid/request/provider/request title/address,建立 chat URL、security hash、dynamic short URL。
5053-50795359-5395email replace content、form summary、template 702 Consumer Want To Provider Quote
5081-50925408-5419push template push.content.new.leadstype=goto_request_page
5094-51025421-5429web push payload,target 指向 quote bid。
5104-51135431-5440sms template sms.new.leads 與短網址。
5115-51235442-5452建立 EventTaskDO 並寫入 EventQueue.New_Leads_Pro

tests:

test 行號職責
tests/QuoteBidsConsumerWantProviderQuoteTest.php:91-162成功路徑:response、bid 狀態、reserve、activity、event payload。
tests/QuoteBidsConsumerWantProviderQuoteTest.php:164-188actor 必須是 request owner。
tests/QuoteBidsConsumerWantProviderQuoteTest.php:190-203已送過 invitation 回 error=7
tests/QuoteBidsConsumerWantProviderQuoteTest.php:205-224非 narrow match 回 error=5
tests/QuoteBidsConsumerWantProviderQuoteTest.php:226-245request closed 回 error=4
tests/QuoteBidsConsumerWantProviderQuoteTest.php:247-265聯繫人數達上限回 error=8

新舊差異與實作注意

項目legacy新專案
endpointconsumer_want_provider_quote()已補同名 action
session / requestor guardcontroller + model 檢查已補;session invalid 轉 legacy error 1
request closed / archived / expired / hired guardmodel 檢查已補
blocked chat user guardBlockedChatUser::isBlocked()已補
provider disabled guardBlockedUserConfig::is_disable_match_user()已補
narrow status guard嚴格要求 NOT_SEND已補
begin_date / end_date會寫 reserve 並合併 note已補
retrydeadlock / timeout retry 5 次已補
provider notificationNew_Leads_Pro已補,PHPUnit 已覆蓋 email / push / sms template
response{"status":"success","error":0}已對齊

資料表關聯

規則資料表欄位
找 bidquote_bidsid, quote_request_id, provider_user_id, quote_service_id
確認 request ownerquote_requestsid, user_id
判斷 request 是否可操作quote_requestsis_archived, is_closed, is_expired, hired_count
narrow invitation 狀態quote_bidsis_narrow_match, narrow_status_id, quote_status_id
新客源狀態quote_bidsis_interested, is_contact_charge, narrow_status_id
活動紀錄quote_activitiesWantProviderQuote, secondary_foreign_id
預約時間quote_request_reservesquote_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}.json

Staging 實測紀錄

2026-05-19 已將 staging load balancer path 切到新專案並完成主要實測。

成功路徑

測試 bid:

quote_bids.id = 2147686294

request:

POST /quote_bids/consumer_want_provider_quote/2147686294.json
reason = SEND_REQUEST
preserve_note = 請與我洽談

response:

{
  "status": "success",
  "error": 0
}

DB side effect:

table確認結果
quote_bidsnarrow_status_id = 1is_interested = 1reason = SEND_REQUESTrequestor_note = 請與我洽談
quote_activities建立 WantProviderQuote activity
event_queue_log_4寫入 New_Leads_Pro 初始 event

Request closed / hired guard

測試 bid:

quote_bids.id = 2147686119

response:

{
  "error": 4,
  "status": "err_want_provider_quote_4"
}

DB side effect:

  • 不應更新 bid invitation 狀態。
  • 不應建立 WantProviderQuote activity。
  • 不應建立初始 New_Leads_Pro event。

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_bidsmodified = 2026-05-19 04:29:44,仍停在第一次成功更新時間
quote_bidsnarrow_status_id = 1is_interested = 1reason = SEND_REQUESTrequestor_note = 請與我洽談
quote_activitiessecondary_foreign_id = 2147686295 只有 1 筆 WantProviderQuote,id 404374
event_queue_log_4quote_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_reserves
  • quote_bids.requestor_note 合併 preserve note 與服務日期文字
  • quote_activities.WantProviderQuote
  • event_queue.New_Leads_Pro 的 email / push / sms template