POST /users/delete.json

快速結論

  • 狀態:已補 action / routing / PHPUnit;staging full diff 待補。
  • legacy JSON path 需要 valid API key + valid user session。成功回 {"status":"success","error":0};invalid request 回 {"error":1,"status":"Invalid request"};未預期例外回 {"error":999,"status":err_new_api_999}
  • 核心邏輯不是只把 users.is_active 關掉。legacy 會更新 user row、archive 該 user 的 services / requests、對進行中或已 hired 的 bids 寫 closed activity 並清 unread、刪 api sessions、刪 nshop item、寫 task/event queue,並在有 life_55688_id 時通知 Life55688。
  • 建議 new 實作只在 Users::delete() 增加本 API 專用 helper;不要改 shared User / QuoteRequest / QuoteService 行為,避免影響其他流程。
  • migration decision:不照搬 legacy 有問題的 5 次 retry 結構;實作時改成每次 attempt 都正確 begin / commit / rollback,且只對 MySQL deadlock / lock wait timeout retry,其他 exception 直接回 legacy-shaped 999
  • migration decision:本次經 reviewer 確認,legacy RestApiHelper::getUserId() 會寫 request info log;new 不再補 endpoint duplicate full request log,只保留 RouterV3 generic request log。這支 API 無必要 body 欄位,若 request 有帶資料會出現在 generic context 的 post data 位置。
  • migration decision:Delete_User email event payload 使用 EmailTaskDO,保留與 legacy EmailParameterDO 相同的 public null 欄位,不改成精簡 object。
  • migration decision:new Life55688UtiladdEventDeleteUser() wrapper,讓 Users delete path 呼叫名稱與 legacy 對齊。
  • 預設 PHPUnit fixture 應使用 life_55688_id 空值,避免測試打外部 Life55688;Life55688 path 另用 staging/manual 驗證。

Legacy 對照

檔案行號行為
/Users/mattsu/Documents/Site/get-lancer-php56/app/Controller/UsersController.php3884-3907delete():JSON + session guard、取 user id、呼叫 User::deleteUserSafety($user_id, $user_id)、回 success / exception response
/Users/mattsu/Documents/Site/get-lancer-php56/app/Controller/RestApiHelper.php75-152hasValidApiSession():檢查 API key、session、user;並更新 user access side effect
同上162-182getUserId():從 request params 取 user id,並用 channel {url}::{method} 寫 request log
/Users/mattsu/Documents/Site/get-lancer-php56/app/Model/User.php1365-1378deleteUserSafety():user id 空值丟 invalid request;用 SELECT ... FOR UPDATE 鎖 user row
同上1394-1416更新 users 刪帳欄位,email / phone 加 DEL{timestamp}_ prefix
同上1418-1426archive services / requests、寫 DeleteUser activity、刪 api_sessions
同上1429-1435有舊 life_55688_id 時呼叫 Life55688;刪 nshop items
同上1437-1445task_event_queuemoengage_erase_usersumsub_deactivate_user
同上1458-1500transaction 後送 Delete_User event,email template Admin User Delete
/Users/mattsu/Documents/Site/get-lancer-php56/app/Plugin/Quotes/Model/QuoteService.php5064-5069archiveUserServices():找 user 所有 services,逐筆設 is_active = 0, is_archived = 1,沒有 active guard
/Users/mattsu/Documents/Site/get-lancer-php56/app/Plugin/Quotes/Model/QuoteRequest.php197-241archiveUserRequests():archive/close 未 archived requests、寫 Close/Archive request activity、對 InProgress/Hired bids 寫 QuoteBidClosed activity 並清 unread
/Users/mattsu/Documents/Site/get-lancer-php56/app/Model/NshopItem.php230-249deleteItemByUserId():user 所有 nshop items,非 deleted 則設 deleted,且每筆呼叫 deleteES();例外只 log 不中斷主流程
/Users/mattsu/Documents/Site/get-lancer-php56/app/Lib/Life55688Util.php175-180addEventDeleteUser():送 Life55688 delete user event
/Users/mattsu/Documents/Site/get-lancer-php56/app/Controller/AppController.php1320-1343handleProApiException()error/status response
同上1351-1360handleException()error=999 response 並寫 error log

New 對照

