POST /users/sign_up.json
快速結論
- 狀態:已補
Users::sign_up()與 PHPUnit;已做 staging qna path 實測,完整 social token diff 待補。 - 2026-06-24 查 legacy 正式機 access log,
POST /users/sign_up.json有 894 筆200,另有 201 筆OPTIONS 200。 - 這支是 consumer 註冊 API,會建立
users、user_profiles、user_logins、api_sessions、activity,並可能寫 referral、phone verification side effect、Consumer_Signupevent。 - 搬移時只以 legacy 實際執行路徑為準;不可直接沿用
search_add/quote_services/addauto signup helper。 - 2026-06-26 已確認 web-app Google 登入不會自然 fallback 呼叫本 API;web Google 目前走
/users/login.json。本 API 的 Google branch 是 backend legacy capability,只有 caller 明確送User[id_token]到/users/sign_up.json才會執行。 - migration decision:本次經 reviewer 確認,legacy 有
sign_upendpoint 專用 request log,new 不再補這份 duplicate full request log;保留RouterV3generic request log,password/passwd key 由 router 層過濾。
Legacy 對照
| 檔案 | 行號 | 行為 |
|---|---|---|
/Users/mattsu/Documents/Site/get-lancer-php56/app/Controller/UsersController.php | 6044-6049 | JSON request + valid API key guard |
| 同上 | 6071-6104 | ibon / qna 判斷、required / optional input、trim |
| 同上 | 6106-6117 | email blacklist、password、第一次 country_code + phone、platform、device id |
| 同上 | 6118-6149 | Google / Facebook / Facebook limited / Apple social auth,並用 social id 找既有 user |
| 同上 | 6151-6163 | social id email mismatch 時忽略該 existing user;email 已存在回 email_existed |
| 同上 | 6166-6176 | 有 verify_code 時第二次 prepend country_code,再跑 PhoneVerification::afterAuth() |
| 同上 | 6179-6243 | 組 users insert data,User::save(false),失敗回 create_user_failed |
| 同上 | 6245-6258 | verified phone side effect、建立 user_profiles |
| 同上 | 6260-6274 | 寫 user_logins、referral、建立或更新 api_sessions |
| 同上 | 6276-6285 | 寫 RequestorSignup activity;非 ibon/qna 才送 welcome event 與 CRM upsert |
| 同上 | 6287-6296 | success response |
/Users/mattsu/Documents/Site/get-lancer-php56/app/Controller/RestApiHelper.php | 297-313 | getSafeUsername() |
/Users/mattsu/Documents/Site/get-lancer-php56/app/Model/User.php | 573-629 | sendWelcomeMail() 寫 Consumer_Signup event,task 是 Moengage |
/Users/mattsu/Documents/Site/get-lancer-php56/app/Model/User.php | 1118-1151 | setVerifiedPhoneForUser() normalize phone、清其他 user phone confirm、更新目前 user |
/Users/mattsu/Documents/Site/get-lancer-php56/app/Model/UserReferral.php | 59-69 | addReferral() 依年度上限寫 user_referrals 或 user_over_referrals |
New 對照
| 檔案 | 行號 | 行為 |
|---|---|---|
Endpoint/V1/Users.php | 585-721 | sign_up() action:guard、input、DB write、side effect、response;不再寫 endpoint duplicate request log |
| 同上 | 1353-1364 | legacy-shaped generic error response |
| 同上 | 1366-1385 | 支援 nested User 與 flat User[...] form key |
| 同上 | 1387-1473 | social auth 與 existing user lookup;lookup 不加 is_active filter |
| 同上 | 1475-1531 | users insert allowlist |
| 同上 | 1533-1548 | user_profiles insert allowlist |
| 同上 | 1550-1577 | scoped insert helper,只允許本 API 會寫的 table |
| 同上 | 1579-1622 | legacy safe username |
| 同上 | 1624-1757 | verified phone side effect、Reset_Phone_Confirm event |
| 同上 | 1759-1833 | user login device id、referral、ApiSession::upsert() 前補 legacy $_SERVER API key |
| 同上 | 1835-1922 | Consumer_Signup event payload、7 天內 SecurityHash reuse,沒有才新增 |
Lib/Common/RouterV3.php | 210, 266-289 | generic request log;password/passwd key 過濾由 router 層統一處理 |
tests/UsersSignUpTest.php | 97-192 | success path:User[...] multipart key、user/profile/login/session/activity/event/phone verification |
| 同上 | 194-242 | phone conflict:舊 user phone reset 並寫 Reset_Phone_Confirm event |
| 同上 | 244-354 | invalid API key、missing verify code、inactive email、country code bug、ibon path |
| 同上 | 356-461 | social 欄位 mapping 與 negative assertion:不可補高風險 insert 欄位,不可加 is_active lookup guard,不可替 Apple code lookup 加 legacy 沒有的 order |
Input / Response
| 項目 | 行為 |
|---|---|
| method | POST |
| auth | 全域 API key;不需要 user session |
| body shape | nested User 或 multipart User[...] |
| required | 一般 user:email、name、phone、device_id、verify_code |
| ibon / qna | is_ibon_user=1 或 is_qna_user=1 時不要求 phone / verify_code,且不送 welcome event |
| success response | {"error":0,"status":"success","user_id":...,"username":email,"session_id":...} |
| known error response | {"error":<code>,"status":"<legacy status>"} |
| generic exception | {"error":999,"status":<translated err_new_api_999>} |
Frontend Caller
2026-06-26 檢查 /Users/mattsu/Documents/Site/web-app:
| 流程 | web-app caller | 實際 API | 備註 |
|---|---|---|---|
| Facebook signup fallback | modules/utils/api-manager.js::signupWithFB() | /users/sign_up.json | 會送 User[is_qna_user]=1、User[facebook_user_id]、User[access_token] |
| Google login | modules/utils/api-manager.js::googleLogin() | /users/login.json | web 不會自然 fallback 到 /users/sign_up.json |
| Apple login | modules/utils/api-manager.js::appleLogin() | /users/login.json | web 不會自然 fallback 到 /users/sign_up.json |
因此驗證本 API 的 Google branch 不能只從 web Google 登入觸發;需要 app / 其他 caller 明確呼叫 /users/sign_up.json,或用 curl 帶真實 User[id_token] 與 User[platform]。
已知 error:
| code | status |
|---|---|
| 1 | invalid request |
| 2 | field <display> required |
| 3 | invalid_email |
| 4 | email_existed |
| 5 | phone_verification_failed |
| 6 | create_user_failed |
Insert 欄位分類
users
| 欄位 | 來源 | 備註 |
|---|---|---|
created / modified | new explicit timestamp | legacy Cake save 自動時間 |
role_id | legacy literal | 固定 2 |
username | legacy safe username | email local part,必要時加 suffix |
email | User[email] | trim 後使用 |
password | User[password] | 只有非空才用 Pro360LoginUtil::getCryptHash() 寫入 |
device_id | User[device_id] | required;ibon/qna 仍 required |
mid | User[mid] | 未帶時 explicit NULL,對齊 legacy $data['mid'] assignment |
is_active | legacy literal | 固定 1 |
ip_id | Ip::toSaveIp() | request IP |
mobile_app_hash | legacy hash | md5(_unum() . username . password . Security.salt) |
registration_source | ApiKey::getClientByKey(header api key) | 依 API key client |
multi_lang_id | MultiTranslation::getHeaderLangId() | 依 accept-language |
| social 欄位 | social token 驗證成功 | Google / Facebook / Apple 欄位只在驗證成功時寫 |
entrance | User[entrance] | 選填 |
未列在上表的 users 欄位不得放入 explicit insert allowlist。2026-06-26 local DB 確認 @@SESSION.sql_mode=NO_ENGINE_SUBSTITUTION,未 explicit 寫入的 NOT NULL 欄位由 MySQL implicit default 保持 legacy/Cake 行為。
高風險欄位不得 explicit insert:
| 類型 | 欄位例 |
|---|---|
| wallet / payment | available_wallet_amount、available_balance_amount、blocked_amount |
| counters | quote_service_count、quote_request_count、user_login_count |
| app timestamps | mobile_app_time_modified、iphone_last_request 初始 insert |
| security | security_question_id、security_answer |
| notification / subscription | auto_refill_package_id、weekly_budget_id |
user_profiles
| 欄位 | 來源 | 備註 |
|---|---|---|
created / modified | new explicit timestamp | legacy Cake save 自動時間 |
user_id | 新增的 users.id | 必寫 |
first_name | User[name],空值 fallback username | 經 scoped StringUtil::filterEmptyString() 等效處理 |
phone | $data['phone'] | legacy 先設定 phone;ibon/qna 無 phone 時會寫 empty string |
Side Effects
| 類型 | 對齊內容 |
|---|---|
| email blacklist | EmailBlacklist::isBadDomain(email),命中回 error 3 |
| existing user guard | email / social lookup 都不加 is_active filter;inactive email 也回 email_existed |
| country code | 保留 legacy 雙 prepend 行為;帶 country_code 時可能造成 phone verification failed |
| phone verification | PhoneVerification::afterAuth(phone, code) 成功後刪除驗證 row |
| verified phone | normalize phone、清其他 user confirmed phone、寫 reset event、更新目前 user phone / confirmed / iphone_last_request;此段用 transaction 對齊 legacy setVerifiedPhoneForUser() |
| phone conflict email | 舊 user 有 quote_service_categories.is_auto_quote_on = 1 時寫 Reset_Phone_Confirm event,template 601 Auto Quote Verified Phone Unconfirmed Notification |
| user login | UserLogin::insertUserLogin($userId, 1, device_id);undefined 字串只在本 API call site 轉 NULL |
| referral | 不共用 new UserReferral::addReferral();本 API 依 legacy 年度上限寫 referral / over-referral |
| session | ApiSession::upsert(),補 legacy HTTP_X_PRO360_REST_API_KEY 讓 api_sessions.api_key 對齊 |
| activity | QuoteActivity::createActivity('User', user_id, RequestorSignup, null, user_id, user_id) |
| welcome event | 非 ibon/qna 才寫 Consumer_Signup;payload 只含 user_id、quote_service_id、tasks |
| CRM | 非 ibon/qna 呼叫 QuoteService::crmUpsertUser();legacy 實際 Zoho call 已註解 |
| request log | 本次經 reviewer 確認,new 只保留 RouterV3 generic request log;不補 legacy sign_up endpoint duplicate full request log |
Config 搬移清單
Lib/Common/Configuration.php 目前被 .gitignore 忽略;以下 key 要搬到 staging / production 實際部署使用的 ProConfig::$config 或等效部署設定,不走一般 commit。
必搬
| key | legacy 來源 | new 落點 | 用途 |
|---|---|---|---|
site.url | 對應 legacy 環境的 app/Config/settings.yml;本機 legacy 範例在 /Users/mattsu/Documents/Site/get-lancer-php56/app/Config/settings.yml:271 | new 部署環境 ProConfig::$config['site.url'] | sign_up welcome / reset link;staging 搬 staging 值,production 搬 production 值 |
email.signature | 對應 legacy 環境的 app/Config/settings.yml;本機 legacy 範例在 /Users/mattsu/Documents/Site/get-lancer-php56/app/Config/settings.yml:22 | new 部署環境 ProConfig::$config['email.signature'] | sign_up welcome / phone conflict email replacement |
quote.referral_annual_limit | /Users/mattsu/Documents/Site/get-lancer-php56/app/Config/settings.yml:212,值 10 | new 部署環境 ProConfig::$config['quote.referral_annual_limit'] | referral 年度上限;程式 fallback 10 只是保護,部署設定仍應 explicit 搬 |
Google token 驗證才搬
來源是 legacy 實際部署環境的 app/Config/settings.yml 或同等 private config,落點是 new 部署環境同名 ProConfig key。本機 legacy checkout 沒有實際值,不能用本機 repo 補。
這些 key 不代表 web-app 會觸發 Google /users/sign_up.json。它們只影響後端收到 id_token 時的 Google token 驗證,例如 /users/login.json,或 caller 明確送 User[id_token] 到本 API 的情境。
| key | 對應 platform |
|---|---|
google.oauth_client_id | default |
google.oauth_client_id_ios | ios |
google.oauth_client_id_android | android |
google.oauth_client_id_ios_consumer | ios_consumer |
google.oauth_client_id_android_consumer | android_consumer |
google.oauth_client_id_android_firebase | android_firebase |
google.oauth_client_id_android_consumer_firebase | android_consumer_firebase |
不搬
| key | 原因 |
|---|---|
Security.salt | 已在本 API scoped 成 Users::LEGACY_SECURITY_SALT,避免改 global config |
area | TW 環境不要設定 area=tw;空值才是目前 phone normalization path |
DB seed
DB seed 不是 config 檔搬移。只有目標 DB 缺資料時,才從 legacy DB 搬 row:
event_actions:Consumer_Signupemail / moengage、Reset_Phone_Confirmemail。email_templates:601 Auto Quote Verified Phone Unconfirmed Notification,需is_active=1。
2026-06-26 local DB 已確認上述 event_actions / email_templates 存在。2026-06-26 local ProConfig 查詢:site.url、email.signature 有值;quote.referral_annual_limit 為 NULL;所有 google.oauth_client_id* 為 NULL。
實測與測試
執行環境:Docker PHP 8.2,/project-data。
docker exec -w /project-data cd63f9147e8d php -l Endpoint/V1/Users.php
docker exec -w /project-data cd63f9147e8d php -l tests/UsersSignUpTest.php
docker exec -w /project-data cd63f9147e8d ./vendor/bin/phpunit tests/UsersSignUpTest.php結果:
Endpoint/V1/Users.php: No syntax errors detected
tests/UsersSignUpTest.php: No syntax errors detected
tests/UsersSignUpTest.php: OK (11 tests, 128 assertions)Staging 實測:
- 2026-06-26
POST /users/sign_up.json,payload 為User[email]、User[name]、User[device_id]、User[is_qna_user]=1。 - response:
{"error":0,"status":"success","user_id":"15782","username":"signup-curl-test-20260626124527@example.com","session_id":"929e4625rdpb9in7eal6n4sdcl"}。 - DB side effect:建立
users、user_profiles、user_logins、api_sessions、RequestorSignupactivity;因is_qna_user=1不寫Consumer_Signupevent,符合 legacy path。
待確認
- 尚未做 staging vs legacy full diff;上線前需用真實 staging request 比對 response 與 DB side effect。
- social token path 已補欄位 mapping test;仍未用真實 Google / Facebook / Apple token 實測。