feat: add document and redis into project

This commit is contained in:
王性驊 2025-10-02 14:43:57 +08:00
parent 4828386370
commit d435713e3b
62 changed files with 1828 additions and 3653 deletions

236
LINTING.md Normal file
View File

@ -0,0 +1,236 @@
# Go Linting 配置說明
本項目使用現代化的 Go linting 工具來確保代碼質量和風格一致性。
## 工具介紹
### golangci-lint
- **現代化的 Go linter 聚合工具**,整合了多個 linter
- 比傳統的 `golint` 更快、更全面
- 支持並行執行和緩存
- 配置文件:`.golangci.yml`
## 安裝
### 安裝 golangci-lint
```bash
# macOS
brew install golangci-lint
# Linux
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.55.2
# Windows
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2
```
### 安裝其他工具
```bash
# 格式化工具
go install mvdan.cc/gofumpt@latest
go install golang.org/x/tools/cmd/goimports@latest
```
## 使用方法
### Makefile 命令
```bash
# 基本代碼檢查
make lint
# 自動修復可修復的問題
make lint-fix
# 詳細輸出
make lint-verbose
# 只檢查新問題(與 main 分支比較)
make lint-new
# 格式化代碼
make fmt
```
### 直接使用 golangci-lint
```bash
# 基本檢查
golangci-lint run
# 自動修復
golangci-lint run --fix
# 檢查特定目錄
golangci-lint run ./pkg/...
# 詳細輸出
golangci-lint run -v
# 只顯示新問題
golangci-lint run --new-from-rev=main
```
## 配置說明
### 啟用的 Linters
我們的配置啟用了以下 linter 類別:
#### 核心檢查
- `errcheck`: 檢查未處理的錯誤
- `gosimple`: 簡化代碼建議
- `govet`: 檢查常見錯誤
- `staticcheck`: 靜態分析
- `typecheck`: 類型檢查
- `unused`: 檢查未使用的變量和函數
#### 代碼質量
- `cyclop`: 循環複雜度檢查
- `dupl`: 代碼重複檢測
- `funlen`: 函數長度檢查
- `gocognit`: 認知複雜度檢查
- `gocyclo`: 循環複雜度檢查
- `nestif`: 嵌套深度檢查
#### 格式化
- `gofmt`: 格式化檢查
- `gofumpt`: 更嚴格的格式化
- `goimports`: 導入排序
#### 命名和風格
- `goconst`: 常量檢查
- `gocritic`: 代碼評論
- `gomnd`: 魔術數字檢查
- `stylecheck`: 風格檢查
- `varnamelen`: 變量名長度檢查
#### 安全
- `gosec`: 安全檢查
#### 錯誤處理
- `errorlint`: 錯誤處理檢查
- `nilerr`: nil 錯誤檢查
- `wrapcheck`: 錯誤包裝檢查
### 配置文件結構
```yaml
# .golangci.yml
run:
timeout: 5m
skip-dirs: [vendor, .git, bin, build, dist, tmp]
skip-files: [".*\\.pb\\.go$", ".*\\.gen\\.go$"]
linters:
disable-all: true
enable: [errcheck, gosimple, govet, ...]
linters-settings:
# 各個 linter 的詳細配置
issues:
# 問題排除規則
exclude-rules:
- path: _test\.go
linters: [gomnd, funlen, dupl]
```
## IDE 整合
### VS Code
項目包含 `.vscode/settings.json` 配置:
- 自動使用 golangci-lint 進行檢查
- 保存時自動格式化
- 使用 gofumpt 作為格式化工具
### GoLand/IntelliJ
1. 安裝 golangci-lint 插件
2. 在設置中指向項目的 `.golangci.yml` 文件
## CI/CD 整合
### GitHub Actions
項目包含 `.github/workflows/ci.yml`
- 自動運行測試
- 執行 golangci-lint 檢查
- 安全掃描
- 依賴檢查
### 本地 Git Hooks
可以設置 pre-commit hook
```bash
#!/bin/sh
# .git/hooks/pre-commit
make lint
```
## 常見問題
### 1. 如何忽略特定的檢查?
在代碼中使用註釋:
```go
//nolint:gosec // 忽略安全檢查
password := "hardcoded"
//nolint:lll // 忽略行長度檢查
url := "https://very-long-url-that-exceeds-line-length-limit.com/api/v1/endpoint"
```
### 2. 如何為測試文件設置不同的規則?
配置文件中已經為測試文件設置了特殊規則:
```yaml
exclude-rules:
- path: _test\.go
linters: [gomnd, funlen, dupl, lll, goconst]
```
### 3. 如何調整複雜度閾值?
`.golangci.yml` 中調整:
```yaml
linters-settings:
cyclop:
max-complexity: 15 # 調整循環複雜度
funlen:
lines: 100 # 調整函數行數限制
statements: 50 # 調整語句數限制
```
### 4. 性能優化
- 使用緩存:`golangci-lint cache clean` 清理緩存
- 只檢查修改的文件:`--new-from-rev=main`
- 並行執行:默認已啟用
## 升級和維護
定期更新 golangci-lint
```bash
# 檢查版本
golangci-lint version
# 升級到最新版本
brew upgrade golangci-lint # macOS
# 或重新下載安裝腳本
```
定期檢查配置文件的新選項和 linter
```bash
# 查看所有可用的 linter
golangci-lint linters
# 查看配置幫助
golangci-lint config -h
```
## 參考資源
- [golangci-lint 官方文檔](https://golangci-lint.run/)
- [Go 代碼風格指南](https://github.com/golang/go/wiki/CodeReviewComments)
- [Effective Go](https://golang.org/doc/effective_go.html)

View File

@ -36,7 +36,6 @@ mock-gen: # 建立 mock 資料
fmt: # 格式優化 fmt: # 格式優化
$(GOFMT) -w $(GOFILES) $(GOFMT) -w $(GOFILES)
goimports -w ./ goimports -w ./
golangci-lint run
.PHONY: build .PHONY: build
build: # 編譯專案 build: # 編譯專案
@ -63,17 +62,13 @@ install: # 安裝依賴
go mod tidy go mod tidy
go mod download go mod download
.PHONY: lint
lint: # 代碼檢查
golangci-lint run
.PHONY: help .PHONY: help
help: # 顯示幫助信息 help: # 顯示幫助信息
@echo "Available commands:" @echo "Available commands:"
@echo " test - 運行測試" @echo " test - 運行測試"
@echo " gen-api - 產生 api" @echo " gen-api - 產生 api"
@echo " gen-swagger - 生成 JSON 格式 Swagger 文檔" @echo " gen-doc - 生成 Swagger 文檔"
@echo " gen-swagger-yaml - 生成 YAML 格式 Swagger 文檔" @echo " mock-gen - 建立 mock 資料"
@echo " fmt - 格式化代碼" @echo " fmt - 格式化代碼"
@echo " build - 編譯專案" @echo " build - 編譯專案"
@echo " run - 運行專案" @echo " run - 運行專案"
@ -81,6 +76,5 @@ help: # 顯示幫助信息
@echo " docker-build - 構建 Docker 映像" @echo " docker-build - 構建 Docker 映像"
@echo " docker-run - 運行 Docker 容器" @echo " docker-run - 運行 Docker 容器"
@echo " install - 安裝依賴" @echo " install - 安裝依賴"
@echo " lint - 代碼檢查"
@echo " help - 顯示幫助信息" @echo " help - 顯示幫助信息"

BIN
bin/gateway Executable file

Binary file not shown.

View File

@ -0,0 +1,31 @@
services:
mongo:
image: mongo:8.0
container_name: mongo
restart: always
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
etcd:
image: quay.io/coreos/etcd:v3.5.5
container_name: etcd
restart: always
command: >
/usr/local/bin/etcd
--data-dir=/etcd-data
--name=etcd
--listen-client-urls=http://0.0.0.0:2379
--advertise-client-urls=http://etcd:2379
ports:
- "2379:2379"
- "2380:2380"
redis:
image: redis:7.0
container_name: redis
restart: always
ports:
- "6379:6379"

View File

@ -1,3 +1,44 @@
Name: gateway Name: gateway
Host: 0.0.0.0 Host: 0.0.0.0
Port: 8888 Port: 8888
Cache:
- Host: 127.0.0.1:6379
type: node
CacheExpireTime: 1s
CacheWithNotFoundExpiry: 1s
RedisConf:
Host: 127.0.0.1:6379
Type: node
Pass: ""
Tls: false
Mongo:
Schema: mongodb
Host: "127.0.0.1:27017"
User: "root"
Password: "example"
Port: ""
Database: digimon_member
ReplicaName: "rs0"
MaxStaleness: 30m
MaxPoolSize: 30
MinPoolSize: 10
MaxConnIdleTime: 30m
Compressors:
- f
EnableStandardReadWriteSplitMode: true
ConnectTimeoutMs : 300
Bcrypt:
Cost: 10
GoogleAuth:
ClientID: xxx.apps.googleusercontent.com
AuthURL: x
LineAuth:
ClientID : "200000000"
ClientSecret : xxxxx
RedirectURI : http://localhost:8080/line.html

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@ syntax = "v1"
// ================ 通用響應 ================ // ================ 通用響應 ================
type ( type (
// 成功響應 // 成功響應
OKResp { RespOK {
Code int `json:"code"` Code int `json:"code"`
Msg string `json:"msg"` Msg string `json:"msg"`
Data interface{} `json:"data,omitempty"` Data interface{} `json:"data,omitempty"`
@ -26,8 +26,7 @@ type (
BaseReq {} BaseReq {}
VerifyHeader { Authorization {
Token string `header:"token" validate:"required"` Authorization string `header:"Authorization" validate:"required"`
} }
) )

View File

@ -1,554 +1,307 @@
syntax = "v1" syntax = "v1"
// ================ 請求/響應結構 ================
// =================================================================
// Type: 授權與驗證 (Auth)
// =================================================================
type ( type (
// 創建帳號請求 // CredentialsPayload 傳統帳號密碼註冊的資料
CreateUserAccountReq { CredentialsPayload {
LoginID string `json:"login_id" validate:"required,min=3,max=50"` Password string `json:"password" validate:"required,min=8,max=128"` // 密碼 (後端應使用 bcrypt 進行雜湊)
Platform string `json:"platform" validate:"required,oneof=platform google line apple"` PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password"` // 確認密碼
Token string `json:"token" validate:"required,min=8,max=128"`
} }
// 綁定用戶請求 // PlatformPayload 第三方平台註冊的資料
BindingUserReq { PlatformPayload {
UID string `json:"uid" validate:"required,min=1,max=50"` Provider string `json:"provider" validate:"required,oneof=google line apple"` // 平台名稱
LoginID string `json:"login_id" validate:"required,min=3,max=50"` Token string `json:"token" validate:"required"` // 平台提供的 Access Token 或 ID Token
Type int `json:"type" validate:"required,oneof=1 2 3"`
} }
// 綁定用戶響應 // RegisterReq 註冊請求 (整合了兩種方式)
BindingUserResp { LoginReq {
UID string `json:"uid"` AuthMethod string `json:"auth_method" validate:"required,oneof=credentials platform"`
LoginID string `json:"login_id"` LoginID string `json:"login_id" validate:"required,min=3,max=50"` // 信箱或手機號碼
Type int `json:"type"` Credentials *CredentialsPayload `json:"credentials,optional"` // AuthMethod 為 'credentials' 時使用
Platform *PlatformPayload `json:"platform,optional"` // AuthMethod 為 'platform' 時使用
} }
// 創建用戶資料請求 // LoginResp 登入/註冊成功後的響應
CreateUserInfoReq { LoginResp {
UID string `json:"uid" validate:"required,min=1,max=50"` UID string `json:"uid"`
AlarmType int `json:"alarm_type" validate:"required,oneof=0 1 2"` AccessToken string `json:"access_token"`
Status int `json:"status" validate:"required,oneof=0 1 2 3"` RefreshToken string `json:"refresh_token"`
Language string `json:"language" validate:"required,min=2,max=10"` TokenType string `json:"token_type"` // 通常固定為 "Bearer"
Currency string `json:"currency" validate:"required,min=3,max=3"` }
Avatar *string `json:"avatar,omitempty"` // --- 密碼重設流程 ---
NickName *string `json:"nick_name,omitempty" validate:"omitempty,min=1,max=50"`
FullName *string `json:"full_name,omitempty" validate:"omitempty,min=1,max=100"` // RequestPasswordResetReq 請求發送「忘記密碼」的驗證碼
Gender *int64 `json:"gender,omitempty" validate:"omitempty,oneof=0 1 2"` RequestPasswordResetReq {
Birthdate *int64 `json:"birthdate,omitempty" validate:"omitempty,min=19000101,max=21001231"` Identifier string `json:"identifier" validate:"required,email|phone"` // 使用者帳號 (信箱或手機)
PhoneNumber *string `json:"phone_number,omitempty" validate:"omitempty,min=10,max=20"` AccountType string `json:"account_type" validate:"required,oneof=email phone"`
Email *string `json:"email,omitempty" validate:"omitempty,email"`
Address *string `json:"address,omitempty" validate:"omitempty,max=200"`
} }
// 獲取帳號資訊響應 // VerifyCodeReq 驗證碼校驗 (通用)
GetAccountInfoResp { VerifyCodeReq {
Data CreateUserAccountReq `json:"data"` Identifier string `json:"identifier" validate:"required"`
VerifyCode string `json:"verify_code" validate:"required,len=6"`
} }
// 更新用戶資料請求 // ResetPasswordReq 使用已驗證的 Code 來重設密碼 只有用帳號密碼的才能發送重設密碼
UpdateUserInfoReq { ResetPasswordReq {
UID string `json:"uid" validate:"required,min=1,max=50"` Identifier string `json:"identifier" validate:"required"`
Language *string `json:"language,omitempty" validate:"omitempty,min=2,max=10"` VerifyCode string `json:"verify_code" validate:"required"` // 來自上一步驗證通過的 Code作為一種「票證」
Currency *string `json:"currency,omitempty" validate:"omitempty,min=3,max=3"` Password string `json:"password" validate:"required,min=8,max=128"` // 新密碼
NickName *string `json:"nick_name,omitempty" validate:"omitempty,min=1,max=50"` PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password"` // 確認新密碼
Avatar *string `json:"avatar,omitempty"`
AlarmType *int `json:"alarm_type,omitempty" validate:"omitempty,oneof=0 1 2"`
Status *int `json:"status,omitempty" validate:"omitempty,oneof=0 1 2 3"`
FullName *string `json:"full_name,omitempty" validate:"omitempty,min=1,max=100"`
Gender *int64 `json:"gender,omitempty" validate:"omitempty,oneof=0 1 2"`
Birthdate *int64 `json:"birthdate,omitempty" validate:"omitempty,min=19000101,max=21001231"`
Address *string `json:"address,omitempty" validate:"omitempty,max=200"`
} }
// 獲取 UID 請求 // --- 4. 權杖刷新 ---
GetUIDByAccountReq { // RefreshTokenReq 更新 AccessToken
Account string `json:"account" validate:"required,min=3,max=50"` RefreshTokenReq {
RefreshToken string `json:"refresh_token" validate:"required"`
} }
// 獲取 UID 響應 // RefreshTokenResp 刷新權杖後的響應
GetUIDByAccountResp { RefreshTokenResp {
UID string `json:"uid"` AccessToken string `json:"access_token"`
Account string `json:"account"` RefreshToken string `json:"refresh_token"` // 可選:某些策略下刷新後也會換發新的 Refresh Token
} TokenType string `json:"token_type"`
// 更新密碼請求
UpdateTokenReq {
Account string `json:"account" validate:"required,min=3,max=50"`
Token string `json:"token" validate:"required,min=8,max=128"`
Platform int `json:"platform" validate:"required,oneof=1 2 3 4"`
}
// 生成驗證碼請求
GenerateRefreshCodeReq {
Account string `json:"account" validate:"required,min=3,max=50"`
CodeType int `json:"code_type" validate:"required,oneof=1 2 3"`
}
// 驗證碼響應
VerifyCodeResp {
VerifyCode string `json:"verify_code"`
}
// 生成驗證碼響應
GenerateRefreshCodeResp {
Data VerifyCodeResp `json:"data"`
}
// 驗證碼請求
VerifyRefreshCodeReq {
Account string `json:"account" validate:"required,min=3,max=50"`
CodeType int `json:"code_type" validate:"required,oneof=1 2 3"`
VerifyCode string `json:"verify_code" validate:"required,min=4,max=10"`
}
// 更新狀態請求
UpdateStatusReq {
UID string `json:"uid" validate:"required,min=1,max=50"`
Status int `json:"status" validate:"required,oneof=0 1 2 3"`
}
// 獲取用戶資訊請求
GetUserInfoReq {
UID string `json:"uid,omitempty" validate:"omitempty,min=1,max=50"`
NickName *string `json:"nick_name,omitempty" validate:"omitempty,min=1,max=50"`
}
// 用戶資訊
UserInfo {
UID string `json:"uid"`
AvatarURL *string `json:"avatar_url,omitempty"`
FullName *string `json:"full_name,omitempty"`
NickName *string `json:"nick_name,omitempty"`
GenderCode *int64 `json:"gender_code,omitempty"`
Birthday *int64 `json:"birthday,omitempty"`
Phone *string `json:"phone,omitempty"`
Email *string `json:"email,omitempty"`
Address *string `json:"address,omitempty"`
AlarmType int `json:"alarm_type"`
Status int `json:"status"`
Language string `json:"language"`
Currency string `json:"currency"`
CreateTime int64 `json:"create_time"`
UpdateTime int64 `json:"update_time"`
}
// 獲取用戶資訊響應
GetUserInfoResp {
Data UserInfo `json:"data"`
}
// 用戶列表請求
ListUserInfoReq {
AlarmType *int `json:"alarm_type,omitempty" validate:"omitempty,oneof=0 1 2"`
Status *int `json:"status,omitempty" validate:"omitempty,oneof=0 1 2 3"`
CreateStartTime *int64 `json:"create_start_time,omitempty"`
CreateEndTime *int64 `json:"create_end_time,omitempty"`
PageSize int `json:"page_size" validate:"required,min=1,max=100"`
PageIndex int `json:"page_index" validate:"required,min=1"`
}
// 用戶列表響應
ListUserInfoResp {
Data []UserInfo `json:"data"`
Pager PagerResp `json:"pager"`
}
// 驗證認證結果請求
VerifyAuthResultReq {
Token string `json:"token" validate:"required,min=1"`
Account *string `json:"account,omitempty" validate:"omitempty,min=3,max=50"`
}
// 驗證認證結果響應
VerifyAuthResultResp {
Status bool `json:"status"`
}
// Google 認證結果響應
VerifyGoogleAuthResultResp {
Status bool `json:"status"`
Iss *string `json:"iss,omitempty"`
Sub *string `json:"sub,omitempty"`
Aud *string `json:"aud,omitempty"`
Exp *string `json:"exp,omitempty"`
Iat *string `json:"iat,omitempty"`
Email *string `json:"email,omitempty"`
EmailVerified *string `json:"email_verified,omitempty"`
Name *string `json:"name,omitempty"`
Picture *string `json:"picture,omitempty"`
}
// 綁定驗證 Email 請求
BindVerifyEmailReq {
UID string `json:"uid" validate:"required,min=1,max=50"`
Email string `json:"email" validate:"required,email"`
}
// 綁定驗證 Phone 請求
BindVerifyPhoneReq {
UID string `json:"uid" validate:"required,min=1,max=50"`
Phone string `json:"phone" validate:"required,min=10,max=20"`
}
// LINE 獲取 Token 請求
LineGetTokenReq {
Code string `json:"code" validate:"required,min=1"`
}
// LINE Access Token 響應
LineAccessTokenResp {
Token string `json:"token"`
}
// LINE 用戶資料
LineUserProfile {
DisplayName string `json:"display_name"`
UserID string `json:"user_id"`
PictureURL string `json:"picture_url"`
StatusMessage string `json:"status_message"`
}
// LINE 獲取用戶資訊請求
LineGetUserInfoReq {
Token string `json:"token" validate:"required,min=1"`
} }
) )
// ================ API 路由 ================ // =================================================================
// Type: 使用者資訊與管理 (User)
// =================================================================
type (
// --- 1. 會員資訊 ---
// UserInfoResp 用於獲取會員資訊的標準響應結構
UserInfoResp {
Platform string `json:"platform"` // 註冊平台
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"` // 性別代碼
Birthdate string `json:"birthdate"` // 生日 (格式: 1993-04-17)
PhoneNumber string `json:"phone_number"` // 電話
IsPhoneVerified bool `json:"is_phone_verified"` // 手機是否已驗證
Email string `json:"email"` // 信箱
IsEmailVerified bool `json:"is_email_verified"` // 信箱是否已驗證
Address string `json:"address"` // 地址
UserStatus string `json:"user_status"` // 用戶狀態
PreferredLanguage string `json:"preferred_language"` // 偏好語言
Currency string `json:"currency"` // 偏好幣種
National string `json:"national"` // 國家
PostCode string `json:"post_code"` // 郵遞區號
Carrier string `json:"carrier"` // 載具
Role string `json:"role"` // 角色
UpdateAt string `json:"update_at"`
CreateAt string `json:"create_at"`
Authorization
}
// UpdateUserInfoReq 更新會員資訊的請求結構
UpdateUserInfoReq {
AvatarURL *string `json:"avatar_url,optional"` // 頭像 URL
FullName *string `json:"full_name,optional"` // 用戶全名
Nickname *string `json:"nickname,optional"` // 暱稱
GenderCode *string `json:"gender_code,optional" validate:"omitempty,oneof=secret male female"` // 性別
Birthdate *string `json:"birthdate,optional"` // 生日 (格式: 1993-04-17)
Address *string `json:"address,optional"` // 地址
PreferredLanguage *string `json:"preferred_language,optional" validate:"omitempty,oneof=zh-tw en-us"` // 語言
Currency *string `json:"currency,optional" validate:"omitempty,oneof=TWD USD"` // 貨幣代號
National *string `json:"national,optional"` // 國家
PostCode *string `json:"post_code,optional"` // 郵遞區號
Carrier *string `json:"carrier,optional"` // 載具
}
// --- 2. 修改密碼 (已登入狀態) ---
// UpdatePasswordReq 修改密碼的請求
UpdatePasswordReq {
CurrentPassword string `json:"current_password" validate:"required"`
NewPassword string `json:"new_password" validate:"required,min=8,max=128"`
NewPasswordConfirm string `json:"new_password_confirm" validate:"eqfield=NewPassword"`
Authorization
}
// --- 3. 通用驗證碼 (已登入狀態) ---
// RequestVerificationCodeReq 請求發送驗證碼
RequestVerificationCodeReq {
// 驗證目的:'email_verification' 或 'phone_verification'
Purpose string `json:"purpose" validate:"required,oneof=email_verification phone_verification"`
Authorization
}
// SubmitVerificationCodeReq 提交驗證碼以完成驗證
SubmitVerificationCodeReq {
Purpose string `json:"purpose" validate:"required,oneof=email_verification phone_verification"`
VerifyCode string `json:"verify_code" validate:"required,len=6"`
Authorization
}
)
// =================================================================
// Service: 公開 API - 無需登入 (Auth Service)
// =================================================================
@server( @server(
group: account group: auth
prefix: /api/v1/account prefix: /api/v1/auth
schemes: https
timeout: 10s
) )
service gateway { service gateway {
@doc( @doc(
summary: "創建帳號" summary: "註冊新帳號"
description: "創建新的帳號,支援多平台登入" description: "使用傳統帳號密碼或第三方平台進行註冊。成功後直接返回登入後的 Token 資訊。"
) )
/* /*
@respdoc-201 () // 創建成功 @respdoc-200 (LoginResp) // 註冊成功,並返回 Token
@respdoc-400 ( @respdoc-400 (ErrorResp) "請求參數格式錯誤"
40001: (ErrorResp) 帳號格式錯誤 @respdoc-409 (ErrorResp) "帳號已被註冊" // 409 Conflict: 資源衝突
40002: (ErrorResp) 密碼強度不足 @respdoc-500 (ErrorResp) // 伺服器內部錯誤
40003: (ErrorResp) 平台類型無效
) // 客戶端錯誤
@respdoc-409 (ErrorResp) // 帳號已存在
@respdoc-500 (ErrorResp) // 服務器錯誤
*/ */
@handler CreateUserAccount @handler register
post /create (CreateUserAccountReq) returns () post /register (LoginReq) returns (LoginResp)
@doc( @doc(
summary: "獲取帳號資訊" summary: "使用者登入"
description: "根據帳號獲取用戶的帳號資訊" description: "使用傳統帳號密碼或第三方平台 Token 進行登入,以創建一個新的會話(Session)。"
) )
/* /*
@respdoc-200 (GetAccountInfoResp) // 獲取成功 @respdoc-200 (LoginResp) // 登入成功
@respdoc-400 ( @respdoc-400 (ErrorResp) "請求參數格式錯誤"
40001: (ErrorResp) 帳號格式錯誤 @respdoc-401 (ErrorResp) "帳號或密碼錯誤 / 無效的平台 Token" // 401 Unauthorized
40004: (ErrorResp) 參數驗證失敗 @respdoc-500 (ErrorResp) // 伺服器內部錯誤
) // 客戶端錯誤 */
@respdoc-404 (ErrorResp) // 帳號不存在 @handler login
@respdoc-500 (ErrorResp) // 服務器錯誤 post /sessions (LoginReq) returns (LoginResp)
*/
@handler GetUserAccountInfo
post /info (GetUIDByAccountReq) returns (GetAccountInfoResp)
@doc( @doc(
summary: "更新用戶密碼" summary: "刷新 Access Token"
description: "更新指定帳號的密碼" description: "使用有效的 Refresh Token 來獲取一組新的 Access Token 和 Refresh Token。"
) )
/* /*
@respdoc-200 (OKResp) // 更新成功 @respdoc-200 (RefreshTokenResp) // 刷新成功
@respdoc-400 ( @respdoc-400 (ErrorResp) "請求參數格式錯誤"
40001: (ErrorResp) 帳號格式錯誤 @respdoc-401 (ErrorResp) "無效或已過期的 Refresh Token" // 401 Unauthorized
40002: (ErrorResp) 密碼強度不足 @respdoc-500 (ErrorResp) // 伺服器內部錯誤
40003: (ErrorResp) 平台類型無效 */
) // 客戶端錯誤 @handler refreshToken
@respdoc-404 (ErrorResp) // 帳號不存在 post /sessions/refresh (RefreshTokenReq) returns (RefreshTokenResp)
@respdoc-500 (ErrorResp) // 服務器錯誤
*/
@handler UpdateUserToken
post /update-token (UpdateTokenReq) returns (OKResp)
@doc( @doc(
summary: "獲取用戶 UID" summary: "請求發送密碼重設驗證碼"
description: "根據帳號獲取對應的用戶 UID" description: "為指定的 email 或 phone 發送一個一次性的密碼重設驗證碼。"
) )
/* /*
@respdoc-200 (GetUIDByAccountResp) // 獲取成功 @respdoc-200 (RespOK) // 請求成功 (為安全起見,即使帳號不存在也應返回成功)
@respdoc-400 ( @respdoc-400 (ErrorResp) "請求參數格式錯誤"
40001: (ErrorResp) 帳號格式錯誤 @respdoc-429 (ErrorResp) "請求過於頻繁" // 429 Too Many Requests
40004: (ErrorResp) 參數驗證失敗 @respdoc-500 (ErrorResp) // 伺服器內部錯誤
) // 客戶端錯誤 */
@respdoc-404 (ErrorResp) // 帳號不存在 @handler requestPasswordReset
@respdoc-500 (ErrorResp) // 服務器錯誤 post /password-resets/request (RequestPasswordResetReq) returns (RespOK)
*/
@handler GetUIDByAccount
post /uid (GetUIDByAccountReq) returns (GetUIDByAccountResp)
@doc( @doc(
summary: "綁定帳號" summary: "校驗密碼重設驗證碼"
description: "將帳號綁定到指定的用戶 UID" description: "在實際重設密碼前,先驗證使用者輸入的驗證碼是否正確。"
) )
/* /*
@respdoc-200 (BindingUserResp) // 綁定成功 @respdoc-200 (RespOK) // 驗證碼正確
@respdoc-400 ( @respdoc-400 (ErrorResp) "驗證碼無效或已過期"
40001: (ErrorResp) 帳號格式錯誤 @respdoc-500 (ErrorResp) // 伺服器內部錯誤
40004: (ErrorResp) 參數驗證失敗 */
40005: (ErrorResp) UID 格式錯誤 @handler verifyPasswordResetCode
) // 客戶端錯誤 post /password-resets/verify (VerifyCodeReq) returns (RespOK)
@respdoc-404 (ErrorResp) // 帳號不存在
@respdoc-409 (ErrorResp) // 帳號已綁定
@respdoc-500 (ErrorResp) // 服務器錯誤
*/
@handler BindAccount
post /bind (BindingUserReq) returns (BindingUserResp)
@doc( @doc(
summary: "綁定用戶資料" summary: "執行密碼重設"
description: "初次綁定用戶的詳細資料" description: "使用有效的驗證碼來設定新的密碼。"
) )
/* /*
@respdoc-200 (OKResp) // 綁定成功 @respdoc-200 (RespOK) // 密碼重設成功
@respdoc-400 ( @respdoc-400 (ErrorResp) "驗證碼無效或請求參數錯誤"
40004: (ErrorResp) 參數驗證失敗 @respdoc-500 (ErrorResp) // 伺服器內部錯誤
40005: (ErrorResp) UID 格式錯誤 */
40006: (ErrorResp) 郵箱格式錯誤 @handler resetPassword
40007: (ErrorResp) 電話格式錯誤 put /password-resets (ResetPasswordReq) returns (RespOK)
) // 客戶端錯誤 }
@respdoc-404 (ErrorResp) // 用戶不存在
@respdoc-409 (ErrorResp) // 用戶資料已存在 // =================================================================
@respdoc-422 (ErrorResp) // 用戶資料不完整 // Service: 授權 API - 需要登入 (User Service)
@respdoc-500 (ErrorResp) // 服務器錯誤 // =================================================================
*/ @server(
@handler BindUserInfo group: user
post /bind-info (CreateUserInfoReq) returns (OKResp) prefix: /api/v1/user
schemes: https
@doc( timeout: 10s
summary: "綁定驗證 Email" middleware: AuthMiddleware // 所有此 group 的路由都需要經過 JWT 驗證
description: "綁定並驗證用戶的 Email 地址" )
) service gateway {
/* @doc(
@respdoc-200 (OKResp) // 綁定成功 summary: "取得當前登入的會員資訊(自己)"
@respdoc-400 ( )
40005: (ErrorResp) UID 格式錯誤 /*
40006: (ErrorResp) 郵箱格式錯誤 @respdoc-200 (UserInfoResp) // 成功獲取
) // 客戶端錯誤 @respdoc-401 (ErrorResp) "未授權或 Token 無效"
@respdoc-404 (ErrorResp) // 用戶不存在 @respdoc-404 (ErrorResp) "找不到使用者"
@respdoc-409 (ErrorResp) // 郵箱已綁定 @respdoc-500 (ErrorResp) // 伺服器內部錯誤
@respdoc-422 (ErrorResp) // 郵箱未驗證 */
@respdoc-500 (ErrorResp) // 服務器錯誤 @handler getUserInfo
*/ get /me (Authorization) returns (UserInfoResp)
@handler BindVerifyEmail
post /bind-email (BindVerifyEmailReq) returns (OKResp) @doc(
summary: "更新當前登入的會員資訊"
@doc( description: "只更新傳入的欄位,未傳入的欄位將保持不變。"
summary: "綁定驗證 Phone" )
description: "綁定並驗證用戶的電話號碼" /*
) @respdoc-200 (UserInfoResp) // 更新成功,並返回更新後的使用者資訊
/* @respdoc-400 (ErrorResp) "請求參數格式錯誤"
@respdoc-200 (OKResp) // 綁定成功 @respdoc-401 (ErrorResp) "未授權或 Token 無效"
@respdoc-400 ( @respdoc-500 (ErrorResp) // 伺服器內部錯誤
40005: (ErrorResp) UID 格式錯誤 */
40007: (ErrorResp) 電話格式錯誤 @handler updateUserInfo
) // 客戶端錯誤 put /me (UpdateUserInfoReq) returns (UserInfoResp)
@respdoc-404 (ErrorResp) // 用戶不存在
@respdoc-409 (ErrorResp) // 電話已綁定 @doc(
@respdoc-422 (ErrorResp) // 電話未驗證 summary: "修改當前登入使用者的密碼"
@respdoc-500 (ErrorResp) // 服務器錯誤 description: "必須提供當前密碼以進行驗證。"
*/ )
@handler BindVerifyPhone /*
post /bind-phone (BindVerifyPhoneReq) returns (OKResp) @respdoc-200 (RespOK) // 密碼修改成功
@respdoc-400 (ErrorResp) "請求參數格式錯誤或新舊密碼不符"
@doc( @respdoc-401 (ErrorResp) "未授權或 Token 無效"
summary: "更新用戶資料" @respdoc-403 (ErrorResp) "當前密碼不正確" // 403 Forbidden
description: "更新用戶的個人資料資訊" @respdoc-500 (ErrorResp) // 伺服器內部錯誤
) */
/* @handler updatePassword
@respdoc-200 (OKResp) // 更新成功 put /me/password (UpdatePasswordReq) returns (RespOK)
@respdoc-400 (
40004: (ErrorResp) 參數驗證失敗 @doc(
40005: (ErrorResp) UID 格式錯誤 summary: "請求發送驗證碼 (用於驗證信箱/手機)"
40006: (ErrorResp) 郵箱格式錯誤 description: "根據傳入的 `purpose` 發送對應的驗證碼。"
40007: (ErrorResp) 電話格式錯誤 )
) // 客戶端錯誤 /*
@respdoc-404 (ErrorResp) // 用戶不存在 @respdoc-200 (RespOK) // 請求已受理
@respdoc-422 (ErrorResp) // 用戶資料無效 @respdoc-400 (ErrorResp) "請求參數格式錯誤"
@respdoc-500 (ErrorResp) // 服務器錯誤 @respdoc-401 (ErrorResp) "未授權或 Token 無效"
*/ @respdoc-429 (ErrorResp) "請求過於頻繁" // 429 Too Many Requests
@handler UpdateUserInfo @respdoc-500 (ErrorResp) // 伺服器內部錯誤
post /update-info (UpdateUserInfoReq) returns (OKResp) */
@handler requestVerificationCode
@doc( post /me/verifications (RequestVerificationCodeReq) returns (RespOK)
summary: "更新用戶狀態"
description: "更新用戶的帳號狀態" @doc(
) summary: "提交驗證碼以完成驗證"
/* description: "提交收到的驗證碼,以完成特定目的的驗證,例如綁定手機或 Email。"
@respdoc-200 (OKResp) // 更新成功 )
@respdoc-400 ( /*
40004: (ErrorResp) 參數驗證失敗 @respdoc-200 (RespOK) // 驗證成功
40005: (ErrorResp) UID 格式錯誤 @respdoc-400 (ErrorResp) "驗證碼無效或已過期"
40008: (ErrorResp) 狀態值無效 @respdoc-401 (ErrorResp) "未授權或 Token 無效"
) // 客戶端錯誤 @respdoc-500 (ErrorResp) // 伺服器內部錯誤
@respdoc-404 (ErrorResp) // 用戶不存在 */
@respdoc-422 (ErrorResp) // 狀態更新無效 @handler submitVerificationCode
@respdoc-500 (ErrorResp) // 服務器錯誤 put /me/verifications (SubmitVerificationCodeReq) returns (RespOK)
*/
@handler UpdateStatus
post /update-status (UpdateStatusReq) returns (OKResp)
@doc(
summary: "獲取用戶資訊"
description: "根據 UID 或暱稱獲取用戶詳細資訊"
)
/*
@respdoc-200 (GetUserInfoResp) // 獲取成功
@respdoc-400 (
40004: (ErrorResp) 參數驗證失敗
40005: (ErrorResp) UID 格式錯誤
) // 客戶端錯誤
@respdoc-404 (ErrorResp) // 用戶不存在
@respdoc-500 (ErrorResp) // 服務器錯誤
*/
@handler GetUserInfo
post /user-info (GetUserInfoReq) returns (GetUserInfoResp)
@doc(
summary: "獲取用戶列表"
description: "分頁獲取用戶列表,支援篩選條件"
)
/*
@respdoc-200 (ListUserInfoResp) // 獲取成功
@respdoc-400 (
40004: (ErrorResp) 參數驗證失敗
40009: (ErrorResp) 分頁參數無效
) // 客戶端錯誤
@respdoc-500 (ErrorResp) // 服務器錯誤
*/
@handler ListMember
post /list (ListUserInfoReq) returns (ListUserInfoResp)
@doc(
summary: "生成驗證碼"
description: "為指定帳號生成驗證碼,用於忘記密碼等功能"
)
/*
@respdoc-200 (GenerateRefreshCodeResp) // 生成成功
@respdoc-400 (
40001: (ErrorResp) 帳號格式錯誤
40004: (ErrorResp) 參數驗證失敗
40010: (ErrorResp) 驗證碼類型無效
) // 客戶端錯誤
@respdoc-404 (ErrorResp) // 帳號不存在
@respdoc-429 (ErrorResp) // 請求過於頻繁
@respdoc-500 (ErrorResp) // 服務器錯誤
*/
@handler GenerateRefreshCode
post /generate-code (GenerateRefreshCodeReq) returns (GenerateRefreshCodeResp)
@doc(
summary: "驗證驗證碼"
description: "驗證並使用驗證碼,驗證後會刪除驗證碼"
)
/*
@respdoc-200 (OKResp) // 驗證成功
@respdoc-400 (
40001: (ErrorResp) 帳號格式錯誤
40004: (ErrorResp) 參數驗證失敗
40010: (ErrorResp) 驗證碼類型無效
40011: (ErrorResp) 驗證碼格式錯誤
) // 客戶端錯誤
@respdoc-404 (ErrorResp) // 驗證碼不存在
@respdoc-422 (ErrorResp) // 驗證碼無效或已過期
@respdoc-500 (ErrorResp) // 服務器錯誤
*/
@handler VerifyRefreshCode
post /verify-code (VerifyRefreshCodeReq) returns (OKResp)
@doc(
summary: "檢查驗證碼"
description: "檢查驗證碼是否正確,但不刪除驗證碼"
)
/*
@respdoc-200 (OKResp) // 檢查成功
@respdoc-400 (
40001: (ErrorResp) 帳號格式錯誤
40004: (ErrorResp) 參數驗證失敗
40010: (ErrorResp) 驗證碼類型無效
40011: (ErrorResp) 驗證碼格式錯誤
) // 客戶端錯誤
@respdoc-404 (ErrorResp) // 驗證碼不存在
@respdoc-422 (ErrorResp) // 驗證碼無效或已過期
@respdoc-500 (ErrorResp) // 服務器錯誤
*/
@handler CheckRefreshCode
post /check-code (VerifyRefreshCodeReq) returns (OKResp)
@doc(
summary: "驗證 Google 認證"
description: "驗證 Google OAuth 登入是否有效"
)
/*
@respdoc-200 (VerifyGoogleAuthResultResp) // 驗證成功
@respdoc-400 (
40004: (ErrorResp) 參數驗證失敗
40012: (ErrorResp) Token 格式錯誤
) // 客戶端錯誤
@respdoc-401 (ErrorResp) // Token 無效或過期
@respdoc-422 (ErrorResp) // Google 認證失敗
@respdoc-500 (ErrorResp) // 服務器錯誤
*/
@handler VerifyGoogleAuthResult
post /verify-google (VerifyAuthResultReq) returns (VerifyGoogleAuthResultResp)
@doc(
summary: "驗證平台認證"
description: "驗證平台登入認證是否有效"
)
/*
@respdoc-200 (VerifyAuthResultResp) // 驗證成功
@respdoc-400 (
40004: (ErrorResp) 參數驗證失敗
40012: (ErrorResp) Token 格式錯誤
) // 客戶端錯誤
@respdoc-401 (ErrorResp) // Token 無效或過期
@respdoc-422 (ErrorResp) // 平台認證失敗
@respdoc-500 (ErrorResp) // 服務器錯誤
*/
@handler VerifyPlatformAuthResult
post /verify-platform (VerifyAuthResultReq) returns (VerifyAuthResultResp)
@doc(
summary: "LINE 獲取 Access Token"
description: "使用 LINE 授權碼獲取 Access Token"
)
/*
@respdoc-200 (LineAccessTokenResp) // 獲取成功
@respdoc-400 (
40004: (ErrorResp) 參數驗證失敗
40013: (ErrorResp) 授權碼格式錯誤
) // 客戶端錯誤
@respdoc-401 (ErrorResp) // 授權碼無效或過期
@respdoc-422 (ErrorResp) // LINE 認證失敗
@respdoc-500 (ErrorResp) // 服務器錯誤
*/
@handler LineCodeToAccessToken
post /line/token (LineGetTokenReq) returns (LineAccessTokenResp)
@doc(
summary: "LINE 獲取用戶資料"
description: "使用 LINE Access Token 獲取用戶資料"
)
/*
@respdoc-200 (LineUserProfile) // 獲取成功
@respdoc-400 (
40004: (ErrorResp) 參數驗證失敗
40012: (ErrorResp) Token 格式錯誤
) // 客戶端錯誤
@respdoc-401 (ErrorResp) // Token 無效或過期
@respdoc-422 (ErrorResp) // LINE 用戶資料獲取失敗
@respdoc-500 (ErrorResp) // 服務器錯誤
*/
@handler LineGetProfileByAccessToken
post /line/profile (LineGetUserInfoReq) returns (LineUserProfile)
} }

2
go.mod
View File

@ -4,7 +4,6 @@ go 1.25.1
require ( require (
code.30cm.net/digimon/library-go/errs v1.2.14 code.30cm.net/digimon/library-go/errs v1.2.14
code.30cm.net/digimon/library-go/mongo v0.0.9
code.30cm.net/digimon/library-go/utils/invited_code v1.2.5 code.30cm.net/digimon/library-go/utils/invited_code v1.2.5
github.com/alicebob/miniredis/v2 v2.35.0 github.com/alicebob/miniredis/v2 v2.35.0
github.com/shopspring/decimal v1.4.0 github.com/shopspring/decimal v1.4.0
@ -61,7 +60,6 @@ require (
github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.0 // indirect github.com/moby/term v0.5.0 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/morikuni/aec v1.0.0 // indirect github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect

4
go.sum
View File

@ -1,7 +1,5 @@
code.30cm.net/digimon/library-go/errs v1.2.14 h1:Un9wcIIjjJW8D2i0ISf8ibzp9oNT4OqLsaSKW0T4RJU= 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/errs v1.2.14/go.mod h1:Hs4v7SbXNggDVBGXSYsFMjkii1qLF+rugrIpWePN4/o=
code.30cm.net/digimon/library-go/mongo v0.0.9 h1:fPciIE5B85tXpLg8aeVQqKVbLnfpVAk9xbMu7pE2tVw=
code.30cm.net/digimon/library-go/mongo v0.0.9/go.mod h1:KBVKz/Ci5IheI77BgZxPUeKkaGvDy8fV8EDHSCOLIO4=
code.30cm.net/digimon/library-go/utils/invited_code v1.2.5 h1:szWsI0K+1iEHmc/AtKx+5c7tDIc1AZdStvT0tVza1pg= code.30cm.net/digimon/library-go/utils/invited_code v1.2.5 h1:szWsI0K+1iEHmc/AtKx+5c7tDIc1AZdStvT0tVza1pg=
code.30cm.net/digimon/library-go/utils/invited_code v1.2.5/go.mod h1:eHmWpbX6N6KXQ2xaY71uj5bwfzTaNL8pQc2njYo5Gj0= code.30cm.net/digimon/library-go/utils/invited_code v1.2.5/go.mod h1:eHmWpbX6N6KXQ2xaY71uj5bwfzTaNL8pQc2njYo5Gj0=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
@ -116,8 +114,6 @@ github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=

View File

@ -1,7 +1,52 @@
package config package config
import "github.com/zeromicro/go-zero/rest" import (
"time"
"github.com/zeromicro/go-zero/core/stores/cache"
"github.com/zeromicro/go-zero/core/stores/redis"
"github.com/zeromicro/go-zero/rest"
)
type Config struct { type Config struct {
rest.RestConf rest.RestConf
// Redis 配置
RedisConf redis.RedisConf
// Redis Cluster (Cache)
Cache cache.CacheConf
CacheExpireTime time.Duration
CacheWithNotFoundExpiry time.Duration
Mongo struct {
Schema string
User string
Password string
Host string
Port string
Database string
ReplicaName string
MaxStaleness time.Duration
MaxPoolSize uint64
MinPoolSize uint64
MaxConnIdleTime time.Duration
Compressors []string
EnableStandardReadWriteSplitMode bool
ConnectTimeoutMs int64
}
// 密碼加密層數
Bcrypt struct {
Cost int
}
GoogleAuth struct {
ClientID string
AuthURL string
}
LineAuth struct {
ClientID string
ClientSecret string
RedirectURI string
}
} }

View File

@ -0,0 +1,30 @@
package auth
import (
"net/http"
"backend/internal/logic/auth"
"backend/internal/svc"
"backend/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 {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := auth.NewLoginLogic(r.Context(), svcCtx)
resp, err := l.Login(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

View File

@ -0,0 +1,30 @@
package auth
import (
"net/http"
"backend/internal/logic/auth"
"backend/internal/svc"
"backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
// 刷新 Access Token
func RefreshTokenHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.RefreshTokenReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := auth.NewRefreshTokenLogic(r.Context(), svcCtx)
resp, err := l.RefreshToken(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

View File

@ -0,0 +1,30 @@
package auth
import (
"net/http"
"backend/internal/logic/auth"
"backend/internal/svc"
"backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
// 註冊新帳號
func RegisterHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.LoginReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := auth.NewRegisterLogic(r.Context(), svcCtx)
resp, err := l.Register(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

View File

@ -0,0 +1,30 @@
package auth
import (
"net/http"
"backend/internal/logic/auth"
"backend/internal/svc"
"backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
// 請求發送密碼重設驗證碼
func RequestPasswordResetHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.RequestPasswordResetReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := auth.NewRequestPasswordResetLogic(r.Context(), svcCtx)
resp, err := l.RequestPasswordReset(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

View File

@ -0,0 +1,30 @@
package auth
import (
"net/http"
"backend/internal/logic/auth"
"backend/internal/svc"
"backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
// 執行密碼重設
func ResetPasswordHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.ResetPasswordReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := auth.NewResetPasswordLogic(r.Context(), svcCtx)
resp, err := l.ResetPassword(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

View File

@ -0,0 +1,30 @@
package auth
import (
"net/http"
"backend/internal/logic/auth"
"backend/internal/svc"
"backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
// 校驗密碼重設驗證碼
func VerifyPasswordResetCodeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.VerifyCodeReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := auth.NewVerifyPasswordResetCodeLogic(r.Context(), svcCtx)
resp, err := l.VerifyPasswordResetCode(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

View File

@ -5,6 +5,7 @@ import (
"backend/internal/logic/ping" "backend/internal/logic/ping"
"backend/internal/svc" "backend/internal/svc"
"github.com/zeromicro/go-zero/rest/httpx" "github.com/zeromicro/go-zero/rest/httpx"
) )

View File

@ -1,5 +1,5 @@
// Code generated by goctl. DO NOT EDIT. // Code generated by goctl. DO NOT EDIT.
// goctl 1.8.1 // goctl 1.8.5
package handler package handler
@ -7,13 +7,58 @@ import (
"net/http" "net/http"
"time" "time"
auth "backend/internal/handler/auth"
ping "backend/internal/handler/ping" ping "backend/internal/handler/ping"
user "backend/internal/handler/user"
"backend/internal/svc" "backend/internal/svc"
"github.com/zeromicro/go-zero/rest" "github.com/zeromicro/go-zero/rest"
) )
func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
server.AddRoutes(
[]rest.Route{
{
// 執行密碼重設
Method: http.MethodPut,
Path: "/password-resets",
Handler: auth.ResetPasswordHandler(serverCtx),
},
{
// 請求發送密碼重設驗證碼
Method: http.MethodPost,
Path: "/password-resets/request",
Handler: auth.RequestPasswordResetHandler(serverCtx),
},
{
// 校驗密碼重設驗證碼
Method: http.MethodPost,
Path: "/password-resets/verify",
Handler: auth.VerifyPasswordResetCodeHandler(serverCtx),
},
{
// 註冊新帳號
Method: http.MethodPost,
Path: "/register",
Handler: auth.RegisterHandler(serverCtx),
},
{
// 使用者登入
Method: http.MethodPost,
Path: "/sessions",
Handler: auth.LoginHandler(serverCtx),
},
{
// 刷新 Access Token
Method: http.MethodPost,
Path: "/sessions/refresh",
Handler: auth.RefreshTokenHandler(serverCtx),
},
},
rest.WithPrefix("/api/v1/auth"),
rest.WithTimeout(10000*time.Millisecond),
)
server.AddRoutes( server.AddRoutes(
[]rest.Route{ []rest.Route{
{ {
@ -26,4 +71,44 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
rest.WithPrefix("/api/v1"), rest.WithPrefix("/api/v1"),
rest.WithTimeout(10000*time.Millisecond), rest.WithTimeout(10000*time.Millisecond),
) )
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.AuthMiddleware},
[]rest.Route{
{
// 取得當前登入的會員資訊(自己)
Method: http.MethodGet,
Path: "/me",
Handler: user.GetUserInfoHandler(serverCtx),
},
{
// 更新當前登入的會員資訊
Method: http.MethodPut,
Path: "/me",
Handler: user.UpdateUserInfoHandler(serverCtx),
},
{
// 修改當前登入使用者的密碼
Method: http.MethodPut,
Path: "/me/password",
Handler: user.UpdatePasswordHandler(serverCtx),
},
{
// 請求發送驗證碼 (用於驗證信箱/手機)
Method: http.MethodPost,
Path: "/me/verifications",
Handler: user.RequestVerificationCodeHandler(serverCtx),
},
{
// 提交驗證碼以完成驗證
Method: http.MethodPut,
Path: "/me/verifications",
Handler: user.SubmitVerificationCodeHandler(serverCtx),
},
}...,
),
rest.WithPrefix("/api/v1/user"),
rest.WithTimeout(10000*time.Millisecond),
)
} }

View File

@ -0,0 +1,30 @@
package user
import (
"net/http"
"backend/internal/logic/user"
"backend/internal/svc"
"backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
// 取得當前登入的會員資訊(自己)
func GetUserInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.Authorization
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := user.NewGetUserInfoLogic(r.Context(), svcCtx)
resp, err := l.GetUserInfo(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

View File

@ -0,0 +1,30 @@
package user
import (
"net/http"
"backend/internal/logic/user"
"backend/internal/svc"
"backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
// 請求發送驗證碼 (用於驗證信箱/手機)
func RequestVerificationCodeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.RequestVerificationCodeReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := user.NewRequestVerificationCodeLogic(r.Context(), svcCtx)
resp, err := l.RequestVerificationCode(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

View File

@ -0,0 +1,30 @@
package user
import (
"net/http"
"backend/internal/logic/user"
"backend/internal/svc"
"backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
// 提交驗證碼以完成驗證
func SubmitVerificationCodeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.SubmitVerificationCodeReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := user.NewSubmitVerificationCodeLogic(r.Context(), svcCtx)
resp, err := l.SubmitVerificationCode(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

View File

@ -0,0 +1,30 @@
package user
import (
"net/http"
"backend/internal/logic/user"
"backend/internal/svc"
"backend/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 {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := user.NewUpdatePasswordLogic(r.Context(), svcCtx)
resp, err := l.UpdatePassword(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

View File

@ -0,0 +1,30 @@
package user
import (
"net/http"
"backend/internal/logic/user"
"backend/internal/svc"
"backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
// 更新當前登入的會員資訊
func UpdateUserInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.UpdateUserInfoReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := user.NewUpdateUserInfoLogic(r.Context(), svcCtx)
resp, err := l.UpdateUserInfo(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

View File

@ -0,0 +1,31 @@
package auth
import (
"context"
"backend/internal/svc"
"backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type LoginLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// 使用者登入
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.LoginResp, err error) {
// todo: add your logic here and delete this line
return
}

View File

@ -0,0 +1,31 @@
package auth
import (
"context"
"backend/internal/svc"
"backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type RefreshTokenLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// 刷新 Access Token
func NewRefreshTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RefreshTokenLogic {
return &RefreshTokenLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *RefreshTokenLogic) RefreshToken(req *types.RefreshTokenReq) (resp *types.RefreshTokenResp, err error) {
// todo: add your logic here and delete this line
return
}

View File

@ -0,0 +1,31 @@
package auth
import (
"context"
"backend/internal/svc"
"backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type RegisterLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// 註冊新帳號
func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic {
return &RegisterLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *RegisterLogic) Register(req *types.LoginReq) (resp *types.LoginResp, err error) {
// todo: add your logic here and delete this line
return
}

View File

@ -0,0 +1,31 @@
package auth
import (
"context"
"backend/internal/svc"
"backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type RequestPasswordResetLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// 請求發送密碼重設驗證碼
func NewRequestPasswordResetLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RequestPasswordResetLogic {
return &RequestPasswordResetLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *RequestPasswordResetLogic) RequestPasswordReset(req *types.RequestPasswordResetReq) (resp *types.RespOK, err error) {
// todo: add your logic here and delete this line
return
}

View File

@ -0,0 +1,31 @@
package auth
import (
"context"
"backend/internal/svc"
"backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type ResetPasswordLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// 執行密碼重設
func NewResetPasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetPasswordLogic {
return &ResetPasswordLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordReq) (resp *types.RespOK, err error) {
// todo: add your logic here and delete this line
return
}

View File

@ -0,0 +1,31 @@
package auth
import (
"context"
"backend/internal/svc"
"backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type VerifyPasswordResetCodeLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// 校驗密碼重設驗證碼
func NewVerifyPasswordResetCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *VerifyPasswordResetCodeLogic {
return &VerifyPasswordResetCodeLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *VerifyPasswordResetCodeLogic) VerifyPasswordResetCode(req *types.VerifyCodeReq) (resp *types.RespOK, err error) {
// todo: add your logic here and delete this line
return
}

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"backend/internal/svc" "backend/internal/svc"
"github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/logx"
) )

View File

@ -0,0 +1,31 @@
package user
import (
"context"
"backend/internal/svc"
"backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetUserInfoLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// 取得當前登入的會員資訊(自己)
func NewGetUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserInfoLogic {
return &GetUserInfoLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetUserInfoLogic) GetUserInfo(req *types.Authorization) (resp *types.UserInfoResp, err error) {
// todo: add your logic here and delete this line
return
}

View File

@ -0,0 +1,31 @@
package user
import (
"context"
"backend/internal/svc"
"backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type RequestVerificationCodeLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// 請求發送驗證碼 (用於驗證信箱/手機)
func NewRequestVerificationCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RequestVerificationCodeLogic {
return &RequestVerificationCodeLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *RequestVerificationCodeLogic) RequestVerificationCode(req *types.RequestVerificationCodeReq) (resp *types.RespOK, err error) {
// todo: add your logic here and delete this line
return
}

View File

@ -0,0 +1,31 @@
package user
import (
"context"
"backend/internal/svc"
"backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type SubmitVerificationCodeLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// 提交驗證碼以完成驗證
func NewSubmitVerificationCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SubmitVerificationCodeLogic {
return &SubmitVerificationCodeLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *SubmitVerificationCodeLogic) SubmitVerificationCode(req *types.SubmitVerificationCodeReq) (resp *types.RespOK, err error) {
// todo: add your logic here and delete this line
return
}

View File

@ -0,0 +1,31 @@
package user
import (
"context"
"backend/internal/svc"
"backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type UpdatePasswordLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// 修改當前登入使用者的密碼
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) (resp *types.RespOK, err error) {
// todo: add your logic here and delete this line
return
}

View File

@ -0,0 +1,31 @@
package user
import (
"context"
"backend/internal/svc"
"backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type UpdateUserInfoLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// 更新當前登入的會員資訊
func NewUpdateUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateUserInfoLogic {
return &UpdateUserInfoLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *UpdateUserInfoLogic) UpdateUserInfo(req *types.UpdateUserInfoReq) (resp *types.UserInfoResp, err error) {
// todo: add your logic here and delete this line
return
}

View File

@ -0,0 +1,19 @@
package middleware
import "net/http"
type AuthMiddleware struct {
}
func NewAuthMiddleware() *AuthMiddleware {
return &AuthMiddleware{}
}
func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// TODO generate middleware implement function, delete after code implementation
// Passthrough to next handler if need
next(w, r)
}
}

View File

@ -0,0 +1,105 @@
package svc
import (
"backend/internal/config"
mgo "backend/pkg/library/mongo"
cfg "backend/pkg/member/domain/config"
"backend/pkg/member/domain/usecase"
"backend/pkg/member/repository"
uc "backend/pkg/member/usecase"
"context"
"github.com/zeromicro/go-zero/core/stores/cache"
"github.com/zeromicro/go-zero/core/stores/mon"
"github.com/zeromicro/go-zero/core/stores/redis"
)
func NewAccountUC(c *config.Config, rds *redis.Redis) usecase.AccountUseCase {
// 準備Mongo Config
conf := &mgo.Conf{
Schema: c.Mongo.Schema,
Host: c.Mongo.Host,
Database: c.Mongo.Database,
MaxStaleness: c.Mongo.MaxStaleness,
MaxPoolSize: c.Mongo.MaxPoolSize,
MinPoolSize: c.Mongo.MinPoolSize,
MaxConnIdleTime: c.Mongo.MaxConnIdleTime,
Compressors: c.Mongo.Compressors,
EnableStandardReadWriteSplitMode: c.Mongo.EnableStandardReadWriteSplitMode,
ConnectTimeoutMs: c.Mongo.ConnectTimeoutMs,
}
if c.Mongo.User != "" {
conf.User = c.Mongo.User
conf.Password = c.Mongo.Password
}
// 快取選項
cacheOpts := []cache.Option{
cache.WithExpiry(c.CacheExpireTime),
cache.WithNotFoundExpiry(c.CacheWithNotFoundExpiry),
}
dbOpts := []mon.Option{
mgo.SetCustomDecimalType(),
mgo.InitMongoOptions(*conf),
}
ac := repository.NewAccountRepository(repository.AccountRepositoryParam{
Conf: conf,
CacheConf: c.Cache,
CacheOpts: cacheOpts,
DBOpts: dbOpts,
})
u := repository.NewUserRepository(repository.UserRepositoryParam{
Conf: conf,
CacheConf: c.Cache,
CacheOpts: cacheOpts,
DBOpts: dbOpts,
})
guid := repository.NewAutoIDRepository(repository.AutoIDRepositoryParam{
Conf: conf,
DBOpts: dbOpts,
})
auid := repository.NewAccountUIDRepository(repository.AccountUIDRepositoryParam{
Conf: conf,
CacheConf: c.Cache,
CacheOpts: cacheOpts,
DBOpts: dbOpts,
})
_, _ = ac.Index20241226001UP(context.Background())
_, _ = u.Index20241226001UP(context.Background())
_, _ = guid.Index20241226001UP(context.Background())
_, _ = auid.Index20241226001UP(context.Background())
return uc.MustMemberUseCase(uc.MemberUseCaseParam{
Account: ac,
User: u,
AccountUID: auid,
VerifyCodeModel: repository.NewVerifyCodeRepository(rds),
GenerateUID: guid,
Config: prepareCfg(c),
})
}
func prepareCfg(c *config.Config) cfg.Config {
return cfg.Config{
Bcrypt: struct{ Cost int }{Cost: c.Bcrypt.Cost},
GoogleAuth: struct {
ClientID string
AuthURL string
}{
ClientID: c.GoogleAuth.ClientID,
AuthURL: c.GoogleAuth.AuthURL,
},
LineAuth: struct {
ClientID string
ClientSecret string
RedirectURI string
}{
ClientID: c.LineAuth.ClientID,
ClientSecret: c.LineAuth.ClientSecret,
RedirectURI: c.LineAuth.RedirectURI,
},
}
}

View File

@ -2,14 +2,28 @@ package svc
import ( import (
"backend/internal/config" "backend/internal/config"
"backend/internal/middleware"
"backend/pkg/member/domain/usecase"
"github.com/zeromicro/go-zero/core/stores/redis"
"github.com/zeromicro/go-zero/rest"
) )
type ServiceContext struct { type ServiceContext struct {
Config config.Config Config config.Config
AuthMiddleware rest.Middleware
AccountUC usecase.AccountUseCase
} }
func NewServiceContext(c config.Config) *ServiceContext { func NewServiceContext(c config.Config) *ServiceContext {
rds, err := redis.NewRedis(c.RedisConf)
if err != nil {
panic(err)
}
return &ServiceContext{ return &ServiceContext{
Config: c, Config: c,
AuthMiddleware: middleware.NewAuthMiddleware().Handle,
AccountUC: NewAccountUC(&c, rds),
} }
} }

View File

@ -1,32 +1,138 @@
// Code generated by goctl. DO NOT EDIT. // Code generated by goctl. DO NOT EDIT.
// goctl 1.8.1 // goctl 1.8.5
package types package types
type Authorization struct {
Authorization string `header:"Authorization" validate:"required"`
}
type BaseReq struct { type BaseReq struct {
} }
type BaseResponse struct { type CredentialsPayload struct {
Status Status `json:"status"` // 狀態 Password string `json:"password" validate:"required,min=8,max=128"` // 密碼 (後端應使用 bcrypt 進行雜湊)
Data interface{} `json:"data"` // 資料 PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password"` // 確認密碼
} }
type Pager struct { type ErrorResp struct {
Total int64 `json:"total"` Code int `json:"code"`
PageSize int64 `json:"page_size"` Msg string `json:"msg"`
PageIndex int64 `json:"page_index"` Details string `json:"details,omitempty"`
}
type RespOK struct {
}
type Status struct {
Code int64 `json:"code"` // 狀態碼
Message string `json:"message"` // 訊息
Data interface{} `json:"data,omitempty"` // 可選的資料,當有返回時才出現
Error interface{} `json:"error,omitempty"` // 可選的錯誤信息 Error interface{} `json:"error,omitempty"` // 可選的錯誤信息
} }
type VerifyHeader struct { type LoginReq struct {
Token string `header:"token" validate:"required"` AuthMethod string `json:"auth_method" validate:"required,oneof=credentials platform"`
LoginID string `json:"login_id" validate:"required,min=3,max=50"` // 信箱或手機號碼
Credentials *CredentialsPayload `json:"credentials,optional"` // AuthMethod 為 'credentials' 時使用
Platform *PlatformPayload `json:"platform,optional"` // AuthMethod 為 'platform' 時使用
}
type LoginResp struct {
UID string `json:"uid"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"` // 通常固定為 "Bearer"
}
type PagerResp struct {
Total int64 `json:"total"`
Size int64 `json:"size"`
Index int64 `json:"index"`
}
type PlatformPayload struct {
Provider string `json:"provider" validate:"required,oneof=google line apple"` // 平台名稱
Token string `json:"token" validate:"required"` // 平台提供的 Access Token 或 ID Token
}
type RefreshTokenReq struct {
RefreshToken string `json:"refresh_token" validate:"required"`
}
type RefreshTokenResp struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"` // 可選:某些策略下刷新後也會換發新的 Refresh Token
TokenType string `json:"token_type"`
}
type RequestPasswordResetReq struct {
Identifier string `json:"identifier" validate:"required,email|phone"` // 使用者帳號 (信箱或手機)
AccountType string `json:"account_type" validate:"required,oneof=email phone"`
}
type RequestVerificationCodeReq struct {
Purpose string `json:"purpose" validate:"required,oneof=email_verification phone_verification"`
Authorization
}
type ResetPasswordReq struct {
Identifier string `json:"identifier" validate:"required"`
VerifyCode string `json:"verify_code" validate:"required"` // 來自上一步驗證通過的 Code作為一種「票證」
Password string `json:"password" validate:"required,min=8,max=128"` // 新密碼
PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password"` // 確認新密碼
}
type RespOK struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data,omitempty"`
}
type SubmitVerificationCodeReq struct {
Purpose string `json:"purpose" validate:"required,oneof=email_verification phone_verification"`
VerifyCode string `json:"verify_code" validate:"required,len=6"`
Authorization
}
type UpdatePasswordReq struct {
CurrentPassword string `json:"current_password" validate:"required"`
NewPassword string `json:"new_password" validate:"required,min=8,max=128"`
NewPasswordConfirm string `json:"new_password_confirm" validate:"eqfield=NewPassword"`
Authorization
}
type UpdateUserInfoReq struct {
AvatarURL *string `json:"avatar_url,optional"` // 頭像 URL
FullName *string `json:"full_name,optional"` // 用戶全名
Nickname *string `json:"nickname,optional"` // 暱稱
GenderCode *string `json:"gender_code,optional" validate:"omitempty,oneof=secret male female"` // 性別
Birthdate *string `json:"birthdate,optional"` // 生日 (格式: 1993-04-17)
Address *string `json:"address,optional"` // 地址
PreferredLanguage *string `json:"preferred_language,optional" validate:"omitempty,oneof=zh-tw en-us"` // 語言
Currency *string `json:"currency,optional" validate:"omitempty,oneof=TWD USD"` // 貨幣代號
National *string `json:"national,optional"` // 國家
PostCode *string `json:"post_code,optional"` // 郵遞區號
Carrier *string `json:"carrier,optional"` // 載具
}
type UserInfoResp struct {
Platform string `json:"platform"` // 註冊平台
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"` // 性別代碼
Birthdate string `json:"birthdate"` // 生日 (格式: 1993-04-17)
PhoneNumber string `json:"phone_number"` // 電話
IsPhoneVerified bool `json:"is_phone_verified"` // 手機是否已驗證
Email string `json:"email"` // 信箱
IsEmailVerified bool `json:"is_email_verified"` // 信箱是否已驗證
Address string `json:"address"` // 地址
UserStatus string `json:"user_status"` // 用戶狀態
PreferredLanguage string `json:"preferred_language"` // 偏好語言
Currency string `json:"currency"` // 偏好幣種
National string `json:"national"` // 國家
PostCode string `json:"post_code"` // 郵遞區號
Carrier string `json:"carrier"` // 載具
Role string `json:"role"` // 角色
UpdateAt string `json:"update_at"`
CreateAt string `json:"create_at"`
Authorization
}
type VerifyCodeReq struct {
Identifier string `json:"identifier" validate:"required"`
VerifyCode string `json:"verify_code" validate:"required,len=6"`
} }

View File

@ -123,7 +123,7 @@ func (document *DocumentDB) GetClient() *mon.Model {
func (document *DocumentDB) yieldIndexModel(keys []string, sorts []int32, unique bool, indexOpt *options.IndexOptionsBuilder) mongo.IndexModel { func (document *DocumentDB) yieldIndexModel(keys []string, sorts []int32, unique bool, indexOpt *options.IndexOptionsBuilder) mongo.IndexModel {
SetKeysDoc := bson.D{} SetKeysDoc := bson.D{}
for index, _ := range keys { for index := range keys {
key := keys[index] key := keys[index]
sort := sorts[index] sort := sorts[index]
SetKeysDoc = append(SetKeysDoc, bson.E{Key: key, Value: sort}) SetKeysDoc = append(SetKeysDoc, bson.E{Key: key, Value: sort})

View File

@ -228,4 +228,3 @@ func TestInitMongoOptions_ReturnType(t *testing.T) {
// Test that we can use the options (basic type check) // Test that we can use the options (basic type check)
var _ mon.Option = opts var _ mon.Option = opts
} }

View File

@ -5,18 +5,19 @@ import (
"time" "time"
"backend/pkg/member/domain/member" "backend/pkg/member/domain/member"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
) )
// Account represents a user account with authentication credentials. // Account represents a user account with authentication credentials.
// It stores login information, hashed passwords, and platform-specific data. // It stores login information, hashed passwords, and platform-specific data.
type Account struct { type Account struct {
ID bson.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` ID bson.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
LoginID string `bson:"login_id"` // Unique login identifier (email, phone, username) LoginID string `bson:"login_id"` // Unique login identifier (email, phone, username)
Token string `bson:"token"` // Hashed password or platform-specific token Token string `bson:"token"` // Hashed password or platform-specific token
Platform member.Platform `bson:"platform"` // Platform type: 1. platform 2. google 3. line 4. apple Platform member.Platform `bson:"platform"` // Platform type: 1. platform 2. google 3. line 4. apple
UpdateAt *int64 `bson:"update_at,omitempty" json:"update_at,omitempty"` UpdateAt *int64 `bson:"update_at,omitempty" json:"update_at,omitempty"`
CreateAt *int64 `bson:"create_at,omitempty" json:"create_at,omitempty"` CreateAt *int64 `bson:"create_at,omitempty" json:"create_at,omitempty"`
} }
// CollectionName returns the MongoDB collection name for Account entities. // CollectionName returns the MongoDB collection name for Account entities.

View File

@ -5,16 +5,17 @@ import (
"time" "time"
"backend/pkg/member/domain/member" "backend/pkg/member/domain/member"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
) )
type AccountUID struct { type AccountUID struct {
ID bson.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` ID bson.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
LoginID string `bson:"login_id"` LoginID string `bson:"login_id"`
UID string `bson:"uid"` UID string `bson:"uid"`
Type member.AccountType `bson:"type"` Type member.AccountType `bson:"type"`
UpdateAt *int64 `bson:"update_at,omitempty" json:"update_at,omitempty"` UpdateAt *int64 `bson:"update_at,omitempty" json:"update_at,omitempty"`
CreateAt *int64 `bson:"create_at,omitempty" json:"create_at,omitempty"` CreateAt *int64 `bson:"create_at,omitempty" json:"create_at,omitempty"`
} }
func (a *AccountUID) CollectionName() string { func (a *AccountUID) CollectionName() string {

View File

@ -5,28 +5,29 @@ import (
"time" "time"
"backend/pkg/member/domain/member" "backend/pkg/member/domain/member"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
) )
// User represents a user profile with personal information and preferences. // User represents a user profile with personal information and preferences.
// It contains detailed user data that is separate from authentication credentials. // It contains detailed user data that is separate from authentication credentials.
type User struct { type User struct {
ID bson.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` ID bson.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
UID string `bson:"uid"` // Unique user identifier UID string `bson:"uid"` // Unique user identifier
AvatarURL *string `bson:"avatar_url,omitempty"` // User avatar URL (optional) AvatarURL *string `bson:"avatar_url,omitempty"` // User avatar URL (optional)
FullName *string `bson:"full_name,omitempty"` // User's full name FullName *string `bson:"full_name,omitempty"` // User's full name
Nickname *string `bson:"nickname,omitempty"` // User's nickname (optional) Nickname *string `bson:"nickname,omitempty"` // User's nickname (optional)
GenderCode *int64 `bson:"gender_code,omitempty"` // Gender code GenderCode *int64 `bson:"gender_code,omitempty"` // Gender code
Birthdate *int64 `bson:"birthdate,omitempty"` // Birth date (format: 19930417) Birthdate *int64 `bson:"birthdate,omitempty"` // Birth date (format: 19930417)
Address *string `bson:"address,omitempty"` // User's address Address *string `bson:"address,omitempty"` // User's address
AlarmCategory member.AlarmType `bson:"alarm_category"` // Alert notification settings AlarmCategory member.AlarmType `bson:"alarm_category"` // Alert notification settings
UserStatus member.Status `bson:"user_status"` // User account status UserStatus member.Status `bson:"user_status"` // User account status
PreferredLanguage string `bson:"preferred_language"` // User's preferred language PreferredLanguage string `bson:"preferred_language"` // User's preferred language
Currency string `bson:"currency"` // User's preferred currency Currency string `bson:"currency"` // User's preferred currency
PhoneNumber *string `bson:"phone_number,omitempty"` // Phone number (appears after verification) PhoneNumber *string `bson:"phone_number,omitempty"` // Phone number (appears after verification)
Email *string `bson:"email,omitempty"` // Email address (appears after verification) Email *string `bson:"email,omitempty"` // Email address (appears after verification)
UpdateAt *int64 `bson:"update_at,omitempty" json:"update_at,omitempty"` UpdateAt *int64 `bson:"update_at,omitempty" json:"update_at,omitempty"`
CreateAt *int64 `bson:"create_at,omitempty" json:"create_at,omitempty"` CreateAt *int64 `bson:"create_at,omitempty" json:"create_at,omitempty"`
} }
// CollectionName returns the MongoDB collection name for User entities. // CollectionName returns the MongoDB collection name for User entities.

View File

@ -108,34 +108,34 @@ func TestValidatePhone(t *testing.T) {
func TestValidatePassword(t *testing.T) { func TestValidatePassword(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
password string password string
wantErr bool wantErr bool
}{ }{
{ {
name: "valid password", name: "valid password",
password: "password123", password: "password123",
wantErr: false, wantErr: false,
}, },
{ {
name: "password too short", name: "password too short",
password: "123", password: "123",
wantErr: true, wantErr: true,
}, },
{ {
name: "password too long", name: "password too long",
password: string(make([]byte, MaxPasswordLength+1)), password: string(make([]byte, MaxPasswordLength+1)),
wantErr: true, wantErr: true,
}, },
{ {
name: "empty password", name: "empty password",
password: "", password: "",
wantErr: true, wantErr: true,
}, },
{ {
name: "minimum length password", name: "minimum length password",
password: "12345678", password: "12345678",
wantErr: false, wantErr: false,
}, },
} }

View File

@ -12,6 +12,7 @@ import (
"backend/pkg/member/domain/repository" "backend/pkg/member/domain/repository"
"backend/pkg/library/mongo" "backend/pkg/library/mongo"
"github.com/zeromicro/go-zero/core/stores/cache" "github.com/zeromicro/go-zero/core/stores/cache"
"github.com/zeromicro/go-zero/core/stores/mon" "github.com/zeromicro/go-zero/core/stores/mon"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"

View File

@ -12,6 +12,7 @@ import (
"backend/pkg/member/domain/repository" "backend/pkg/member/domain/repository"
mgo "backend/pkg/library/mongo" mgo "backend/pkg/library/mongo"
"github.com/alicebob/miniredis/v2" "github.com/alicebob/miniredis/v2"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/stores/cache" "github.com/zeromicro/go-zero/core/stores/cache"

View File

@ -10,6 +10,7 @@ import (
"backend/pkg/member/domain/repository" "backend/pkg/member/domain/repository"
"backend/pkg/library/mongo" "backend/pkg/library/mongo"
"github.com/zeromicro/go-zero/core/stores/cache" "github.com/zeromicro/go-zero/core/stores/cache"
"github.com/zeromicro/go-zero/core/stores/mon" "github.com/zeromicro/go-zero/core/stores/mon"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"

View File

@ -10,6 +10,7 @@ import (
"backend/pkg/member/domain/repository" "backend/pkg/member/domain/repository"
mgo "backend/pkg/library/mongo" mgo "backend/pkg/library/mongo"
"github.com/alicebob/miniredis/v2" "github.com/alicebob/miniredis/v2"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/stores/cache" "github.com/zeromicro/go-zero/core/stores/cache"

View File

@ -11,6 +11,7 @@ import (
GIDLib "code.30cm.net/digimon/library-go/utils/invited_code" GIDLib "code.30cm.net/digimon/library-go/utils/invited_code"
"backend/pkg/library/mongo" "backend/pkg/library/mongo"
"github.com/zeromicro/go-zero/core/stores/mon" "github.com/zeromicro/go-zero/core/stores/mon"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
mongodriver "go.mongodb.org/mongo-driver/v2/mongo" mongodriver "go.mongodb.org/mongo-driver/v2/mongo"

View File

@ -10,6 +10,7 @@ import (
"backend/pkg/member/domain/repository" "backend/pkg/member/domain/repository"
mgo "backend/pkg/library/mongo" mgo "backend/pkg/library/mongo"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )

View File

@ -12,6 +12,7 @@ import (
"backend/pkg/member/domain/repository" "backend/pkg/member/domain/repository"
mgo "backend/pkg/library/mongo" mgo "backend/pkg/library/mongo"
"github.com/alicebob/miniredis/v2" "github.com/alicebob/miniredis/v2"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/stores/cache" "github.com/zeromicro/go-zero/core/stores/cache"

View File

@ -5,6 +5,7 @@ import (
"backend/pkg/member/domain" "backend/pkg/member/domain"
"backend/pkg/member/domain/repository" "backend/pkg/member/domain/repository"
"github.com/zeromicro/go-zero/core/stores/redis" "github.com/zeromicro/go-zero/core/stores/redis"
) )

View File

@ -2,6 +2,7 @@ package usecase
import ( import (
"errors" "errors"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )