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_activeis_archivedshow_status_typerole_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 = OFFprohibited_type != CLOSE_SERVICE_PAGE 時,編輯後改成 WAIT_ON 並寫 prohibited log。
  • 成功後一定寫 UpdateService activity;若本次 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-1748edit($id) 入口、freelancer user type guard、null id guard、初始化 content review / change log model。
1749-1754JSON + valid API session 時標記 $is_from_api = true、取 session user id、把 path id 塞入 QuoteService.id
1755-1785QuoteServiceContact 全量同步:trim、validate、edit / remove;remove Website 清 website_url,remove LINE 清 line_id
1787-1790Attachment[filename] 存在時呼叫 _saveAttachment($id, null, filename) 更新 logo attachment。
1792-1866QuoteServicePhoto 新增 / 更新 / 刪除;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-1900JSON API 才允許更新 is_active,並判斷本次是否 activate / deactivate;非 API path 保留原 is_active
1903-1915QuoteService.user_id 時沿用原 service owner;支援 top-level City[name/id]State[name/id]QuoteService[country_id] lookup。
1918-1939API path 只做 ownership guard;category / travel checkbox validation 只在非 API path。
1940-1951通過 validation 後初始化 rematch flag、取得 language 與 AreaUtilFactory::get(Configure::read('area'), lang_id)
1952-1965zip_code key 存在且與原 service 不同時設 $to_rematch = true,用 zip 覆蓋 city/state/country,country 固定 222
1967-1991is_not_this_area 為 1 時清 zip/city/state/country;非 foreign 且 address 或 zip key 存在時重算 full_address
1993-2013service_area key 存在且非空時做 300/600 展開、JSON encode、更新 service_cities。空值時不改欄位。
2015-2036QuoteCategory 非空 array 時去重、用 active category 過濾、比較新增 diff 決定 rematch、更新 quote_category_ids
2038-2041phone_number 非空 array 時 JSON encode。
2043-2044每次成功更新前補 external_review_url = QuoteService::getExternalReviewUrl(...)
2046-2047request 有 is_public key 時同步到 is_show_my_address_to_all_visitor
2049-2089business_name:trim、去全形空格、空白回 error=1,XML invalid filter、截 30 字、content review、TW 公司名驗證。
2091-2093description 非空時寫 content review。
2095-2101service 原本 OFF 且不是 CLOSE_SERVICE_PAGE 時,改 show_status_type = WAIT_ON 並寫 prohibited log。
2102-2118QuoteService->save() 最多 retry 5 次;成功後 updateProfileScoreById();lock timeout 寫 quote_service_edit log。
2120-2127request 有 QuoteService[phone_number] key 時送 task_event_queue.service_update
2129-2132UpdateService / ActivateService / DeactivateService activity,param1 = request data JSONparam2 = tabIndex;此時 request data 已被補入 QuoteService.id/user_id/external_review_url
2134-2141category 更新後依 API key 判斷 is_sync_service_category,呼叫 _checkToRestoreCategoryMap()
2143-2146zip 或新增 category diff 時,送 task_queue.newQuoteService,expiry time() + 90
2148-2152API success response:error=0, message=Success
2169-2174API failure response:error=1, message=Failed to edit service: {error} {checkbox_error}
3530-3568_saveAttachment():upsert attachments class QuoteServicereview_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-23contact available keys:LINE、WhatsApp、WeChat、Website、Facebook、Instagram、Behance、Dribbble、Github。
QuoteServiceContact.php:40-69contact value validation;通訊 ID pattern 與 URL pattern。
QuoteServiceContact.php:71-110contact edit;存在則 update + active,值變更寫 change log;不存在則 insert + change log。
QuoteServiceContact.php:112-135contact remove;存在則設 inactive,原本 active 才寫 delete change log。
QuoteServiceChangeLog.php:12-18quote_service_change_logs.quote_service_id/save_data
QuoteServiceContentReview.php:13-51content review upsert;鎖 service row,新增或 merge original_content,非 BYPASS 狀態重設 WAITING。
QuoteService.php:357-388getExternalReviewUrl();已有 URL 直接回傳,空值時建立 security hash + short URL 並寫回 service。
QuoteService.php:5628-5646updateProfileScoreById();重算 profile score 並更新 quote_services.profile_score
QuoteService.php:6133-6137XML invalid char filter。
QuoteServiceCategory.php:1989-2060removeServiceCategory();移除分類時把未接觸的 auto quote bid 改 Removed、刪 quote_service_categories、寫 category log;需要同步 shop 時刪 nshop category 並標 nshop items deleted。
ServiceCertificate.php:98-104isCompanyConfirm()service_certificates.user_id = user_id AND type = 1 AND status = 5

