init project

This commit is contained in:
王性驊 2025-03-12 21:46:41 +08:00
commit 79b21c7264
48 changed files with 4675 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.idea/
etc/gateway.yaml
.DS_Store

135
.golangci.yaml Executable file
View File

@ -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

20
Makefile Normal file
View File

@ -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

43
etc/gateway.example.yaml Normal file
View File

@ -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

32
gateway.go Normal file
View File

@ -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()
}

785
gateway.json Normal file
View File

@ -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": " 密碼或平台tokentoken 請保持原樣,填在這邊,不用管 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"
}
}
}

21
generate/api/gateway.api Normal file
View File

@ -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"

252
generate/api/member.api Normal file
View File

@ -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"` // 密碼或平台tokentoken 請保持原樣,填在這邊,不用管 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)
}

126
go.mod Normal file
View File

@ -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
)

366
go.sum Normal file
View File

@ -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=

33
internal/config/config.go Normal file
View File

@ -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
}

9
internal/domain/const.go Normal file
View File

@ -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"

8
internal/domain/error.go Normal file
View File

@ -0,0 +1,8 @@
package domain
const (
APIErrorCode = 1 + iota
TokenNotTheSameAPIErrorCode
FailedToCheckVerifyCodeErrorCode
FailedToSetVerifyCodeErrorCode
)

39
internal/domain/gender.go Normal file
View File

@ -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
}

View File

@ -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 ""
}

23
internal/domain/redis.go Normal file
View File

@ -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()
}

View File

@ -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,
})
}
}
}

View File

@ -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,
})
}
}
}

View File

@ -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,
})
}
}
}

View File

@ -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,
})
}
}
}

View File

@ -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,
})
}
}
}

View File

@ -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,
})
}
}
}

View File

@ -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,
})
}
}
}

View File

@ -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,
})
}
}
}

View File

@ -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,
})
}
}
}

View File

@ -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,
})
}
}
}

View File

@ -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,
})
}
}
}

View File

@ -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,
})
}
}
}

105
internal/handler/routes.go Normal file
View File

@ -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),
)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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, &notificationProto.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, &notificationProto.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)
}
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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, &notificationProto.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, &notificationProto.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)
}
}

View File

@ -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
}

View File

@ -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,
},
})
}

46
internal/svc/pyroscope.go Normal file
View File

@ -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()))
}
}
}

View File

@ -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(),
),
}
}

83
internal/svc/validate.go Executable file
View File

@ -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
},
}
}

127
internal/types/types.go Normal file
View File

@ -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"` // 密碼或平台tokentoken 請保持原樣,填在這邊,不用管 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"`
}

17
internal/utils/time.go Normal file
View File

@ -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
}

3
internal/utils/utils.go Normal file
View File

@ -0,0 +1,3 @@
package utils
func ToPointer[T any](v T) *T { return &v }