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;不要改 sharedUser/QuoteRequest/QuoteService行為,避免影響其他流程。 - migration decision:不照搬 legacy 有問題的 5 次 retry 結構;實作時改成每次 attempt 都正確
begin/commit/rollback,且只對 MySQL deadlock / lock wait timeout retry,其他 exception 直接回 legacy-shaped999。 - migration decision:本次經 reviewer 確認,legacy
RestApiHelper::getUserId()會寫 request info log;new 不再補 endpoint duplicate full request log,只保留RouterV3generic request log。這支 API 無必要 body 欄位,若 request 有帶資料會出現在 generic context 的 post data 位置。 - migration decision:
Delete_Useremail event payload 使用EmailTaskDO,保留與 legacyEmailParameterDO相同的 public null 欄位,不改成精簡 object。 - migration decision:new
Life55688Util補addEventDeleteUser()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.php | 3884-3907 | delete():JSON + session guard、取 user id、呼叫 User::deleteUserSafety($user_id, $user_id)、回 success / exception response |
/Users/mattsu/Documents/Site/get-lancer-php56/app/Controller/RestApiHelper.php | 75-152 | hasValidApiSession():檢查 API key、session、user;並更新 user access side effect |
| 同上 | 162-182 | getUserId():從 request params 取 user id,並用 channel {url}::{method} 寫 request log |
/Users/mattsu/Documents/Site/get-lancer-php56/app/Model/User.php | 1365-1378 | deleteUserSafety():user id 空值丟 invalid request;用 SELECT ... FOR UPDATE 鎖 user row |
| 同上 | 1394-1416 | 更新 users 刪帳欄位,email / phone 加 DEL{timestamp}_ prefix |
| 同上 | 1418-1426 | archive services / requests、寫 DeleteUser activity、刪 api_sessions |
| 同上 | 1429-1435 | 有舊 life_55688_id 時呼叫 Life55688;刪 nshop items |
| 同上 | 1437-1445 | 寫 task_event_queue:moengage_erase_user、sumsub_deactivate_user |
| 同上 | 1458-1500 | transaction 後送 Delete_User event,email template Admin User Delete |
/Users/mattsu/Documents/Site/get-lancer-php56/app/Plugin/Quotes/Model/QuoteService.php | 5064-5069 | archiveUserServices():找 user 所有 services,逐筆設 is_active = 0, is_archived = 1,沒有 active guard |
/Users/mattsu/Documents/Site/get-lancer-php56/app/Plugin/Quotes/Model/QuoteRequest.php | 197-241 | archiveUserRequests():archive/close 未 archived requests、寫 Close/Archive request activity、對 InProgress/Hired bids 寫 QuoteBidClosed activity 並清 unread |
/Users/mattsu/Documents/Site/get-lancer-php56/app/Model/NshopItem.php | 230-249 | deleteItemByUserId():user 所有 nshop items,非 deleted 則設 deleted,且每筆呼叫 deleteES();例外只 log 不中斷主流程 |
/Users/mattsu/Documents/Site/get-lancer-php56/app/Lib/Life55688Util.php | 175-180 | addEventDeleteUser():送 Life55688 delete user event |
/Users/mattsu/Documents/Site/get-lancer-php56/app/Controller/AppController.php | 1320-1343 | handleProApiException():error/status response |
| 同上 | 1351-1360 | handleException():error=999 response 並寫 error log |
New 對照
| 項目 | 狀態 |
|---|---|
Lib/Common/RouterRule/Mapping.php:92-95 | legacy path users/delete.json 明確 routing 到 V1/Users.php::delete() |
Endpoint/V1/Users.php:971-1004 | delete() action;auth invalid 回 legacy Invalid request,success 回 status=success,error=0,generic exception 回 999 |
Lib/Common/RouterV3.php:210,266-289 | generic request log;不再補 endpoint duplicate log;password/passwd key 由 router 層過濾 |
Endpoint/V1/Users.php:1006-1050 | deleteLegacyUserSafety() 與正確 retry transaction;每次 attempt 都 begin/commit/rollback,只 retry MySQL deadlock / lock wait timeout |
Endpoint/V1/Users.php:1052-1115 | user row lock/update、services/requests archive、DeleteUser activity、刪 session、Life55688、nshop、task queue |
Endpoint/V1/Users.php:1118-1127 | archive user services;不加 active / archived filter |
Endpoint/V1/Users.php:1129-1214 | archive user requests;只處理 is_archived=0 request,寫 Close/Archive request activity;InProgress/Hired bid 寫 QuoteBidClosed activity 並清 unread |
Endpoint/V1/Users.php:1216-1245 | nshop delete-by-user helper;例外只 log NshopItem__deleteItemByUserId,不 rollback 主流程 |
Endpoint/V1/Users.php:1247-1280 | Delete_User event payload;用 EmailTaskDO 後移除 new-only shopList,維持 legacy email task 欄位 |
Endpoint/V1/Users.php:1282-1290 | retry 判斷只接受 MySQL 1205 / 1213 |
Endpoint/V1/Users.php:1353-1364 | generic 999 error message helper,sign_up / delete 共用 |
Lib/Util/Life55688Util.php:179-186 | 補 addEventDeleteUser() wrapper,內部呼叫 addEvent(EVENT_DELETE_USER, getMember(user_id)) |
Input / Response
| 項目 | legacy 行為 | new 建議 |
|---|---|---|
| method | POST | POST |
| path | /users/delete.json | 同 legacy |
| auth | valid API key + valid user session | hasValidApiSession(..., 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 | 欄位 | 來源 / 規則 |
|---|---|---|
users | email | 若原本已是 DEL\d+_ prefix,保留原 email;否則改為 DEL{time()}_{old_email} |
users | phone | 改為 DEL{time()}_{old_phone};legacy 不判斷空值 |
users | is_active | 固定 0 |
users | facebook_user_id / facebook_access_token | 固定 NULL |
users | google_user_id / google_access_token | 固定 NULL |
users | apple_user_id | 固定 NULL |
users | life_55688_id | 固定 NULL;Life55688 event 用 update 前舊值 |
users | is_apple_connected / is_facebook_connected / is_google_connected | 固定 0 |
users | is_phone_confirmed | 固定 0 |
不得更新 legacy 沒碰的高風險欄位:
| 類型 | 欄位例 |
|---|---|
| auth/security | password、mobile_app_hash、security question/answer |
| wallet/payment | available_wallet_amount、available_balance_amount、blocked_amount、card/debt 欄位 |
| counters | quote_service_count、quote_request_count、quote_bid_sent_count、login count |
| profile | user_profiles.phone、姓名、avatar、identity |
| notification/subscription | notification settings、auto refill / package / subscription foreign key |
Side Effects
| 類型 | legacy 行為 | new 實作方向 |
|---|---|---|
| user lock | SELECT id,email,phone,is_active,life_55688_id FROM users WHERE id=:id FOR UPDATE | 使用 transaction + FOR UPDATE |
| service archive | user 所有 quote_services 設 is_active=0,is_archived=1 | 不加 legacy 沒有的 active / archived filter |
| request archive | user 未 archived quote_requests 設 is_archived=1,is_closed=1 | 只查 user_id + is_archived=0 |
| request activity | 每個被 archive request 寫 CloseRequest 與 ArchiveRequest | 用 QuoteActivity::createQuoteRequestActivityById() |
| bid activity/unread | request 下 quote_status_id IN (InProgress,Hired) bids 寫 QuoteBidClosed,並 set unread d=0 | 不更新 quote_bids 欄位 |
| user activity | 寫 DeleteUser activity | migration decision:/users/delete.json 是 self-delete path,new 固定寫 param1 = '(self)',對齊 legacy DB 實際結果;不修成 admin:{id}(self) |
| sessions | ApiSession.user_id = id 全刪 | DELETE FROM api_sessions WHERE user_id = ? |
| Life55688 | 舊 life_55688_id 非空才送 delete event | 空值不呼叫;非空 path staging/manual 驗證 |
| nshop | user 所有 nshop items 設 deleted,且每筆 delete ES;例外只 log | 不讓 nshop/ES 失敗 rollback 主流程 |
| task queue | moengage_erase_user、sumsub_deactivate_user | payload 照 legacy |
| delete email | transaction 後寫 Delete_User event | email recipient 用刪除 prefix 移除後的 email |
| request info log | legacy 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_key | params |
|---|---|
moengage_erase_user | {"user_id":<id>,"email":"<original email without DEL prefix>"} |
sumsub_deactivate_user | {"user_id":<id>} |
event_queue
| event_key | params |
|---|---|
Delete_User | {"user_id":<id>,"quote_service_id":null,"tasks":[EmailTaskDO]};EmailTaskDO 保留 legacy public null 欄位,並移除 new-only shopList |
EmailTaskDO 欄位:
| 欄位 | 值 |
|---|---|
templateType | email |
templateKey | Admin 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## | 空字串 |
langId | User::getMultiLangId(user_id) |
Config 對照
| key | legacy 使用點 | new 狀態 |
|---|---|---|
api_life55688 | Life55688Util::addEvent() endpoint base URL | Lib/Common/Configuration.php 已有 key;部署時需確認 staging / production 實際值 |
api_life55688_key | Life55688 Authorization header | Lib/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_value | count | first_created | last_created |
|---|---|---|---|
(self) | 908 | 2018-12-03 08:20:40 | 2026-06-18 04:19:05 |
最近 20 筆 DeleteUser activity 的 param1 也全部是 (self),沒有 admin:{id} 或空值。因此 new 實作直接寫 (self) 是依 DB 實際行為對齊,不是自行修正 legacy 原意。
PHP 5.6 -> 8.2 注意
| 項目 | legacy | new 建議 |
|---|---|---|
| phone/email prefix | legacy 用字串 concat;空值會當空字串 | new 需明確 cast string,避免 PHP 8.2 null warning |
deleteUserSafety() retry | legacy begin() 在 retry loop 外,exception rollback 後沒有重新 begin,語意不乾淨 | 已確認 migration decision:new 改成正確 retry;每次 attempt 都重新 begin,成功 commit,失敗 rollback;只 retry DB deadlock / lock wait timeout,其他錯誤直接回 999 |
| user activity param | legacy 表達式 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 user | legacy 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(),因為它不等同 legacyarchiveUserRequests()。 - 不把 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 user | response success;users 只更新刪帳欄位;api_sessions 被刪;DeleteUser activity、task queues、Delete_User event 存在 |
| services / requests / bids | user services archived;未 archived requests close+archive;Close/Archive request activity;InProgress bid 寫 QuoteBidClosed activity,Rejected bid 不寫 |
| negative fields | password、wallet、counters、user_profiles 不被改 |
already DEL email | email 不重複 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。 - 查
users、quote_services、quote_requests、quote_activities、api_sessions、task_event_queue/ log、event_queue/ log。 - 另找有
life_55688_id的測試資料或人工設值,確認 Life55688 delete event 行為;不要在一般 PHPUnit 打外部 API。
待確認
- 無。