New PHP 8.2 對照

new 檔案行號對應 legacy狀態
Lib/Common/RouterRule/Mapping.php:53-56API route已補 /quote_services/edit/{quote_service_id}.jsonQuoteServices::edit()
Endpoint/V1/QuoteServices.php:148-1861734-1754, 2148-2152, 2169-2174endpoint 入口、API key/session guard、request data 初始化、依 legacy 順序呼叫 contact / attachment / photo / scan / service update。
Endpoint/V1/QuoteServices.php:188-2271755-1785QuoteServiceContact 全量同步;invalid contact 直接回 error 2;remove Website / LINE 時清 website_url / line_id
Endpoint/V1/QuoteServices.php:229-3011787-1790, 3530-3568attachment upsert:class=QuoteServicereview_status=0、更新 amazon_s3_original_urlfilename,呼叫 thumbnail helper,original URL 存在時設 has_logo=1
Endpoint/V1/QuoteServices.php:303-4311792-1866QuoteServicePhoto 新增 / 更新 / 刪除、YouTube URL validation、change log;一般照片 type_id=0 呼叫 thumbnail helper,影片不呼叫。
Endpoint/V1/QuoteServices.php:433-4571867-1880abnormal keyword scan;例外只寫 service_edit log,不中斷。
Endpoint/V1/QuoteServices.php:463-5481887-1939, 2102-2152, 2169-2174owner guard、is_active transition、主 service update、profile score、phone queue、回填 activity payload 欄位、activity、category rebuild、rematch queue、response。
Endpoint/V1/QuoteServices.php:563-7391903-2101edit 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-7991993-2036service area 展開 300/600;category 去重、active 過濾。
Endpoint/V1/QuoteServices.php:801-8522102-2108explicit quote_services update allowlist;不全量寫 request。
Endpoint/V1/QuoteServices.php:859-9584175-4295category mapping restore:row lock、刪舊 mapping、移除舊 service category 時呼叫 removeServiceCategory()、保留舊統計欄位、active+not archived 才重建、新 category 呼叫 Nshop check。
Endpoint/V1/QuoteServices.php:960-9702143-2146rematch 時寫 task_queue.newQuoteServiceexpiry=time()+90;不改 shared TaskQueue::insert()
Endpoint/V1/QuoteServices.php:972-9782169-2174failed response helper。
Lib/Model/QuoteServiceCategory.php:2397-2445QuoteServiceCategory.php:1989-2060已補 removeServiceCategory():更新 quote_bids Removed、刪除 quote_service_categories、寫 quote_service_category_logs,並依 $isSyncServiceCategory 呼叫 NshopServiceCategory::deleteBySC()
Lib/Util/UploadS3Util.php:221-312QuoteUtil.php:100-174genAttachmentThumb():支援 Attachment / QuoteServicePhoto,下載 original、480x480 resize、小圖 thumb=original、大圖上傳 S3 thumb/...,normalize http->https 與 CDN,更新 original/thumb URL;失敗回 false 不中斷 caller。
Lib/Model/QuoteServiceContact.php:18-43QuoteServiceContact.php:13-38available keys 已存在。
Lib/Model/QuoteServiceContact.php:45-74QuoteServiceContact.php:137-150讀取 active contact map,維持既有 caller 行為。
Lib/Model/QuoteServiceContact.php:76-105QuoteServiceContact.php:40-69已補 contact value validation。
Lib/Model/QuoteServiceContact.php:107-158QuoteServiceContact.php:71-110已補 edit:update/insert active contact,並寫 change log。
Lib/Model/QuoteServiceContact.php:160-189QuoteServiceContact.php:112-135已補 remove:設 inactive,原本 active 才寫 delete change log。
Lib/Model/QuoteServiceChangeLog.php:11-16QuoteServiceChangeLog.php:12-18已有同語意 add,可直接用或包在 edit 專用流程。
Lib/Model/QuoteServiceContentReview.php:28-67QuoteServiceContentReview.php:13-51已有同語意 addReview,且補 PHP 8.2 empty JSON fallback。
Lib/Model/QuoteService.php:948-968QuoteService.php:5628-5646已有 updateProfileScoreById()
Lib/Model/QuoteService.php:1957-1995QuoteService.php:357-388已有 getExternalReviewUrl()
Lib/Model/QuoteService.php:2095-2098QuoteService.php:6133-6137已有 XML invalid char filter。
Lib/Model/ServiceCertificate.php:73-77ServiceCertificate.php:98-104method 名稱不同:new 是 isCompanyConfirmed()
Lib/Model/QuoteActivity.php:108-129legacy activity helper已有 createQuoteServiceActivityById()
Lib/Constant/ConstQuoteActivityType.php:37-39legacy constantsUpdateService=26ActivateService=27DeactivateService=28 已對齊。