項目狀態
Lib/Common/RouterRule/Mapping.php:92-95legacy path users/delete.json 明確 routing 到 V1/Users.php::delete()
Endpoint/V1/Users.php:971-1004delete() action;auth invalid 回 legacy Invalid request,success 回 status=success,error=0,generic exception 回 999
Lib/Common/RouterV3.php:210,266-289generic request log;不再補 endpoint duplicate log;password/passwd key 由 router 層過濾
Endpoint/V1/Users.php:1006-1050deleteLegacyUserSafety() 與正確 retry transaction;每次 attempt 都 begin/commit/rollback,只 retry MySQL deadlock / lock wait timeout
Endpoint/V1/Users.php:1052-1115user row lock/update、services/requests archive、DeleteUser activity、刪 session、Life55688、nshop、task queue
Endpoint/V1/Users.php:1118-1127archive user services;不加 active / archived filter
Endpoint/V1/Users.php:1129-1214archive user requests;只處理 is_archived=0 request,寫 Close/Archive request activity;InProgress/Hired bid 寫 QuoteBidClosed activity 並清 unread
Endpoint/V1/Users.php:1216-1245nshop delete-by-user helper;例外只 log NshopItem__deleteItemByUserId,不 rollback 主流程
Endpoint/V1/Users.php:1247-1280Delete_User event payload;用 EmailTaskDO 後移除 new-only shopList,維持 legacy email task 欄位
Endpoint/V1/Users.php:1282-1290retry 判斷只接受 MySQL 1205 / 1213
Endpoint/V1/Users.php:1353-1364generic 999 error message helper,sign_up / delete 共用
Lib/Util/Life55688Util.php:179-186addEventDeleteUser() wrapper,內部呼叫 addEvent(EVENT_DELETE_USER, getMember(user_id))

Input / Response

項目legacy 行為new 建議
methodPOSTPOST
path/users/delete.json同 legacy
authvalid API key + valid user sessionhasValidApiSession(..., false, $usersDO),invalid 時手動回 legacy response
input body無必要欄位不新增 body validation
success response{"status":"success","error":0}欄位與型別照 legacy;status 是小寫 success
invalid request{"error":1,"status":"Invalid request"}不使用 new exception response shape
generic exception{"error":999,"status":<err_new_api_999 翻譯>}對齊 handleException()

User Update 欄位分類

只更新 legacy deleteUserSafety() 明確列出的欄位。

table欄位來源 / 規則
usersemail若原本已是 DEL\d+_ prefix,保留原 email;否則改為 DEL{time()}_{old_email}
usersphone改為 DEL{time()}_{old_phone};legacy 不判斷空值
usersis_active固定 0
usersfacebook_user_id / facebook_access_token固定 NULL
usersgoogle_user_id / google_access_token固定 NULL
usersapple_user_id固定 NULL
userslife_55688_id固定 NULL;Life55688 event 用 update 前舊值
usersis_apple_connected / is_facebook_connected / is_google_connected固定 0
usersis_phone_confirmed固定 0

不得更新 legacy 沒碰的高風險欄位:

類型欄位例
auth/securitypasswordmobile_app_hash、security question/answer
wallet/paymentavailable_wallet_amountavailable_balance_amountblocked_amount、card/debt 欄位
countersquote_service_countquote_request_countquote_bid_sent_count、login count
profileuser_profiles.phone、姓名、avatar、identity
notification/subscriptionnotification settings、auto refill / package / subscription foreign key

Side Effects

類型legacy 行為new 實作方向
user lockSELECT id,email,phone,is_active,life_55688_id FROM users WHERE id=:id FOR UPDATE使用 transaction + FOR UPDATE
service archiveuser 所有 quote_servicesis_active=0,is_archived=1不加 legacy 沒有的 active / archived filter
request archiveuser 未 archived quote_requestsis_archived=1,is_closed=1只查 user_id + is_archived=0
request activity每個被 archive request 寫 CloseRequestArchiveRequestQuoteActivity::createQuoteRequestActivityById()
bid activity/unreadrequest 下 quote_status_id IN (InProgress,Hired) bids 寫 QuoteBidClosed,並 set unread d=0不更新 quote_bids 欄位
user activityDeleteUser activitymigration decision:/users/delete.json 是 self-delete path,new 固定寫 param1 = '(self)',對齊 legacy DB 實際結果;不修成 admin:{id}(self)
sessionsApiSession.user_id = id 全刪DELETE FROM api_sessions WHERE user_id = ?
Life55688life_55688_id 非空才送 delete event空值不呼叫;非空 path staging/manual 驗證
nshopuser 所有 nshop items 設 deleted,且每筆 delete ES;例外只 log不讓 nshop/ES 失敗 rollback 主流程
task queuemoengage_erase_usersumsub_deactivate_userpayload 照 legacy
delete emailtransaction 後寫 Delete_User eventemail recipient 用刪除 prefix 移除後的 email
request info loglegacy RestApiHelper::getUserId() 會寫 channel {url}::{method},message 為 user id,context 含 api_key 與 request data/form本次 migration decision:經 reviewer 確認,new 不補 endpoint duplicate log,只保留 RouterV3 generic request log;context shape 為 [api_key, path, subDir, post_data, request_id]

Queue Payload

task_event_queue

event_keyparams
moengage_erase_user{"user_id":<id>,"email":"<original email without DEL prefix>"}
sumsub_deactivate_user{"user_id":<id>}

event_queue

event_keyparams
Delete_User{"user_id":<id>,"quote_service_id":null,"tasks":[EmailTaskDO]}EmailTaskDO 保留 legacy public null 欄位,並移除 new-only shopList

EmailTaskDO 欄位:

