From 639879c08953b9a3a7375f60a21ba32e8045185b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=80=A7=E9=A9=8A?= Date: Thu, 2 Oct 2025 17:52:22 +0800 Subject: [PATCH] feat: add register func for digimon --- gateway.json | 1156 +++++++++++++++++++++ generate/api/gateway.api | 2 +- generate/api/member.api | 1 + internal/domain/const.go | 4 + internal/handler/auth/login_handler.go | 36 +- internal/handler/auth/register_handler.go | 32 +- internal/handler/ping/ping_handler.go | 2 +- internal/logic/auth/register_logic.go | 198 +++- internal/types/types.go | 1 + pkg/member/domain/member/default.go | 21 + pkg/member/domain/member/platform.go | 2 +- pkg/member/usecase/member.go | 7 +- 12 files changed, 1445 insertions(+), 17 deletions(-) create mode 100644 gateway.json create mode 100644 internal/domain/const.go create mode 100644 pkg/member/domain/member/default.go diff --git a/gateway.json b/gateway.json new file mode 100644 index 0000000..ca217b9 --- /dev/null +++ b/gateway.json @@ -0,0 +1,1156 @@ +{ + "components": { + "schemas": { + "Authorization": { + "type": "object" + }, + "BaseReq": { + "type": "object" + }, + "CredentialsPayload": { + "properties": { + "accountType": { + "type": "string" + }, + "password": { + "description": "密碼 (後端應使用 bcrypt 進行雜湊)", + "type": "string" + }, + "password_confirm": { + "description": "確認密碼", + "type": "string" + } + }, + "required": [ + "password", + "password_confirm", + "accountType" + ], + "type": "object" + }, + "ErrorResp": { + "properties": { + "code": { + "type": "integer" + }, + "details": { + "type": "string" + }, + "error": { + "description": "可選的錯誤信息" + }, + "msg": { + "type": "string" + } + }, + "required": [ + "code", + "msg" + ], + "type": "object" + }, + "LoginReq": { + "properties": { + "auth_method": { + "type": "string" + }, + "credentials": { + "$ref": "#/components/schemas/CredentialsPayload" + }, + "login_id": { + "description": "信箱或手機號碼", + "type": "string" + }, + "platform": { + "$ref": "#/components/schemas/PlatformPayload" + } + }, + "required": [ + "auth_method", + "login_id" + ], + "type": "object" + }, + "LoginResp": { + "properties": { + "access_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + }, + "token_type": { + "description": "通常固定為 \"Bearer\"", + "type": "string" + }, + "uid": { + "type": "string" + } + }, + "required": [ + "uid", + "access_token", + "refresh_token", + "token_type" + ], + "type": "object" + }, + "PagerResp": { + "properties": { + "index": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + }, + "required": [ + "total", + "size", + "index" + ], + "type": "object" + }, + "PlatformPayload": { + "properties": { + "provider": { + "description": "平台名稱", + "type": "string" + }, + "token": { + "description": "平台提供的 Access Token 或 ID Token", + "type": "string" + } + }, + "required": [ + "provider", + "token" + ], + "type": "object" + }, + "RefreshTokenReq": { + "properties": { + "refresh_token": { + "type": "string" + } + }, + "required": [ + "refresh_token" + ], + "type": "object" + }, + "RefreshTokenResp": { + "properties": { + "access_token": { + "type": "string" + }, + "refresh_token": { + "description": "可選:某些策略下刷新後也會換發新的 Refresh Token", + "type": "string" + }, + "token_type": { + "type": "string" + } + }, + "required": [ + "access_token", + "refresh_token", + "token_type" + ], + "type": "object" + }, + "RequestPasswordResetReq": { + "properties": { + "account_type": { + "type": "string" + }, + "identifier": { + "description": "使用者帳號 (信箱或手機)", + "type": "string" + } + }, + "required": [ + "identifier", + "account_type" + ], + "type": "object" + }, + "RequestVerificationCodeReq": { + "properties": { + "purpose": { + "type": "string" + } + }, + "required": [ + "purpose" + ], + "type": "object" + }, + "ResetPasswordReq": { + "properties": { + "identifier": { + "type": "string" + }, + "password": { + "description": "新密碼", + "type": "string" + }, + "password_confirm": { + "description": "確認新密碼", + "type": "string" + }, + "verify_code": { + "description": "來自上一步驗證通過的 Code,作為一種「票證」", + "type": "string" + } + }, + "required": [ + "identifier", + "verify_code", + "password", + "password_confirm" + ], + "type": "object" + }, + "RespOK": { + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "msg": { + "type": "string" + } + }, + "required": [ + "code", + "msg" + ], + "type": "object" + }, + "SubmitVerificationCodeReq": { + "properties": { + "purpose": { + "type": "string" + }, + "verify_code": { + "type": "string" + } + }, + "required": [ + "purpose", + "verify_code" + ], + "type": "object" + }, + "UpdatePasswordReq": { + "properties": { + "current_password": { + "type": "string" + }, + "new_password": { + "type": "string" + }, + "new_password_confirm": { + "type": "string" + } + }, + "required": [ + "current_password", + "new_password", + "new_password_confirm" + ], + "type": "object" + }, + "UpdateUserInfoReq": { + "properties": { + "address": { + "description": "地址", + "type": "string" + }, + "avatar_url": { + "description": "頭像 URL", + "type": "string" + }, + "birthdate": { + "description": "生日 (格式: 1993-04-17)", + "type": "string" + }, + "carrier": { + "description": "載具", + "type": "string" + }, + "currency": { + "description": "貨幣代號", + "type": "string" + }, + "full_name": { + "description": "用戶全名", + "type": "string" + }, + "gender_code": { + "description": "性別", + "type": "string" + }, + "national": { + "description": "國家", + "type": "string" + }, + "nickname": { + "description": "暱稱", + "type": "string" + }, + "post_code": { + "description": "郵遞區號", + "type": "string" + }, + "preferred_language": { + "description": "語言", + "type": "string" + } + }, + "type": "object" + }, + "UserInfoResp": { + "properties": { + "address": { + "description": "地址", + "type": "string" + }, + "avatar_url": { + "description": "頭像 URL", + "type": "string" + }, + "birthdate": { + "description": "生日 (格式: 1993-04-17)", + "type": "string" + }, + "carrier": { + "description": "載具", + "type": "string" + }, + "create_at": { + "type": "string" + }, + "currency": { + "description": "偏好幣種", + "type": "string" + }, + "email": { + "description": "信箱", + "type": "string" + }, + "full_name": { + "description": "用戶全名", + "type": "string" + }, + "gender_code": { + "description": "性別代碼", + "type": "string" + }, + "is_email_verified": { + "description": "信箱是否已驗證", + "type": "boolean" + }, + "is_phone_verified": { + "description": "手機是否已驗證", + "type": "boolean" + }, + "national": { + "description": "國家", + "type": "string" + }, + "nickname": { + "description": "暱稱", + "type": "string" + }, + "phone_number": { + "description": "電話", + "type": "string" + }, + "platform": { + "description": "註冊平台", + "type": "string" + }, + "post_code": { + "description": "郵遞區號", + "type": "string" + }, + "preferred_language": { + "description": "偏好語言", + "type": "string" + }, + "role": { + "description": "角色", + "type": "string" + }, + "uid": { + "description": "用戶 UID", + "type": "string" + }, + "update_at": { + "type": "string" + }, + "user_status": { + "description": "用戶狀態", + "type": "string" + } + }, + "required": [ + "platform", + "uid", + "avatar_url", + "full_name", + "nickname", + "gender_code", + "birthdate", + "phone_number", + "is_phone_verified", + "email", + "is_email_verified", + "address", + "user_status", + "preferred_language", + "currency", + "national", + "post_code", + "carrier", + "role", + "update_at", + "create_at" + ], + "type": "object" + }, + "VerifyCodeReq": { + "properties": { + "identifier": { + "type": "string" + }, + "verify_code": { + "type": "string" + } + }, + "required": [ + "identifier", + "verify_code" + ], + "type": "object" + } + } + }, + "info": { + "contact": { + "email": "igs170911@gmail.com", + "name": "Daniel Wang" + }, + "description": "This is Digimon Platform ", + "title": "Digimon Platform API Gateway", + "version": "v1" + }, + "openapi": "3.0.3", + "paths": { + "/api/v1/auth/password-resets": { + "put": { + "description": "使用有效的驗證碼來設定新的密碼。", + "operationId": "authResetPassword", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResetPasswordReq" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RespOK" + } + } + }, + "description": "// 密碼重設成功" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "\"驗證碼無效或請求參數錯誤\"" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "// 伺服器內部錯誤" + } + }, + "summary": "執行密碼重設" + } + }, + "/api/v1/auth/password-resets/request": { + "post": { + "description": "為指定的 email 或 phone 發送一個一次性的密碼重設驗證碼。", + "operationId": "authRequestPasswordReset", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestPasswordResetReq" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RespOK" + } + } + }, + "description": "// 請求成功 (為安全起見,即使帳號不存在也應返回成功)" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "\"請求參數格式錯誤\"" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "\"請求過於頻繁\" // 429 Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "// 伺服器內部錯誤" + } + }, + "summary": "請求發送密碼重設驗證碼" + } + }, + "/api/v1/auth/password-resets/verify": { + "post": { + "description": "在實際重設密碼前,先驗證使用者輸入的驗證碼是否正確。", + "operationId": "authVerifyPasswordResetCode", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyCodeReq" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RespOK" + } + } + }, + "description": "// 驗證碼正確" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "\"驗證碼無效或已過期\"" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "// 伺服器內部錯誤" + } + }, + "summary": "校驗密碼重設驗證碼" + } + }, + "/api/v1/auth/register": { + "post": { + "description": "使用傳統帳號密碼或第三方平台進行註冊。成功後直接返回登入後的 Token 資訊。", + "operationId": "authRegister", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginReq" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginResp" + } + } + }, + "description": "// 註冊成功,並返回 Token" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "\"請求參數格式錯誤\"" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "\"帳號已被註冊\" // 409 Conflict: 資源衝突" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "// 伺服器內部錯誤" + } + }, + "summary": "註冊新帳號" + } + }, + "/api/v1/auth/sessions": { + "post": { + "description": "使用傳統帳號密碼或第三方平台 Token 進行登入,以創建一個新的會話(Session)。", + "operationId": "authLogin", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginReq" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginResp" + } + } + }, + "description": "// 登入成功" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "\"請求參數格式錯誤\"" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "\"帳號或密碼錯誤 / 無效的平台 Token\" // 401 Unauthorized" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "// 伺服器內部錯誤" + } + }, + "summary": "使用者登入" + } + }, + "/api/v1/auth/sessions/refresh": { + "post": { + "description": "使用有效的 Refresh Token 來獲取一組新的 Access Token 和 Refresh Token。", + "operationId": "authRefreshToken", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefreshTokenReq" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefreshTokenResp" + } + } + }, + "description": "// 刷新成功" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "\"請求參數格式錯誤\"" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "\"無效或已過期的 Refresh Token\" // 401 Unauthorized" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "// 伺服器內部錯誤" + } + }, + "summary": "刷新 Access Token" + } + }, + "/api/v1/health": { + "get": { + "description": "檢查系統服務狀態,用於監控和負載均衡器健康檢查。返回系統運行狀態信息。", + "operationId": "pingPing", + "responses": { + "200": { + "content": { + "application/json": { + "schema": {} + } + }, + "description": "" + } + }, + "summary": "系統健康檢查" + } + }, + "/api/v1/user/me": { + "get": { + "operationId": "userGetUserInfo", + "parameters": [ + { + "in": "header", + "name": "Authorization", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserInfoResp" + } + } + }, + "description": "// 成功獲取" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "\"未授權或 Token 無效\"" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "\"找不到使用者\"" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "// 伺服器內部錯誤" + } + }, + "summary": "取得當前登入的會員資訊(自己)" + }, + "put": { + "description": "只更新傳入的欄位,未傳入的欄位將保持不變。", + "operationId": "userUpdateUserInfo", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateUserInfoReq" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserInfoResp" + } + } + }, + "description": "// 更新成功,並返回更新後的使用者資訊" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "\"請求參數格式錯誤\"" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "\"未授權或 Token 無效\"" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "// 伺服器內部錯誤" + } + }, + "summary": "更新當前登入的會員資訊" + } + }, + "/api/v1/user/me/password": { + "put": { + "description": "必須提供當前密碼以進行驗證。", + "operationId": "userUpdatePassword", + "parameters": [ + { + "in": "header", + "name": "Authorization", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdatePasswordReq" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RespOK" + } + } + }, + "description": "// 密碼修改成功" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "\"請求參數格式錯誤或新舊密碼不符\"" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "\"未授權或 Token 無效\"" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "\"當前密碼不正確\" // 403 Forbidden" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "// 伺服器內部錯誤" + } + }, + "summary": "修改當前登入使用者的密碼" + } + }, + "/api/v1/user/me/verifications": { + "post": { + "description": "根據傳入的 `purpose` 發送對應的驗證碼。", + "operationId": "userRequestVerificationCode", + "parameters": [ + { + "in": "header", + "name": "Authorization", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestVerificationCodeReq" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RespOK" + } + } + }, + "description": "// 請求已受理" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "\"請求參數格式錯誤\"" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "\"未授權或 Token 無效\"" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "\"請求過於頻繁\" // 429 Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "// 伺服器內部錯誤" + } + }, + "summary": "請求發送驗證碼 (用於驗證信箱/手機)" + }, + "put": { + "description": "提交收到的驗證碼,以完成特定目的的驗證,例如綁定手機或 Email。", + "operationId": "userSubmitVerificationCode", + "parameters": [ + { + "in": "header", + "name": "Authorization", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubmitVerificationCodeReq" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RespOK" + } + } + }, + "description": "// 驗證成功" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "\"驗證碼無效或已過期\"" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "\"未授權或 Token 無效\"" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResp" + } + } + }, + "description": "// 伺服器內部錯誤" + } + }, + "summary": "提交驗證碼以完成驗證" + } + } + }, + "servers": [ + { + "url": "http://localhost:8888" + }, + { + "url": "https://localhost:8888" + } + ], + "x-date": "2025-10-02 17:47:43", + "x-description": "This is a go-doc generated swagger file.", + "x-generator": "go-doc", + "x-github": "https://github.com/danielchan-25/go-doc", + "x-source": "go-zero API specification" +} \ No newline at end of file diff --git a/generate/api/gateway.api b/generate/api/gateway.api index 051d28d..f06716c 100755 --- a/generate/api/gateway.api +++ b/generate/api/gateway.api @@ -9,7 +9,7 @@ info ( consumes: "application/json" produces: "application/json" schemes: "http,https" - host: "127.0.0.1:8888" + host: "localhost:8888" ) import ( diff --git a/generate/api/member.api b/generate/api/member.api index 445718c..e313d3b 100644 --- a/generate/api/member.api +++ b/generate/api/member.api @@ -10,6 +10,7 @@ type ( CredentialsPayload { Password string `json:"password" validate:"required,min=8,max=128"` // 密碼 (後端應使用 bcrypt 進行雜湊) PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password"` // 確認密碼 + AccountType string `json:"accountType" validate:"required,oneof=email phone any"` } // PlatformPayload 第三方平台註冊的資料 diff --git a/internal/domain/const.go b/internal/domain/const.go new file mode 100644 index 0000000..3cee543 --- /dev/null +++ b/internal/domain/const.go @@ -0,0 +1,4 @@ +package domain + +const SuccessCode = 10200 +const SuccessMessage = "success" diff --git a/internal/handler/auth/login_handler.go b/internal/handler/auth/login_handler.go index c3fdedf..c7dbb37 100644 --- a/internal/handler/auth/login_handler.go +++ b/internal/handler/auth/login_handler.go @@ -1,11 +1,13 @@ package auth import ( - "net/http" - + "backend/internal/domain" "backend/internal/logic/auth" "backend/internal/svc" "backend/internal/types" + "code.30cm.net/digimon/library-go/errs" + ers "code.30cm.net/digimon/library-go/errs" + "net/http" "github.com/zeromicro/go-zero/rest/httpx" ) @@ -15,16 +17,40 @@ func LoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.LoginReq if err := httpx.Parse(r, &req); err != nil { - httpx.ErrorCtx(r.Context(), w, err) + e := errs.InvalidFormat(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.RespOK{ + Code: int(e.FullCode()), + Msg: err.Error(), + }) + return } + //if err := svcCtx.Validate.ValidateAll(req); err != nil { + // e := errs.InvalidFormat(err.Error()) + // httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.RespOK{ + // Code: int(e.FullCode()), + // Msg: err.Error(), + // }) + // + // return + //} + l := auth.NewLoginLogic(r.Context(), svcCtx) resp, err := l.Login(&req) if err != nil { - httpx.ErrorCtx(r.Context(), w, err) + e := ers.FromError(err) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.ErrorResp{ + Code: int(e.FullCode()), + Msg: e.Error(), + Error: e, + }) } else { - httpx.OkJsonCtx(r.Context(), w, resp) + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.RespOK{ + Code: domain.SuccessCode, + Msg: domain.SuccessMessage, + Data: resp, + }) } } } diff --git a/internal/handler/auth/register_handler.go b/internal/handler/auth/register_handler.go index 26c8744..93e77b3 100644 --- a/internal/handler/auth/register_handler.go +++ b/internal/handler/auth/register_handler.go @@ -1,6 +1,8 @@ package auth import ( + "backend/internal/domain" + "code.30cm.net/digimon/library-go/errs" "net/http" "backend/internal/logic/auth" @@ -15,16 +17,38 @@ func RegisterHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.LoginReq if err := httpx.Parse(r, &req); err != nil { - httpx.ErrorCtx(r.Context(), w, err) + e := errs.InvalidFormat(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.RespOK{ + Code: int(e.FullCode()), + Msg: err.Error(), + }) + return } - + //if err := svcCtx.Validate.ValidateAll(req); err != nil { + // e := errs.InvalidFormat(err.Error()) + // httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.RespOK{ + // Code: int(e.FullCode()), + // Msg: err.Error(), + // }) + // + // return + //} l := auth.NewRegisterLogic(r.Context(), svcCtx) resp, err := l.Register(&req) if err != nil { - httpx.ErrorCtx(r.Context(), w, err) + e := errs.FromError(err) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.ErrorResp{ + Code: int(e.FullCode()), + Msg: e.Error(), + Error: e, + }) } else { - httpx.OkJsonCtx(r.Context(), w, resp) + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.RespOK{ + Code: domain.SuccessCode, + Msg: domain.SuccessMessage, + Data: resp, + }) } } } diff --git a/internal/handler/ping/ping_handler.go b/internal/handler/ping/ping_handler.go index 10b71a3..b7a26b4 100644 --- a/internal/handler/ping/ping_handler.go +++ b/internal/handler/ping/ping_handler.go @@ -9,7 +9,7 @@ import ( "github.com/zeromicro/go-zero/rest/httpx" ) -// 系統健康檢查 +// PingHandler 系統健康檢查 func PingHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { l := ping.NewPingLogic(r.Context(), svcCtx) diff --git a/internal/logic/auth/register_logic.go b/internal/logic/auth/register_logic.go index 6274d7d..5ea914b 100644 --- a/internal/logic/auth/register_logic.go +++ b/internal/logic/auth/register_logic.go @@ -1,10 +1,15 @@ package auth import ( - "context" - "backend/internal/svc" "backend/internal/types" + mb "backend/pkg/member/domain/member" + member "backend/pkg/member/domain/usecase" + "code.30cm.net/digimon/library-go/errs" + "code.30cm.net/digimon/library-go/errs/code" + "context" + "google.golang.org/protobuf/proto" + "time" "github.com/zeromicro/go-zero/core/logx" ) @@ -15,6 +20,12 @@ type RegisterLogic struct { svcCtx *svc.ServiceContext } +var PrepareFunc map[string]func(ctx context.Context, req *types.LoginReq, svc *svc.ServiceContext) (buildData, error) = map[string]func(ctx context.Context, req *types.LoginReq, svc *svc.ServiceContext) (buildData, error){ + mb.Digimon.ToString(): buildCredentialsData, + mb.Google.ToString(): buildGoogleData, + mb.Line.ToString(): buildLineData, +} + // 註冊新帳號 func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic { return &RegisterLogic{ @@ -25,7 +36,186 @@ func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Register } func (l *RegisterLogic) Register(req *types.LoginReq) (resp *types.LoginResp, err error) { - // todo: add your logic here and delete this line + // Step 1: 根據平台構建相關數據 + var bd buildData + switch req.AuthMethod { + case "credentials": + fn, ok := PrepareFunc[mb.Digimon.ToString()] + if !ok { + return nil, errs.InvalidRangeWithScope(code.CloudEPMember, 0, "failed to get correct credentials method") + } + bd, err = fn(l.ctx, req, l.svcCtx) + if err != nil { + return nil, err + } + case "platform": + fn, ok := PrepareFunc[req.Platform.Provider] + if !ok { + return nil, errs.InvalidRangeWithScope(code.CloudEPMember, 0, "failed to get correct credentials method") + } + bd, err = fn(l.ctx, req, l.svcCtx) + if err != nil { + return nil, err + } + default: + return nil, errs.InvalidFormatWithScope(code.CloudEPMember, "failed to get correct auth method") + } + + // Step 2: 建立帳號 + if err := l.svcCtx.AccountUC.CreateUserAccount(l.ctx, bd.CreateAccountReq); err != nil { + return nil, err + } + // Step 3: 綁定帳號並獲取 UID + account, err := l.svcCtx.AccountUC.BindAccount(l.ctx, bd.BindingUserReq) + if err != nil { + return nil, err + } + + bd.CreateUserInfoRequest.UID = account.UID + // Step 4: 更新使用者資訊 + if err := l.svcCtx.AccountUC.BindUserInfo(l.ctx, bd.CreateUserInfoRequest); err != nil { + return nil, err + } + + // Step 5: 生成 Token + req.LoginID = bd.CreateAccountReq.LoginID + token, err := l.generateToken(req, account.UID) + if err != nil { + return nil, err + } + + return &types.LoginResp{ + UID: account.UID, + AccessToken: token.AccessToken, + RefreshToken: token.RefreshToken, + TokenType: token.TokenType, + }, nil +} + +type buildData struct { + CreateAccountReq member.CreateLoginUserRequest + BindingUserReq member.BindingUser + CreateUserInfoRequest member.CreateUserInfoRequest +} + +func buildCredentialsData(_ context.Context, req *types.LoginReq, _ *svc.ServiceContext) (buildData, error) { + return buildData{ + CreateAccountReq: member.CreateLoginUserRequest{ + LoginID: req.LoginID, + Token: req.Credentials.Password, + Platform: mb.Digimon, + }, + BindingUserReq: member.BindingUser{ + LoginID: req.LoginID, + Type: mb.GetAccountTypeByCode(req.Credentials.AccountType), + }, + CreateUserInfoRequest: member.CreateUserInfoRequest{ + AlarmCategory: mb.AlarmNoAlert, + UserStatus: mb.AccountStatusUnverified, + PreferredLanguage: mb.LangTW.ToString(), + Currency: mb.CurrencyTWD.ToString(), + }, + }, nil +} + +func buildGoogleData(ctx context.Context, req *types.LoginReq, svc *svc.ServiceContext) (buildData, error) { + googleToken, err := svc.AccountUC.VerifyGoogleAuthResult(ctx, member.VerifyAuthResultRequest{ + Account: req.LoginID, + Token: req.Platform.Token, + }) + if err != nil { + return buildData{}, err + } + + return buildData{ + CreateAccountReq: member.CreateLoginUserRequest{ + LoginID: googleToken.Email, + Token: "", + Platform: mb.Google, + }, + BindingUserReq: member.BindingUser{ + LoginID: req.LoginID, + Type: mb.AccountTypeNone, + }, + CreateUserInfoRequest: member.CreateUserInfoRequest{ + AvatarURL: proto.String(googleToken.Picture), + FullName: proto.String(googleToken.Name), + Nickname: proto.String(googleToken.Name), + Email: proto.String(googleToken.Email), + AlarmCategory: mb.AlarmNoAlert, + UserStatus: mb.AccountStatusUnverified, + PreferredLanguage: mb.LangTW.ToString(), + Currency: mb.CurrencyTWD.ToString(), + }, + }, nil +} + +func buildLineData(ctx context.Context, req *types.LoginReq, svc *svc.ServiceContext) (buildData, error) { + lineAccessToken, err := svc.AccountUC.LineCodeToAccessToken(ctx, req.LoginID) + if err != nil { + return buildData{}, err + } + + userInfo, err := svc.AccountUC.LineGetProfileByAccessToken(ctx, lineAccessToken.AccessToken) + if err != nil { + return buildData{}, err + } + return buildData{ + CreateAccountReq: member.CreateLoginUserRequest{ + LoginID: userInfo.UserID, + Token: "", + Platform: mb.Line, + }, + BindingUserReq: member.BindingUser{ + LoginID: userInfo.UserID, + Type: mb.AccountTypeNone, + }, + CreateUserInfoRequest: member.CreateUserInfoRequest{ + AvatarURL: proto.String(userInfo.PictureURL), + FullName: proto.String(userInfo.DisplayName), + Nickname: proto.String(userInfo.DisplayName), + AlarmCategory: mb.AlarmNoAlert, + UserStatus: mb.AccountStatusUnverified, + PreferredLanguage: mb.LangTW.ToString(), + Currency: mb.CurrencyTWD.ToString(), + }, + }, nil +} + +type MockToken struct { + AccessToken string `json:"access_token"` // 訪問令牌 + TokenType string `json:"token_type"` // 令牌類型 + ExpiresIn int64 `json:"expires_in"` // 過期時間(秒) + RefreshToken string `json:"refresh_token"` // 刷新令牌 +} + +// 生成 Token +func (l *RegisterLogic) generateToken(req *types.LoginReq, uid string) (MockToken, error) { + //credentials := tokenModule.ClientCredentials + //role := "user" + //if isTruHeartEmail(req.Account) { + // role = "admin" + //} + // + //return l.svcCtx.TokenUseCase.NewToken(l.ctx, tokenModule.AuthorizationReq{ + // GrantType: credentials.ToString(), + // DeviceID: req.DeviceID, + // Scope: domain.DefaultScope, + // IsRefreshToken: true, + // Expires: time.Now().UTC().Add(l.svcCtx.Config.Token.Expired).Unix(), + // Data: map[string]string{ + // "uid": uid, + // "role": role, + // "account": req.Account, + // }, + // Role: role, + //}) + + return MockToken{ + AccessToken: "gg88g88", + TokenType: "Bearer", + ExpiresIn: time.Now().UTC().Add(100000000000).Unix(), + RefreshToken: "gg88g88", + }, nil - return } diff --git a/internal/types/types.go b/internal/types/types.go index bb017a8..46f4e3c 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -13,6 +13,7 @@ type BaseReq struct { type CredentialsPayload struct { Password string `json:"password" validate:"required,min=8,max=128"` // 密碼 (後端應使用 bcrypt 進行雜湊) PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password"` // 確認密碼 + AccountType string `json:"accountType" validate:"required,oneof=email phone any"` } type ErrorResp struct { diff --git a/pkg/member/domain/member/default.go b/pkg/member/domain/member/default.go new file mode 100644 index 0000000..458bafb --- /dev/null +++ b/pkg/member/domain/member/default.go @@ -0,0 +1,21 @@ +package member + +type Lang string + +func (l Lang) ToString() string { + return string(l) +} + +type Currency string + +func (c Currency) ToString() string { + return string(c) +} + +const ( + LangTW Lang = "zh-tw" +) + +const ( + CurrencyTWD Currency = "TWD" +) diff --git a/pkg/member/domain/member/platform.go b/pkg/member/domain/member/platform.go index 6cdfeed..63c78f3 100644 --- a/pkg/member/domain/member/platform.go +++ b/pkg/member/domain/member/platform.go @@ -12,7 +12,7 @@ const ( ) const ( - DigimonString = "platform" + DigimonString = "credentials" GoogleString = "google" LineString = "line" AppleString = "apple" diff --git a/pkg/member/usecase/member.go b/pkg/member/usecase/member.go index 01479e3..a57c1f8 100644 --- a/pkg/member/usecase/member.go +++ b/pkg/member/usecase/member.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "go.mongodb.org/mongo-driver/v2/mongo" "math" "backend/pkg/member/domain" @@ -82,7 +83,11 @@ func (use *MemberUseCase) processPasswordForPlatform(platform member.Platform, p // insertAccount inserts the account into the database func (use *MemberUseCase) insertAccount(ctx context.Context, account *entity.Account, req usecase.CreateLoginUserRequest) error { err := use.Account.Insert(ctx, account) + if err != nil { + if mongo.IsDuplicateKeyError(err) { + return errs.DBDuplicate("account duplicate").Wrap(err) + } return errs.DatabaseErrorWithScopeL( code.CloudEPMember, domain.InsertAccountErrorCode, @@ -92,7 +97,7 @@ func (use *MemberUseCase) insertAccount(ctx context.Context, account *entity.Acc {Key: "func", Value: "Account.Insert"}, {Key: "err", Value: err.Error()}, }, - "account duplicate").Wrap(err) + "failed to insert to database").Wrap(err) } return nil