Request Mapping

inputlegacy 規則實作注意
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_idsAPI 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 param1new 需回填到 $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 明確衍生欄位。

欄位來源
idpath {quote_service_id},legacy 1753
user_idrequest 未帶時沿用原 service owner,legacy 1903-1905
is_activeAPI request QuoteService[is_active],legacy 1894-1900
city_idCity[id]City[name] find-or-create;zip lookup 也可覆蓋,legacy 1907-1909, 1958-1963
state_idState[id]State[name] find-or-create;zip lookup / foreign 也可覆蓋,legacy 1913-1915, 1958-1963, 1969-1981
country_idQuoteService[country_id] lookup、zip lookup 固定 222、foreign 清 0,legacy 1910-1912, 1958-1963, 1969-1981
zip_coderequest zip 或 foreign 清 0,legacy 1952-1965, 1969-1981
is_not_this_arearequest flag normalization,legacy 1967-1981
addressrequest QuoteService[address]。legacy 由 Cake save request data 寫入。
full_addressaddress/zip key 存在且非 foreign 時由 AreaUtil 格式化,legacy 1984-1991
service_areanon-empty request service_area JSON encode,legacy 1993-2010
service_citiesservice_area 對應 city ids,legacy 2010-2011
quote_category_idsactive category 過濾後 implode,legacy 2015-2036
phone_numberrequest phone;非空 array JSON encode,legacy 2038-2041
external_review_urlQuoteService::getExternalReviewUrl(),legacy 2043-2044
is_show_my_address_to_all_visitorrequest is_public 同步,legacy 2046-2047
business_nametrim/filter/substr 後 request value,legacy 2049-2072
is_valid_nameTW 公司名驗證結果,legacy 2074-2087
descriptionrequest QuoteService[description]。legacy 由 Cake save request data 寫入。
show_status_type原 service OFF 且非 close page 時改 WAIT_ON,legacy 2095-2101
website_urlremove Website contact 時清空,legacy 1777-1780
line_idremove LINE contact 時清空,legacy 1780-1782
profile_scoreQuoteService::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_idpath id,legacy 1775, model 99
contact_keyQuoteServiceContact available key,legacy model 13-23, 100
contact_valuetrim 後 request value,legacy 1760-1771, model 86, 101
is_activeedit 設 1,remove 設 0,legacy model 87, 102, 125