欄位
templateTypeemail
templateKeyAdmin User Delete
to移除 DEL\d+_ prefix 後的 user email
replaceContent.##USERNAME##users.username
replaceContent.##ACTION_1_URL## / ##ACTION_1_TEXT## / ##ACTION_2_URL## / ##ACTION_2_TEXT##空字串
langIdUser::getMultiLangId(user_id)

Config 對照

keylegacy 使用點new 狀態
api_life55688Life55688Util::addEvent() endpoint base URLLib/Common/Configuration.php 已有 key;部署時需確認 staging / production 實際值
api_life55688_keyLife55688 Authorization headerLib/Common/Configuration.php 已有 key;部署時需確認 staging / production 實際值

Delete_User / Admin User Delete 是 DB seed,不是 config 檔;實測前需確認目標 DB 有對應 event_actions / email_templates row。

DB 實際確認

2026-07-01 用 new 專案 DBFactory 查 staging DB quote_activities

SELECT
    COALESCE(param1, '<NULL>') AS param1_value,
    COUNT(*) AS c,
    MIN(created) AS first_created,
    MAX(created) AS last_created
FROM quote_activities
WHERE quote_activity_type_id = 49
GROUP BY param1_value
ORDER BY c DESC;

結果:

param1_valuecountfirst_createdlast_created
(self)9082018-12-03 08:20:402026-06-18 04:19:05

最近 20 筆 DeleteUser activity 的 param1 也全部是 (self),沒有 admin:{id} 或空值。因此 new 實作直接寫 (self) 是依 DB 實際行為對齊,不是自行修正 legacy 原意。

PHP 5.6 -> 8.2 注意

項目legacynew 建議
phone/email prefixlegacy 用字串 concat;空值會當空字串new 需明確 cast string,避免 PHP 8.2 null warning
deleteUserSafety() retrylegacy begin() 在 retry loop 外,exception rollback 後沒有重新 begin,語意不乾淨已確認 migration decision:new 改成正確 retry;每次 attempt 都重新 begin,成功 commit,失敗 rollback;只 retry DB deadlock / lock wait timeout,其他錯誤直接回 999
user activity paramlegacy 表達式 admin:' . $admin_user_id . ($admin_user_id === $id) ? '(self)' : '' 實際 DB 結果是 (self),沒有留下 admin:{id}已確認 migration decision:本 API path 只會傳 $user_id, $user_id,new 直接寫 (self);若未來搬 admin delete,再另外決定是否修正 admin activity 格式
missing userlegacy SELECT ... FOR UPDATE 找不到 user 時 return true,但 API auth 通常不會讓 missing user 進來new auth invalid 時回 invalid request;helper 可保留 missing user success 但正常不可達

不搬 / 不新增

  • 不更新 user_profiles
  • 不更新 password / wallet / payment / counter / notification / subscription 欄位。
  • 不自行新增 active/archive/status guard;services 沒有 active filter,requests 只用 is_archived=0,bids 只用 quote_status_id IN (InProgress,Hired)
  • 不重用 QuoteRequest::closeRequest(),因為它不等同 legacy archiveUserRequests()
  • 不把 nshop ES delete 失敗變成主流程失敗;legacy 只 log。

測試結果

2026-07-03 Docker PHP 8.2:

docker exec -w /project-data cd63f9147e8d php -l Endpoint/V1/Users.php
docker exec -w /project-data cd63f9147e8d php -l Lib/Common/RouterRule/Mapping.php
docker exec -w /project-data cd63f9147e8d php -l Lib/Util/Life55688Util.php
docker exec -w /project-data cd63f9147e8d php -l tests/UsersDeleteTest.php
docker exec -w /project-data cd63f9147e8d ./vendor/bin/phpunit tests/UsersDeleteTest.php

結果:

tests/UsersDeleteTest.php: OK (3 tests, 99 assertions)

覆蓋:

case驗證
invalid session{"error":1,"status":"Invalid request"};不寫 delete side effect
success userresponse success;users 只更新刪帳欄位;api_sessions 被刪;DeleteUser activity、task queues、Delete_User event 存在
services / requests / bidsuser services archived;未 archived requests close+archive;Close/Archive request activity;InProgress bid 寫 QuoteBidClosed activity,Rejected bid 不寫
negative fieldspassword、wallet、counters、user_profiles 不被改
already DEL emailemail 不重複 prefix;moengage_erase_user.email 與 email recipient 去掉原 prefix
request info log只保留 RouterV3 generic request log;channel starts with users/delete.json::POST 且不是 exact users/delete.json::POST,context 含 user id、api key、path 與 request data

Staging/manual 待補:

  • 用 disposable user + 真 session 呼叫 /users/delete.json
  • usersquote_servicesquote_requestsquote_activitiesapi_sessionstask_event_queue / log、event_queue / log。
  • 另找有 life_55688_id 的測試資料或人工設值,確認 Life55688 delete event 行為;不要在一般 PHPUnit 打外部 API。

待確認

  • 無。