QuoteServices Edit
API:
POST /quote_services/edit/{quote_service_id}.json功能說明
provider 編輯既有專家檔案時呼叫。legacy edit() 不是單純更新 quote_services;JSON API path 會同時處理 service ownership、contact method、logo attachment、照片 / YouTube、地址與服務區域、category mapping、內容審核、show status、activity、profile score、service update event 與 rematch queue。
成功 response:
{"error":0,"message":"Success"}搬移狀態
- legacy API:
QuoteServicesController::edit($id)。 - new API:已補
Endpoint/V1/QuoteServices.php::edit()。 - routing:已補
/quote_services/edit/{quote_service_id}.json。 - 本文件狀態:已補 action + routing + PHPUnit。
正式機流量
前次查正式機 access log 約:
14327 POST /quote_services/edit/{id}.json 200
4307 OPTIONS /quote_services/edit/{id}.json 200判斷:quote_services 目前未搬 API 中,edit 正式流量最高,且與 add 同屬 service profile 表單主線。
快速結論
- 呼叫者:必須是 JSON request 且有有效 API session;legacy path 會取 session user id。
- ownership guard:legacy 只用
quote_services.id = path id查 service,接著比對quote_services.user_id != session user id。不可自行加is_active、is_archived、show_status_type、role_id或 provider-only 類條件。 - API path 不做 legacy web form 的 category / travel checkbox required validation;那些 validation 在
$is_from_api === false分支才執行。 QuoteServiceContact若有出現在 request,legacy 會針對所有 available keys 做全量同步:沒送或送空值會 remove,Website / LINE remove 時也會清舊欄位website_url/line_id。- category 更新不是單純改
quote_category_ids:legacy 會過濾 active category、更新主欄位、必要時 transaction 內重建quote_categories_quote_services,保留舊 mapping 的統計欄位,並對移除的quote_service_categories執行 bid status / category log / nshop 同步 side effect。 phone_number只要 key 存在,就送task_event_queue.service_update;array 值會先 JSON encode。business_name會 trim 半形 / 全形空格、空白直接回error=1,做 XML invalid char filter、截 30 字,寫 content review,並依台灣公司字樣檢查is_valid_name。- 原 service
show_status_type = OFF且prohibited_type != CLOSE_SERVICE_PAGE時,編輯後改成WAIT_ON並寫 prohibited log。 - 成功後一定寫
UpdateServiceactivity;若本次 API 把is_active從 0->1 或 1->0,activity type 改為ActivateService/DeactivateService。
Legacy Rule
legacy controller:
/Users/mattsu/Documents/Site/get-lancer-php56/app/Plugin/Quotes/Controller/QuoteServicesController.php| legacy 行號 | 職責 |
|---|---|
1734-1748 | edit($id) 入口、freelancer user type guard、null id guard、初始化 content review / change log model。 |
1749-1754 | JSON + valid API session 時標記 $is_from_api = true、取 session user id、把 path id 塞入 QuoteService.id。 |
1755-1785 | QuoteServiceContact 全量同步:trim、validate、edit / remove;remove Website 清 website_url,remove LINE 清 line_id。 |
1787-1790 | Attachment[filename] 存在時呼叫 _saveAttachment($id, null, filename) 更新 logo attachment。 |
1792-1866 | QuoteServicePhoto 新增 / 更新 / 刪除;YouTube URL / preview 格式驗證;照片異動寫 quote_service_change_logs;一般照片產縮圖。 |
1867-1880 | 編輯前用目前 service business_name + description 做 abnormal keyword scan;例外只寫 service_edit log,不中斷。 |
1887-1893 | 有 request data 時只用 QuoteService.id = id 查原 service,recursive = -1;沒有其他 active / archived guard。 |
1894-1900 | JSON API 才允許更新 is_active,並判斷本次是否 activate / deactivate;非 API path 保留原 is_active。 |
1903-1915 | 缺 QuoteService.user_id 時沿用原 service owner;支援 top-level City[name/id]、State[name/id]、QuoteService[country_id] lookup。 |
1918-1939 | API path 只做 ownership guard;category / travel checkbox validation 只在非 API path。 |
1940-1951 | 通過 validation 後初始化 rematch flag、取得 language 與 AreaUtilFactory::get(Configure::read('area'), lang_id)。 |
1952-1965 | zip_code key 存在且與原 service 不同時設 $to_rematch = true,用 zip 覆蓋 city/state/country,country 固定 222。 |
1967-1991 | is_not_this_area 為 1 時清 zip/city/state/country;非 foreign 且 address 或 zip key 存在時重算 full_address。 |
1993-2013 | service_area key 存在且非空時做 300/600 展開、JSON encode、更新 service_cities。空值時不改欄位。 |
2015-2036 | QuoteCategory 非空 array 時去重、用 active category 過濾、比較新增 diff 決定 rematch、更新 quote_category_ids。 |
2038-2041 | phone_number 非空 array 時 JSON encode。 |
2043-2044 | 每次成功更新前補 external_review_url = QuoteService::getExternalReviewUrl(...)。 |
2046-2047 | request 有 is_public key 時同步到 is_show_my_address_to_all_visitor。 |
2049-2089 | business_name:trim、去全形空格、空白回 error=1,XML invalid filter、截 30 字、content review、TW 公司名驗證。 |
2091-2093 | description 非空時寫 content review。 |
2095-2101 | service 原本 OFF 且不是 CLOSE_SERVICE_PAGE 時,改 show_status_type = WAIT_ON 並寫 prohibited log。 |
2102-2118 | QuoteService->save() 最多 retry 5 次;成功後 updateProfileScoreById();lock timeout 寫 quote_service_edit log。 |
2120-2127 | request 有 QuoteService[phone_number] key 時送 task_event_queue.service_update。 |
2129-2132 | 寫 UpdateService / ActivateService / DeactivateService activity,param1 = request data JSON,param2 = tabIndex;此時 request data 已被補入 QuoteService.id/user_id/external_review_url。 |
2134-2141 | category 更新後依 API key 判斷 is_sync_service_category,呼叫 _checkToRestoreCategoryMap()。 |
2143-2146 | zip 或新增 category diff 時,送 task_queue.newQuoteService,expiry time() + 90。 |
2148-2152 | API success response:error=0, message=Success。 |
2169-2174 | API failure response:error=1, message=Failed to edit service: {error} {checkbox_error}。 |
3530-3568 | _saveAttachment():upsert attachments class QuoteService、review_status=0、存 filename 或 S3 URL、產縮圖,有 original URL 時設 quote_services.has_logo = 1。 |
4175-4295 | _checkToRestoreCategoryMap():transaction + row lock、刪舊 mapping、刪除不在新 category 的 service category、active 且未 archived 時重建 mapping 並保留舊統計欄位、對新增 category 呼叫 NshopServiceCategory::checkNew()。 |
legacy helper:
| legacy 檔案行號 | 職責 |
|---|---|
QuoteServiceContact.php:13-23 | contact available keys:LINE、WhatsApp、WeChat、Website、Facebook、Instagram、Behance、Dribbble、Github。 |
QuoteServiceContact.php:40-69 | contact value validation;通訊 ID pattern 與 URL pattern。 |
QuoteServiceContact.php:71-110 | contact edit;存在則 update + active,值變更寫 change log;不存在則 insert + change log。 |
QuoteServiceContact.php:112-135 | contact remove;存在則設 inactive,原本 active 才寫 delete change log。 |
QuoteServiceChangeLog.php:12-18 | 寫 quote_service_change_logs.quote_service_id/save_data。 |
QuoteServiceContentReview.php:13-51 | content review upsert;鎖 service row,新增或 merge original_content,非 BYPASS 狀態重設 WAITING。 |
QuoteService.php:357-388 | getExternalReviewUrl();已有 URL 直接回傳,空值時建立 security hash + short URL 並寫回 service。 |
QuoteService.php:5628-5646 | updateProfileScoreById();重算 profile score 並更新 quote_services.profile_score。 |
QuoteService.php:6133-6137 | XML invalid char filter。 |
QuoteServiceCategory.php:1989-2060 | removeServiceCategory();移除分類時把未接觸的 auto quote bid 改 Removed、刪 quote_service_categories、寫 category log;需要同步 shop 時刪 nshop category 並標 nshop items deleted。 |
ServiceCertificate.php:98-104 | isCompanyConfirm():service_certificates.user_id = user_id AND type = 1 AND status = 5。 |
New PHP 8.2 對照
| new 檔案行號 | 對應 legacy | 狀態 |
|---|---|---|
Lib/Common/RouterRule/Mapping.php:53-56 | API route | 已補 /quote_services/edit/{quote_service_id}.json 到 QuoteServices::edit()。 |
Endpoint/V1/QuoteServices.php:148-186 | 1734-1754, 2148-2152, 2169-2174 | endpoint 入口、API key/session guard、request data 初始化、依 legacy 順序呼叫 contact / attachment / photo / scan / service update。 |
Endpoint/V1/QuoteServices.php:188-227 | 1755-1785 | QuoteServiceContact 全量同步;invalid contact 直接回 error 2;remove Website / LINE 時清 website_url / line_id。 |
Endpoint/V1/QuoteServices.php:229-301 | 1787-1790, 3530-3568 | attachment upsert:class=QuoteService、review_status=0、更新 amazon_s3_original_url 或 filename,呼叫 thumbnail helper,original URL 存在時設 has_logo=1。 |
Endpoint/V1/QuoteServices.php:303-431 | 1792-1866 | QuoteServicePhoto 新增 / 更新 / 刪除、YouTube URL validation、change log;一般照片 type_id=0 呼叫 thumbnail helper,影片不呼叫。 |
Endpoint/V1/QuoteServices.php:433-457 | 1867-1880 | abnormal keyword scan;例外只寫 service_edit log,不中斷。 |
Endpoint/V1/QuoteServices.php:463-548 | 1887-1939, 2102-2152, 2169-2174 | owner guard、is_active transition、主 service update、profile score、phone queue、回填 activity payload 欄位、activity、category rebuild、rematch queue、response。 |
Endpoint/V1/QuoteServices.php:563-739 | 1903-2101 | edit update normalization:top-level City/State、country/zip/address/service_area/category/phone/is_public/business_name/description/show_status。 |
Endpoint/V1/QuoteServices.php:750-799 | 1993-2036 | service area 展開 300/600;category 去重、active 過濾。 |
Endpoint/V1/QuoteServices.php:801-852 | 2102-2108 | explicit quote_services update allowlist;不全量寫 request。 |
Endpoint/V1/QuoteServices.php:859-958 | 4175-4295 | category mapping restore:row lock、刪舊 mapping、移除舊 service category 時呼叫 removeServiceCategory()、保留舊統計欄位、active+not archived 才重建、新 category 呼叫 Nshop check。 |
Endpoint/V1/QuoteServices.php:960-970 | 2143-2146 | rematch 時寫 task_queue.newQuoteService,expiry=time()+90;不改 shared TaskQueue::insert()。 |
Endpoint/V1/QuoteServices.php:972-978 | 2169-2174 | failed response helper。 |
Lib/Model/QuoteServiceCategory.php:2397-2445 | QuoteServiceCategory.php:1989-2060 | 已補 removeServiceCategory():更新 quote_bids Removed、刪除 quote_service_categories、寫 quote_service_category_logs,並依 $isSyncServiceCategory 呼叫 NshopServiceCategory::deleteBySC()。 |
Lib/Util/UploadS3Util.php:221-312 | QuoteUtil.php:100-174 | genAttachmentThumb():支援 Attachment / QuoteServicePhoto,下載 original、480x480 resize、小圖 thumb=original、大圖上傳 S3 thumb/...,normalize http->https 與 CDN,更新 original/thumb URL;失敗回 false 不中斷 caller。 |
Lib/Model/QuoteServiceContact.php:18-43 | QuoteServiceContact.php:13-38 | available keys 已存在。 |
Lib/Model/QuoteServiceContact.php:45-74 | QuoteServiceContact.php:137-150 | 讀取 active contact map,維持既有 caller 行為。 |
Lib/Model/QuoteServiceContact.php:76-105 | QuoteServiceContact.php:40-69 | 已補 contact value validation。 |
Lib/Model/QuoteServiceContact.php:107-158 | QuoteServiceContact.php:71-110 | 已補 edit:update/insert active contact,並寫 change log。 |
Lib/Model/QuoteServiceContact.php:160-189 | QuoteServiceContact.php:112-135 | 已補 remove:設 inactive,原本 active 才寫 delete change log。 |
Lib/Model/QuoteServiceChangeLog.php:11-16 | QuoteServiceChangeLog.php:12-18 | 已有同語意 add,可直接用或包在 edit 專用流程。 |
Lib/Model/QuoteServiceContentReview.php:28-67 | QuoteServiceContentReview.php:13-51 | 已有同語意 addReview,且補 PHP 8.2 empty JSON fallback。 |
Lib/Model/QuoteService.php:948-968 | QuoteService.php:5628-5646 | 已有 updateProfileScoreById()。 |
Lib/Model/QuoteService.php:1957-1995 | QuoteService.php:357-388 | 已有 getExternalReviewUrl()。 |
Lib/Model/QuoteService.php:2095-2098 | QuoteService.php:6133-6137 | 已有 XML invalid char filter。 |
Lib/Model/ServiceCertificate.php:73-77 | ServiceCertificate.php:98-104 | method 名稱不同:new 是 isCompanyConfirmed()。 |
Lib/Model/QuoteActivity.php:108-129 | legacy activity helper | 已有 createQuoteServiceActivityById()。 |
Lib/Constant/ConstQuoteActivityType.php:37-39 | legacy constants | UpdateService=26、ActivateService=27、DeactivateService=28 已對齊。 |
Request Mapping
| input | legacy 規則 | 實作注意 |
|---|---|---|
path quote_service_id | 寫入 QuoteService.id,也是查原 service id。 | 需 cast int,但 response 語意仍跟 legacy。 |
| API session | 只有 JSON + valid session 進 API path。 | invalid session 已由 2026-06-16 legacy staging curl 確認為 HTTP 403 + {"error":99999,"message":"invalid_user"}。 |
QuoteServiceContact[...] | key 存在且為 array 才處理;所有 available keys 全量同步。 | 不可只更新 request 有送的 contact key。 |
Attachment[filename] | 非空才更新 logo attachment;第三參數是 filename。 | 欄位命名容易誤導:legacy 傳到 _saveAttachment($id, null, filename),new 依 legacy 寫入 amazon_s3_original_url。 |
QuoteServicePhoto | 可單筆或 array;新增、更新、刪除;YouTube 做 URL validation。 | PHP 8.2 要避免 delete 未定義 warning 改變流程。 |
QuoteService[is_active] | API path 可更新,並影響 activity type。 | 非 API path 不搬。 |
City[name/id]、State[name/id] | name 存在時用 id 或 find-or-create。 | 可沿用 add 的城市/州 helper,但不可新增不同 guard。 |
QuoteService[country_id] | 非空時用 legacy country lookup。 | 需確認 edit 傳入值格式。 |
QuoteService[zip_code] | key 存在且值改變才 rematch;zip 找到 city data 時覆蓋 city/state/country。 | 不可因同 zip 重算 rematch。 |
QuoteService[is_not_this_area] | 1 清 location 欄位;非 1 設回 0。 | full_address 只在非 foreign 且 address/zip key 存在時重算。 |
QuoteService[service_area] | key 存在且非空才展開與 JSON encode;空值不改。 | 需保留 legacy 空值不清欄位的行為。 |
QuoteCategory | 非空 array 才處理;過濾 active category;更新 quote_category_ids。 | API path 不要求 category 必填。若沒送,不重建 mapping。 |
QuoteService[phone_number] | array 非空 JSON encode;key 存在時送 service_update queue。 | 即使值是空字串,只要 key 存在仍要送 queue;JSON encode 只在非空 array。 |
QuoteService[is_public] | key 存在時同步到 is_show_my_address_to_all_visitor。 | 不可自行改成只接受 boolean。 |
QuoteService[business_name] | trim、去全形空格;空白回 error=1;filter、截 30 字、content review、公司驗證。 | 台灣環境 Configure::read('area') empty 才做「公司」字樣驗證;不要補 area=tw。 |
QuoteService[description] | 非空時寫 content review。 | legacy 只檢查 non-empty,不處理空字串清 content review。 |
QuoteService[id/user_id/external_review_url] | controller 會補回 request data 後寫入 activity param1。 | new 需回填到 $requestData,否則 DB audit payload 與 legacy 不一致。 |
tabIndex | 寫入 activity param2。 | 缺少時 legacy 可能 notice;new 需以 null 兼容 PHP 8.2。 |
DB Write 欄位分類
以下是目前 explicit write allowlist。未列入欄位不得寫入;若後續實作需新增欄位,需先補本表並標明來源。
quote_services update
來源:legacy QuoteServicesController::edit() 以 request data 為主,再補 legacy 明確衍生欄位。
| 欄位 | 來源 |
|---|---|
id | path {quote_service_id},legacy 1753。 |
user_id | request 未帶時沿用原 service owner,legacy 1903-1905。 |
is_active | API request QuoteService[is_active],legacy 1894-1900。 |
city_id | City[id] 或 City[name] find-or-create;zip lookup 也可覆蓋,legacy 1907-1909, 1958-1963。 |
state_id | State[id] 或 State[name] find-or-create;zip lookup / foreign 也可覆蓋,legacy 1913-1915, 1958-1963, 1969-1981。 |
country_id | QuoteService[country_id] lookup、zip lookup 固定 222、foreign 清 0,legacy 1910-1912, 1958-1963, 1969-1981。 |
zip_code | request zip 或 foreign 清 0,legacy 1952-1965, 1969-1981。 |
is_not_this_area | request flag normalization,legacy 1967-1981。 |
address | request QuoteService[address]。legacy 由 Cake save request data 寫入。 |
full_address | address/zip key 存在且非 foreign 時由 AreaUtil 格式化,legacy 1984-1991。 |
service_area | non-empty request service_area JSON encode,legacy 1993-2010。 |
service_cities | service_area 對應 city ids,legacy 2010-2011。 |
quote_category_ids | active category 過濾後 implode,legacy 2015-2036。 |
phone_number | request phone;非空 array JSON encode,legacy 2038-2041。 |
external_review_url | QuoteService::getExternalReviewUrl(),legacy 2043-2044。 |
is_show_my_address_to_all_visitor | request is_public 同步,legacy 2046-2047。 |
business_name | trim/filter/substr 後 request value,legacy 2049-2072。 |
is_valid_name | TW 公司名驗證結果,legacy 2074-2087。 |
description | request QuoteService[description]。legacy 由 Cake save request data 寫入。 |
show_status_type | 原 service OFF 且非 close page 時改 WAIT_ON,legacy 2095-2101。 |
website_url | remove Website contact 時清空,legacy 1777-1780。 |
line_id | remove LINE contact 時清空,legacy 1780-1782。 |
profile_score | QuoteService::updateProfileScoreById() 重算,legacy 2106-2108, model 5628-5646。 |
has_logo | _saveAttachment() 在 attachment 有 original URL 時設 1,legacy 3563-3567。 |
Migration decision:new 不全量寫入 request;只寫本表與 updateQuoteServiceEditRow() allowlist 中欄位。legacy Cake save($this->request->data) 可能允許更多 QuoteService[...] 欄位透過 request 更新,但前端 service edit 主線目前只送本 allowlist 內的 profile 欄位。若後續發現舊 client 依賴其他欄位,需先補本表與測試。
quote_service_contacts insert/update
| 欄位 | 來源 |
|---|---|
quote_service_id | path id,legacy 1775, model 99。 |
contact_key | QuoteServiceContact available key,legacy model 13-23, 100。 |
contact_value | trim 後 request value,legacy 1760-1771, model 86, 101。 |
is_active | edit 設 1,remove 設 0,legacy model 87, 102, 125。 |
attachments upsert
| 欄位 | 來源 |
|---|---|
class | literal QuoteService,legacy 3535-3547。 |
foreign_id | path service id,legacy 3536-3547。 |
review_status | literal 0,legacy 3543, 3548。 |
amazon_s3_original_url | _saveAttachment() 第三參數 $s3_url;API path 傳 Attachment[filename] 到第三參數,legacy 1787-1789, 3550。 |
filename | 只有 _saveAttachment() 第二參數 $filename 時寫;edit API path 目前未使用,legacy 3551。 |
quote_service_photos insert/update/delete
| 欄位 | 來源 |
|---|---|
user_id | session user id,新增照片時設定,legacy 1825。 |
quote_service_id | path service id,新增照片時設定,legacy 1826。 |
| request photo fields | QuoteServicePhoto request data,legacy 1827-1850 走 model validation。 |
type_id | request type_id cast int,default 0,legacy 1800, 1820。 |
review_status | 更新既有照片時設 ConstAttachmentReviewStatus::NOT_YET,legacy 1847。 |
new 已查 quote_service_photos schema,實作只寫 user_id、quote_service_id、caption、sort_order、amazon_s3_original_url、amazon_s3_thumb_url、type_id、review_status 與 timestamps。
quote_categories_quote_services rebuild
| 欄位 | 來源 |
|---|---|
created, modified | current timestamp,legacy 4231-4236。 |
quote_service_id | path service id,legacy 4237。 |
quote_category_id | active numeric category id,legacy 4232-4239。 |
| hired/count/rate/order fields | 若舊 mapping 有同 category,保留舊 row 統計欄位,legacy 4239-4251。 |
其他 side-effect tables
| table | 寫入來源 |
|---|---|
quote_service_change_logs | contact / photo add/update/delete,legacy contact model 90-108, 128-132;photo 1857-1862。 |
quote_service_content_reviews | business_name / description content review,legacy 2072, 2091-2093。 |
quote_service_prohibited_logs | OFF service 編輯後 WAIT_ON,legacy 2095-2101。 |
task_event_queue | request 有 phone_number 時 service_update,legacy 2120-2127。 |
quote_activities | UpdateService / ActivateService / DeactivateService,legacy 2129-2132。 |
task_queue | rematch 時 newQuoteService,expiry time()+90,legacy 2143-2146。 |
quote_bids | 移除 service category 時,未接觸的 auto quote bid 改 Removed,legacy QuoteServiceCategory::removeServiceCategory() 1995-2004。 |
quote_service_category_logs | 移除 service category 時寫 ["deleted"];同步 shop 刪除時寫 is_shop/is_delete/service_notice,legacy 2009-2014, 2041-2051。 |
quote_service_categories | category rebuild 時,不在新 category set 的 row 走 removeServiceCategory() 刪除,legacy _checkToRestoreCategoryMap() + model 4207-4216, 2006-2007。 |
nshop_service_categories | 新增 category mapping 時 checkNew();移除 category 且 $is_sync_service_category true 時 deleteBySC(),legacy _checkToRestoreCategoryMap() 4268-4272, model 2016-2054。 |
Query / Guard 對齊
實作時不可新增 legacy 沒有的 guard:
- 查原 service 只用
QuoteService.id = id,不可加is_active = 1、is_archived = 0、show_status_type或 role condition。 - ownership guard 是查到 row 後比對
quote_service.user_id != session user id,錯誤字串為Invalid user,最後走error=1failed response。 - category mapping rebuild 只有在
QuoteCategory非空 array 且更新成功後執行;不可在未送 category 時自行同步 mapping。 - rematch queue 只在 zip 改變或新增 category diff 時送;刪 category 是否 rematch 依 legacy
array_diff(new, old),只看新增項目,不看移除項目。 QuoteCategory過濾 active category 依 legacy 註解與 intended behavior 實作為quote_categories.is_active > 0。
PHP 5.6 -> PHP 8.2 注意
trim($this->request->data['QuoteServiceContact'][$availableKey]):legacy 未送 key 時給 null;new 需避免trim(null)warning。else if ($photo['delete']):legacy 未定義 key 只是 notice;new 需用!empty($photo['delete'])或等價行為。$this->request->data['tabIndex']:legacy 缺 key 只是 notice;new activityparam2應允許 null。business_nameempty 判斷使用處理後字串;"0"在 PHPempty()為 true,legacy 會視為空白,new 需保留。service_area空值不清欄位;不要因 PHP 8.2 型別正規化把空 array 寫成[]。phone_numberarray JSON encode 只在非空 array;空 array 依 legacy 不 encode,但 key 存在仍觸發 service_update。
Response Mapping
| 情境 | legacy response |
|---|---|
| 成功 | {"error":0,"message":"Success"} |
| contact value invalid | {"error":2,"message":"<validation error>"},直接 json_ajax() return。 |
| YouTube link invalid | {"error":3,"message":"invalid youtube link"} |
| YouTube preview invalid | {"error":3,"message":"invalid youtube preview image"} |
business_name 處理後空白 | {"error":1,"message":"專家檔案名稱不可為空白"} |
| ownership / validation / save failure | {"error":1,"message":"Failed to edit service: {error} {checkbox_error}"} |
| invalid API session | HTTP 403 + {"error":99999,"message":"invalid_user"};2026-06-16 legacy staging curl 已確認。 |
missing service row 目前依 legacy ownership guard 語意回 Failed to edit service: Invalid user 。
測試矩陣
測試檔:
tests/QuoteServicesEditTest.php| 測試 | 驗證 |
|---|---|
testProviderCanEditOwnService | owner session 可更新 service,response Success;驗證 business_name trim、description、phone JSON、is_public mapping、full_address、activity type / payload、content review、service_update queue、rematch queue。 |
testRejectsServiceNotOwnedBySessionUser | 非 owner 回 failed response;不更新主 service、不寫 activity/queue。 |
testInvalidSessionMatchesLegacyStagingResponse | invalid API session 回 legacy staging 已確認的 HTTP 403 payload:error=99999, message=invalid_user;不更新 service。 |
testBlankBusinessNameReturnsLegacyMessage | 全形空白回 error=1 與中文 message。 |
testContactFullSyncUpdatesAndRemovesLegacyColumns | 送部分 contact 時,未送 keys 會 inactive;Website/LINE remove 會清舊欄位;change log 正確。 |
testInvalidContactReturnsLegacyErrorAndStops | URL 格式錯誤回 error 2,不更新後續 service。 |
testOmittedQuoteCategoryDoesNotChangeMapping | 未送 QuoteCategory 不改 quote_category_ids / mapping。 |
testCategoryUpdateRebuildsMapAndPreservesCounters | 更新 category 後 quote_category_ids、mapping rebuild、舊統計欄位保留、新 category 建 mapping,並送 rematch queue。 |
testCategoryRemovalRunsLegacySideEffects | 移除 category 時走 legacy side effect:刪 quote_service_categories、auto quote bid 改 Removed、寫 category deleted log、web 平台同步刪 nshop_service_categories 並寫 shop delete log。 |
testDeactivateUsesLegacyActivityType | is_active 1->0 寫 DeactivateService。 |
testOffShowStatusMovesToWaitOnAndWritesProhibitedLog | OFF 且非 close page 編輯後設 WAIT_ON 並寫 prohibited log。 |
testYoutubePhotoValidationMatchesLegacy | invalid YouTube URL 回 error 3。 |
testLogoEditGeneratesAttachmentThumb | Attachment[filename] 更新 logo attachment、產 thumb,並設 quote_services.has_logo = 1。 |
testPhotoEditGeneratesThumbForImageOnly | 一般照片 type_id=0 產 amazon_s3_thumb_url;YouTube validation case 不走此路。 |
DB 驗證時需查:
quote_servicesquote_service_contactsattachmentsquote_service_photosquote_service_change_logsquote_service_content_reviewsquote_service_prohibited_logsquote_categories_quote_servicesquote_service_categoriesquote_service_category_logsquote_activitiestask_event_queue+task_event_queue_logtask_queue+task_queue_logquote_requestsquote_bidsnshop_service_categories- application log:
service_edit、quote_service_edit、_checkToRestoreCategoryMap
Config 對照
| key / source | legacy 使用處 | new 狀態 |
|---|---|---|
Configure::read('area') | AreaUtilFactory::get(area, lang_id);TW 公司名驗證只在 empty area。 | new 不可補 ProConfig::$config['area']='tw';台灣環境保持 empty。 |
MultiTranslation::getHeaderLangId() | service area helper。 | add 已有對應 helper,edit 實作需沿用。 |
| YOURLS / site URL | external review URL。 | add/external_review 已對照;edit 可沿用 QuoteService::getExternalReviewUrl()。 |
Configure::read('s3.*'), Configure::read('is_cloudfront_only') | QuoteUtil::gen_attachment_thumb() 上傳 thumbnail 到 S3 並替換 CDN。 | new 使用既有 s3_endpoint、s3_bucket_name、s3_region、s3_aws_access_key、s3_aws_secret_key、s3_is_cloudfront_only constants 與 UploadS3Util::replaceCDN()。 |
Thumbnail Parity
legacy QuoteUtil::gen_attachment_thumb() 行為:
Attachment:_saveAttachment()upsert 後呼叫;若更新後有amazon_s3_original_url,設quote_services.has_logo = 1。QuoteServicePhoto:新增或更新 photo 後,只有type_id === 0的一般照片呼叫;YouTube 影片不呼叫。- 下載 original URL 到
/tmp,用 480x480 resize;若圖片小於 480x480,image_resize()回true,thumb URL 直接等於 original URL。 - 若有產新縮圖,上傳到 S3
thumb/...,再把 original/thumb 做http:->https:與 CDN replacement 後 update 回 DB。 - caller 不檢查回傳值;縮圖失敗不改 API response。
new UploadS3Util::genAttachmentThumb() 已依同語意補上,並只允許更新 attachments / quote_service_photos 兩張表,避免任意 table update。
待確認
- empty request 在 legacy JSON path 的實際 response;new 目前用 failed response helper。
- application log channel 是否必須完全重建 file log;目前至少需保留 DB log 可查錯誤路徑。
- legacy
QuoteService->save()model validation 失敗時 controller 沒檢查 return value;例如 invalidservice_provider_travels_upto可能是成功 response 但主 row 不更新。new 目前走 explicit update,這種 error-path 需用 legacy staging 實測後再決定是否仿照。 - legacy
QuoteService->save()/updateProfileScoreById()exception path 最多 retry 5 次且只寫 log;new 目前遇到 exception 會回 failed response。這是 DB exception path,切換前需確認是否要為了 legacy parity 改成 retry / log-only。
測試結果
2026-06-16 local container PHP 8.2:
docker exec -w /project-data cd63f9147e8d php vendor/bin/phpunit tests/QuoteServicesEditTest.php
OK (14 tests, 90 assertions)同日回歸既有 quote_services tests,不含 edit 單檔:
docker exec -w /project-data cd63f9147e8d php vendor/bin/phpunit \
tests/QuoteServicesAddTest.php \
tests/QuoteServicesChangeStatusArchiveTest.php \
tests/QuoteServicesChangeStatusProfileReadyTest.php \
tests/QuoteServicesExternalReviewInviteTest.php \
tests/QuoteServicesExternalReviewLinkTest.php \
tests/QuoteServicesExternalReviewSubmitTest.php \
tests/QuoteServicesStaticFieldsTest.php
OK (16 tests, 53 assertions)