attachments upsert

欄位來源
classliteral QuoteService,legacy 3535-3547
foreign_idpath service id,legacy 3536-3547
review_statusliteral 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_idsession user id,新增照片時設定,legacy 1825
quote_service_idpath service id,新增照片時設定,legacy 1826
request photo fieldsQuoteServicePhoto request data,legacy 1827-1850 走 model validation。
type_idrequest type_id cast int,default 0,legacy 1800, 1820
review_status更新既有照片時設 ConstAttachmentReviewStatus::NOT_YET,legacy 1847

new 已查 quote_service_photos schema,實作只寫 user_idquote_service_idcaptionsort_orderamazon_s3_original_urlamazon_s3_thumb_urltype_idreview_status 與 timestamps。

quote_categories_quote_services rebuild

欄位來源
created, modifiedcurrent timestamp,legacy 4231-4236
quote_service_idpath service id,legacy 4237
quote_category_idactive numeric category id,legacy 4232-4239
hired/count/rate/order fields若舊 mapping 有同 category,保留舊 row 統計欄位,legacy 4239-4251

其他 side-effect tables

table寫入來源
quote_service_change_logscontact / photo add/update/delete,legacy contact model 90-108, 128-132;photo 1857-1862
quote_service_content_reviewsbusiness_name / description content review,legacy 2072, 2091-2093
quote_service_prohibited_logsOFF service 編輯後 WAIT_ON,legacy 2095-2101
task_event_queuerequest 有 phone_numberservice_update,legacy 2120-2127
quote_activitiesUpdateService / ActivateService / DeactivateService,legacy 2129-2132
task_queuerematch 時 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_categoriescategory 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 = 1is_archived = 0show_status_type 或 role condition。
  • ownership guard 是查到 row 後比對 quote_service.user_id != session user id,錯誤字串為 Invalid user,最後走 error=1 failed 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 activity param2 應允許 null。
  • business_name empty 判斷使用處理後字串;"0" 在 PHP empty() 為 true,legacy 會視為空白,new 需保留。
  • service_area 空值不清欄位;不要因 PHP 8.2 型別正規化把空 array 寫成 []
  • phone_number array 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 sessionHTTP 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
測試驗證
testProviderCanEditOwnServiceowner 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。
testInvalidSessionMatchesLegacyStagingResponseinvalid 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 正確。
testInvalidContactReturnsLegacyErrorAndStopsURL 格式錯誤回 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。
testDeactivateUsesLegacyActivityTypeis_active 1->0 寫 DeactivateService。
testOffShowStatusMovesToWaitOnAndWritesProhibitedLogOFF 且非 close page 編輯後設 WAIT_ON 並寫 prohibited log。
testYoutubePhotoValidationMatchesLegacyinvalid YouTube URL 回 error 3。
testLogoEditGeneratesAttachmentThumbAttachment[filename] 更新 logo attachment、產 thumb,並設 quote_services.has_logo = 1
testPhotoEditGeneratesThumbForImageOnly一般照片 type_id=0amazon_s3_thumb_url;YouTube validation case 不走此路。

DB 驗證時需查:

  • quote_services
  • quote_service_contacts
  • attachments
  • quote_service_photos
  • quote_service_change_logs
  • quote_service_content_reviews
  • quote_service_prohibited_logs
  • quote_categories_quote_services
  • quote_service_categories
  • quote_service_category_logs
  • quote_activities
  • task_event_queue + task_event_queue_log
  • task_queue + task_queue_log
  • quote_requests
  • quote_bids
  • nshop_service_categories
  • application log:service_editquote_service_edit_checkToRestoreCategoryMap

Config 對照

key / sourcelegacy 使用處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 URLexternal 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_endpoints3_bucket_names3_regions3_aws_access_keys3_aws_secret_keys3_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;例如 invalid service_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)