From 79b21c7264cee1b8bac5a9d5307f3dac3ece378a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=80=A7=E9=A9=8A?= Date: Wed, 12 Mar 2025 21:46:41 +0800 Subject: [PATCH] init project --- .gitignore | 3 + .golangci.yaml | 135 +++ Makefile | 20 + etc/gateway.example.yaml | 43 + gateway.go | 32 + gateway.json | 785 ++++++++++++++++++ generate/api/gateway.api | 21 + generate/api/member.api | 252 ++++++ go.mod | 126 +++ go.sum | 366 ++++++++ internal/config/config.go | 33 + internal/domain/const.go | 9 + internal/domain/error.go | 8 + internal/domain/gender.go | 39 + internal/domain/payload.go | 47 ++ internal/domain/redis.go | 23 + .../handler/member/account_create_handler.go | 68 ++ .../member/check_verify_code_handler.go | 79 ++ .../member/forget_password_code_handler.go | 67 ++ internal/handler/member/info_handler.go | 67 ++ internal/handler/member/login_handler.go | 67 ++ internal/handler/member/logout_handler.go | 55 ++ .../member/modify_member_info_handler.go | 67 ++ .../handler/member/modify_passwd_handler.go | 67 ++ ...pre_verify_update_password_code_handler.go | 67 ++ .../member/refresh_access_token_handler.go | 67 ++ .../member/send_verify_code_handler.go | 67 ++ .../handler/member/update_password_handler.go | 67 ++ internal/handler/routes.go | 105 +++ internal/logic/member/account_create_logic.go | 242 ++++++ .../logic/member/check_verify_code_logic.go | 104 +++ .../member/forget_password_code_logic.go | 192 +++++ internal/logic/member/info_logic.go | 95 +++ internal/logic/member/login_logic.go | 168 ++++ internal/logic/member/logout_logic.go | 38 + .../logic/member/modify_member_info_logic.go | 144 ++++ internal/logic/member/modify_passwd_logic.go | 64 ++ .../pre_verify_update_password_code_logic.go | 42 + .../member/refresh_access_token_logic.go | 61 ++ .../logic/member/send_verify_code_logic.go | 157 ++++ .../logic/member/update_password_logic.go | 92 ++ internal/middleware/auth_middleware.go | 84 ++ internal/svc/pyroscope.go | 46 + internal/svc/service_context.go | 64 ++ internal/svc/validate.go | 83 ++ internal/types/types.go | 127 +++ internal/utils/time.go | 17 + internal/utils/utils.go | 3 + 48 files changed, 4675 insertions(+) create mode 100644 .gitignore create mode 100755 .golangci.yaml create mode 100644 Makefile create mode 100644 etc/gateway.example.yaml create mode 100644 gateway.go create mode 100644 gateway.json create mode 100644 generate/api/gateway.api create mode 100644 generate/api/member.api create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/config/config.go create mode 100644 internal/domain/const.go create mode 100644 internal/domain/error.go create mode 100644 internal/domain/gender.go create mode 100644 internal/domain/payload.go create mode 100644 internal/domain/redis.go create mode 100644 internal/handler/member/account_create_handler.go create mode 100644 internal/handler/member/check_verify_code_handler.go create mode 100644 internal/handler/member/forget_password_code_handler.go create mode 100644 internal/handler/member/info_handler.go create mode 100644 internal/handler/member/login_handler.go create mode 100644 internal/handler/member/logout_handler.go create mode 100644 internal/handler/member/modify_member_info_handler.go create mode 100644 internal/handler/member/modify_passwd_handler.go create mode 100644 internal/handler/member/pre_verify_update_password_code_handler.go create mode 100644 internal/handler/member/refresh_access_token_handler.go create mode 100644 internal/handler/member/send_verify_code_handler.go create mode 100644 internal/handler/member/update_password_handler.go create mode 100644 internal/handler/routes.go create mode 100644 internal/logic/member/account_create_logic.go create mode 100644 internal/logic/member/check_verify_code_logic.go create mode 100644 internal/logic/member/forget_password_code_logic.go create mode 100644 internal/logic/member/info_logic.go create mode 100644 internal/logic/member/login_logic.go create mode 100644 internal/logic/member/logout_logic.go create mode 100644 internal/logic/member/modify_member_info_logic.go create mode 100644 internal/logic/member/modify_passwd_logic.go create mode 100644 internal/logic/member/pre_verify_update_password_code_logic.go create mode 100644 internal/logic/member/refresh_access_token_logic.go create mode 100644 internal/logic/member/send_verify_code_logic.go create mode 100644 internal/logic/member/update_password_logic.go create mode 100644 internal/middleware/auth_middleware.go create mode 100644 internal/svc/pyroscope.go create mode 100644 internal/svc/service_context.go create mode 100755 internal/svc/validate.go create mode 100644 internal/types/types.go create mode 100644 internal/utils/time.go create mode 100644 internal/utils/utils.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..957184a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea/ +etc/gateway.yaml +.DS_Store \ No newline at end of file diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100755 index 0000000..3505d4f --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,135 @@ +run: + timeout: 3m + # Exit code when at least one issue was found. + # Default: 1 + issues-exit-code: 2 + # Include test files or not. + # Default: true + tests: false + +# Reference URL: https://golangci-lint.run/usage/linters/ +linters: + # Disable everything by default so upgrades to not include new - default + # enabled- linters. + disable-all: true + # Specifically enable linters we want to use. + enable: + # - depguard + - errcheck + # - godot + - gofmt + - goimports + - gosimple + - govet + - ineffassign + - misspell + - revive + - typecheck + - unused + - asasalint + - asciicheck + - bidichk + - bodyclose + - contextcheck + - wastedassign + - whitespace + - thelper + - tparallel + - unconvert + - unparam + - usestdlibvars + - tenv + - testableexamples + - stylecheck + - sqlclosecheck + - nosprintfhostport + - paralleltest + - prealloc + - predeclared + - promlinter + - reassign + - rowserrcheck + - nakedret + - nestif + - nilerr + - nilnil + - nlreturn + - noctx + - nolintlint + - nonamedreturns + - decorder + - dogsled + - dupword + - durationcheck + - errchkjson + - errname + - errorlint + # - execinquery + - exhaustive + - exportloopref + - forbidigo + - forcetypeassert + - gochecknoinits + - gocognit + - goconst + - gocritic + - gocyclo + - goheader + - gomoddirectives + - goprintffuncname + - gosec + - grouper + - importas + - interfacebloat + - lll + - loggercheck + - maintidx + - makezero + +issues: + exclude-rules: + - path: _test\.go + linters: + - funlen + - goconst + - interfacer + - dupl + - lll + - goerr113 + - errcheck + - gocritic + - cyclop + - wrapcheck + - gocognit + - contextcheck + + exclude-dirs: + - internal/module/seckill/usecase +# - internal/logic + + exclude-files: + - .*_test.go + + + +linters-settings: + gci: + sections: + - standard # Standard section: captures all standard packages. + - default # Default section: contains all imports that could not be matched to another section type. + gocognit: + # Minimal code complexity to report. + # Default: 30 (but we recommend 10-20) + min-complexity: 40 + nestif: + # Minimal complexity of if statements to report. + # Default: 5 + min-complexity: 10 + lll: + # Max line length, lines longer will be reported. + # '\t' is counted as 1 character by default, and can be changed with the tab-width option. + # Default: 120. + line-length: 200 + # Tab width in spaces. + # Default: 1 + tab-width: 1 \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..adf9df9 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +#goctl api plugin -plugin goctl-swagger="swagger -filename gateway.json -host dev-api.30cm.net" -api ./generate/api/gateway.api -dir . +#goctl api go -api ./generate/api/gateway.api -dir . -style go_zero + +GOFMT ?= gofmt +GOFILES := $(shell find . -name "*.go") + +.PHONY: fmt +fmt: # 格式優化 + $(GOFMT) -s -w $(GOFILES) + goimports -w ./ + golangci-lint run + + +.PHONY: gen-doc +gen-doc: # 格式優化 + goctl api plugin -plugin goctl-swagger="swagger -filename gateway.json -host dev-api.30cm.net" -api ./generate/api/gateway.api -dir . + +.PHONY: gen-api +gen-api: # 格式優化 + goctl api go -api ./generate/api/gateway.api -dir . -style go_zero diff --git a/etc/gateway.example.yaml b/etc/gateway.example.yaml new file mode 100644 index 0000000..61471bb --- /dev/null +++ b/etc/gateway.example.yaml @@ -0,0 +1,43 @@ +Name: gateway +Host: 0.0.0.0 +Port: 8888 + +DevServer: + Enabled: false + Port: 6470 + MetricsPath: /metrics + EnableMetrics: true + +PyroScope: + Enable: false + URL: http://10.0.0.1:4040 + Token: "xxx" + +RedisCfg: + Host: 127.0.0.1:6379 + Type: node + +MemberRPC: + Etcd: + Hosts: + - 10.0.0.13:2379 + Key: member.rpc +NotificationRPC: + Etcd: + Hosts: + - 10.0.0.13:2379 + Key: notification.rpc +PermissionRPC: + Etcd: + Hosts: + - 10.0.0.13:2379 + Key: permission.rpc + +MailSender: service@30cm.net + +Token: + Expired: 86500s + RefreshExpired: 86500s + +Member: + ForgetTimeOutInSec: 180 \ No newline at end of file diff --git a/gateway.go b/gateway.go new file mode 100644 index 0000000..4fb39ac --- /dev/null +++ b/gateway.go @@ -0,0 +1,32 @@ +package main + +import ( + "flag" + + "github.com/zeromicro/go-zero/core/logx" + + "biz-member-gateway/internal/config" + "biz-member-gateway/internal/handler" + "biz-member-gateway/internal/svc" + + "github.com/zeromicro/go-zero/core/conf" + "github.com/zeromicro/go-zero/rest" +) + +var configFile = flag.String("f", "etc/gateway.yaml", "the config file") + +func main() { + flag.Parse() + + var c config.Config + conf.MustLoad(*configFile, &c) + + server := rest.MustNewServer(c.RestConf) + defer server.Stop() + + ctx := svc.NewServiceContext(c) + handler.RegisterHandlers(server, ctx) + + logx.Infof("Starting server at %s:%d...\n", c.Host, c.Port) + server.Start() +} diff --git a/gateway.json b/gateway.json new file mode 100644 index 0000000..3280d91 --- /dev/null +++ b/gateway.json @@ -0,0 +1,785 @@ +{ + "swagger": "2.0", + "info": { + "title": "", + "version": "" + }, + "host": "dev-api.30cm.net", + "schemes": [ + "http", + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/api/v1/member": { + "post": { + "summary": "創建新會員", + "description": "創建一個全新的帳號,創完成之後會自動登入", + "operationId": "AccountCreate", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/LoginTokenResp" + } + } + }, + "parameters": [ + { + "name": "body", + "description": " -------------------------------------------", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateAccountRequest" + } + } + ], + "tags": [ + "member" + ], + "consumes": [ + "multipart/form-data" + ] + } + }, + "/api/v1/member/check-verify-code": { + "post": { + "summary": "確認邀請 - 綁定會員", + "description": "確認驗證碼是否有效", + "operationId": "CheckVerifyCode", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/RespOK" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CheckoutVerifyReq" + } + } + ], + "tags": [ + "member" + ] + } + }, + "/api/v1/member/forget-password-code": { + "post": { + "summary": "發送忘記密碼驗證", + "description": "發送忘記密碼驗證(三分鐘內只能發一次信)", + "operationId": "ForgetPasswordCode", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/RespOK" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ForgetPasswordCodeReq" + } + } + ], + "tags": [ + "member" + ] + } + }, + "/api/v1/member/info": { + "get": { + "summary": "取得會員資訊", + "operationId": "Info", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/UserInfo" + } + } + }, + "parameters": [ + { + "name": "required", + "in": "header", + "required": true, + "type": "string" + } + ], + "tags": [ + "member" + ] + }, + "put": { + "summary": "更新會員詳細資訊", + "operationId": "ModifyMemberInfo", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/UserInfo" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/BindingUserInfoReq" + } + } + ], + "tags": [ + "member" + ] + } + }, + "/api/v1/member/login": { + "post": { + "summary": "登入", + "description": "會員登入", + "operationId": "Login", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/LoginTokenResp" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/LoginReq" + } + } + ], + "tags": [ + "member" + ], + "consumes": [ + "multipart/form-data" + ] + } + }, + "/api/v1/member/logout": { + "get": { + "summary": "會員登出", + "operationId": "Logout", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/RespOK" + } + } + }, + "parameters": [ + { + "name": "required", + "in": "header", + "required": true, + "type": "string" + } + ], + "tags": [ + "member" + ] + } + }, + "/api/v1/member/modify-passwd": { + "put": { + "summary": "修改密碼", + "description": "修改密碼", + "operationId": "ModifyPasswdHandler", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/RespOK" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ModifyPasswdReq" + } + } + ], + "tags": [ + "member" + ] + } + }, + "/api/v1/member/pre-verify": { + "put": { + "summary": "預先驗證驗證碼", + "description": "忘記密碼的時候看 ui. 流程要預先驗證一次才給送,", + "operationId": "PreVerifyUpdatePasswordCode", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/RespOK" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PreVerifyForgetPasswdReq" + } + } + ], + "tags": [ + "member" + ] + } + }, + "/api/v1/member/refresh_access_token": { + "put": { + "summary": "更新 Access Token", + "description": "用 RefreshToken 換取 AccessToken", + "operationId": "RefreshAccessToken", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/LoginTokenResp" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateTokenReq" + } + } + ], + "tags": [ + "member" + ] + } + }, + "/api/v1/member/update-password": { + "put": { + "summary": "更新密碼(要發送驗證碼才可以的流程)", + "description": "更新密碼(要發送驗證碼才可以的流程)", + "operationId": "UpdatePassword", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/RespOK" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdatePasswordReq" + } + } + ], + "tags": [ + "member" + ] + } + }, + "/api/v1/member/verify": { + "post": { + "summary": "發送邀請 - 綁定會員", + "description": "可以依照類別(手機驗證,email驗證),同一個類型十分鐘內只能發送一次", + "operationId": "SendVerifyCode", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/RespOK" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/VerificationCodeRequest" + } + } + ], + "tags": [ + "member" + ] + } + } + }, + "definitions": { + "BaseResponse": { + "type": "object", + "properties": { + "status": { + "$ref": "#/definitions/Status", + "description": " 狀態" + }, + "data": { + "type": "object", + "description": " 資料" + } + }, + "title": "BaseResponse", + "required": [ + "status", + "data" + ] + }, + "BindingUserInfoReq": { + "type": "object", + "properties": { + "preferred_language": { + "type": "string", + "description": " 使用語言" + }, + "currency": { + "type": "string" + }, + "avatar_url": { + "type": "string", + "description": " 頭像 URL(可選)" + }, + "nickname": { + "type": "string" + }, + "full_name": { + "type": "string", + "description": " 用戶全名" + }, + "gender_code": { + "type": "string", + "description": " 性別代碼" + }, + "birthdate": { + "type": "string", + "description": " 生日 (格式: unix)" + }, + "address": { + "type": "string", + "description": " 地址" + } + }, + "title": "BindingUserInfoReq", + "required": [ + "gender_code" + ] + }, + "CheckoutVerifyReq": { + "type": "object", + "properties": { + "account": { + "type": "string", + "description": " 帳號名稱" + }, + "code_type": { + "type": "string", + "description": " 驗證碼類型 1 信箱 2 手機" + }, + "verify_code": { + "type": "string", + "description": " 驗證碼,長度為6" + }, + "uid": { + "type": "string" + } + }, + "title": "CheckoutVerifyReq", + "required": [ + "account", + "code_type", + "verify_code", + "uid" + ] + }, + "CreateAccountRequest": { + "type": "object", + "properties": { + "account": { + "type": "string", + "description": " 帳號名稱(line code 輸入在這邊)" + }, + "token": { + "type": "string", + "description": " 密碼或平台token,密碼請 sha256 轉碼,如果三方token 請隨便給一個 sha256 字串" + }, + "token_check": { + "type": "string", + "description": " 密碼或平台token,token 請保持原樣,填在這邊,不用管 token" + }, + "platform": { + "type": "string", + "description": " 平台名稱 (platform) 平台、google、line" + }, + "account_type": { + "type": "string", + "description": " 帳號類型 phone(手機)、email(信箱)、platform(自定義帳號) -\u003e (如果為第三方都寫 platform)" + } + }, + "title": "CreateAccountRequest", + "required": [ + "account", + "token", + "token_check", + "platform", + "account_type" + ] + }, + "ForgetPasswordCodeReq": { + "type": "object", + "properties": { + "account": { + "type": "string", + "description": " 帳號名稱" + }, + "account_type": { + "type": "string", + "description": " 帳號類型 (phone) 手機 (email) 信箱" + } + }, + "title": "ForgetPasswordCodeReq", + "required": [ + "account", + "account_type" + ] + }, + "LoginReq": { + "type": "object", + "properties": { + "account": { + "type": "string", + "description": " 帳號名稱" + }, + "token": { + "type": "string", + "description": " 密碼或平台token,密碼請 sha256 轉碼" + }, + "platform": { + "type": "string", + "description": " 平台名稱 platform, google" + }, + "account_type": { + "type": "string", + "description": " 帳號類型 1 手機 2 信箱 3 自定義帳號" + } + }, + "title": "LoginReq", + "required": [ + "account", + "token", + "platform", + "account_type" + ] + }, + "LoginTokenResp": { + "type": "object", + "properties": { + "uid": { + "type": "string", + "description": " Account" + }, + "access_token": { + "type": "string", + "description": " 訪問令牌 預設 5 分鐘過期" + }, + "refresh_token": { + "type": "string", + "description": " 刷新令牌 (預設一天過期,只能用一次),當呼叫更新token api 時,會自動把舊的失效,變成新的 refresh_token ,前端要記得過其實協助刷新,刷新不過表示全失效了(重新登入)" + }, + "token_type": { + "type": "string", + "description": " Bearer" + } + }, + "title": "LoginTokenResp", + "required": [ + "uid", + "access_token", + "refresh_token", + "token_type" + ] + }, + "MemberLoginHeader": { + "type": "object", + "title": "MemberLoginHeader" + }, + "ModifyPasswdReq": { + "type": "object", + "properties": { + "token": { + "type": "string", + "description": " 密碼或平台token,密碼請 sha256 轉碼" + }, + "token_check": { + "type": "string", + "description": " 密碼或平台token,密碼請 sha256 轉碼" + } + }, + "title": "ModifyPasswdReq", + "required": [ + "token", + "token_check" + ] + }, + "PreVerifyForgetPasswdReq": { + "type": "object", + "properties": { + "identifier": { + "type": "string", + "description": " 聯繫方式,可以是 email 或 phone" + }, + "verify_code": { + "type": "string", + "description": " 驗證碼,長度為6" + } + }, + "title": "PreVerifyForgetPasswdReq", + "required": [ + "identifier", + "verify_code" + ] + }, + "RespOK": { + "type": "object", + "title": "RespOK" + }, + "Status": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int64", + "description": " 狀態碼" + }, + "message": { + "type": "string", + "description": " 訊息" + }, + "data": { + "type": "object", + "description": " 可選的數據,當有返回時才出現" + }, + "error": { + "type": "object", + "description": " 可選的錯誤信息" + } + }, + "title": "Status", + "required": [ + "code", + "message" + ] + }, + "UpdatePasswordReq": { + "type": "object", + "properties": { + "account": { + "type": "string", + "description": " 帳號名稱" + }, + "verify_code": { + "type": "string", + "description": " 驗證碼,長度為6" + }, + "token": { + "type": "string", + "description": " 密碼或平台token,密碼請 sha256 轉碼" + }, + "token_check": { + "type": "string", + "description": " 密碼或平台token,密碼請 sha256 轉碼" + } + }, + "title": "UpdatePasswordReq", + "required": [ + "account", + "verify_code", + "token", + "token_check" + ] + }, + "UpdateTokenReq": { + "type": "object", + "properties": { + "uid": { + "type": "string", + "description": " 誰要更新" + }, + "token": { + "type": "string", + "description": " access token -\u003e 已過期要被更新的" + }, + "refresh_token": { + "type": "string", + "description": " refresh token -\u003e 重點,要驗證他的" + } + }, + "title": "UpdateTokenReq", + "required": [ + "uid", + "token", + "refresh_token" + ] + }, + "UserInfo": { + "type": "object", + "properties": { + "platform": { + "type": "string", + "description": " 用戶平台 platform, google, line" + }, + "uid": { + "type": "string", + "description": " 用戶 UID" + }, + "avatar_url": { + "type": "string", + "description": " 頭像 URL(可選)" + }, + "full_name": { + "type": "string", + "description": " 用戶全名" + }, + "nickname": { + "type": "string", + "description": " 暱稱(可選)" + }, + "gender_code": { + "type": "string", + "description": " 性別代碼 mail, femail ,sec" + }, + "birthdate": { + "type": "string", + "description": " 生日 (格式: 19930417)" + }, + "phone_number": { + "type": "string", + "description": " 電話" + }, + "address": { + "type": "string", + "description": " 地址" + }, + "email": { + "type": "string", + "description": " 驗證後的信箱" + }, + "alarm_category": { + "type": "string", + "description": " 告警狀態" + }, + "user_status": { + "type": "string", + "description": " 用戶狀態" + }, + "preferred_language": { + "type": "string", + "description": " 使用語言" + }, + "currency": { + "type": "string", + "description": " 使用幣種" + }, + "update_at": { + "type": "string" + }, + "create_at": { + "type": "string" + } + }, + "title": "UserInfo", + "required": [ + "platform", + "uid", + "avatar_url", + "full_name", + "nickname", + "gender_code", + "birthdate", + "phone_number", + "address", + "email", + "alarm_category", + "user_status", + "preferred_language", + "currency", + "update_at", + "create_at" + ] + }, + "VerificationCodeRequest": { + "type": "object", + "properties": { + "identifier": { + "type": "string", + "description": " 聯繫方式,可以是 email 或 phone" + }, + "code_type": { + "type": "string", + "description": " 驗證碼類型" + } + }, + "title": "VerificationCodeRequest", + "required": [ + "identifier", + "code_type" + ] + }, + "VerifyHeader": { + "type": "object", + "title": "VerifyHeader" + } + }, + "securityDefinitions": { + "apiKey": { + "type": "apiKey", + "description": "Enter JWT Bearer token **_only_**", + "name": "Authorization", + "in": "header" + } + } +} diff --git a/generate/api/gateway.api b/generate/api/gateway.api new file mode 100644 index 0000000..854e4a6 --- /dev/null +++ b/generate/api/gateway.api @@ -0,0 +1,21 @@ +syntax = "v1" + +type Status { + Code int64 `json:"code"` // 狀態碼 + Message string `json:"message"` // 訊息 + Data interface{} `json:"data,omitempty"` // 可選的數據,當有返回時才出現 + Error interface{} `json:"error,omitempty"` // 可選的錯誤信息 +} + +type BaseResponse { + Status Status `json:"status"` // 狀態 + Data interface{} `json:"data"` // 資料 +} + +type VerifyHeader { + Token string `header:"token" validate:"required"` +} + +type RespOK {} + +import "member.api" diff --git a/generate/api/member.api b/generate/api/member.api new file mode 100644 index 0000000..3db016f --- /dev/null +++ b/generate/api/member.api @@ -0,0 +1,252 @@ +syntax = "v1" + +info( + title: "Portal-Api-Gateway (PGW)" + desc: "netpute web portal api gateway" + author: "daniel wang" + email: "daniel.wang@30cm.net" + version: "0.0.1" +) + +type MemberLoginHeader { + DeviceID string `header:"device_id"` + IpAddress string `header:"ip_address"` + Brewser string `header:"brewser"` +} + +// ------------------------------------------- + +type CreateAccountRequest { + Account string `json:"account" validate:"required"` // 帳號名稱(line code 輸入在這邊) + Token string `json:"token" validate:"required"` // 密碼或平台token,密碼請 sha256 轉碼,如果三方token 請隨便給一個 sha256 字串 + TokenCheck string `json:"token_check" validate:"required"` // 密碼或平台token,token 請保持原樣,填在這邊,不用管 token + Platform string `json:"platform" validate:"oneof=platform google line"` // 平台名稱 (platform) 平台、google、line + AccountType string `json:"account_type" validate:"oneof=phone email platform"` // 帳號類型 phone(手機)、email(信箱)、platform(自定義帳號) -> (如果為第三方都寫 platform) + MemberLoginHeader +} + +type LoginReq { + Account string `json:"account" validate:"required"` // 帳號名稱 + Token string `json:"token"` // 密碼或平台token,密碼請 sha256 轉碼 + Platform string `json:"platform" validate:"oneof=platform google line"` // 平台名稱 platform, google + AccountType string `json:"account_type" validate:"oneof=phone email platform"` // 帳號類型 1 手機 2 信箱 3 自定義帳號 + MemberLoginHeader +} + +type LoginTokenResp { + UID string `json:"uid"` // Account + AccessToken string `json:"access_token"` // 訪問令牌 預設 5 分鐘過期 + RefreshToken string `json:"refresh_token"` // 刷新令牌 (預設一天過期,只能用一次),當呼叫更新token api 時,會自動把舊的失效,變成新的 refresh_token ,前端要記得過其實協助刷新,刷新不過表示全失效了(重新登入) + TokenType string `json:"token_type"` // Bearer +} + + +type ForgetPasswordCodeReq { + Account string `json:"account" validate:"required"` // 帳號名稱 + AccountType string `json:"account_type" validate:"oneof=phone email"` // 帳號類型 (phone) 手機 (email) 信箱 +} + +type PreVerifyForgetPasswdReq { + Identifier string `json:"identifier" validate:"required"` // 聯繫方式,可以是 email 或 phone + VerifyCode string `json:"verify_code" validate:"required,len=6"` // 驗證碼,長度為6 +} + +type UpdateTokenReq { + UID string `json:"uid" validate:"required"` // 誰要更新 + Token string `json:"token" validate:"required"` // access token -> 已過期要被更新的 + RefreshToken string `json:"refresh_token" validate:"required"` // refresh token -> 重點,要驗證他的 + // MemberLoginHeader +} + +type UpdatePasswordReq { + Account string `json:"account" validate:"required"` // 帳號名稱 + VerifyCode string `json:"verify_code" validate:"required,len=6"` // 驗證碼,長度為6 + Token string `json:"token" validate:"required,len=64"` // 密碼或平台token,密碼請 sha256 轉碼 + TokenCheck string `json:"token_check" validate:"required,len=64"` // 密碼或平台token,密碼請 sha256 轉碼 +} + +@server( + group: member + prefix: /api/v1 + schemes: https + timeout: 10s +) +service gateway { +/* @respdoc-400 (BaseResponse) // 輸入的參數錯誤 */ +/* @respdoc-500 (BaseResponse) // 伺服器出錯 */ + @doc( + summary:"創建新會員" + description: "創建一個全新的帳號,創完成之後會自動登入" + ) + @handler AccountCreate + post /member (CreateAccountRequest) returns (LoginTokenResp) + +/* @respdoc-400 (BaseResponse) // 輸入的參數錯誤 */ +/* @respdoc-401 (BaseResponse) // 輸入的帳號密碼未經驗證-> 帳號密碼錯誤的意思 */ +/* @respdoc-500 (BaseResponse) // 伺服器出錯 */ + @doc( + summary:"登入" + description: "會員登入" + ) + @handler Login + post /member/login (LoginReq) returns (LoginTokenResp) + +/* @respdoc-400 (BaseResponse) // 輸入的參數錯誤 */ +/* @respdoc-500 (BaseResponse) // 伺服器出錯 */ + @doc( + summary:"發送忘記密碼驗證" + description: "發送忘記密碼驗證(三分鐘內只能發一次信)" + ) + @handler ForgetPasswordCode + post /member/forget-password-code (ForgetPasswordCodeReq) returns (RespOK) + +/* @respdoc-400 (BaseResponse) // 輸入的參數錯誤 */ +/* @respdoc-401 (BaseResponse) // 無效的驗證碼 */ +/* @respdoc-500 (BaseResponse) // 伺服器出錯 */ + @doc( + summary:"更新密碼(要發送驗證碼才可以的流程)" + description: "更新密碼(要發送驗證碼才可以的流程)" + ) + @handler UpdatePassword + put /member/update-password (UpdatePasswordReq) returns (RespOK) + +/* @respdoc-400 (BaseResponse) // 輸入的參數錯誤 */ +/* @respdoc-401 (BaseResponse) // 無效的驗證碼 */ +/* @respdoc-500 (BaseResponse) // 伺服器出錯 */ + @doc( + summary:"預先驗證驗證碼" + description: "忘記密碼的時候看 ui. 流程要預先驗證一次才給送," + ) + @handler PreVerifyUpdatePasswordCode + put /member/pre-verify (PreVerifyForgetPasswdReq) returns (RespOK) + +/* @respdoc-400 (BaseResponse) // 輸入的參數錯誤 */ +/* @respdoc-401 (BaseResponse) // 無效的 Refresh Token */ +/* @respdoc-500 (BaseResponse) // 伺服器出錯 */ + @doc( + summary:"更新 Access Token" + description: "用 RefreshToken 換取 AccessToken" + ) + @handler RefreshAccessToken + put /member/refresh_access_token (UpdateTokenReq) returns (LoginTokenResp) +} + +// ------------------- 要登入之後才可以做的事情 ------------------------ + +type UserInfo { + Platform string `json:"platform"` // 用戶平台 platform, google, line + UID string `json:"uid"` // 用戶 UID + AvatarURL string `json:"avatar_url"` // 頭像 URL(可選) + FullName string `json:"full_name"` // 用戶全名 + Nickname string `json:"nickname"` // 暱稱(可選) + GenderCode string `json:"gender_code"` // 性別代碼 mail, femail ,sec + Birthdate string `json:"birthdate"` // 生日 (格式: 19930417) + PhoneNumber string `json:"phone_number"` // 電話 + Address string `json:"address"` // 地址 + Email string `json:"email"` // 驗證後的信箱 + AlarmCategory string `json:"alarm_category"` // 告警狀態 + UserStatus string `json:"user_status"` // 用戶狀態 + PreferredLanguage string `json:"preferred_language"` // 使用語言 + Currency string `json:"currency"` // 使用幣種 + UpdateAt string `json:"update_at"` + CreateAt string `json:"create_at"` +} + +type BindingUserInfoReq { + VerifyHeader + PreferredLanguage string `json:"preferred_language,optional" validate:"oneof=zh-tw en-us"` // 使用語言 + Currency string `json:"currency,optional" validate:"oneof=TWD USD"` + AvatarURL string `json:"avatar_url,optional"` // 頭像 URL(可選) + Nickname string `json:"nickname,optional"` + FullName string `json:"full_name,optional"` // 用戶全名 + GenderCode string `json:"gender_code" validate:"oneof=secret male female"` // 性別代碼 + Birthday string `json:"birthday,optional" validate:"rfc3339"` // 生日 (格式: unix) + Address string `json:"address,optional"` // 地址 +} + +type VerificationCodeRequest { + VerifyHeader + Identifier string `json:"identifier" validate:"required"` // 聯繫方式,可以是 email 或 phone + CodeType string `json:"code_type" validate:"oneof=email phone forget_password"` // 驗證碼類型 +} + +type CheckoutVerifyReq { + VerifyHeader + Account string `json:"account" validate:"required"` // 帳號名稱 + CodeType string `json:"code_type" validate:"oneof=email phone"` // 驗證碼類型 1 信箱 2 手機 + VerifyCode string `json:"verify_code" validate:"required,len=6"` // 驗證碼,長度為6 + UID string `json:"uid" validate:"required"` +} + +type ModifyPasswdReq { + VerifyHeader + NewToken string `json:"token" validate:"required,len=64"` // 密碼或平台token,密碼請 sha256 轉碼 + NewTokenCheck string `json:"token_check" validate:"required,len=64"` // 密碼或平台token,密碼請 sha256 轉碼 +} + +@server( + group: member + prefix: /api/v1 + schemes: https + timeout: 10s + middleware: AuthMiddleware +) + +service gateway { +/* @respdoc-400 (BaseResponse) // 輸入的參數錯誤 */ +/* @respdoc-401 (BaseResponse) // 無效的Token */ +/* @respdoc-500 (BaseResponse) // 伺服器出錯 */ + @doc( + summary: "會員登出" + ) + @handler Logout + get /member/logout (VerifyHeader) returns (RespOK) + +/* @respdoc-400 (BaseResponse) // 輸入的參數錯誤 */ +/* @respdoc-401 (BaseResponse) // 無效的Token */ +/* @respdoc-500 (BaseResponse) // 伺服器出錯 */ + @doc( + summary: "取得會員資訊" + ) + @handler Info + get /member/info (VerifyHeader) returns (UserInfo) + +/* @respdoc-400 (BaseResponse) // 輸入的參數錯誤 */ +/* @respdoc-403 (BaseResponse) // 無效的Token */ +/* @respdoc-500 (BaseResponse) // 伺服器出錯 */ + @doc( + summary: "更新會員詳細資訊" + ) + @handler ModifyMemberInfo + put /member/info (BindingUserInfoReq) returns (UserInfo) + +/* @respdoc-400 (BaseResponse) // 輸入的參數錯誤 */ +/* @respdoc-401 (BaseResponse) // 無效的Token */ +/* @respdoc-500 (BaseResponse) // 伺服器出錯 */ + @doc( + summary: "發送邀請 - 綁定會員" + description: "可以依照類別(手機驗證,email驗證),同一個類型十分鐘內只能發送一次" + ) + @handler SendVerifyCode + post /member/verify (VerificationCodeRequest) returns (RespOK) + +/* @respdoc-400 (BaseResponse) // 輸入的參數錯誤 */ +/* @respdoc-401 (BaseResponse) // 無效的驗證碼 */ +/* @respdoc-500 (BaseResponse) // 伺服器出錯 */ + @doc( + summary: "確認邀請 - 綁定會員" + description: "確認驗證碼是否有效" + ) + @handler CheckVerifyCode + post /member/check-verify-code (CheckoutVerifyReq) returns (RespOK) + +/* @respdoc-400 (BaseResponse) // 輸入的參數錯誤 */ +/* @respdoc-401 (BaseResponse) // 無效的驗證碼 */ +/* @respdoc-500 (BaseResponse) // 伺服器出錯 */ + @doc( + summary: "修改密碼" + description: "修改密碼" + ) + @handler ModifyPasswdHandler + put /member/modify-passwd (ModifyPasswdReq) returns (RespOK) +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9200160 --- /dev/null +++ b/go.mod @@ -0,0 +1,126 @@ +module biz-member-gateway + +go 1.24.0 + +require ( + code.30cm.net/digimon/app-cloudep-member-server v0.0.0-20250208060713-714163bbe6c6 + code.30cm.net/digimon/app-cloudep-notification-service v0.0.2 + code.30cm.net/digimon/app-cloudep-permission-server v0.0.2 + code.30cm.net/digimon/library-go/errors v1.2.2 + code.30cm.net/digimon/library-go/errs v1.2.14 + code.30cm.net/digimon/library-go/validator v1.0.0 + code.30cm.net/digimon/proto-all v0.0.0-20250309093454-2a2927ced9e2 + github.com/go-playground/validator/v10 v10.25.0 + github.com/golang/protobuf v1.5.4 + github.com/grafana/pyroscope-go v1.2.0 + github.com/shopspring/decimal v1.4.0 + github.com/zeromicro/go-zero v1.8.1 +) + +require ( + github.com/Masterminds/semver v1.4.2 // indirect + github.com/Masterminds/sprig v2.16.0+incompatible // indirect + github.com/PuerkitoBio/goquery v1.5.0 // indirect + github.com/andybalholm/cascadia v1.0.0 // indirect + github.com/aokoli/goutils v1.0.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/coreos/go-semver v0.3.1 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.4 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.1 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/css v1.0.0 // indirect + github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect + github.com/huandu/xstrings v1.2.0 // indirect + github.com/imdario/mergo v0.3.6 // indirect + github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/matcornic/hermes/v2 v2.1.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/openzipkin/zipkin-go v0.4.3 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/prometheus/client_golang v1.21.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/redis/go-redis/v9 v9.7.1 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/russross/blackfriday/v2 v2.0.1 // indirect + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect + github.com/vanng822/css v0.0.0-20190504095207-a21e860bcd04 // indirect + github.com/vanng822/go-premailer v0.0.0-20191214114701-be27abe028fe // indirect + go.etcd.io/etcd/api/v3 v3.5.15 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect + go.etcd.io/etcd/client/v3 v3.5.15 // indirect + go.mongodb.org/mongo-driver v1.17.3 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 // indirect + go.opentelemetry.io/otel/exporters/zipkin v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/sdk v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect + go.opentelemetry.io/proto/otlp v1.5.0 // indirect + go.uber.org/atomic v1.10.0 // indirect + go.uber.org/automaxprocs v1.6.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + go.uber.org/zap v1.24.0 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/oauth2 v0.24.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/term v0.29.0 // indirect + golang.org/x/text v0.22.0 // indirect + golang.org/x/time v0.10.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect + google.golang.org/grpc v1.70.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.29.3 // indirect + k8s.io/apimachinery v0.29.4 // indirect + k8s.io/client-go v0.29.3 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect + k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8da771a --- /dev/null +++ b/go.sum @@ -0,0 +1,366 @@ +code.30cm.net/digimon/app-cloudep-member-server v0.0.0-20250208060713-714163bbe6c6 h1:yDeI9SQyPd2XDHoxpaLavw5AU0s4NPNa4is9G/ZbxNM= +code.30cm.net/digimon/app-cloudep-member-server v0.0.0-20250208060713-714163bbe6c6/go.mod h1:4r33vmU2j7PZWzWFIn+7NAE05fuF/6/kdVdfnyHmf6Y= +code.30cm.net/digimon/app-cloudep-notification-service v0.0.2 h1:6GkX4oPXbftD4DV1wBM0e0SD9KdfdcNTxLSEASzEaWo= +code.30cm.net/digimon/app-cloudep-notification-service v0.0.2/go.mod h1:TdUdjsHafpkrQl6AKPDv3VdH1tSX4f+UWqnW5TaPvsM= +code.30cm.net/digimon/app-cloudep-permission-server v0.0.2 h1:1n8RVs1/R3z1WA7uPPUC+EuQIrIqHA9Uj8Cy2eFP2rg= +code.30cm.net/digimon/app-cloudep-permission-server v0.0.2/go.mod h1:ga0L3jN4FspRHfifGpT2nIzW0APPgzQqv5tBLf2JM2Q= +code.30cm.net/digimon/library-go/errors v1.2.2 h1:InY2PB+6eSZi91n6dcnAwsihOQuqe+GrrDMKlHJPxXA= +code.30cm.net/digimon/library-go/errors v1.2.2/go.mod h1:4yVE3lyHi/cLB3W59Tw70FBqO10ZhXo470NWp7aQ0tc= +code.30cm.net/digimon/library-go/errs v1.2.14 h1:Un9wcIIjjJW8D2i0ISf8ibzp9oNT4OqLsaSKW0T4RJU= +code.30cm.net/digimon/library-go/errs v1.2.14/go.mod h1:Hs4v7SbXNggDVBGXSYsFMjkii1qLF+rugrIpWePN4/o= +code.30cm.net/digimon/library-go/validator v1.0.0 h1:F48CU0+Z4ZZFhKHvjgm4u0M7v5wanFfVLKz+d92o+ks= +code.30cm.net/digimon/library-go/validator v1.0.0/go.mod h1:haNzZMm0PhdvhuX8R6N/ZMp4CjOhs7/dHgTHzeOTNrY= +code.30cm.net/digimon/proto-all v0.0.0-20250309093454-2a2927ced9e2 h1:eF4/vGVf9Q+EW1wVMSeklXbHm4Hvm38Iwlz+VaDh9YM= +code.30cm.net/digimon/proto-all v0.0.0-20250309093454-2a2927ced9e2/go.mod h1:jyO0y8lBk81kiBd5BP5bDo1rRDkKBN1983FnAANocR8= +github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= +github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/sprig v2.16.0+incompatible h1:QZbMUPxRQ50EKAq3LFMnxddMu88/EUUG3qmxwtDmPsY= +github.com/Masterminds/sprig v2.16.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk= +github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg= +github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= +github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= +github.com/alicebob/miniredis/v2 v2.34.0 h1:mBFWMaJSNL9RwdGRyEDoAAv8OQc5UlEhLDQggTglU/0= +github.com/alicebob/miniredis/v2 v2.34.0/go.mod h1:kWShP4b58T1CW0Y5dViCd5ztzrDqRWqM3nksiyXk5s8= +github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o= +github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/aokoli/goutils v1.0.1 h1:7fpzNGoJ3VA8qcrm++XEE1QUe0mIwNeLa02Nwq7RDkg= +github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df/go.mod h1:GJr+FCSXshIwgHBtLglIg9M2l2kQSi6QjVAngtzI08Y= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8= +github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= +github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/grafana/pyroscope-go v1.2.0 h1:aILLKjTj8CS8f/24OPMGPewQSYlhmdQMBmol1d3KGj8= +github.com/grafana/pyroscope-go v1.2.0/go.mod h1:2GHr28Nr05bg2pElS+dDsc98f3JTUh2f6Fz1hWXrqwk= +github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= +github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0= +github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= +github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 h1:xqgexXAGQgY3HAjNPSaCqn5Aahbo5TKsmhp8VRfr1iQ= +github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matcornic/hermes/v2 v2.1.0 h1:9TDYFBPFv6mcXanaDmRDEp/RTWj0dTTi+LpFnnnfNWc= +github.com/matcornic/hermes/v2 v2.1.0/go.mod h1:2+ziJeoyRfaLiATIL8VZ7f9hpzH4oDHqTmn0bhrsgVI= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= +github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= +github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= +github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= +github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA= +github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/redis/go-redis/v9 v9.7.1 h1:4LhKRCIduqXqtvCUlaq9c8bdHOkICjDMrr1+Zb3osAc= +github.com/redis/go-redis/v9 v9.7.1/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= +github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/vanng822/css v0.0.0-20190504095207-a21e860bcd04 h1:L0rPdfzq43+NV8rfIx2kA4iSSLRj2jN5ijYHoeXRwvQ= +github.com/vanng822/css v0.0.0-20190504095207-a21e860bcd04/go.mod h1:tcnB1voG49QhCrwq1W0w5hhGasvOg+VQp9i9H1rCM1w= +github.com/vanng822/go-premailer v0.0.0-20191214114701-be27abe028fe h1:9YnI5plmy+ad6BM+JCLJb2ZV7/TNiE5l7SNKfumYKgc= +github.com/vanng822/go-premailer v0.0.0-20191214114701-be27abe028fe/go.mod h1:JTFJA/t820uFDoyPpErFQ3rb3amdZoPtxcKervG0OE4= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/zeromicro/go-zero v1.8.1 h1:iUYQEMQzS9Pb8ebzJtV3FGtv/YTjZxAh/NvLW/316wo= +github.com/zeromicro/go-zero v1.8.1/go.mod h1:gc54Ad4qt7OJ0PbKajnYsSKsZBYN4JLRIXKlqDX2A2I= +go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk= +go.etcd.io/etcd/api/v3 v3.5.15/go.mod h1:N9EhGzXq58WuMllgH9ZvnEr7SI9pS0k0+DHZezGp7jM= +go.etcd.io/etcd/client/pkg/v3 v3.5.15 h1:fo0HpWz/KlHGMCC+YejpiCmyWDEuIpnTDzpJLB5fWlA= +go.etcd.io/etcd/client/pkg/v3 v3.5.15/go.mod h1:mXDI4NAOwEiszrHCb0aqfAYNCrZP4e9hRca3d1YK8EU= +go.etcd.io/etcd/client/v3 v3.5.15 h1:23M0eY4Fd/inNv1ZfU3AxrbbOdW79r9V9Rl62Nm6ip4= +go.etcd.io/etcd/client/v3 v3.5.15/go.mod h1:CLSJxrYjvLtHsrPKsy7LmZEE+DK2ktfd2bN4RhBMwlU= +go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= +go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDOpt2M8OTG92cWqUESvzh2MxiR5xY8= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA= +go.opentelemetry.io/otel/exporters/zipkin v1.24.0 h1:3evrL5poBuh1KF51D9gO/S+N/1msnm4DaBqs/rpXUqY= +go.opentelemetry.io/otel/exporters/zipkin v1.24.0/go.mod h1:0EHgD8R0+8yRhUYJOGR8Hfg2dpiJQxDOszd5smVO9wM= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= +go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/crypto v0.0.0-20181029175232-7e6ffbd03851/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190225065934-cc5685c2db12/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= +google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= +gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw= +k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80= +k8s.io/apimachinery v0.29.4 h1:RaFdJiDmuKs/8cm1M6Dh1Kvyh59YQFDcFuFTSmXes6Q= +k8s.io/apimachinery v0.29.4/go.mod h1:i3FJVwhvSp/6n8Fl4K97PJEP8C+MM+aoDq4+ZJBf70Y= +k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg= +k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..f04d1ed --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,33 @@ +package config + +import ( + "time" + + "github.com/zeromicro/go-zero/core/stores/redis" + + "github.com/zeromicro/go-zero/rest" + "github.com/zeromicro/go-zero/zrpc" +) + +type Config struct { + rest.RestConf + MemberRPC zrpc.RpcClientConf + PermissionRPC zrpc.RpcClientConf + NotificationRPC zrpc.RpcClientConf + // PyroScope 監控 + PyroScope struct { + Enable bool + URL string + Token string + } + Token struct { + Expired time.Duration + RefreshExpired time.Duration + } + // Redis Cluster + RedisCfg redis.RedisConf + Member struct { + ForgetTimeOutInSec int64 + } + MailSender string +} diff --git a/internal/domain/const.go b/internal/domain/const.go new file mode 100644 index 0000000..91b5839 --- /dev/null +++ b/internal/domain/const.go @@ -0,0 +1,9 @@ +package domain + +const SuccessCode = 10200 +const SuccessMessage = "success" + +const DefaultLang = "zh-tw" +const DefaultCurrency = "USDT" +const DefaultRole = "user" +const TokenTypeBearer = "Bearer" diff --git a/internal/domain/error.go b/internal/domain/error.go new file mode 100644 index 0000000..6eba736 --- /dev/null +++ b/internal/domain/error.go @@ -0,0 +1,8 @@ +package domain + +const ( + APIErrorCode = 1 + iota + TokenNotTheSameAPIErrorCode + FailedToCheckVerifyCodeErrorCode + FailedToSetVerifyCodeErrorCode +) diff --git a/internal/domain/gender.go b/internal/domain/gender.go new file mode 100644 index 0000000..eeaef16 --- /dev/null +++ b/internal/domain/gender.go @@ -0,0 +1,39 @@ +package domain + +import ( + "fmt" +) + +// 字串轉數字的對應表 +var genderStringToCode = map[string]int64{ + "secret": 0, + "male": 1, + "female": 2, +} + +// 數字轉字串的對應表 +var genderCodeToString = map[int64]string{ + 0: "secret", + 1: "male", + 2: "female", +} + +// StringToGenderCode 將 "male"/"female"/"secret" 轉為對應數字 +func StringToGenderCode(gender string) (int64, error) { + code, ok := genderStringToCode[gender] + if !ok { + return -1, fmt.Errorf("invalid gender string") + } + + return code, nil +} + +// GenderCodeToString 將 0/1/2 轉為對應字串 "male"/"female"/"secret" +func GenderCodeToString(code int64) (string, error) { + gender, ok := genderCodeToString[code] + if !ok { + return "", fmt.Errorf("invalid gender code") + } + + return gender, nil +} diff --git a/internal/domain/payload.go b/internal/domain/payload.go new file mode 100644 index 0000000..33f255f --- /dev/null +++ b/internal/domain/payload.go @@ -0,0 +1,47 @@ +package domain + +import ( + "context" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/token" +) + +func UID(ctx context.Context) string { + if uid, ok := ctx.Value(token.UID.String()).(string); ok { + return uid + } + + return "" +} + +func Scope(ctx context.Context) string { + if scope, ok := ctx.Value(token.Scope.String()).(string); ok { + return scope + } + + return "" +} + +func Role(ctx context.Context) string { + if role, ok := ctx.Value(token.Role.String()).(string); ok { + return role + } + + return "" +} + +func DeviceID(ctx context.Context) string { + if deviceID, ok := ctx.Value(token.Device.String()).(string); ok { + return deviceID + } + + return "" +} + +func Account(ctx context.Context) string { + if account, ok := ctx.Value(token.Account.String()).(string); ok { + return account + } + + return "" +} diff --git a/internal/domain/redis.go b/internal/domain/redis.go new file mode 100644 index 0000000..5ff8646 --- /dev/null +++ b/internal/domain/redis.go @@ -0,0 +1,23 @@ +package domain + +import "strings" + +type RedisKey string + +const ( + SendCodeRedisKey RedisKey = "vc" +) + +func (key RedisKey) ToString() string { + return "biz-member-gw:" + string(key) +} + +func (key RedisKey) With(s ...string) RedisKey { + parts := append([]string{string(key)}, s...) + + return RedisKey(strings.Join(parts, ":")) +} + +func GetSendCodeRedisKey(id string) string { + return SendCodeRedisKey.With(id).ToString() +} diff --git a/internal/handler/member/account_create_handler.go b/internal/handler/member/account_create_handler.go new file mode 100644 index 0000000..d2e3f62 --- /dev/null +++ b/internal/handler/member/account_create_handler.go @@ -0,0 +1,68 @@ +package member + +import ( + "biz-member-gateway/internal/domain" + + ers "code.30cm.net/digimon/library-go/errors" + + "net/http" + + "biz-member-gateway/internal/logic/member" + "biz-member-gateway/internal/svc" + "biz-member-gateway/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// AccountCreateHandler 創建新會員 +func AccountCreateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.CreateAccountRequest + if err := httpx.Parse(r, &req); err != nil { + e := ers.InvalidFormat(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: err.Error(), + }, + }) + + return + } + + if err := svcCtx.Validate.ValidateAll(req); err != nil { + e := ers.InvalidFormat(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: err.Error(), + }, + }) + + return + } + + l := member.NewAccountCreateLogic(r.Context(), svcCtx) + resp, err := l.AccountCreate(&req) + if err != nil { + e := ers.FromError(err) + if e.FullCode() == 0 { + e = ers.FromGRPCError(err) + } + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: e.Error(), + }, + }) + } else { + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.BaseResponse{ + Status: types.Status{ + Code: domain.SuccessCode, + Message: domain.SuccessMessage, + }, + Data: resp, + }) + } + } +} diff --git a/internal/handler/member/check_verify_code_handler.go b/internal/handler/member/check_verify_code_handler.go new file mode 100644 index 0000000..834cc4a --- /dev/null +++ b/internal/handler/member/check_verify_code_handler.go @@ -0,0 +1,79 @@ +package member + +import ( + "biz-member-gateway/internal/domain" + "net/http" + + ers "code.30cm.net/digimon/library-go/errors" + + "biz-member-gateway/internal/logic/member" + "biz-member-gateway/internal/svc" + "biz-member-gateway/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// CheckVerifyCodeHandler 確認邀請 - 綁定會員 +func CheckVerifyCodeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.CheckoutVerifyReq + if err := httpx.Parse(r, &req); err != nil { + e := ers.InvalidFormat(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: err.Error(), + }, + }) + + return + } + + if err := svcCtx.Validate.ValidateAll(req); err != nil { + e := ers.InvalidFormat(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: err.Error(), + }, + }) + + return + } + + if err := svcCtx.Validate.ValidateAll(req); err != nil { + e := ers.InvalidFormat(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: err.Error(), + }, + }) + + return + } + + l := member.NewCheckVerifyCodeLogic(r.Context(), svcCtx) + resp, err := l.CheckVerifyCode(&req) + if err != nil { + e := ers.FromError(err) + if e.FullCode() == 0 { + e = ers.FromGRPCError(err) + } + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: e.Error(), + }, + }) + } else { + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.BaseResponse{ + Status: types.Status{ + Code: domain.SuccessCode, + Message: domain.SuccessMessage, + }, + Data: resp, + }) + } + } +} diff --git a/internal/handler/member/forget_password_code_handler.go b/internal/handler/member/forget_password_code_handler.go new file mode 100644 index 0000000..47fe49e --- /dev/null +++ b/internal/handler/member/forget_password_code_handler.go @@ -0,0 +1,67 @@ +package member + +import ( + "biz-member-gateway/internal/domain" + "net/http" + + ers "code.30cm.net/digimon/library-go/errors" + + "biz-member-gateway/internal/logic/member" + "biz-member-gateway/internal/svc" + "biz-member-gateway/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 發送忘記密碼驗證 +func ForgetPasswordCodeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.ForgetPasswordCodeReq + if err := httpx.Parse(r, &req); err != nil { + e := ers.InvalidFormat(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: err.Error(), + }, + }) + + return + } + + if err := svcCtx.Validate.ValidateAll(req); err != nil { + e := ers.InvalidFormat(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: err.Error(), + }, + }) + + return + } + + l := member.NewForgetPasswordCodeLogic(r.Context(), svcCtx) + resp, err := l.ForgetPasswordCode(&req) + if err != nil { + e := ers.FromError(err) + if e.FullCode() == 0 { + e = ers.FromGRPCError(err) + } + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: e.Error(), + }, + }) + } else { + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.BaseResponse{ + Status: types.Status{ + Code: domain.SuccessCode, + Message: domain.SuccessMessage, + }, + Data: resp, + }) + } + } +} diff --git a/internal/handler/member/info_handler.go b/internal/handler/member/info_handler.go new file mode 100644 index 0000000..53145d4 --- /dev/null +++ b/internal/handler/member/info_handler.go @@ -0,0 +1,67 @@ +package member + +import ( + "biz-member-gateway/internal/domain" + "net/http" + + ers "code.30cm.net/digimon/library-go/errors" + + "biz-member-gateway/internal/logic/member" + "biz-member-gateway/internal/svc" + "biz-member-gateway/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 取得會員資訊 +func InfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.VerifyHeader + if err := httpx.Parse(r, &req); err != nil { + e := ers.InvalidFormat(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: err.Error(), + }, + }) + + return + } + + if err := svcCtx.Validate.ValidateAll(req); err != nil { + e := ers.InvalidFormat(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: err.Error(), + }, + }) + + return + } + + l := member.NewInfoLogic(r.Context(), svcCtx) + resp, err := l.Info(&req) + if err != nil { + e := ers.FromError(err) + if e.FullCode() == 0 { + e = ers.FromGRPCError(err) + } + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: e.Error(), + }, + }) + } else { + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.BaseResponse{ + Status: types.Status{ + Code: domain.SuccessCode, + Message: domain.SuccessMessage, + }, + Data: resp, + }) + } + } +} diff --git a/internal/handler/member/login_handler.go b/internal/handler/member/login_handler.go new file mode 100644 index 0000000..a4acda6 --- /dev/null +++ b/internal/handler/member/login_handler.go @@ -0,0 +1,67 @@ +package member + +import ( + "biz-member-gateway/internal/domain" + "net/http" + + ers "code.30cm.net/digimon/library-go/errors" + + "biz-member-gateway/internal/logic/member" + "biz-member-gateway/internal/svc" + "biz-member-gateway/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 登入 +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 { + e := ers.InvalidFormat(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: err.Error(), + }, + }) + + return + } + + if err := svcCtx.Validate.ValidateAll(req); err != nil { + e := ers.InvalidFormat(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: err.Error(), + }, + }) + + return + } + + l := member.NewLoginLogic(r.Context(), svcCtx) + resp, err := l.Login(&req) + if err != nil { + e := ers.FromError(err) + if e.FullCode() == 0 { + e = ers.FromGRPCError(err) + } + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: e.Error(), + }, + }) + } else { + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.BaseResponse{ + Status: types.Status{ + Code: domain.SuccessCode, + Message: domain.SuccessMessage, + }, + Data: resp, + }) + } + } +} diff --git a/internal/handler/member/logout_handler.go b/internal/handler/member/logout_handler.go new file mode 100644 index 0000000..c177165 --- /dev/null +++ b/internal/handler/member/logout_handler.go @@ -0,0 +1,55 @@ +package member + +import ( + "biz-member-gateway/internal/domain" + "net/http" + + ers "code.30cm.net/digimon/library-go/errors" + + "biz-member-gateway/internal/logic/member" + "biz-member-gateway/internal/svc" + "biz-member-gateway/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 會員登出 +func LogoutHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.VerifyHeader + if err := httpx.Parse(r, &req); err != nil { + e := ers.InvalidFormat(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: err.Error(), + }, + }) + + return + } + + l := member.NewLogoutLogic(r.Context(), svcCtx) + resp, err := l.Logout(&req) + if err != nil { + e := ers.FromError(err) + if e.FullCode() == 0 { + e = ers.FromGRPCError(err) + } + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: e.Error(), + }, + }) + } else { + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.BaseResponse{ + Status: types.Status{ + Code: domain.SuccessCode, + Message: domain.SuccessMessage, + }, + Data: resp, + }) + } + } +} diff --git a/internal/handler/member/modify_member_info_handler.go b/internal/handler/member/modify_member_info_handler.go new file mode 100644 index 0000000..9994696 --- /dev/null +++ b/internal/handler/member/modify_member_info_handler.go @@ -0,0 +1,67 @@ +package member + +import ( + "biz-member-gateway/internal/domain" + "net/http" + + ers "code.30cm.net/digimon/library-go/errors" + + "biz-member-gateway/internal/logic/member" + "biz-member-gateway/internal/svc" + "biz-member-gateway/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 更新會員詳細資訊 +func ModifyMemberInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.BindingUserInfoReq + if err := httpx.Parse(r, &req); err != nil { + e := ers.InvalidFormat(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: err.Error(), + }, + }) + + return + } + + if err := svcCtx.Validate.ValidateAll(req); err != nil { + e := ers.InvalidFormat(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: err.Error(), + }, + }) + + return + } + + l := member.NewModifyMemberInfoLogic(r.Context(), svcCtx) + resp, err := l.ModifyMemberInfo(&req) + if err != nil { + e := ers.FromError(err) + if e.FullCode() == 0 { + e = ers.FromGRPCError(err) + } + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: e.Error(), + }, + }) + } else { + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.BaseResponse{ + Status: types.Status{ + Code: domain.SuccessCode, + Message: domain.SuccessMessage, + }, + Data: resp, + }) + } + } +} diff --git a/internal/handler/member/modify_passwd_handler.go b/internal/handler/member/modify_passwd_handler.go new file mode 100644 index 0000000..dc33449 --- /dev/null +++ b/internal/handler/member/modify_passwd_handler.go @@ -0,0 +1,67 @@ +package member + +import ( + "biz-member-gateway/internal/domain" + "net/http" + + ers "code.30cm.net/digimon/library-go/errors" + + "biz-member-gateway/internal/logic/member" + "biz-member-gateway/internal/svc" + "biz-member-gateway/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 修改密碼 +func ModifyPasswdHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.ModifyPasswdReq + if err := httpx.Parse(r, &req); err != nil { + e := ers.InvalidFormat(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: err.Error(), + }, + }) + + return + } + + if err := svcCtx.Validate.ValidateAll(req); err != nil { + e := ers.InvalidFormat(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: err.Error(), + }, + }) + + return + } + + l := member.NewModifyPasswdLogic(r.Context(), svcCtx) + resp, err := l.ModifyPasswd(&req) + if err != nil { + e := ers.FromError(err) + if e.FullCode() == 0 { + e = ers.FromGRPCError(err) + } + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: e.Error(), + }, + }) + } else { + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.BaseResponse{ + Status: types.Status{ + Code: domain.SuccessCode, + Message: domain.SuccessMessage, + }, + Data: resp, + }) + } + } +} diff --git a/internal/handler/member/pre_verify_update_password_code_handler.go b/internal/handler/member/pre_verify_update_password_code_handler.go new file mode 100644 index 0000000..72b4595 --- /dev/null +++ b/internal/handler/member/pre_verify_update_password_code_handler.go @@ -0,0 +1,67 @@ +package member + +import ( + "biz-member-gateway/internal/domain" + "net/http" + + ers "code.30cm.net/digimon/library-go/errors" + + "biz-member-gateway/internal/logic/member" + "biz-member-gateway/internal/svc" + "biz-member-gateway/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 預先驗證驗證碼 +func PreVerifyUpdatePasswordCodeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.PreVerifyForgetPasswdReq + if err := httpx.Parse(r, &req); err != nil { + e := ers.InvalidFormat(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: err.Error(), + }, + }) + + return + } + + if err := svcCtx.Validate.ValidateAll(req); err != nil { + e := ers.InvalidFormat(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: err.Error(), + }, + }) + + return + } + + l := member.NewPreVerifyUpdatePasswordCodeLogic(r.Context(), svcCtx) + resp, err := l.PreVerifyUpdatePasswordCode(&req) + if err != nil { + e := ers.FromError(err) + if e.FullCode() == 0 { + e = ers.FromGRPCError(err) + } + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: e.Error(), + }, + }) + } else { + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.BaseResponse{ + Status: types.Status{ + Code: domain.SuccessCode, + Message: domain.SuccessMessage, + }, + Data: resp, + }) + } + } +} diff --git a/internal/handler/member/refresh_access_token_handler.go b/internal/handler/member/refresh_access_token_handler.go new file mode 100644 index 0000000..54d4422 --- /dev/null +++ b/internal/handler/member/refresh_access_token_handler.go @@ -0,0 +1,67 @@ +package member + +import ( + "biz-member-gateway/internal/domain" + "net/http" + + ers "code.30cm.net/digimon/library-go/errors" + + "biz-member-gateway/internal/logic/member" + "biz-member-gateway/internal/svc" + "biz-member-gateway/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 更新 Access Token +func RefreshAccessTokenHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.UpdateTokenReq + if err := httpx.Parse(r, &req); err != nil { + e := ers.InvalidFormat(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: err.Error(), + }, + }) + + return + } + + if err := svcCtx.Validate.ValidateAll(req); err != nil { + e := ers.InvalidFormat(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: err.Error(), + }, + }) + + return + } + + l := member.NewRefreshAccessTokenLogic(r.Context(), svcCtx) + resp, err := l.RefreshAccessToken(&req) + if err != nil { + e := ers.FromError(err) + if e.FullCode() == 0 { + e = ers.FromGRPCError(err) + } + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: e.Error(), + }, + }) + } else { + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.BaseResponse{ + Status: types.Status{ + Code: domain.SuccessCode, + Message: domain.SuccessMessage, + }, + Data: resp, + }) + } + } +} diff --git a/internal/handler/member/send_verify_code_handler.go b/internal/handler/member/send_verify_code_handler.go new file mode 100644 index 0000000..db5e877 --- /dev/null +++ b/internal/handler/member/send_verify_code_handler.go @@ -0,0 +1,67 @@ +package member + +import ( + "biz-member-gateway/internal/domain" + "net/http" + + ers "code.30cm.net/digimon/library-go/errors" + + "biz-member-gateway/internal/logic/member" + "biz-member-gateway/internal/svc" + "biz-member-gateway/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// SendVerifyCodeHandler 發送邀請 - 綁定會員 +func SendVerifyCodeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.VerificationCodeRequest + if err := httpx.Parse(r, &req); err != nil { + e := ers.InvalidFormat(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: err.Error(), + }, + }) + + return + } + + if err := svcCtx.Validate.ValidateAll(req); err != nil { + e := ers.InvalidFormat(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: err.Error(), + }, + }) + + return + } + + l := member.NewSendVerifyCodeLogic(r.Context(), svcCtx) + resp, err := l.SendVerifyCode(&req) + if err != nil { + e := ers.FromError(err) + if e.FullCode() == 0 { + e = ers.FromGRPCError(err) + } + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: e.Error(), + }, + }) + } else { + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.BaseResponse{ + Status: types.Status{ + Code: domain.SuccessCode, + Message: domain.SuccessMessage, + }, + Data: resp, + }) + } + } +} diff --git a/internal/handler/member/update_password_handler.go b/internal/handler/member/update_password_handler.go new file mode 100644 index 0000000..113f177 --- /dev/null +++ b/internal/handler/member/update_password_handler.go @@ -0,0 +1,67 @@ +package member + +import ( + "biz-member-gateway/internal/domain" + "net/http" + + ers "code.30cm.net/digimon/library-go/errors" + + "biz-member-gateway/internal/logic/member" + "biz-member-gateway/internal/svc" + "biz-member-gateway/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 更新密碼(要發送驗證碼才可以的流程) +func UpdatePasswordHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.UpdatePasswordReq + if err := httpx.Parse(r, &req); err != nil { + e := ers.InvalidFormat(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: err.Error(), + }, + }) + + return + } + + if err := svcCtx.Validate.ValidateAll(req); err != nil { + e := ers.InvalidFormat(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: err.Error(), + }, + }) + + return + } + + l := member.NewUpdatePasswordLogic(r.Context(), svcCtx) + resp, err := l.UpdatePassword(&req) + if err != nil { + e := ers.FromError(err) + if e.FullCode() == 0 { + e = ers.FromGRPCError(err) + } + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.BaseResponse{ + Status: types.Status{ + Code: int64(e.FullCode()), + Message: e.Error(), + }, + }) + } else { + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.BaseResponse{ + Status: types.Status{ + Code: domain.SuccessCode, + Message: domain.SuccessMessage, + }, + Data: resp, + }) + } + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go new file mode 100644 index 0000000..695be74 --- /dev/null +++ b/internal/handler/routes.go @@ -0,0 +1,105 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.8.1 + +package handler + +import ( + "net/http" + "time" + + member "biz-member-gateway/internal/handler/member" + "biz-member-gateway/internal/svc" + + "github.com/zeromicro/go-zero/rest" +) + +func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { + server.AddRoutes( + []rest.Route{ + { + // 創建新會員 + Method: http.MethodPost, + Path: "/member", + Handler: member.AccountCreateHandler(serverCtx), + }, + { + // 發送忘記密碼驗證 + Method: http.MethodPost, + Path: "/member/forget-password-code", + Handler: member.ForgetPasswordCodeHandler(serverCtx), + }, + { + // 登入 + Method: http.MethodPost, + Path: "/member/login", + Handler: member.LoginHandler(serverCtx), + }, + { + // 預先驗證驗證碼 + Method: http.MethodPut, + Path: "/member/pre-verify", + Handler: member.PreVerifyUpdatePasswordCodeHandler(serverCtx), + }, + { + // 更新 Access Token + Method: http.MethodPut, + Path: "/member/refresh_access_token", + Handler: member.RefreshAccessTokenHandler(serverCtx), + }, + { + // 更新密碼(要發送驗證碼才可以的流程) + Method: http.MethodPut, + Path: "/member/update-password", + Handler: member.UpdatePasswordHandler(serverCtx), + }, + }, + rest.WithPrefix("/api/v1"), + rest.WithTimeout(10000*time.Millisecond), + ) + + server.AddRoutes( + rest.WithMiddlewares( + []rest.Middleware{serverCtx.AuthMiddleware}, + []rest.Route{ + { + // 確認邀請 - 綁定會員 + Method: http.MethodPost, + Path: "/member/check-verify-code", + Handler: member.CheckVerifyCodeHandler(serverCtx), + }, + { + // 取得會員資訊 + Method: http.MethodGet, + Path: "/member/info", + Handler: member.InfoHandler(serverCtx), + }, + { + // 更新會員詳細資訊 + Method: http.MethodPut, + Path: "/member/info", + Handler: member.ModifyMemberInfoHandler(serverCtx), + }, + { + // 會員登出 + Method: http.MethodGet, + Path: "/member/logout", + Handler: member.LogoutHandler(serverCtx), + }, + { + // 修改密碼 + Method: http.MethodPut, + Path: "/member/modify-passwd", + Handler: member.ModifyPasswdHandler(serverCtx), + }, + { + // 發送邀請 - 綁定會員 + Method: http.MethodPost, + Path: "/member/verify", + Handler: member.SendVerifyCodeHandler(serverCtx), + }, + }..., + ), + rest.WithPrefix("/api/v1"), + rest.WithTimeout(10000*time.Millisecond), + ) +} diff --git a/internal/logic/member/account_create_logic.go b/internal/logic/member/account_create_logic.go new file mode 100644 index 0000000..b2d75b4 --- /dev/null +++ b/internal/logic/member/account_create_logic.go @@ -0,0 +1,242 @@ +package member + +import ( + bizDomain "biz-member-gateway/internal/domain" + + "code.30cm.net/digimon/app-cloudep-member-server/pkg/domain/member" + + memberUC "code.30cm.net/digimon/app-cloudep-member-server/pkg/domain/usecase" + tokenUC "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/usecase" + + "time" + + "code.30cm.net/digimon/library-go/errs" + "code.30cm.net/digimon/library-go/errs/code" + + "context" + + "github.com/golang/protobuf/proto" + + "biz-member-gateway/internal/svc" + "biz-member-gateway/internal/types" + + memberProto "code.30cm.net/digimon/proto-all/pkg/member" + permissionProto "code.30cm.net/digimon/proto-all/pkg/permission" + "github.com/zeromicro/go-zero/core/logx" +) + +type AccountCreateLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewAccountCreateLogic 創建新會員 +func NewAccountCreateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AccountCreateLogic { + return &AccountCreateLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// AccountCreate 建立新帳號 -> 業務邏輯 +func (l *AccountCreateLogic) AccountCreate(req *types.CreateAccountRequest) (resp *types.LoginTokenResp, err error) { + platform := member.GetPlatformByPlatformCode(req.Platform) + if platform == member.PlatformNone { + // http 400 + return nil, errs.InvalidFormat("platform not support") + } + + // Step 1: 根據平台構建相關數據 + createAccountReq, bindingUserReq, createUserInfoRequest, err := l.buildAccountRequests(req, platform) + if err != nil { + // 裡面有錯從 Grpc 已經轉換完成 + return nil, err + } + + // Step 2: 建立帳號 + _, err = l.svcCtx.MemberRPC.CreateUserAccount(l.ctx, &memberProto.CreateLoginUserReq{ + LoginId: createAccountReq.LoginID, + Token: createAccountReq.Token, + Platform: createAccountReq.Platform.ToInt64(), + }) + if err != nil { + return nil, err + } + + // Step 3: 綁定帳號並獲取 UID + account, err := l.svcCtx.MemberRPC.BindAccount(l.ctx, &memberProto.BindingUserReq{ + LoginId: bindingUserReq.LoginID, + Type: int64(bindingUserReq.Type), + }) + if err != nil { + return nil, err + } + + // Step 4: 更新使用者資訊 + _, err = l.svcCtx.MemberRPC.BindUserInfo(l.ctx, &memberProto.CreateUserInfoReq{ + Uid: account.Uid, + AlarmType: memberProto.AlarmType(createUserInfoRequest.AlarmCategory), + Status: memberProto.MemberStatus(createUserInfoRequest.UserStatus), + Language: createUserInfoRequest.PreferredLanguage, + Currency: createUserInfoRequest.Currency, + Avatar: createUserInfoRequest.AvatarURL, + NickName: createUserInfoRequest.Nickname, + FullName: createUserInfoRequest.FullName, + Gender: createUserInfoRequest.GenderCode, + Birthdate: createUserInfoRequest.Birthdate, + PhoneNumber: createUserInfoRequest.PhoneNumber, + Email: createUserInfoRequest.Email, + Address: createUserInfoRequest.Address, + }) + if err != nil { + return nil, err + } + // TODO 綁定角色 + + // Step 5: 生成 Token + t, err := l.generateToken(req, account.GetUid()) + + if err != nil { + return nil, err + } + + return &types.LoginTokenResp{ + UID: account.GetUid(), + AccessToken: t.AccessToken, + RefreshToken: t.RefreshToken, + TokenType: bizDomain.TokenTypeBearer, + }, nil +} + +// 構建不同平台的請求數據 +func (l *AccountCreateLogic) buildAccountRequests(req *types.CreateAccountRequest, platform member.Platform) ( + memberUC.CreateLoginUserRequest, memberUC.BindingUser, memberUC.CreateUserInfoRequest, error) { + var createAccountReq memberUC.CreateLoginUserRequest + var bindingUserReq memberUC.BindingUser + var createUserInfoRequest memberUC.CreateUserInfoRequest + switch platform { + case member.Digimon: + // 驗證 Token 是否一致 + if req.Token != req.TokenCheck { + return createAccountReq, bindingUserReq, createUserInfoRequest, errs.NewError( + code.CloudEPMember, code.InvalidRange, bizDomain.TokenNotTheSameAPIErrorCode, + "failed to verify check token", + ) + } + createAccountReq = memberUC.CreateLoginUserRequest{ + LoginID: req.Account, + Token: req.Token, + Platform: platform, + } + bindingUserReq = memberUC.BindingUser{ + LoginID: req.Account, + Type: member.GetAccountTypeByCode(req.AccountType), + } + createUserInfoRequest = memberUC.CreateUserInfoRequest{ + AlarmCategory: member.AlarmNoAlert, + UserStatus: member.AccountStatusUnverified, + PreferredLanguage: bizDomain.DefaultLang, + Currency: bizDomain.DefaultCurrency, + } + + case member.Google: + googleToken, err := l.svcCtx.MemberRPC.VerifyGoogleAuthResult(l.ctx, &memberProto.VerifyAuthResultReq{ + Account: proto.String(req.Account), + Token: req.Token, + }) + if err != nil { + return createAccountReq, bindingUserReq, createUserInfoRequest, err + } + + createAccountReq = memberUC.CreateLoginUserRequest{ + LoginID: *googleToken.Email, + Token: "", + Platform: platform, + } + bindingUserReq = memberUC.BindingUser{ + LoginID: *googleToken.Email, + Type: member.GetAccountTypeByCode(req.AccountType), + } + createUserInfoRequest = memberUC.CreateUserInfoRequest{ + AvatarURL: googleToken.Picture, + FullName: googleToken.Name, + Nickname: googleToken.Name, + Email: googleToken.Email, + AlarmCategory: member.AlarmNoAlert, + UserStatus: member.AccountStatusActive, + PreferredLanguage: bizDomain.DefaultLang, + Currency: bizDomain.DefaultCurrency, + } + + case member.Line: + // 用 code 換取 line access token + lineAccessToken, err := l.svcCtx.MemberRPC.LineCodeToAccessToken(l.ctx, &memberProto.LineGetTokenReq{ + Code: req.Account, + }) + if err != nil { + return createAccountReq, bindingUserReq, createUserInfoRequest, err + } + // 用 access token 換取 line 使用者資料 + userInfo, err := l.svcCtx.MemberRPC.LineGetProfileByAccessToken(l.ctx, &memberProto.LineGetUserInfoReq{ + Token: lineAccessToken.Token, + }) + if err != nil { + return createAccountReq, bindingUserReq, createUserInfoRequest, err + } + + createAccountReq = memberUC.CreateLoginUserRequest{ + LoginID: userInfo.UserId, + Token: "", + Platform: platform, + } + bindingUserReq = memberUC.BindingUser{ + LoginID: userInfo.UserId, + Type: member.GetAccountTypeByCode(req.AccountType), + } + + createUserInfoRequest = memberUC.CreateUserInfoRequest{ + AvatarURL: proto.String(userInfo.PictureUrl), + FullName: proto.String(userInfo.DisplayName), + Nickname: proto.String(userInfo.DisplayName), + AlarmCategory: member.AlarmNoAlert, + UserStatus: member.AccountStatusActive, + PreferredLanguage: bizDomain.DefaultLang, + Currency: bizDomain.DefaultCurrency, + } + case member.PlatformNone: + default: + return createAccountReq, bindingUserReq, createUserInfoRequest, errs.InvalidFormat("invalid platform") + } + + return createAccountReq, bindingUserReq, createUserInfoRequest, nil +} + +// 生成 Token 這裡是註冊的,所以角色是固定的,不能讓平常註冊的使用者選擇角色 +func (l *AccountCreateLogic) generateToken(req *types.CreateAccountRequest, uid string) (tokenUC.AccessTokenResponse, error) { + credentials := "client_credentials" + // 生成預設的Role -> 所有邏輯上的其他角色,應該都要從 user 開始,後續再通過admin 做更改 + // 第一個 admin 應該是直接插入資料庫的 + t, err := l.svcCtx.TokenRPC.NewToken(l.ctx, &permissionProto.AuthorizationReq{ + GrantType: credentials, + DeviceId: req.DeviceID, + Scope: bizDomain.DefaultRole, + IsRefreshToken: true, + Account: req.Account, + Uid: uid, + Expires: proto.Int64(time.Now().UTC().Add(l.svcCtx.Config.Token.Expired).UnixNano()), // 指定到期的時間 + RefreshExpire: proto.Int64(time.Now().UTC().Add(l.svcCtx.Config.Token.RefreshExpired).UnixNano()), // 指定到期的時間 + Data: map[string]string{}, + Role: bizDomain.DefaultRole, + }) + if err != nil { + return tokenUC.AccessTokenResponse{}, errs.FromGRPCError(err) + } + + return tokenUC.AccessTokenResponse{ + AccessToken: t.AccessToken, + ExpiresIn: t.ExpiresIn, + RefreshToken: t.RefreshToken, + }, nil +} diff --git a/internal/logic/member/check_verify_code_logic.go b/internal/logic/member/check_verify_code_logic.go new file mode 100644 index 0000000..5fa5a91 --- /dev/null +++ b/internal/logic/member/check_verify_code_logic.go @@ -0,0 +1,104 @@ +package member + +import ( + "biz-member-gateway/internal/domain" + "context" + + "code.30cm.net/digimon/app-cloudep-member-server/pkg/domain/member" + "code.30cm.net/digimon/library-go/errs" + "code.30cm.net/digimon/library-go/errs/code" + + "biz-member-gateway/internal/svc" + "biz-member-gateway/internal/types" + + "github.com/zeromicro/go-zero/core/logx" + + memberProto "code.30cm.net/digimon/proto-all/pkg/member" +) + +type CheckVerifyCodeLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewCheckVerifyCodeLogic 確認邀請 - 綁定會員 +func NewCheckVerifyCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CheckVerifyCodeLogic { + return &CheckVerifyCodeLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CheckVerifyCodeLogic) CheckVerifyCode(req *types.CheckoutVerifyReq) (resp *types.RespOK, err error) { + var account string + accountType := member.GetAccountTypeByCode(req.CodeType) + + switch accountType { + case member.AccountTypePhone: + phone, isPhone := normalizeTaiwanMobile(req.Account) + if !isPhone { + return nil, errs.InvalidFormat("phone number is invalid") + } + account = phone + case member.AccountTypeMail: + if !isValidEmail(req.Account) { + return nil, errs.InvalidFormat("email is invalid") + } + account = req.Account + default: + return nil, errs.InvalidFormat("invalid account type") + } + + ct := member.GetGetCodeNameByCode(req.CodeType) + if _, err := l.svcCtx.MemberRPC.CheckRefreshCode(l.ctx, &memberProto.VerifyRefreshCodeReq{ + VerifyCode: req.VerifyCode, + Account: account, + CodeType: int32(ct), + }); err != nil { + return nil, errs.Forbidden("failed to get verify code").Wrap(err) + } + + // 處理綁定 Email 或 Phone + switch ct { + case member.GenerateCodeTypeEmail: + if _, err := l.svcCtx.MemberRPC.BindVerifyEmail(l.ctx, &memberProto.BindVerifyEmailReq{ + Uid: req.UID, + Email: account, + }); err != nil { + return nil, errs.DatabaseErrorWithScope( + code.CloudEPMember, domain.FailedToCheckVerifyCodeErrorCode, + "failed to bind email", err.Error()) + } + case member.GenerateCodeTypePhone: + if _, err := l.svcCtx.MemberRPC.BindVerifyPhone(l.ctx, &memberProto.BindVerifyPhoneReq{ + Uid: req.UID, + Phone: account, + }); err != nil { + return nil, errs.DatabaseErrorWithScope( + code.CloudEPMember, domain.FailedToCheckVerifyCodeErrorCode, "failed to bind phone", err.Error()) + } + } + + // 更新帳戶狀態 + _, err = l.svcCtx.MemberRPC.UpdateStatus(l.ctx, &memberProto.UpdateStatusReq{ + Uid: req.UID, + Status: memberProto.MemberStatus(member.AccountStatusActive), + }) + if err != nil { + return nil, errs.DatabaseErrorWithScope( + code.CloudEPMember, domain.FailedToCheckVerifyCodeErrorCode, "failed to update status", err.Error()) + } + + // 最後刪除驗證碼 + if _, err := l.svcCtx.MemberRPC.VerifyRefreshCode(l.ctx, &memberProto.VerifyRefreshCodeReq{ + VerifyCode: req.VerifyCode, + Account: account, + CodeType: int32(ct), + }); err != nil { + return nil, errs.Forbidden("failed to get verify code").Wrap(err) + } + + return &types.RespOK{}, nil +} diff --git a/internal/logic/member/forget_password_code_logic.go b/internal/logic/member/forget_password_code_logic.go new file mode 100644 index 0000000..840d669 --- /dev/null +++ b/internal/logic/member/forget_password_code_logic.go @@ -0,0 +1,192 @@ +package member + +import ( + "biz-member-gateway/internal/domain" + "biz-member-gateway/internal/svc" + "biz-member-gateway/internal/types" + "bytes" + + "code.30cm.net/digimon/app-cloudep-member-server/pkg/domain/member" + "code.30cm.net/digimon/library-go/errs/code" + + "context" + "fmt" + "html/template" + + "code.30cm.net/digimon/library-go/errs" + + ntpl "code.30cm.net/digimon/app-cloudep-notification-service/pkg/domain/template" + memberProto "code.30cm.net/digimon/proto-all/pkg/member" + notificationProto "code.30cm.net/digimon/proto-all/pkg/notification" + "github.com/zeromicro/go-zero/core/logx" +) + +type ForgetPasswordCodeLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewForgetPasswordCodeLogic 發送忘記密碼驗證 +func NewForgetPasswordCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ForgetPasswordCodeLogic { + return &ForgetPasswordCodeLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ForgetPasswordCodeLogic) ForgetPasswordCode(req *types.ForgetPasswordCodeReq) (resp *types.RespOK, err error) { + // 1. 驗證並標準化帳號 + acc, err := normalizeAccount(req.AccountType, req.Account) + if err != nil { + return nil, err + } + + // 2. 統一檢查冷卻時間與平台正確性 + if err = l.checkAccountValidity(acc); err != nil { + return nil, err + } + + // 3. 生成驗證碼 + vcode, err := l.svcCtx.MemberRPC.GenerateRefreshCode(l.ctx, &memberProto.GenerateRefreshCodeReq{ + Account: acc, + CodeType: int32(member.GenerateCodeTypeForgetPassword), + }) + if err != nil { + return nil, err + } + + // 4. 取得用戶資訊 + uidInfo, err := l.svcCtx.MemberRPC.GetUIDByAccount(l.ctx, &memberProto.GetUIDByAccountReq{Account: acc}) + if err != nil { + return nil, err + } + userInfo, err := l.svcCtx.MemberRPC.GetUserInfo(l.ctx, &memberProto.GetUserInfoReq{Uid: uidInfo.GetUid()}) + if err != nil { + return nil, err + } + + // 5. 發送驗證碼 + nickname := getEmailShowName(userInfo) + if err = l.sendVerificationCode(req.AccountType, acc, userInfo.Data, vcode.Data.VerifyCode, nickname); err != nil { + return nil, err + } + + // 6. 設置 Redis 驗證碼鍵值 + rk := domain.GetSendCodeRedisKey(fmt.Sprintf("%s:%d", acc, member.GenerateCodeTypeForgetPassword)) + l.setRedisKeyWithExpiry(rk, vcode.Data.VerifyCode, int(l.svcCtx.Config.Member.ForgetTimeOutInSec)) + + return &types.RespOK{}, nil +} + +// normalizeAccount 驗證並標準化帳號 +func normalizeAccount(accountType, account string) (string, error) { + at := member.GetAccountTypeByCode(accountType) + switch at { + case member.AccountTypePhone: + phone, ok := normalizeTaiwanMobile(account) + if !ok { + return "", errs.InvalidFormat("phone number is invalid") + } + return phone, nil + case member.AccountTypeMail: + if !isValidEmail(account) { + return "", errs.InvalidFormat("email is invalid") + } + return account, nil + default: + return "", errs.InvalidFormat("unsupported account type") + } +} + +// checkAccountValidity 同時檢查發送冷卻與平台正確性 +func (l *ForgetPasswordCodeLogic) checkAccountValidity(acc string) error { + // 檢查發送冷卻 + rk := domain.GetSendCodeRedisKey(fmt.Sprintf("%s:%d", acc, member.GenerateCodeTypeForgetPassword)) + if cached, err := l.svcCtx.Redis.GetCtx(l.ctx, rk); err != nil || cached != "" { + return errs.InvalidRange("verification code already sent, please wait for system to send again") + } + + // 檢查平台是否正確(只允許平台帳號進行忘記密碼操作) + accountInfo, err := l.svcCtx.MemberRPC.GetUserAccountInfo(l.ctx, &memberProto.GetUIDByAccountReq{Account: acc}) + if err != nil { + return err + } + if accountInfo.Data.Platform != member.Digimon.ToInt64() { + return errs.InvalidResourceState("failed to send verify code since platform not correct") + } + return nil +} + +// sendVerificationCode 根據帳號類型發送驗證碼 +func (l *ForgetPasswordCodeLogic) sendVerificationCode(accountType, acc string, info *memberProto.UserInfo, verifyCode, nickname string) error { + switch member.GetAccountTypeByCode(accountType) { + case member.AccountTypePhone: + // TODO: 傳送簡訊 + fmt.Printf("SMS Template: %s\n", verifyCode) + case member.AccountTypeMail: + return l.sendVerificationEmail(acc, info, verifyCode, nickname) + default: + return errs.InvalidResourceState("unsupported account type") + } + return nil +} + +type EmailTmpInfo struct { + Username string + VerifyCode string +} + +// sendVerificationEmail 發送驗證郵件 +func (l *ForgetPasswordCodeLogic) sendVerificationEmail(recipientEmail string, info *memberProto.UserInfo, verifyCode, nickname string) error { + tpl, err := l.svcCtx.NotificationRPC.GetStaticTemplate(l.ctx, ¬ificationProto.TemplateReq{ + Language: info.Language, + TemplateId: string(ntpl.ForgetPasswordVerify), + }) + if err != nil { + return err + } + + tmpl, err := template.New("ForgetPasswordEmail").Parse(tpl.GetBody()) + if err != nil { + return err + } + + emailParams := EmailTmpInfo{Username: nickname, VerifyCode: verifyCode} + var buf bytes.Buffer + if err := tmpl.Execute(&buf, emailParams); err != nil { + return err + } + + _, err = l.svcCtx.NotificationRPC.SendMail(l.ctx, ¬ificationProto.SendMailReq{ + To: recipientEmail, + Subject: tpl.Title, + Body: buf.String(), + From: l.svcCtx.Config.MailSender, + }) + return err +} + +// getEmailShowName 取得寄信用的顯示名稱 +func getEmailShowName(info *memberProto.GetUserInfoResp) string { + if info.Data.FullName != nil { + return *info.Data.FullName + } + if info.Data.NickName != nil { + return *info.Data.NickName + } + return info.Data.Uid +} + +// setRedisKeyWithExpiry 設置 Redis 鍵與過期時間 +func (l *ForgetPasswordCodeLogic) setRedisKeyWithExpiry(rk, verifyCode string, expiry int) { + if status, err := l.svcCtx.Redis.SetnxExCtx(l.ctx, rk, verifyCode, expiry); err != nil || !status { + _ = errs.DatabaseErrorWithScopeL(code.CloudEPMember, domain.FailedToSetVerifyCodeErrorCode, + logx.WithContext(l.ctx), + []logx.LogField{ + {Key: "redisKey", Value: rk}, + {Key: "error", Value: err.Error()}, + }, "failed to set redis expire").Wrap(err) + } +} diff --git a/internal/logic/member/info_logic.go b/internal/logic/member/info_logic.go new file mode 100644 index 0000000..67ceb88 --- /dev/null +++ b/internal/logic/member/info_logic.go @@ -0,0 +1,95 @@ +package member + +import ( + "biz-member-gateway/internal/domain" + "biz-member-gateway/internal/svc" + "biz-member-gateway/internal/types" + "biz-member-gateway/internal/utils" + "context" + + "code.30cm.net/digimon/app-cloudep-member-server/pkg/domain/member" + memberProto "code.30cm.net/digimon/proto-all/pkg/member" + + "github.com/zeromicro/go-zero/core/logx" +) + +type InfoLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewInfoLogic 取得會員資訊 +func NewInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *InfoLogic { + return &InfoLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *InfoLogic) Info(req *types.VerifyHeader) (resp *types.UserInfo, err error) { + info, err := l.svcCtx.MemberRPC.GetUserInfo(l.ctx, &memberProto.GetUserInfoReq{ + Uid: domain.UID(l.ctx), + }) + if err != nil { + return nil, err + } + + accountInfo, err := l.svcCtx.MemberRPC.GetUserAccountInfo(l.ctx, &memberProto.GetUIDByAccountReq{ + Account: domain.Account(l.ctx), + }) + if err != nil { + return nil, err + } + + alarmType := member.AlarmType(info.Data.AlarmType) + status := member.Status(info.Data.Status) + res := &types.UserInfo{ + Platform: member.Platform(accountInfo.Data.Platform).ToString(), + UID: domain.UID(l.ctx), + UpdateAt: utils.UnixToRfc3339(info.Data.UpdateTime), + CreateAt: utils.UnixToRfc3339(info.Data.CreateTime), + AlarmCategory: alarmType.CodeToString(), + UserStatus: status.CodeToString(), + PreferredLanguage: info.Data.Language, + Currency: info.Data.Currency, + } + + if info.Data.AvatarUrl != nil { + res.AvatarURL = *info.Data.AvatarUrl + } + if info.Data.FullName != nil { + res.FullName = *info.Data.FullName + } + if info.Data.NickName != nil { + res.Nickname = *info.Data.NickName + } + + if info.Data.GenderCode != nil { + gc, err := domain.GenderCodeToString(*info.Data.GenderCode) + if err != nil { + gc = "secret" + } + res.GenderCode = gc + } + + if info.Data.Birthday != nil { + // RFC 3339 + res.Birthdate = utils.UnixToRfc3339(*info.Data.Birthday) + } + + if info.Data.Phone != nil { + res.PhoneNumber = *info.Data.Phone + } + + if info.Data.Address != nil { + res.Address = *info.Data.Address + } + + if info.Data.Email != nil { + res.Email = *info.Data.Email + } + + return res, nil +} diff --git a/internal/logic/member/login_logic.go b/internal/logic/member/login_logic.go new file mode 100644 index 0000000..ac993ef --- /dev/null +++ b/internal/logic/member/login_logic.go @@ -0,0 +1,168 @@ +package member + +import ( + "biz-member-gateway/internal/domain" + bizDomain "biz-member-gateway/internal/domain" + "biz-member-gateway/internal/svc" + "biz-member-gateway/internal/types" + "context" + "regexp" + "strings" + "time" + + "code.30cm.net/digimon/app-cloudep-member-server/pkg/domain/member" + memberUC "code.30cm.net/digimon/app-cloudep-member-server/pkg/domain/usecase" + "code.30cm.net/digimon/library-go/errs" + memberProto "code.30cm.net/digimon/proto-all/pkg/member" + permissionProto "code.30cm.net/digimon/proto-all/pkg/permission" + "github.com/golang/protobuf/proto" + "github.com/zeromicro/go-zero/core/logx" +) + +type LoginLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewLoginLogic 登入 +func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic { + return &LoginLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginTokenResp, err error) { + var result memberUC.VerifyAuthResultResponse + // Step 1 驗證進來的 Token + platform := member.GetPlatformByPlatformCode(req.Platform) + + switch platform { + case member.Digimon: + switch member.GetAccountTypeByCode(req.AccountType) { + case member.AccountTypePhone: + phone, isPhone := normalizeTaiwanMobile(req.Account) + if !isPhone { + return nil, errs.InvalidFormat("phone number is invalid") + } + req.Account = phone + case member.AccountTypeMail: + if !isValidEmail(req.Account) { + return nil, errs.InvalidFormat("email is invalid") + } + } + + // 原始平台驗證 + res, err := l.svcCtx.MemberRPC.VerifyPlatformAuthResult(l.ctx, &memberProto.VerifyAuthResultReq{ + Account: proto.String(req.Account), + Token: req.Token, + }) + if err != nil { + return nil, err + } + result.Status = res.Status + case member.Google: + // 原始平台驗證 + _, err := l.svcCtx.MemberRPC.VerifyGoogleAuthResult(l.ctx, &memberProto.VerifyAuthResultReq{ + Account: proto.String(req.Account), + Token: req.Token, + }) + + if err != nil { + return nil, err + } + result.Status = true + case member.Line: + // 原始平台驗證 + accessToken, err := l.svcCtx.MemberRPC.LineCodeToAccessToken(l.ctx, &memberProto.LineGetTokenReq{ + Code: req.Account, + }) + if err != nil { + return nil, err + } + + // 換資料 + userInfo, err := l.svcCtx.MemberRPC.LineGetProfileByAccessToken(l.ctx, &memberProto.LineGetUserInfoReq{ + Token: accessToken.Token, + }) + if err != nil { + return nil, err + } + + result.Status = true + req.Account = userInfo.UserId + case member.PlatformNone: + default: + return nil, errs.InvalidFormat("invalid platform") + } + + if !result.Status { + return nil, errs.Unauthorized("failed to validate password ") + } + + account, err := l.svcCtx.MemberRPC.GetUIDByAccount(l.ctx, &memberProto.GetUIDByAccountReq{ + Account: req.Account, + }) + if err != nil { + return nil, err + } + + credentials := "client_credentials" + // TODO 去拿這個使用者Role + role := bizDomain.DefaultRole + + t, err := l.svcCtx.TokenRPC.NewToken(l.ctx, &permissionProto.AuthorizationReq{ + GrantType: credentials, + DeviceId: req.DeviceID, + Scope: role, + IsRefreshToken: true, + Account: req.Account, + Uid: account.GetUid(), + Expires: proto.Int64(time.Now().UTC().Add(l.svcCtx.Config.Token.Expired).UnixNano()), // 指定到期的時間 + RefreshExpire: proto.Int64(time.Now().UTC().Add(l.svcCtx.Config.Token.RefreshExpired).UnixNano()), // 指定到期的時間 + Data: map[string]string{}, + Role: role, + }) + if err != nil { + return nil, err + } + + return &types.LoginTokenResp{ + UID: account.GetUid(), + AccessToken: t.AccessToken, + RefreshToken: t.RefreshToken, + TokenType: domain.TokenTypeBearer, + }, nil +} + +// 標準化號碼並驗證是否為合法台灣手機號碼 +func normalizeTaiwanMobile(phone string) (string, bool) { + // 移除空格 + phone = strings.ReplaceAll(phone, " ", "") + + // 移除 "+886" 並將剩餘部分標準化 + if strings.HasPrefix(phone, "+886") { + phone = strings.TrimPrefix(phone, "+886") + if !strings.HasPrefix(phone, "0") { + phone = "0" + phone + } + } + + // 正則表達式驗證標準化後的號碼 + regex := regexp.MustCompile(`^(09\d{8})$`) + if regex.MatchString(phone) { + return phone, true + } + + return "", false +} + +// 驗證 Email 格式的函數 +func isValidEmail(email string) bool { + // 定義正則表達式 + regex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) + + return regex.MatchString(email) +} diff --git a/internal/logic/member/logout_logic.go b/internal/logic/member/logout_logic.go new file mode 100644 index 0000000..9d324f1 --- /dev/null +++ b/internal/logic/member/logout_logic.go @@ -0,0 +1,38 @@ +package member + +import ( + "context" + + permissionProto "code.30cm.net/digimon/proto-all/pkg/permission" + + "biz-member-gateway/internal/svc" + "biz-member-gateway/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type LogoutLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewLogoutLogic 會員登出 +func NewLogoutLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LogoutLogic { + return &LogoutLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *LogoutLogic) Logout(req *types.VerifyHeader) (*types.RespOK, error) { + _, err := l.svcCtx.TokenRPC.CancelToken(l.ctx, &permissionProto.CancelTokenReq{ + Token: req.Token, + }) + if err != nil { + return nil, err + } + + return &types.RespOK{}, nil +} diff --git a/internal/logic/member/modify_member_info_logic.go b/internal/logic/member/modify_member_info_logic.go new file mode 100644 index 0000000..b3998fa --- /dev/null +++ b/internal/logic/member/modify_member_info_logic.go @@ -0,0 +1,144 @@ +package member + +import ( + "biz-member-gateway/internal/domain" + "biz-member-gateway/internal/svc" + "biz-member-gateway/internal/types" + "biz-member-gateway/internal/utils" + "context" + + "code.30cm.net/digimon/app-cloudep-member-server/pkg/domain/member" + memberProto "code.30cm.net/digimon/proto-all/pkg/member" + "github.com/golang/protobuf/proto" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ModifyMemberInfoLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 更新會員詳細資訊 +func NewModifyMemberInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ModifyMemberInfoLogic { + return &ModifyMemberInfoLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ModifyMemberInfoLogic) ModifyMemberInfo(req *types.BindingUserInfoReq) (*types.UserInfo, error) { + update := &memberProto.UpdateUserInfoReq{ + Uid: domain.UID(l.ctx), + } + if req.Nickname != "" { + update.NickName = proto.String(req.Nickname) + } + + if req.PreferredLanguage != "" { + update.Language = proto.String(req.PreferredLanguage) + } + + if req.Currency != "" { + update.Currency = proto.String(req.Currency) + } + + if req.AvatarURL != "" { + update.Avatar = proto.String(req.AvatarURL) + } + + if req.FullName != "" { + update.FullName = proto.String(req.FullName) + } + + if req.GenderCode != "" { + gc, err := domain.StringToGenderCode(req.GenderCode) + if err != nil { + gc = 0 + } + update.Gender = proto.Int64(gc) + } + + if req.Birthday != "" { + unix, err := utils.Rfc3339ToUnix(req.Birthday) + if err == nil { + update.Birthdate = proto.Int64(unix) + } + } + + if req.Address != "" { + update.Address = proto.String(req.Address) + } + + _, err := l.svcCtx.MemberRPC.UpdateUserInfo(l.ctx, update) + if err != nil { + return nil, err + } + + // 再次取得資訊 + info, err := l.svcCtx.MemberRPC.GetUserInfo(l.ctx, &memberProto.GetUserInfoReq{ + Uid: domain.UID(l.ctx), + }) + if err != nil { + return nil, err + } + + accountInfo, err := l.svcCtx.MemberRPC.GetUserAccountInfo(l.ctx, &memberProto.GetUIDByAccountReq{ + Account: domain.Account(l.ctx), + }) + if err != nil { + return nil, err + } + + alarmType := member.AlarmType(info.Data.AlarmType) + status := member.Status(info.Data.Status) + res := &types.UserInfo{ + Platform: member.Platform(accountInfo.Data.Platform).ToString(), + UID: domain.UID(l.ctx), + UpdateAt: utils.UnixToRfc3339(info.Data.UpdateTime), + CreateAt: utils.UnixToRfc3339(info.Data.CreateTime), + AlarmCategory: alarmType.CodeToString(), + UserStatus: status.CodeToString(), + PreferredLanguage: info.Data.Language, + Currency: info.Data.Currency, + } + + if info.Data.AvatarUrl != nil { + res.AvatarURL = *info.Data.AvatarUrl + } + if info.Data.FullName != nil { + res.FullName = *info.Data.FullName + } + if info.Data.NickName != nil { + res.Nickname = *info.Data.NickName + } + + if info.Data.GenderCode != nil { + gc, err := domain.GenderCodeToString(*info.Data.GenderCode) + if err != nil { + gc = "secret" + } + res.GenderCode = gc + } + + if info.Data.Birthday != nil { + // RFC 3339 + res.Birthdate = utils.UnixToRfc3339(*info.Data.Birthday) + } + + if info.Data.Phone != nil { + res.PhoneNumber = *info.Data.Phone + } + + if info.Data.Address != nil { + res.Address = *info.Data.Address + } + + if info.Data.Email != nil { + res.Email = *info.Data.Email + } + + return res, nil +} diff --git a/internal/logic/member/modify_passwd_logic.go b/internal/logic/member/modify_passwd_logic.go new file mode 100644 index 0000000..88f1d9d --- /dev/null +++ b/internal/logic/member/modify_passwd_logic.go @@ -0,0 +1,64 @@ +package member + +import ( + "biz-member-gateway/internal/domain" + "context" + + "code.30cm.net/digimon/app-cloudep-member-server/pkg/domain/member" + "code.30cm.net/digimon/library-go/errs" + memberRPC "code.30cm.net/digimon/proto-all/pkg/member" + permissionProto "code.30cm.net/digimon/proto-all/pkg/permission" + + "biz-member-gateway/internal/svc" + "biz-member-gateway/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ModifyPasswdLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewModifyPasswdLogic 修改密碼 +func NewModifyPasswdLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ModifyPasswdLogic { + return &ModifyPasswdLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ModifyPasswdLogic) ModifyPasswd(req *types.ModifyPasswdReq) (*types.RespOK, error) { + account := domain.Account(l.ctx) + + // 檢查是否有此帳號 + _, err := l.svcCtx.MemberRPC.GetUIDByAccount(l.ctx, &memberRPC.GetUIDByAccountReq{ + Account: account, + }) + if err != nil { + return nil, err + } + + if req.NewToken != req.NewTokenCheck { + return nil, errs.InvalidFormat("failed to check token") + } + + // 更新 + _, err = l.svcCtx.MemberRPC.UpdateUserToken(l.ctx, &memberRPC.UpdateTokenReq{ + Account: account, + Token: req.NewToken, + Platform: member.Digimon.ToInt64(), + }) + if err != nil { + return nil, err + } + + // 登出所有,請使用者中新登入 -> 射後不理,因為已經更新成功了 + _, _ = l.svcCtx.TokenRPC.CancelToken(l.ctx, &permissionProto.CancelTokenReq{ + Token: req.Token, + }) + + return &types.RespOK{}, nil +} diff --git a/internal/logic/member/pre_verify_update_password_code_logic.go b/internal/logic/member/pre_verify_update_password_code_logic.go new file mode 100644 index 0000000..adbe592 --- /dev/null +++ b/internal/logic/member/pre_verify_update_password_code_logic.go @@ -0,0 +1,42 @@ +package member + +import ( + "context" + + "code.30cm.net/digimon/app-cloudep-member-server/pkg/domain/member" + "code.30cm.net/digimon/library-go/errs" + memberProto "code.30cm.net/digimon/proto-all/pkg/member" + + "biz-member-gateway/internal/svc" + "biz-member-gateway/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type PreVerifyUpdatePasswordCodeLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewPreVerifyUpdatePasswordCodeLogic 預先驗證驗證碼 +func NewPreVerifyUpdatePasswordCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PreVerifyUpdatePasswordCodeLogic { + return &PreVerifyUpdatePasswordCodeLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *PreVerifyUpdatePasswordCodeLogic) PreVerifyUpdatePasswordCode(req *types.PreVerifyForgetPasswdReq) (resp *types.RespOK, err error) { + // 先驗證,不刪除 + if _, err := l.svcCtx.MemberRPC.CheckRefreshCode(l.ctx, &memberProto.VerifyRefreshCodeReq{ + Account: req.Identifier, + CodeType: int32(member.GenerateCodeTypeForgetPassword), + VerifyCode: req.VerifyCode, + }); err != nil { + return nil, errs.Forbidden("failed to get verify code") + } + + return +} diff --git a/internal/logic/member/refresh_access_token_logic.go b/internal/logic/member/refresh_access_token_logic.go new file mode 100644 index 0000000..e4b789b --- /dev/null +++ b/internal/logic/member/refresh_access_token_logic.go @@ -0,0 +1,61 @@ +package member + +import ( + "biz-member-gateway/internal/domain" + "biz-member-gateway/internal/svc" + "biz-member-gateway/internal/types" + "context" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/token" + "code.30cm.net/digimon/library-go/errs" + "code.30cm.net/digimon/proto-all/pkg/permission" + + "github.com/zeromicro/go-zero/core/logx" +) + +type RefreshAccessTokenLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewRefreshAccessTokenLogic 更新 Access Token +func NewRefreshAccessTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RefreshAccessTokenLogic { + return &RefreshAccessTokenLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *RefreshAccessTokenLogic) RefreshAccessToken(req *types.UpdateTokenReq) (*types.LoginTokenResp, error) { + data, err := l.svcCtx.TokenRPC.GetSystemClaimByAccessToken(l.ctx, &permission.GetSystemClaimReq{ + AccessToken: req.Token, + IsExpired: false, + }) + if err != nil { + return nil, err + } + + uid := data.Data[token.UID.String()] + if uid != req.UID { + return nil, errs.Forbidden("failed to verify token user") + } + + t, err := l.svcCtx.TokenRPC.RefreshToken(l.ctx, &permission.RefreshTokenReq{ + Token: req.RefreshToken, + Scope: data.Data[token.Scope.String()], + Expires: 0, // 指定到期的時間,不給會交由底層給(token repo) + DeviceId: data.Data[token.Device.String()], + }) + if err != nil { + return nil, err + } + + return &types.LoginTokenResp{ + UID: req.UID, + AccessToken: t.Token, + RefreshToken: t.OneTimeToken, + TokenType: domain.TokenTypeBearer, + }, nil +} diff --git a/internal/logic/member/send_verify_code_logic.go b/internal/logic/member/send_verify_code_logic.go new file mode 100644 index 0000000..fa7674b --- /dev/null +++ b/internal/logic/member/send_verify_code_logic.go @@ -0,0 +1,157 @@ +package member + +import ( + "biz-member-gateway/internal/domain" + "bytes" + "context" + "fmt" + "html/template" + + "code.30cm.net/digimon/app-cloudep-member-server/pkg/domain/member" + ntpl "code.30cm.net/digimon/app-cloudep-notification-service/pkg/domain/template" + "code.30cm.net/digimon/library-go/errs" + "code.30cm.net/digimon/library-go/errs/code" + memberProto "code.30cm.net/digimon/proto-all/pkg/member" + notificationProto "code.30cm.net/digimon/proto-all/pkg/notification" + + "biz-member-gateway/internal/svc" + "biz-member-gateway/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type SendVerifyCodeLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewSendVerifyCodeLogic 發送邀請 - 綁定會員 +func NewSendVerifyCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendVerifyCodeLogic { + return &SendVerifyCodeLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *SendVerifyCodeLogic) SendVerifyCode(req *types.VerificationCodeRequest) (resp *types.RespOK, err error) { + at := member.GetGetCodeNameByCode(req.CodeType) + // 1. 驗證並標準化帳號 + acc, err := normalizeAccount(req.CodeType, req.Identifier) + if err != nil { + return nil, err + } + + // 2. 統一檢查冷卻時間與平台正確性 + if err = l.checkAccountValidity(acc, at); err != nil { + return nil, err + } + + // 3. 生成驗證碼 + vcode, err := l.svcCtx.MemberRPC.GenerateRefreshCode(l.ctx, &memberProto.GenerateRefreshCodeReq{ + Account: acc, + CodeType: int32(at), + }) + if err != nil { + return nil, err + } + + // 4. 取得用戶資訊 + uidInfo, err := l.svcCtx.MemberRPC.GetUIDByAccount(l.ctx, &memberProto.GetUIDByAccountReq{Account: acc}) + if err != nil { + return nil, err + } + userInfo, err := l.svcCtx.MemberRPC.GetUserInfo(l.ctx, &memberProto.GetUserInfoReq{Uid: uidInfo.GetUid()}) + if err != nil { + return nil, err + } + + // 5. 發送驗證碼 + nickname := getEmailShowName(userInfo) + if err = l.sendVerificationCode(req.CodeType, acc, userInfo.Data, vcode.Data.VerifyCode, nickname); err != nil { + return nil, err + } + + // 6. 設置 Redis 驗證碼鍵值 + rk := domain.GetSendCodeRedisKey(fmt.Sprintf("%s:%d", acc, at)) + l.setRedisKeyWithExpiry(rk, vcode.Data.VerifyCode, int(l.svcCtx.Config.Member.ForgetTimeOutInSec)) + + return &types.RespOK{}, nil +} + +// checkAccountValidity 同時檢查發送冷卻與平台正確性 +func (l *SendVerifyCodeLogic) checkAccountValidity(acc string, gc member.GenerateCodeType) error { + // 檢查發送冷卻 + rk := domain.GetSendCodeRedisKey(fmt.Sprintf("%s:%d", acc, gc)) + if cached, err := l.svcCtx.Redis.GetCtx(l.ctx, rk); err != nil || cached != "" { + return errs.InvalidRange("verification code already sent, please wait for system to send again") + } + + // 檢查平台是否正確(只允許平台帳號進行綁定) + accountInfo, err := l.svcCtx.MemberRPC.GetUserAccountInfo(l.ctx, &memberProto.GetUIDByAccountReq{Account: acc}) + if err != nil { + return err + } + if accountInfo.Data.Platform != member.Digimon.ToInt64() { + return errs.InvalidResourceState("failed to send verify code since platform not correct") + } + return nil +} + +// sendVerificationEmail 發送驗證郵件 +func (l *SendVerifyCodeLogic) sendVerificationEmail(recipientEmail string, info *memberProto.UserInfo, verifyCode, nickname string) error { + tpl, err := l.svcCtx.NotificationRPC.GetStaticTemplate(l.ctx, ¬ificationProto.TemplateReq{ + Language: info.Language, + TemplateId: string(ntpl.BindingEmail), + }) + if err != nil { + return err + } + + tmpl, err := template.New("SendVerificationEmail").Parse(tpl.GetBody()) + if err != nil { + return err + } + + emailParams := EmailTmpInfo{Username: nickname, VerifyCode: verifyCode} + var buf bytes.Buffer + if err := tmpl.Execute(&buf, emailParams); err != nil { + return err + } + + _, err = l.svcCtx.NotificationRPC.SendMail(l.ctx, ¬ificationProto.SendMailReq{ + To: recipientEmail, + Subject: tpl.Title, + Body: buf.String(), + From: l.svcCtx.Config.MailSender, + }) + return err +} + +// sendVerificationCode 根據帳號類型發送驗證碼 +func (l *SendVerifyCodeLogic) sendVerificationCode(accountType, acc string, info *memberProto.UserInfo, verifyCode, nickname string) error { + switch member.GetAccountTypeByCode(accountType) { + case member.AccountTypePhone: + // TODO: 傳送簡訊 + fmt.Printf("SMS Template: %s\n", verifyCode) + case member.AccountTypeMail: + return l.sendVerificationEmail(acc, info, verifyCode, nickname) + default: + return errs.InvalidResourceState("unsupported account type") + } + return nil +} + +// setRedisKeyWithExpiry 設置 Redis 鍵與過期時間 +func (l *SendVerifyCodeLogic) setRedisKeyWithExpiry(rk, verifyCode string, expiry int) { + if status, err := l.svcCtx.Redis.SetnxExCtx(l.ctx, rk, verifyCode, expiry); err != nil || !status { + _ = errs.DatabaseErrorWithScopeL(code.CloudEPMember, domain.FailedToSetVerifyCodeErrorCode, + logx.WithContext(l.ctx), + []logx.LogField{ + {Key: "func", Value: "SendVerifyCodeLogic.setRedisKeyWithExpiry"}, + {Key: "redisKey", Value: rk}, + {Key: "error", Value: err.Error()}, + }, "failed to set redis expire").Wrap(err) + } +} diff --git a/internal/logic/member/update_password_logic.go b/internal/logic/member/update_password_logic.go new file mode 100644 index 0000000..2772e58 --- /dev/null +++ b/internal/logic/member/update_password_logic.go @@ -0,0 +1,92 @@ +package member + +import ( + "biz-member-gateway/internal/domain" + "context" + "fmt" + + "code.30cm.net/digimon/app-cloudep-member-server/pkg/domain/member" + ers "code.30cm.net/digimon/library-go/errors" + "code.30cm.net/digimon/library-go/errs" + memberProto "code.30cm.net/digimon/proto-all/pkg/member" + permissionProto "code.30cm.net/digimon/proto-all/pkg/permission" + + "biz-member-gateway/internal/svc" + "biz-member-gateway/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type UpdatePasswordLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewUpdatePasswordLogic 更新密碼(要發送驗證碼才可以的流程) +func NewUpdatePasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdatePasswordLogic { + return &UpdatePasswordLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdatePasswordLogic) UpdatePassword(req *types.UpdatePasswordReq) (*types.RespOK, error) { + // 驗證密碼,兩次密碼要一致 + if req.Token != req.TokenCheck { + return nil, errs.InvalidFormat("password confirmation does not match") + } + + // 驗證碼 + _, err := l.svcCtx.MemberRPC.VerifyRefreshCode(l.ctx, &memberProto.VerifyRefreshCodeReq{ + Account: req.Account, + CodeType: int32(member.GenerateCodeTypeForgetPassword), + VerifyCode: req.VerifyCode, + }) + + if err != nil { + // 表使沒有這驗證碼 -> 未授權 + return nil, errs.Unauthorized("failed to get verify code") + } + + info, err := l.svcCtx.MemberRPC.GetUserAccountInfo(l.ctx, &memberProto.GetUIDByAccountReq{Account: req.Account}) + if err != nil { + return nil, err + } + + if info.Data.Platform != member.Digimon.ToInt64() { + return nil, ers.InvalidRange("invalid platform") + } + + // 更新 + _, err = l.svcCtx.MemberRPC.UpdateUserToken(l.ctx, &memberProto.UpdateTokenReq{ + Account: req.Account, + Token: req.Token, + Platform: member.Digimon.ToInt64(), + }) + if err != nil { + return nil, err + } + + // 刪除驗證碼 + rk := domain.GetSendCodeRedisKey(fmt.Sprintf("%s:%s", req.Account, member.GenerateCodeTypeForgetPassword)) + _, err = l.svcCtx.Redis.Del(rk) + if err != nil { + return nil, ers.DBError("failed to get account data") + } + + // 登出舊 Token 如果有的話 + ac, err := l.svcCtx.MemberRPC.GetUIDByAccount(l.ctx, &memberProto.GetUIDByAccountReq{Account: req.Account}) + if err != nil { + return nil, err + } + + _, err = l.svcCtx.TokenRPC.CancelTokens(l.ctx, &permissionProto.DoTokenByUIDReq{Uid: ac.GetUid()}) + if err != nil { + return nil, err + } + + // 返回成功響應 + return &types.RespOK{}, nil +} diff --git a/internal/middleware/auth_middleware.go b/internal/middleware/auth_middleware.go new file mode 100644 index 0000000..3690cd9 --- /dev/null +++ b/internal/middleware/auth_middleware.go @@ -0,0 +1,84 @@ +package middleware + +import ( + "biz-member-gateway/internal/types" + "context" + "net/http" + + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/token" + ers "code.30cm.net/digimon/library-go/errors" + permissionRPC "code.30cm.net/digimon/proto-all/pkg/permission" + "github.com/zeromicro/go-zero/rest/httpx" +) + +type AuthMiddlewareParam struct { + TokenRPC permissionRPC.TokenServiceClient +} + +type AuthMiddleware struct { + TokenRPC permissionRPC.TokenServiceClient +} + +func NewAuthMiddleware(param AuthMiddlewareParam) *AuthMiddleware { + return &AuthMiddleware{TokenRPC: param.TokenRPC} +} + +func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // 解析 Header + header := types.VerifyHeader{} + if err := httpx.ParseHeaders(r, &header); err != nil { + m.writeErrorResponse(w, r, http.StatusBadRequest, "Failed to parse headers", int64(ers.InvalidFormat().FullCode())) + + return + } + + // 驗證 Token + claim, err := m.TokenRPC.GetSystemClaimByAccessToken(r.Context(), &permissionRPC.GetSystemClaimReq{ + AccessToken: header.Token, + IsExpired: true, + }) + if err != nil { + // 是否需要紀錄錯誤,是不是只要紀錄除了驗證失敗或過期之外的真錯誤 + m.writeErrorResponse(w, r, + http.StatusUnauthorized, "failed to verify toke", + int64(100400)) + + return + } + + // 驗證 Token 是否在黑名單中 + if _, err := m.TokenRPC.ValidationToken(r.Context(), &permissionRPC.ValidationTokenReq{Token: header.Token}); err != nil { + m.writeErrorResponse(w, r, http.StatusForbidden, + "failed to get toke", + int64(100400)) + + return + } + + // 設置 context 並傳遞給下一個處理器 + ctx := SetContext(r, claim.Data) + //nolint:contextcheck + next(w, r.WithContext(ctx)) + } +} + +func SetContext(r *http.Request, claim map[string]string) context.Context { + ctx := context.WithValue(r.Context(), token.Role.String(), claim[token.Role.String()]) + ctx = context.WithValue(ctx, token.UID.String(), claim[token.UID.String()]) + ctx = context.WithValue(ctx, token.Account.String(), claim[token.Account.String()]) + ctx = context.WithValue(ctx, token.Scope.String(), claim[token.Scope.String()]) + ctx = context.WithValue(ctx, token.Device.String(), claim[token.Device.String()]) + + return ctx +} + +// writeErrorResponse 用於處理錯誤回應 +func (m *AuthMiddleware) writeErrorResponse(w http.ResponseWriter, r *http.Request, statusCode int, message string, code int64) { + httpx.WriteJsonCtx(r.Context(), w, statusCode, types.BaseResponse{ + Status: types.Status{ + Code: code, + Message: message, + }, + }) +} diff --git a/internal/svc/pyroscope.go b/internal/svc/pyroscope.go new file mode 100644 index 0000000..f9e1d80 --- /dev/null +++ b/internal/svc/pyroscope.go @@ -0,0 +1,46 @@ +package svc + +import ( + "biz-member-gateway/internal/config" + "context" + "fmt" + "os" + + "github.com/grafana/pyroscope-go" + "github.com/zeromicro/go-zero/core/logx" +) + +func InitPyroScope(c config.Config) { + if c.PyroScope.Enable { + podName := os.Getenv("POD_NAME") + + _, err := pyroscope.Start(pyroscope.Config{ + ApplicationName: fmt.Sprintf("biz-%s-%s", c.Name, podName), + ServerAddress: c.PyroScope.URL, + Logger: logx.WithContext(context.Background()), + ProfileTypes: []pyroscope.ProfileType{ + pyroscope.ProfileCPU, + + pyroscope.ProfileInuseObjects, + pyroscope.ProfileAllocObjects, + pyroscope.ProfileInuseSpace, + pyroscope.ProfileAllocSpace, + + pyroscope.ProfileGoroutines, + + pyroscope.ProfileMutexCount, + pyroscope.ProfileMutexDuration, + + pyroscope.ProfileBlockCount, + pyroscope.ProfileBlockDuration, + }, + }) + if err != nil { + logx.WithCallerSkip(1).WithFields(logx.LogField{ + Key: "error", Value: err.Error(), + }).Error("failed to init pyroscope") + + panic(fmt.Sprintf("Pyroscope start err: %s", err.Error())) + } + } +} diff --git a/internal/svc/service_context.go b/internal/svc/service_context.go new file mode 100644 index 0000000..6eb826d --- /dev/null +++ b/internal/svc/service_context.go @@ -0,0 +1,64 @@ +package svc + +import ( + "biz-member-gateway/internal/config" + "biz-member-gateway/internal/middleware" + + "github.com/zeromicro/go-zero/core/limit" + "github.com/zeromicro/go-zero/core/stores/redis" + + "github.com/zeromicro/go-zero/rest" + "github.com/zeromicro/go-zero/zrpc" + + ers "code.30cm.net/digimon/library-go/errors" + "code.30cm.net/digimon/library-go/errors/code" + vi "code.30cm.net/digimon/library-go/validator" + memberRPC "code.30cm.net/digimon/proto-all/pkg/member" + notificationRPC "code.30cm.net/digimon/proto-all/pkg/notification" + permissionRPC "code.30cm.net/digimon/proto-all/pkg/permission" +) + +type ServiceContext struct { + Config config.Config + AuthMiddleware rest.Middleware + + MemberRPC memberRPC.AccountClient + TokenRPC permissionRPC.TokenServiceClient + NotificationRPC notificationRPC.SenderServiceClient + + // 但是底線就是 redis 其他db 絕對不行 + Redis *redis.Redis + + VerifyLimiter *limit.PeriodLimit + Validate vi.Validate +} + +func NewServiceContext(c config.Config) *ServiceContext { + ers.Scope = code.CloudEPPortalGW + tokenService := permissionRPC.NewTokenServiceClient(zrpc.MustNewClient(c.PermissionRPC).Conn()) + + InitPyroScope(c) + + // 啟動Redis + newRedis, err := redis.NewRedis(c.RedisCfg) + if err != nil { + panic(err) + } + + return &ServiceContext{ + Config: c, + AuthMiddleware: middleware.NewAuthMiddleware(middleware.AuthMiddlewareParam{ + TokenRPC: tokenService, + }).Handle, + MemberRPC: memberRPC.NewAccountClient(zrpc.MustNewClient(c.MemberRPC).Conn()), + TokenRPC: tokenService, + NotificationRPC: notificationRPC.NewSenderServiceClient(zrpc.MustNewClient(c.NotificationRPC).Conn()), + Redis: newRedis, + Validate: vi.MustValidator( + WithDecimalGt(), + WithDecimalGte(), + WithHTTPURL(), + WithRfc3339Format(), + ), + } +} diff --git a/internal/svc/validate.go b/internal/svc/validate.go new file mode 100755 index 0000000..cb8ff40 --- /dev/null +++ b/internal/svc/validate.go @@ -0,0 +1,83 @@ +package svc + +import ( + "regexp" + "time" + + vi "code.30cm.net/digimon/library-go/validator" + "github.com/go-playground/validator/v10" + "github.com/shopspring/decimal" +) + +// WithDecimalGt 是否大於等於 +func WithDecimalGt() vi.Option { + return vi.Option{ + ValidatorName: "decimalGt", + ValidatorFunc: func(fl validator.FieldLevel) bool { + if val, ok := fl.Field().Interface().(string); ok { + value, err := decimal.NewFromString(val) + if err != nil { + return false + } + + conditionValue, err := decimal.NewFromString(fl.Param()) + if err != nil { + return false + } + + return value.GreaterThan(conditionValue) + } + + return true + }, + } +} + +// WithDecimalGte 是否大於等於 +func WithDecimalGte() vi.Option { + return vi.Option{ + ValidatorName: "decimalGte", + ValidatorFunc: func(fl validator.FieldLevel) bool { + if val, ok := fl.Field().Interface().(string); ok { + value, err := decimal.NewFromString(val) + if err != nil { + return false + } + + conditionValue, err := decimal.NewFromString(fl.Param()) + if err != nil { + return false + } + + return value.GreaterThanOrEqual(conditionValue) + } + + return true + }, + } +} + +func WithHTTPURL() vi.Option { + return vi.Option{ + ValidatorName: "httpurl", + ValidatorFunc: func(fl validator.FieldLevel) bool { + value := fl.Field().String() + // 使用正則檢查是否以 http:// 或 https:// 開頭 + re := regexp.MustCompile(`^https?://`) + + return re.MatchString(value) + }, + } +} + +func WithRfc3339Format() vi.Option { + return vi.Option{ + ValidatorName: "rfc3339", + ValidatorFunc: func(fl validator.FieldLevel) bool { + // 使用正則檢查是否以 http:// 或 https:// 開頭 + _, err := time.Parse(time.RFC3339, fl.Field().String()) + + return err == nil + }, + } +} diff --git a/internal/types/types.go b/internal/types/types.go new file mode 100644 index 0000000..8f1b7f6 --- /dev/null +++ b/internal/types/types.go @@ -0,0 +1,127 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.8.1 + +package types + +type BaseResponse struct { + Status Status `json:"status"` // 狀態 + Data interface{} `json:"data"` // 資料 +} + +type BindingUserInfoReq struct { + VerifyHeader + PreferredLanguage string `json:"preferred_language,optional" validate:"oneof=zh-tw en-us"` // 使用語言 + Currency string `json:"currency,optional" validate:"oneof=TWD USD"` + AvatarURL string `json:"avatar_url,optional"` // 頭像 URL(可選) + Nickname string `json:"nickname,optional"` + FullName string `json:"full_name,optional"` // 用戶全名 + GenderCode string `json:"gender_code" validate:"oneof=secret male female"` // 性別代碼 + Birthday string `json:"birthday,optional" validate:"rfc3339"` // 生日 (格式: unix) + Address string `json:"address,optional"` // 地址 +} + +type CheckoutVerifyReq struct { + VerifyHeader + Account string `json:"account" validate:"required"` // 帳號名稱 + CodeType string `json:"code_type" validate:"oneof=email phone"` // 驗證碼類型 1 信箱 2 手機 + VerifyCode string `json:"verify_code" validate:"required,len=6"` // 驗證碼,長度為6 + UID string `json:"uid" validate:"required"` +} + +type CreateAccountRequest struct { + Account string `json:"account" validate:"required"` // 帳號名稱(line code 輸入在這邊) + Token string `json:"token" validate:"required"` // 密碼或平台token,密碼請 sha256 轉碼,如果三方token 請隨便給一個 sha256 字串 + TokenCheck string `json:"token_check" validate:"required"` // 密碼或平台token,token 請保持原樣,填在這邊,不用管 token + Platform string `json:"platform" validate:"oneof=platform google line"` // 平台名稱 (platform) 平台、google、line + AccountType string `json:"account_type" validate:"oneof=phone email platform"` // 帳號類型 phone(手機)、email(信箱)、platform(自定義帳號) -> (如果為第三方都寫 platform) + MemberLoginHeader +} + +type ForgetPasswordCodeReq struct { + Account string `json:"account" validate:"required"` // 帳號名稱 + AccountType string `json:"account_type" validate:"oneof=phone email"` // 帳號類型 (phone) 手機 (email) 信箱 +} + +type LoginReq struct { + Account string `json:"account" validate:"required"` // 帳號名稱 + Token string `json:"token"` // 密碼或平台token,密碼請 sha256 轉碼 + Platform string `json:"platform" validate:"oneof=platform google line"` // 平台名稱 platform, google + AccountType string `json:"account_type" validate:"oneof=phone email platform"` // 帳號類型 1 手機 2 信箱 3 自定義帳號 + MemberLoginHeader +} + +type LoginTokenResp struct { + UID string `json:"uid"` // Account + AccessToken string `json:"access_token"` // 訪問令牌 預設 5 分鐘過期 + RefreshToken string `json:"refresh_token"` // 刷新令牌 (預設一天過期,只能用一次),當呼叫更新token api 時,會自動把舊的失效,變成新的 refresh_token ,前端要記得過其實協助刷新,刷新不過表示全失效了(重新登入) + TokenType string `json:"token_type"` // Bearer +} + +type MemberLoginHeader struct { + DeviceID string `header:"device_id"` + IpAddress string `header:"ip_address"` + Brewser string `header:"brewser"` +} + +type ModifyPasswdReq struct { + VerifyHeader + NewToken string `json:"token" validate:"required,len=64"` // 密碼或平台token,密碼請 sha256 轉碼 + NewTokenCheck string `json:"token_check" validate:"required,len=64"` // 密碼或平台token,密碼請 sha256 轉碼 +} + +type PreVerifyForgetPasswdReq struct { + Identifier string `json:"identifier" validate:"required"` // 聯繫方式,可以是 email 或 phone + VerifyCode string `json:"verify_code" validate:"required,len=6"` // 驗證碼,長度為6 +} + +type RespOK struct { +} + +type Status struct { + Code int64 `json:"code"` // 狀態碼 + Message string `json:"message"` // 訊息 + Data interface{} `json:"data,omitempty"` // 可選的數據,當有返回時才出現 + Error interface{} `json:"error,omitempty"` // 可選的錯誤信息 +} + +type UpdatePasswordReq struct { + Account string `json:"account" validate:"required"` // 帳號名稱 + VerifyCode string `json:"verify_code" validate:"required,len=6"` // 驗證碼,長度為6 + Token string `json:"token" validate:"required,len=64"` // 密碼或平台token,密碼請 sha256 轉碼 + TokenCheck string `json:"token_check" validate:"required,len=64"` // 密碼或平台token,密碼請 sha256 轉碼 +} + +type UpdateTokenReq struct { + UID string `json:"uid" validate:"required"` // 誰要更新 + Token string `json:"token" validate:"required"` // access token -> 已過期要被更新的 + RefreshToken string `json:"refresh_token" validate:"required"` // refresh token -> 重點,要驗證他的 +} + +type UserInfo struct { + Platform string `json:"platform"` // 用戶平台 platform, google, line + UID string `json:"uid"` // 用戶 UID + AvatarURL string `json:"avatar_url"` // 頭像 URL(可選) + FullName string `json:"full_name"` // 用戶全名 + Nickname string `json:"nickname"` // 暱稱(可選) + GenderCode string `json:"gender_code"` // 性別代碼 mail, femail ,sec + Birthdate string `json:"birthdate"` // 生日 (格式: 19930417) + PhoneNumber string `json:"phone_number"` // 電話 + Address string `json:"address"` // 地址 + Email string `json:"email"` // 驗證後的信箱 + AlarmCategory string `json:"alarm_category"` // 告警狀態 + UserStatus string `json:"user_status"` // 用戶狀態 + PreferredLanguage string `json:"preferred_language"` // 使用語言 + Currency string `json:"currency"` // 使用幣種 + UpdateAt string `json:"update_at"` + CreateAt string `json:"create_at"` +} + +type VerificationCodeRequest struct { + VerifyHeader + Identifier string `json:"identifier" validate:"required"` // 聯繫方式,可以是 email 或 phone + CodeType string `json:"code_type" validate:"oneof=email phone forget_password"` // 驗證碼類型 +} + +type VerifyHeader struct { + Token string `header:"token" validate:"required"` +} diff --git a/internal/utils/time.go b/internal/utils/time.go new file mode 100644 index 0000000..2e6f387 --- /dev/null +++ b/internal/utils/time.go @@ -0,0 +1,17 @@ +package utils + +import "time" + +func UnixToRfc3339(t int64) string { + res := time.Unix(0, t).UTC() + + return res.Format(time.RFC3339) +} + +func Rfc3339ToUnix(rfc string) (int64, error) { + t, err := time.Parse(time.RFC3339, rfc) + if err != nil { + return 0, err + } + return t.UnixNano(), nil +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go new file mode 100644 index 0000000..22c890c --- /dev/null +++ b/internal/utils/utils.go @@ -0,0 +1,3 @@ +package utils + +func ToPointer[T any](v T) *T { return &v }