QuoteServices Add
API:
POST /quote_services/add.json功能說明
provider 新增服務時呼叫。這支不是單純新增 quote_services;legacy 同時支援既有 session、新 email 自動建立 provider user、social login / signup、service category mapping、外部評價短網址、activity、queue、notification 與 other category 回報。
成功 response:
{"error":0,"service_id":"123","session_id":"...","user_id":"456"}service_id 與 user_id 的 JSON 型別都是 string;這是 legacy response contract,不可因 DB id 在新版內部以 int 處理而輸出成 number。
正式機流量
2026-06-12 查正式機 access log:
2209 POST /quote_services/add.json 200
887 OPTIONS /quote_services/add.json 200同次查詢 /quote_services/add 與 /quote_services/add/... legacy HTML path,未看到流量。正式流量只確認到 JSON API。
快速結論
- 呼叫者:需有效 REST API key;有 user session 時使用 session user,無 session 時依
User[...]做登入或建立 provider user。 - legacy API:
QuoteServicesController::add()。 - new API:已補
Endpoint/V1/QuoteServices.php::add()。 - routing:已補
/quote_services/add.json。 - 完整搬移範圍:
- existing session path
- email / password / device login path
- Google / Facebook / Facebook Limited / Apple social path
- provider user signup、
user_profiles、user_logins、api_sessions quote_servicesexplicit insert,不用既有 row 當 templatequote_categories_quote_services、nshop_service_categoriessecurity_hashes/quote_services.external_review_urltask_event_queue.service_update,僅限 request 原本帶QuoteService[phone_number]quote_activities:ProviderSignup、CreateServicetask_queue.newQuoteServiceevent_queue.Pro_Signup- other category 的
quote_reports、admin email、Require_New_Categoryevent - application log:
newServiceScan;legacy 的quote_services/add::POST、quote_services_addrequest dump 不在 endpoint 重複補,改保留RouterV3generic request log
前端呼叫鏈:
ProQuestionnairePopupContainer
-> questionnaireActions.submitServiceQuestionnaire()
-> APIManager.postService()
-> POST /quote_services/add.jsonServicesContainer / GoogleReservePro 也會用 postService() 新增 service。這些路徑可能送 QuoteCategory: [""];legacy 視為有效 input,會建立 service 並保存空白 category id,不建立 numeric category mapping。新版必須保留這個行為。
前端 session handling 已確認:
modules/actions/questionnaireActions.js會把 responsesession_id放到tempSession。modules/containers/ProQuestionnairePopupContainer.js收到tempSession後呼叫auth.saveTokenIfMissing(tempSession)。modules/utils/user-auth.js會寫入session_tokencookie。ServicesContainer/GoogleReservePropath 是已登入 provider 新增 service,呼叫postService()時會用既有session_tokenheader;回傳主要使用service_id。
Logging 對照
legacy 在 JSON path 有五類 log:
CakeLog::info('add service: ...'):每次 JSON request 寫 CakePHP file log,內容是 request data。LogHelper('quote_services/add::POST')->info($user_id, [api_key, request data, $_SERVER]):每次 JSON request 寫monolog_YYYYMM。LogHelper('quote_services_add')->info($user_id, request data):只在 auto-signup 建立新 user 成功後寫monolog_YYYYMM。LogHelper('newServiceScan')->error(...):只在 auto-signup 後 abnormal scan 例外時寫monolog_error,不中斷新增 service。$this->log(...):debug file log,包含 provided email、login success、auto signup username、user resolve result、validation fail request dump。
new project 的 PRO360\Common\LogHelper 仍寫 monolog_* table。本次經 reviewer 確認的 migration decision:request body 不在 endpoint 重複寫 full dump,因為 RouterV3 已有 generic request log。
RouterV3generic request log:保留 API key、path、subDir、post data、request id;password/passwd key 由 router 層過濾。- 不補
quote_services/add::POSTendpoint duplicate log:legacy context 會把[api_key, request data, $_SERVER]再寫一次,與 generic log 重複且範圍過大。 - 不補
quote_services_addendpoint duplicate log:auto-signup request data 已在同一 request 的 generic log 中可查。 newServiceScan:abnormal scan 例外時寫ERROR到monolog_error,不中斷新增 service。
legacy 的 CakeLog::info(...) 與 $this->log(...) 是 CakePHP file log。new project 沒有 CakePHP file logging;FileLogHandler 目前是 no-op,所以這些 file log 尚未 1:1 重建。切換前若要求 literal file-log parity,需另外補新專案 file log handler 或指定可接受的替代 log channel。
上述 legacy request log 可能包含 email / phone / social token / password 類 input;new 目前只在 RouterV3 層做 password/passwd 過濾,其他敏感 token 類欄位若需額外過濾需另開 migration decision。request / service / category / queue / event 的可查狀態仍以 DB side effect 為主。
Request 重點
| input | 規則 |
|---|---|
User[...] | 無 session 時用來登入或建立 provider user。支援 email/password/device、email verify code、phone verify code、referral hash、social token。 |
QuoteService[...] | service 主資料;會正規化地址、zip、city/state/country、service_area、travel option、phone、company name;is_not_this_area=1 時保留原 full_address。 |
QuoteCategory | 空 array 代表缺 category,回 error=4;[""] 不算空 array,要照 legacy 允許。 |
QuoteCategory[]=other|... | 其他分類;會追加到 description,並觸發 quote report / admin email / provider email event。 |
QuoteService[travel_pref] | 非空 JSON 時依 legacy web-app 格式轉 travel flags:travel_vendor.value、travel_customer.value、remote.value。新版也保留 flat key 相容,但主要驗證要用 legacy 巢狀格式。 |
無 travel_pref | 必須符合 legacy checkbox 規則:至少一個 travel option;勾距離但無 km 回 Fill Km value;填 km 但未勾距離回 Make check km check box。 |
Legacy Rule
legacy controller:
/Users/mattsu/Documents/Site/get-lancer-docker-2/get-lancer/app/Plugin/Quotes/Controller/QuoteServicesController.php| legacy 行號 | 職責 |
|---|---|
1009-1069 | add() 入口;separate user type guard、API key / session 檢查、讀 request data、初始化 response。 |
1071-1126 | 解析 User[...],處理 Google / Facebook / Apple social token。 |
1128-1136 | 有效 API session 時使用 session user。 |
1137-1174 | 無 session 時依 email / social id / device id / password 找既有 user,必要時綁 social id。 |
1175-1318 | 新 email 建立 provider user、寫 legacy quote_services_add log、profile、login、referral、CRM upsert。 |
1330-1344 | user resolve 失敗:空 password 回 error=2,密碼錯誤回 error=3。 |
1372-1387 | 設定 service owner、IP、top-level City / State 與 country 基本值。 |
1388-1415 | category 與 travel option validation;travel_pref 用 travel_vendor.value、travel_customer.value、remote.value;empty(['']) === false,所以 QuoteCategory: [""] 可通過。 |
1417-1438 | country default / ISO lookup、service_area JSON、service_cities。 |
1440-1460 | category 去重、quote_category_ids、other category 追加 description。 |
1462-1501 | phone fallback、array phone JSON encode、zip/address/full_address 處理;zip 找到 city data 時覆蓋前面 city/state/country。 |
1503-1535 | XML invalid char 過濾、公司名驗證、start_year / number_of_people 範圍。 |
1537-1546 | 建立 quote_services,補 slug,建立 external review URL。 |
1548-1565 | first service 且非新 user 時更新 users.onboard_state = 1;quote_service_count 由 model counter cache 更新。 |
1567-1574 | request 原本帶 QuoteService[phone_number] 時送 task_event_queue.service_update;profile phone fallback 不觸發此 queue。 |
1576-1589 | 新 user 寫 ProviderSignup activity、abnormal scan、checkNewUser;scan exception 只寫 newServiceScan log,不中斷成功流程。 |
1591-1597 | 寫 CreateService activity;first service 或 new signup 送 provider welcome event。 |
1599-1615 | numeric category mapping、Nshop category、task_queue.newQuoteService。 |
1620-1643 | other category:quote report、admin email、provider need-new-category event。 |
1646-1667 | success response;沒有 session 時 upsert session。 |
1679-1682 | save validation failure 回 error=4。 |
legacy helper:
/Users/mattsu/Documents/Site/get-lancer-docker-2/get-lancer/app/Plugin/Quotes/Model/QuoteService.php| legacy 行號 | 職責 |
|---|---|
QuoteService.php:17-32 | belongsTo User / City counter cache;新增 service 時會更新 users.quote_service_count 與 cities.quote_service_count。 |
360-389 | getExternalReviewUrl() / _getExternalReviewUrl():建立 security hash,組 /review/service/{service_id}/{hash} long URL,呼叫 YOURLS 轉短網址。 |
4828-4864 | sendProviderNeedNewCategoryMail():建立 Require_New_Category email event。 |
4866-4929 | sendProviderWelcomeMail():legacy email task 已註解,實際只送 MoEngage Signup_Pro。 |
5196-5213 | CRM user / service methods 目前是 no-op。 |
6133-6137 | XML invalid char filter。 |
New PHP 8.2 對照
| new 檔案行號 | 對應 legacy | 職責 |
|---|---|---|
Lib/Common/RouterRule/Mapping.php:49-52 | access log path | route /quote_services/add.json 到 QuoteServices::add()。 |
Endpoint/V1/QuoteServices.php:373-409 | 1009-1069, 1646-1667 | endpoint 入口、API key guard、呼叫 user/service 建立流程、success response;service_id / user_id 在 response boundary 轉成 string;不再寫 endpoint duplicate request log。 |
Endpoint/V1/QuoteServices.php:1248-1377 | 1128-1174, 1269, 1330-1344 | session user、existing email/social/device/password login、auto-signup、error 2/3 mapping、session upsert;user lookup 不加 is_active filter,對齊 legacy User::getById() / getByEmail();不再寫 quote_services_add duplicate request log。 |
Lib/Common/RouterV3.php:210,266-289 | legacy request info log | generic request log;password/passwd key 過濾由 router 層統一處理。 |
Endpoint/V1/QuoteServices.php:272-530 | 1071-1126, 1137-1174, User.php:841-849 | Google / Facebook / Facebook Limited / Apple identity resolve 與既有 user social lookup / attach;social id lookup 不加 is_active filter,對齊 legacy User::getBySocialId()。 |
Endpoint/V1/QuoteServices.php:532-736 | 1175-1318 | provider user 建立;使用 explicit allowlist,不複製既有 row;無 name 時 profile fallback 用 safe username。 |
Endpoint/V1/QuoteServices.php:738-829 | 1238-1307 | 建立 profile、phone verification、referral。 |
Endpoint/V1/QuoteServices.php:831-927 | 1646-1667, 1178 | API session upsert、safe username、generic explicit insert helper。 |
Endpoint/V1/QuoteServices.php:930-1013 | 1537-1546, 1548-1565, QuoteService.php:17-32 | 建立 service、補 slug、更新 user / city counter cache、更新 user onboard、category mapping、external review URL。 |
Endpoint/V1/QuoteServices.php:996-1036 | 1388-1460 | category parse;保留 QuoteCategory: [""],只把正整數放進 numeric mapping。 |
Endpoint/V1/QuoteServices.php:1038-1192 | 1417-1535 | service 欄位 normalization、zip/address/city/service_area/company name、is_not_this_area、insert data allowlist;zip-derived city/state 會覆蓋 request city/state。 |
Endpoint/V1/QuoteServices.php:1194-1300 | 1376-1383, AppModel.php:101-118, SluggableBehavior.php | top-level City[name/id]、State[name/id] 相容;有 id 用 id,否則依 name find-or-create。新增 city 時補 legacy Sluggable 格式 slug。 |
Endpoint/V1/QuoteServices.php:1302-1431 | 1388-1438 | travel option、model validation、service_area、country resolve。 |
Endpoint/V1/QuoteServices.php:1433-1561 | 1567-1643 | side effect:category mapping、service_update、activity、abnormal scan、welcome event、Nshop、task queue、other category。 |
Lib/Model/QuoteService.php:1957-1995 | 382-388, 1537-1546 | 建立 security hash 與 YOURLS short URL,並寫回 quote_services.external_review_url。 |
Lib/Model/QuoteService.php:1997-2034 | 4828-4864 | Require_New_Category email event。 |
Lib/Model/QuoteService.php:2036-2082 | 4866-4929 | Pro_Signup MoEngage event。 |
Lib/Model/QuoteService.php:2085-2097 | 5196-5213, 6133-6137 | CRM no-op 與 XML invalid char filter。 |
Lib/Model/NshopServiceCategory.php:44-79 | NshopServiceCategory.php:13-36 | 補 checkNew(),非 HK/SG 時建立缺少的 Nshop category mapping。 |
Lib/Model/AbnormalConfig.php:208-292, 418-541 | AbnormalConfig.php:307-378 | user / service keyword / device scan;legacy 已停用的 consumer / multi-user device scan 保留 return false。 |
Lib/Util/AreaUtil.php:132-143, 288-351 | legacy AreaUtil | 補 service city、zip / address helper,維持 legacy full_address prepend 行為。 |
現有 Method 細對狀態
quote_services/add 直接呼叫多個新專案既有 model / helper / util。這些 method 存在不代表已自動等同 legacy;review 時需逐項確認 caller、legacy 行號與 side effect。
已確認本次可改且已對齊:
| 範圍 | 結論 |
|---|---|
normalizeQuoteServiceAddTravelOptions() | 這是 quote_services/add 專用 method。legacy 在 travel_upto 未勾時一定把 service_provider_travels_upto 清成 0;新版已同步,不再從 travel_pref JSON 保留距離。 |
email_verify_code social guard | legacy 只在 Google / Facebook / Facebook Limited 有驗證資料時跳過 email verify code;Apple 不會跳過。新版已同步,Apple + email_verify_code 仍會 consume verification、改用 verification email、設定 is_email_confirmed = 1。 |
auto-signup users 欄位 | legacy QuoteServicesController::add() 沒有設定 mobile_app_hash、mobile_app_time_modified、iphone_last_request、security_question_id。新版已移除這些非 legacy 指派;mobile_app_* 維持 DB default NULL,iphone_last_request / security_question_id 走 legacy 非 strict MySQL implicit value。 |
| user lookup active filter | legacy User::getById()、User::getByEmail()、User::getBySocialId() 都沒有 is_active condition。新版 fetchQuoteServiceAddUserWithProfileById()、fetchQuoteServiceAddUserWithProfileByEmail()、fetchQuoteServiceAddUserBySocialIdentity() 已移除 u.is_active = 1,避免 inactive user path 與 legacy 不一致。 |
Migration decision:
- 既有 user 用 Google id token 登入且原本沒有
google_user_id時,legacy 會呼叫QuoteUtil::update_user_google_link($user_id, $google_auth_data)。 - legacy helper 會更新
google_access_token,但呼叫端沒有傳第三個$id_token,所以實際會把既有users.google_access_token清成空字串。 - new 不照搬這個清空 token 的 side effect。
quote_services/add的責任是新增 service 與必要 social linkage;保留既有google_access_token可避免新增 service 流程破壞其他 Google token 使用情境。 - new 仍對齊必要 linkage 欄位:
is_google_connected = 1、google_user_id、google_avatar_url。
auto-signup user 欄位對照原則:
- legacy 明確指派:
role_id、username、email、password、device_id、is_active、onboard_state、ip_id、social register / connected 欄位、email verification 欄位、entrance、registration_source、iphone_last_access、last_access_client、multi_lang_id。 - new explicit allowlist 只可補 legacy/Cake/DB 會產生的等價 default,例如 counter
0、nullable 欄位NULL、非 strict MySQL 會落出的空字串 / zero date。 - 不可加入其他註冊流程才有的欄位,例如
mobile_app_hash。
已查 shared method,暫不改共用行為:
| new method | legacy 對照 | 結論 |
|---|---|---|
ApiSession::upsert() | legacy ApiSession::upsert() | 兩邊皆依 user / device upsert session、更新 expires、寫 api_key 與 api_session log。new 只多 PHP 8.2 / CLI fallback;此 method 有其他 caller,不直接改 shared behavior。quote_services/add 透過 upsertQuoteServiceAddSession() 補齊 legacy 需要的 API key server value。 |
仍需在後續 review 細對,不可因已呼叫 existing method 就視為完成:
| 類型 | method / source | 要確認 |
|---|---|---|
| Social auth | Pro360LoginUtil::verify_google_id_token()、verify_fb_access_token()、FacebookUtil::verify()、AppleVerifyLog | token 驗證來源、Android fallback、user lookup / social id attach、錯誤時是否繼續走 signup。 |
| Email / phone 驗證 | EmailVerification::getByVerifyCode()/consume()、PhoneVerification::afterAuth()、PhoneUtilFactory::get()、User::runResetPhoneConfirm() | verify code consume、phone normalize、confirmed flag、log / side effect。 |
| Referral / metadata | SecurityHash::getByHash('UserReferral')、UserReferral::addReferral()、ApiKey::getClientByKey()、MultiTranslation::getHeaderLangId()、Ip::toSaveIp() | referral 寫入、registration source、language、IP 欄位是否與 legacy 一致。 |
| Service helper | ServiceCertificate::isCompanyConfirmed()、QuoteService::getExternalReviewUrl()、sendProviderWelcomeMail()、sendProviderNeedNewCategoryMail() | 公司名驗證、短網址、MoEngage / email event payload 與 config。 |
| Queue / email / report | TaskEventQueue、TaskQueue、EmailAdminList、EmailQueue、NshopServiceCategory::checkNew()、AbnormalConfig::newServiceScan() | queue key、payload、worker log、error path 是否中斷主流程。 |
已知需留意:new sendProviderNeedNewCategoryMail() 目前有 empty email guard;legacy helper 沒有這個 guard。此 method 也被 shared 使用,目前 /Users/mattsu/Documents/Site/web-app 未看到 quote_services/add 送 other|... category,所以先不改 shared helper。若 staging 測到 add 的 other category 且 email 可為空,需補 add 專用相容處理。
Legacy HTML View-only 行為
legacy QuoteServicesController::add() 在 user resolve 成功後會查 Sudopay payout 連接狀態:
QuoteServicesController.php:1354-1368這段只設定 is_payout_error、userDetails、step 給 QuoteServices/add.ctp 使用;JSON success response 沒有輸出這些欄位。Sudopay::isHavingPendingGatewayConnect() 與 Sudopay::GetUserConnectedGateways() 只讀 sudopay_payment_gateways / sudopay_payment_gateways_users,不寫 DB。
2026-06-12 正式機 log 只看到 /quote_services/add.json,未看到 legacy HTML path /quote_services/add 或 /quote_services/add/...。因此新版 API 不搬這段 Sudopay view state;這是基於流量與 JSON response 對照,不是漏檢。若未來要重建 legacy HTML 新增服務頁,這段需另列為頁面行為搬移。
前端目前 /Users/mattsu/Documents/Site/web-app 的 add 呼叫主要送 QuoteService[travel_pref] JSON:
ProQuestionnairePopupContainer -> APIManager.postService()ServicesContainer.handleNewService() -> APIManager.postService()GoogleReservePro -> APIManager.postService()
目前未看到這些前端路徑送 legacy checkbox 欄位 travel_upto;但 API 仍保留舊客戶端的 checkbox validation 與距離清除規則。
DB Side Effect
成功時可能寫入或更新:
userscitiesuser_profilesuser_loginsapi_sessionsphone_verification_logsuser_referralsquote_servicessecurity_hashesquote_categories_quote_servicesnshop_service_categoriestask_event_queuequote_activitiesabnormal_check_itemstask_queueevent_queuequote_reportsemail_queue
驗證時不要只看 quote_services;至少要依情境確認 service row、category mapping、activity、queue/event,以及新 user path 的 user/profile/session。
other category path 的 quote_reports 要對齊 legacy:
type = need_categoryreported_user_id = user_idmessage = {user_email}, {other_category_1}, {other_category_2}- admin email subject / body 都是
Pro signup other categories: {message}
Staging Legacy 實測
2026-06-12 以 legacy staging 呼叫:
POST /quote_services/add.json
QuoteService[business_name]=longlong
QuoteService[travel_pref]={"travel_vendor":{"value":"1"},"travel_customer":{"value":"1"},"remote":{"value":"1"}}
QuoteService[full_address]=台北市中山區中正路1號
QuoteService[zip_code]=104
QuoteService[country_id]=222
QuoteCategory[0]=response:
{"error":0,"service_id":"13664","session_id":"baigkocqghnqpu60bipc5ork97","user_id":"8617"}DB / log 重點:
- legacy staging
monolog_202606.channel = quote_services/add::POST保留原始 request,確認QuoteCategory = [""]。這是歷史 legacy 驗證資料;new 不再補同名 endpoint duplicate log。 quote_services.id = 13664建立成功,slug = 861713664,external_review_url = https://p36.net/sw/QFUWt,並建立security_hashes(model=QuoteService, foreign_id=13664)。- request 沒有
QuoteService[address],legacy 保留傳入的full_address = 台北市中山區中正路1號,address = "";只用zip_code = 104補city_id = 360、state_id = 5466、country_id = 222。 - legacy
QuoteServicemodel 透過 counter cache 更新users.quote_service_count與cities.quote_service_count;新版需明確更新,不能只寫 service row。 phone_number未傳時 fallback 到user_profiles.phone = 0908189361。- travel flags 依
travel_pref設為三個1,service_provider_travels_upto = 0。 - 建立
quote_activities.quote_activity_type_id = 25(Create Service)。 - request 沒有
QuoteService[phone_number],未建立task_event_queue.service_update。 - worker 後續建立
event_queue_log_4.event_key = New_Service_Match;因此驗 notification 要查 queue-or-log。
注意:這筆 service 後續又被前端流程更新過。09:34:46 出現 Update Service activity,並把 quote_category_ids / quote_categories_quote_services 改成 category 346。因此目前 DB 上的 quote_category_ids = 346 不是 add.json 初始結果;判斷 add 初始 category 要以當時 legacy request log 與 update 前後時間線為準。new 切換後改看 RouterV3 generic request log 或測試時保存的 request body。
Config / 部署檢查
切 staging / production 前需確認實際部署 config,不可只看本地 legacy checkout:
area 是非台灣環境開關,不是台灣設定。台灣環境不可補 ProConfig::$config['area'] = 'tw';只有確認部署為 sg / hk 等非 TW 環境時才設定 area。sys_area = tw 不能當成要補 area = tw 的依據。
| key / source | 用途 |
|---|---|
site.url | external review long URL。 |
yourls.api_url, yourls.api_key | service 建立後產生 short URL。 |
email.signature | other category provider email。 |
mail.icon.help_center | Require_New_Category template replacement;new 有 fallback URL,但部署環境仍應同步。 |
mail.icon.resume_instruction | Require_New_Category template replacement;new 有 fallback URL,但部署環境仍應同步。 |
newCategoryEmailList | other category admin email list。 |
EmailTemplate.from_email | other category admin email 的 default from。 |
| API key client/platform setting | registration_source、last_access_client、Google token Android fallback。 |
| Google / Facebook / Apple login settings | social signup / login path。 |
site.is_enabel_freelancer_user_type | legacy 入口有 guard;legacy schema 預設 0。new 目前沒有同名設定,切換前需確認實際 production 仍為 false;若 production 為 true,需補 role guard。 |
QuoteService::getExternalReviewUrl() 會實際呼叫 YOURLS;本機 PHPUnit 不直接打 full endpoint,以免依賴外部短網址服務。部署後仍需用 staging curl + DB 查驗 full path。
PHP 5.6 -> 8.2 注意
- 不可把 nullable input 直接丟給
trim()、mb_strpos()、json_decode()後假設一定是原型態;新版 helper 先做 cast / type guard。 - 不可用
SELECT * FROM quote_services LIMIT 1當 insert template;新版對users、quote_services、mapping/report 類 insert 都使用 explicit allowlist。 travel_pref非空時 legacy 會跳過 checkbox validation;新版也維持這個分支,但用 type guard 避免 PHP 8.2 nullable / property warning。full_address是否重組看 request 的is_not_this_area,不是看部署area;is_not_this_area=1時要保留前端傳入值。QuoteCategory: [""]是 legacy 可通過的 array,不可改成空 category error。- Facebook Limited login 的 legacy profile name fallback 最終會落到 safe username;新版維持可見結果,不用完整 email,也不改用 Facebook Limited name。
測試
php -l Endpoint/V1/QuoteServices.php
php -l Lib/Model/QuoteService.php
php -l Lib/Model/NshopServiceCategory.php
php -l Lib/Model/AbnormalConfig.php
php -l Lib/Util/AreaUtil.php
docker exec -w /project-data cd63f9147e8d ./vendor/bin/phpunit tests/QuoteServicesAddTest.php目前結果:
OK (17 tests, 59 assertions)測試覆蓋:
- success response 的
service_id、user_id維持 legacy JSON string 型別,並以assertIsString()防止 regression。 QuoteCategory: ["", "123", "other|測試分類"]會保留空白 category id,只把123放入 numeric mapping,other category 另行記錄。- service normalization:zip/address/city/service_cities、phone fallback、other category 追加 description、legacy 巢狀
travel_prefflags。 - top-level
City[name/id]、State[name/id]相容;新增 city slug 維持 legacy Sluggable 格式;zip-derived city/state 會覆蓋 request city/state。 - 沒有送
QuoteService[address]時,即使有zip_code也保留傳入的full_address;有送address時才照 legacy 重組full_address。 - legacy model counter cache 會更新
users.quote_service_count與cities.quote_service_count;新版已補 explicit counter update。 service_updatequeue 只看 request 是否原本帶QuoteService[phone_number];profile phone fallback 不觸發。travel_upto未勾時,service_provider_travels_upto會照 legacy 清成0,即使 request /travel_pref帶了距離。email_verify_codesocial guard 對齊 legacy:Google / Facebook / Facebook Limited 會跳過;Apple 不會跳過。- auto-signup user insert method 不可包含 legacy 未指定的高風險欄位:
mobile_app_hash、mobile_app_time_modified、iphone_last_request、security_question_id。 - legacy validation message:缺 category、缺 travel option。
is_not_this_area=1時保留原full_address,不套台灣地址重組。- 新 user 無
User[name]時,profile first name fallback 使用 safe username,不使用完整 email。 - Facebook Limited signup 無
User[name]時,profile first name 也維持 legacy fallback。
待 staging 驗證
部署後需補一筆完整 curl / DB 實測,至少覆蓋:
- logged-in provider 新增 service。
QuoteCategory: [""]新增 service。- numeric category mapping。
- other category side effect。
- 新 email signup path。