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: # 格式優化
$(GOFMT) -w $(GOFILES)
goimports -w ./
golangci-lint run
.PHONY: build
build: # 編譯專案
@ -63,17 +62,13 @@ install: # 安裝依賴
go mod tidy
go mod download
.PHONY: lint
lint: # 代碼檢查
golangci-lint run
.PHONY: help
help: # 顯示幫助信息
@echo "Available commands:"
@echo " test - 運行測試"
@echo " gen-api - 產生 api"
@echo " gen-swagger - 生成 JSON 格式 Swagger 文檔"
@echo " gen-swagger-yaml - 生成 YAML 格式 Swagger 文檔"
@echo " gen-doc - 生成 Swagger 文檔"
@echo " mock-gen - 建立 mock 資料"
@echo " fmt - 格式化代碼"
@echo " build - 編譯專案"
@echo " run - 運行專案"
@ -81,6 +76,5 @@ help: # 顯示幫助信息
@echo " docker-build - 構建 Docker 映像"
@echo " docker-run - 運行 Docker 容器"
@echo " install - 安裝依賴"
@echo " lint - 代碼檢查"
@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
Host: 0.0.0.0
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 (
// 成功響應
OKResp {
RespOK {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data,omitempty"`
@ -26,8 +26,7 @@ type (
BaseReq {}
VerifyHeader {
Token string `header:"token" validate:"required"`
Authorization {
Authorization string `header:"Authorization" validate:"required"`
}
)

View File

@ -1,554 +1,307 @@
syntax = "v1"
// ================ 請求/響應結構 ================
// =================================================================
// Type: 授權與驗證 (Auth)
// =================================================================
type (
// 創建帳號請求
CreateUserAccountReq {
LoginID string `json:"login_id" validate:"required,min=3,max=50"`
Platform string `json:"platform" validate:"required,oneof=platform google line apple"`
Token string `json:"token" validate:"required,min=8,max=128"`
// CredentialsPayload 傳統帳號密碼註冊的資料
CredentialsPayload {
Password string `json:"password" validate:"required,min=8,max=128"` // 密碼 (後端應使用 bcrypt 進行雜湊)
PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password"` // 確認密碼
}
// 綁定用戶請求
BindingUserReq {
UID string `json:"uid" validate:"required,min=1,max=50"`
LoginID string `json:"login_id" validate:"required,min=3,max=50"`
Type int `json:"type" validate:"required,oneof=1 2 3"`
// PlatformPayload 第三方平台註冊的資料
PlatformPayload {
Provider string `json:"provider" validate:"required,oneof=google line apple"` // 平台名稱
Token string `json:"token" validate:"required"` // 平台提供的 Access Token 或 ID Token
}
// 綁定用戶響應
BindingUserResp {
UID string `json:"uid"`
LoginID string `json:"login_id"`
Type int `json:"type"`
// RegisterReq 註冊請求 (整合了兩種方式)
LoginReq {
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' 時使用
}
// 創建用戶資料請求
CreateUserInfoReq {
UID string `json:"uid" validate:"required,min=1,max=50"`
AlarmType int `json:"alarm_type" validate:"required,oneof=0 1 2"`
Status int `json:"status" validate:"required,oneof=0 1 2 3"`
Language string `json:"language" validate:"required,min=2,max=10"`
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"`
Gender *int64 `json:"gender,omitempty" validate:"omitempty,oneof=0 1 2"`
Birthdate *int64 `json:"birthdate,omitempty" validate:"omitempty,min=19000101,max=21001231"`
PhoneNumber *string `json:"phone_number,omitempty" validate:"omitempty,min=10,max=20"`
Email *string `json:"email,omitempty" validate:"omitempty,email"`
Address *string `json:"address,omitempty" validate:"omitempty,max=200"`
// LoginResp 登入/註冊成功後的響應
LoginResp {
UID string `json:"uid"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"` // 通常固定為 "Bearer"
}
// --- 密碼重設流程 ---
// RequestPasswordResetReq 請求發送「忘記密碼」的驗證碼
RequestPasswordResetReq {
Identifier string `json:"identifier" validate:"required,email|phone"` // 使用者帳號 (信箱或手機)
AccountType string `json:"account_type" validate:"required,oneof=email phone"`
}
// 獲取帳號資訊響應
GetAccountInfoResp {
Data CreateUserAccountReq `json:"data"`
// VerifyCodeReq 驗證碼校驗 (通用)
VerifyCodeReq {
Identifier string `json:"identifier" validate:"required"`
VerifyCode string `json:"verify_code" validate:"required,len=6"`
}
// 更新用戶資料請求
UpdateUserInfoReq {
UID string `json:"uid" validate:"required,min=1,max=50"`
Language *string `json:"language,omitempty" validate:"omitempty,min=2,max=10"`
Currency *string `json:"currency,omitempty" validate:"omitempty,min=3,max=3"`
NickName *string `json:"nick_name,omitempty" validate:"omitempty,min=1,max=50"`
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"`
// ResetPasswordReq 使用已驗證的 Code 來重設密碼 只有用帳號密碼的才能發送重設密碼
ResetPasswordReq {
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"` // 確認新密碼
}
// 獲取 UID 請求
GetUIDByAccountReq {
Account string `json:"account" validate:"required,min=3,max=50"`
// --- 4. 權杖刷新 ---
// RefreshTokenReq 更新 AccessToken
RefreshTokenReq {
RefreshToken string `json:"refresh_token" validate:"required"`
}
// 獲取 UID 響應
GetUIDByAccountResp {
UID string `json:"uid"`
Account string `json:"account"`
}
// 更新密碼請求
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"`
// RefreshTokenResp 刷新權杖後的響應
RefreshTokenResp {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"` // 可選:某些策略下刷新後也會換發新的 Refresh Token
TokenType string `json:"token_type"`
}
)
// ================ 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(
group: account
prefix: /api/v1/account
group: auth
prefix: /api/v1/auth
schemes: https
timeout: 10s
)
service gateway {
@doc(
summary: "創建帳號"
description: "創建新的帳號,支援多平台登入"
summary: "註冊新帳號"
description: "使用傳統帳號密碼或第三方平台進行註冊。成功後直接返回登入後的 Token 資訊。"
)
/*
@respdoc-201 () // 創建成功
@respdoc-400 (
40001: (ErrorResp) 帳號格式錯誤
40002: (ErrorResp) 密碼強度不足
40003: (ErrorResp) 平台類型無效
) // 客戶端錯誤
@respdoc-409 (ErrorResp) // 帳號已存在
@respdoc-500 (ErrorResp) // 服務器錯誤
@respdoc-200 (LoginResp) // 註冊成功,並返回 Token
@respdoc-400 (ErrorResp) "請求參數格式錯誤"
@respdoc-409 (ErrorResp) "帳號已被註冊" // 409 Conflict: 資源衝突
@respdoc-500 (ErrorResp) // 伺服器內部錯誤
*/
@handler CreateUserAccount
post /create (CreateUserAccountReq) returns ()
@handler register
post /register (LoginReq) returns (LoginResp)
@doc(
summary: "獲取帳號資訊"
description: "根據帳號獲取用戶的帳號資訊"
summary: "使用者登入"
description: "使用傳統帳號密碼或第三方平台 Token 進行登入,以創建一個新的會話(Session)。"
)
/*
@respdoc-200 (GetAccountInfoResp) // 獲取成功
@respdoc-400 (
40001: (ErrorResp) 帳號格式錯誤
40004: (ErrorResp) 參數驗證失敗
) // 客戶端錯誤
@respdoc-404 (ErrorResp) // 帳號不存在
@respdoc-500 (ErrorResp) // 服務器錯誤
*/
@handler GetUserAccountInfo
post /info (GetUIDByAccountReq) returns (GetAccountInfoResp)
/*
@respdoc-200 (LoginResp) // 登入成功
@respdoc-400 (ErrorResp) "請求參數格式錯誤"
@respdoc-401 (ErrorResp) "帳號或密碼錯誤 / 無效的平台 Token" // 401 Unauthorized
@respdoc-500 (ErrorResp) // 伺服器內部錯誤
*/
@handler login
post /sessions (LoginReq) returns (LoginResp)
@doc(
summary: "更新用戶密碼"
description: "更新指定帳號的密碼"
summary: "刷新 Access Token"
description: "使用有效的 Refresh Token 來獲取一組新的 Access Token 和 Refresh Token。"
)
/*
@respdoc-200 (OKResp) // 更新成功
@respdoc-400 (
40001: (ErrorResp) 帳號格式錯誤
40002: (ErrorResp) 密碼強度不足
40003: (ErrorResp) 平台類型無效
) // 客戶端錯誤
@respdoc-404 (ErrorResp) // 帳號不存在
@respdoc-500 (ErrorResp) // 服務器錯誤
*/
@handler UpdateUserToken
post /update-token (UpdateTokenReq) returns (OKResp)
/*
@respdoc-200 (RefreshTokenResp) // 刷新成功
@respdoc-400 (ErrorResp) "請求參數格式錯誤"
@respdoc-401 (ErrorResp) "無效或已過期的 Refresh Token" // 401 Unauthorized
@respdoc-500 (ErrorResp) // 伺服器內部錯誤
*/
@handler refreshToken
post /sessions/refresh (RefreshTokenReq) returns (RefreshTokenResp)
@doc(
summary: "獲取用戶 UID"
description: "根據帳號獲取對應的用戶 UID"
summary: "請求發送密碼重設驗證碼"
description: "為指定的 email 或 phone 發送一個一次性的密碼重設驗證碼。"
)
/*
@respdoc-200 (GetUIDByAccountResp) // 獲取成功
@respdoc-400 (
40001: (ErrorResp) 帳號格式錯誤
40004: (ErrorResp) 參數驗證失敗
) // 客戶端錯誤
@respdoc-404 (ErrorResp) // 帳號不存在
@respdoc-500 (ErrorResp) // 服務器錯誤
*/
@handler GetUIDByAccount
post /uid (GetUIDByAccountReq) returns (GetUIDByAccountResp)
/*
@respdoc-200 (RespOK) // 請求成功 (為安全起見,即使帳號不存在也應返回成功)
@respdoc-400 (ErrorResp) "請求參數格式錯誤"
@respdoc-429 (ErrorResp) "請求過於頻繁" // 429 Too Many Requests
@respdoc-500 (ErrorResp) // 伺服器內部錯誤
*/
@handler requestPasswordReset
post /password-resets/request (RequestPasswordResetReq) returns (RespOK)
@doc(
summary: "綁定帳號"
description: "將帳號綁定到指定的用戶 UID"
summary: "校驗密碼重設驗證碼"
description: "在實際重設密碼前,先驗證使用者輸入的驗證碼是否正確。"
)
/*
@respdoc-200 (BindingUserResp) // 綁定成功
@respdoc-400 (
40001: (ErrorResp) 帳號格式錯誤
40004: (ErrorResp) 參數驗證失敗
40005: (ErrorResp) UID 格式錯誤
) // 客戶端錯誤
@respdoc-404 (ErrorResp) // 帳號不存在
@respdoc-409 (ErrorResp) // 帳號已綁定
@respdoc-500 (ErrorResp) // 服務器錯誤
*/
@handler BindAccount
post /bind (BindingUserReq) returns (BindingUserResp)
/*
@respdoc-200 (RespOK) // 驗證碼正確
@respdoc-400 (ErrorResp) "驗證碼無效或已過期"
@respdoc-500 (ErrorResp) // 伺服器內部錯誤
*/
@handler verifyPasswordResetCode
post /password-resets/verify (VerifyCodeReq) returns (RespOK)
@doc(
summary: "綁定用戶資料"
description: "初次綁定用戶的詳細資料"
summary: "執行密碼重設"
description: "使用有效的驗證碼來設定新的密碼。"
)
/*
@respdoc-200 (OKResp) // 綁定成功
@respdoc-400 (
40004: (ErrorResp) 參數驗證失敗
40005: (ErrorResp) UID 格式錯誤
40006: (ErrorResp) 郵箱格式錯誤
40007: (ErrorResp) 電話格式錯誤
) // 客戶端錯誤
@respdoc-404 (ErrorResp) // 用戶不存在
@respdoc-409 (ErrorResp) // 用戶資料已存在
@respdoc-422 (ErrorResp) // 用戶資料不完整
@respdoc-500 (ErrorResp) // 服務器錯誤
*/
@handler BindUserInfo
post /bind-info (CreateUserInfoReq) returns (OKResp)
@doc(
summary: "綁定驗證 Email"
description: "綁定並驗證用戶的 Email 地址"
)
/*
@respdoc-200 (OKResp) // 綁定成功
@respdoc-400 (
40005: (ErrorResp) UID 格式錯誤
40006: (ErrorResp) 郵箱格式錯誤
) // 客戶端錯誤
@respdoc-404 (ErrorResp) // 用戶不存在
@respdoc-409 (ErrorResp) // 郵箱已綁定
@respdoc-422 (ErrorResp) // 郵箱未驗證
@respdoc-500 (ErrorResp) // 服務器錯誤
*/
@handler BindVerifyEmail
post /bind-email (BindVerifyEmailReq) returns (OKResp)
@doc(
summary: "綁定驗證 Phone"
description: "綁定並驗證用戶的電話號碼"
)
/*
@respdoc-200 (OKResp) // 綁定成功
@respdoc-400 (
40005: (ErrorResp) UID 格式錯誤
40007: (ErrorResp) 電話格式錯誤
) // 客戶端錯誤
@respdoc-404 (ErrorResp) // 用戶不存在
@respdoc-409 (ErrorResp) // 電話已綁定
@respdoc-422 (ErrorResp) // 電話未驗證
@respdoc-500 (ErrorResp) // 服務器錯誤
*/
@handler BindVerifyPhone
post /bind-phone (BindVerifyPhoneReq) returns (OKResp)
@doc(
summary: "更新用戶資料"
description: "更新用戶的個人資料資訊"
)
/*
@respdoc-200 (OKResp) // 更新成功
@respdoc-400 (
40004: (ErrorResp) 參數驗證失敗
40005: (ErrorResp) UID 格式錯誤
40006: (ErrorResp) 郵箱格式錯誤
40007: (ErrorResp) 電話格式錯誤
) // 客戶端錯誤
@respdoc-404 (ErrorResp) // 用戶不存在
@respdoc-422 (ErrorResp) // 用戶資料無效
@respdoc-500 (ErrorResp) // 服務器錯誤
*/
@handler UpdateUserInfo
post /update-info (UpdateUserInfoReq) returns (OKResp)
@doc(
summary: "更新用戶狀態"
description: "更新用戶的帳號狀態"
)
/*
@respdoc-200 (OKResp) // 更新成功
@respdoc-400 (
40004: (ErrorResp) 參數驗證失敗
40005: (ErrorResp) UID 格式錯誤
40008: (ErrorResp) 狀態值無效
) // 客戶端錯誤
@respdoc-404 (ErrorResp) // 用戶不存在
@respdoc-422 (ErrorResp) // 狀態更新無效
@respdoc-500 (ErrorResp) // 服務器錯誤
*/
@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)
/*
@respdoc-200 (RespOK) // 密碼重設成功
@respdoc-400 (ErrorResp) "驗證碼無效或請求參數錯誤"
@respdoc-500 (ErrorResp) // 伺服器內部錯誤
*/
@handler resetPassword
put /password-resets (ResetPasswordReq) returns (RespOK)
}
// =================================================================
// Service: 授權 API - 需要登入 (User Service)
// =================================================================
@server(
group: user
prefix: /api/v1/user
schemes: https
timeout: 10s
middleware: AuthMiddleware // 所有此 group 的路由都需要經過 JWT 驗證
)
service gateway {
@doc(
summary: "取得當前登入的會員資訊(自己)"
)
/*
@respdoc-200 (UserInfoResp) // 成功獲取
@respdoc-401 (ErrorResp) "未授權或 Token 無效"
@respdoc-404 (ErrorResp) "找不到使用者"
@respdoc-500 (ErrorResp) // 伺服器內部錯誤
*/
@handler getUserInfo
get /me (Authorization) returns (UserInfoResp)
@doc(
summary: "更新當前登入的會員資訊"
description: "只更新傳入的欄位,未傳入的欄位將保持不變。"
)
/*
@respdoc-200 (UserInfoResp) // 更新成功,並返回更新後的使用者資訊
@respdoc-400 (ErrorResp) "請求參數格式錯誤"
@respdoc-401 (ErrorResp) "未授權或 Token 無效"
@respdoc-500 (ErrorResp) // 伺服器內部錯誤
*/
@handler updateUserInfo
put /me (UpdateUserInfoReq) returns (UserInfoResp)
@doc(
summary: "修改當前登入使用者的密碼"
description: "必須提供當前密碼以進行驗證。"
)
/*
@respdoc-200 (RespOK) // 密碼修改成功
@respdoc-400 (ErrorResp) "請求參數格式錯誤或新舊密碼不符"
@respdoc-401 (ErrorResp) "未授權或 Token 無效"
@respdoc-403 (ErrorResp) "當前密碼不正確" // 403 Forbidden
@respdoc-500 (ErrorResp) // 伺服器內部錯誤
*/
@handler updatePassword
put /me/password (UpdatePasswordReq) returns (RespOK)
@doc(
summary: "請求發送驗證碼 (用於驗證信箱/手機)"
description: "根據傳入的 `purpose` 發送對應的驗證碼。"
)
/*
@respdoc-200 (RespOK) // 請求已受理
@respdoc-400 (ErrorResp) "請求參數格式錯誤"
@respdoc-401 (ErrorResp) "未授權或 Token 無效"
@respdoc-429 (ErrorResp) "請求過於頻繁" // 429 Too Many Requests
@respdoc-500 (ErrorResp) // 伺服器內部錯誤
*/
@handler requestVerificationCode
post /me/verifications (RequestVerificationCodeReq) returns (RespOK)
@doc(
summary: "提交驗證碼以完成驗證"
description: "提交收到的驗證碼,以完成特定目的的驗證,例如綁定手機或 Email。"
)
/*
@respdoc-200 (RespOK) // 驗證成功
@respdoc-400 (ErrorResp) "驗證碼無效或已過期"
@respdoc-401 (ErrorResp) "未授權或 Token 無效"
@respdoc-500 (ErrorResp) // 伺服器內部錯誤
*/
@handler submitVerificationCode
put /me/verifications (SubmitVerificationCodeReq) returns (RespOK)
}

2
go.mod
View File

@ -4,7 +4,6 @@ go 1.25.1
require (
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
github.com/alicebob/miniredis/v2 v2.35.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/userns v0.1.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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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/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/go.mod h1:eHmWpbX6N6KXQ2xaY71uj5bwfzTaNL8pQc2njYo5Gj0=
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/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
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/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=

View File

@ -1,7 +1,52 @@
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 {
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/svc"
"github.com/zeromicro/go-zero/rest/httpx"
)

View File

@ -1,5 +1,5 @@
// Code generated by goctl. DO NOT EDIT.
// goctl 1.8.1
// goctl 1.8.5
package handler
@ -7,13 +7,58 @@ import (
"net/http"
"time"
auth "backend/internal/handler/auth"
ping "backend/internal/handler/ping"
user "backend/internal/handler/user"
"backend/internal/svc"
"github.com/zeromicro/go-zero/rest"
)
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(
[]rest.Route{
{
@ -26,4 +71,44 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
rest.WithPrefix("/api/v1"),
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"
"backend/internal/svc"
"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 (
"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 {
Config config.Config
Config config.Config
AuthMiddleware rest.Middleware
AccountUC usecase.AccountUseCase
}
func NewServiceContext(c config.Config) *ServiceContext {
rds, err := redis.NewRedis(c.RedisConf)
if err != nil {
panic(err)
}
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.
// goctl 1.8.1
// goctl 1.8.5
package types
type Authorization struct {
Authorization string `header:"Authorization" validate:"required"`
}
type BaseReq struct {
}
type BaseResponse struct {
Status Status `json:"status"` // 狀態
Data interface{} `json:"data"` // 資料
type CredentialsPayload struct {
Password string `json:"password" validate:"required,min=8,max=128"` // 密碼 (後端應使用 bcrypt 進行雜湊)
PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password"` // 確認密碼
}
type Pager struct {
Total int64 `json:"total"`
PageSize int64 `json:"page_size"`
PageIndex int64 `json:"page_index"`
}
type RespOK struct {
}
type Status struct {
Code int64 `json:"code"` // 狀態碼
Message string `json:"message"` // 訊息
Data interface{} `json:"data,omitempty"` // 可選的資料,當有返回時才出現
type ErrorResp struct {
Code int `json:"code"`
Msg string `json:"msg"`
Details string `json:"details,omitempty"`
Error interface{} `json:"error,omitempty"` // 可選的錯誤信息
}
type VerifyHeader struct {
Token string `header:"token" validate:"required"`
type LoginReq struct {
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

@ -7,7 +7,7 @@ import (
func TestConf_DefaultValues(t *testing.T) {
conf := &Conf{}
// Test default values
if conf.Schema != "" {
t.Errorf("Expected empty Schema, got %s", conf.Schema)
@ -66,7 +66,7 @@ func TestConf_WithValues(t *testing.T) {
EnableStandardReadWriteSplitMode: true,
ConnectTimeoutMs: 5000,
}
// Test set values
if conf.Schema != "mongodb" {
t.Errorf("Expected 'mongodb' Schema, got %s", conf.Schema)

View File

@ -11,18 +11,18 @@ import (
func TestMgoDecimal_InterfaceCompliance(t *testing.T) {
encoder := &MgoDecimal{}
decoder := &MgoDecimal{}
// Test that they implement the required interfaces
var _ bson.ValueEncoder = encoder
var _ bson.ValueDecoder = decoder
// Test that they can be used in TypeCodec
codec := TypeCodec{
ValueType: reflect.TypeOf(decimal.Decimal{}),
Encoder: encoder,
Decoder: decoder,
}
if codec.Encoder != encoder {
t.Error("Expected encoder to be set correctly")
}
@ -33,15 +33,15 @@ func TestMgoDecimal_InterfaceCompliance(t *testing.T) {
func TestMgoDecimal_EncodeValue_InvalidType(t *testing.T) {
encoder := &MgoDecimal{}
// Test with invalid type
value := reflect.ValueOf("not a decimal")
err := encoder.EncodeValue(bson.EncodeContext{}, nil, value)
if err == nil {
t.Error("Expected error for invalid type, got nil")
}
expectedErr := "value not a decimal to encode is not of type decimal.Decimal"
if err.Error() != expectedErr {
t.Errorf("Expected error '%s', got '%s'", expectedErr, err.Error())
@ -61,7 +61,7 @@ func TestDecimalConversion(t *testing.T) {
{"9999999999999999999.999999999999999", "9999999999999999999.999999999999999"},
{"-9999999999999999999.999999999999999", "-9999999999999999999.999999999999999"},
}
for _, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
// Test decimal to string conversion
@ -69,17 +69,17 @@ func TestDecimalConversion(t *testing.T) {
if err != nil {
t.Fatalf("Failed to create decimal from %s: %v", tc.input, err)
}
if dec.String() != tc.expected {
t.Errorf("Expected %s, got %s", tc.expected, dec.String())
}
// Test BSON decimal128 conversion
primDec, err := bson.ParseDecimal128(dec.String())
if err != nil {
t.Fatalf("Failed to parse decimal128 from %s: %v", dec.String(), err)
}
if primDec.String() != tc.expected {
t.Errorf("Expected %s, got %s", tc.expected, primDec.String())
}
@ -96,14 +96,14 @@ func TestDecimalConversionErrors(t *testing.T) {
"123.45.67",
"abc123",
}
for _, invalid := range invalidCases {
t.Run(invalid, func(t *testing.T) {
_, err := decimal.NewFromString(invalid)
if err == nil {
t.Errorf("Expected error for invalid decimal string: %s", invalid)
}
_, err = bson.ParseDecimal128(invalid)
if err == nil {
t.Errorf("Expected error for invalid decimal128 string: %s", invalid)
@ -125,7 +125,7 @@ func TestDecimalEdgeCases(t *testing.T) {
{"positive large", decimal.NewFromInt(999999999999999), "999999999999999"},
{"negative large", decimal.NewFromInt(-999999999999999), "-999999999999999"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Test conversion to BSON Decimal128
@ -133,13 +133,13 @@ func TestDecimalEdgeCases(t *testing.T) {
if err != nil {
t.Fatalf("Failed to parse decimal128 from %s: %v", tc.value.String(), err)
}
// Test conversion back to decimal
dec, err := decimal.NewFromString(primDec.String())
if err != nil {
t.Fatalf("Failed to create decimal from %s: %v", primDec.String(), err)
}
if !dec.Equal(tc.value) {
t.Errorf("Round trip failed: original=%s, result=%s", tc.value.String(), dec.String())
}
@ -150,7 +150,7 @@ func TestDecimalEdgeCases(t *testing.T) {
// Test error handling in encoder
func TestMgoDecimal_EncoderErrors(t *testing.T) {
encoder := &MgoDecimal{}
testCases := []struct {
name string
value interface{}
@ -159,7 +159,7 @@ func TestMgoDecimal_EncoderErrors(t *testing.T) {
{"int", 123},
{"float", 123.45},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
value := reflect.ValueOf(tc.value)
@ -183,26 +183,26 @@ func TestDecimalPrecision(t *testing.T) {
"0.0000001",
"0.00000001",
}
for _, tc := range testCases {
t.Run(tc, func(t *testing.T) {
dec, err := decimal.NewFromString(tc)
if err != nil {
t.Fatalf("Failed to create decimal from %s: %v", tc, err)
}
// Test conversion to BSON Decimal128
primDec, err := bson.ParseDecimal128(dec.String())
if err != nil {
t.Fatalf("Failed to parse decimal128 from %s: %v", dec.String(), err)
}
// Test conversion back to decimal
result, err := decimal.NewFromString(primDec.String())
if err != nil {
t.Fatalf("Failed to create decimal from %s: %v", primDec.String(), err)
}
if !result.Equal(dec) {
t.Errorf("Precision lost: original=%s, result=%s", dec.String(), result.String())
}
@ -218,26 +218,26 @@ func TestDecimalLargeNumbers(t *testing.T) {
"100000000000000000",
"1000000000000000000",
}
for _, tc := range testCases {
t.Run(tc, func(t *testing.T) {
dec, err := decimal.NewFromString(tc)
if err != nil {
t.Fatalf("Failed to create decimal from %s: %v", tc, err)
}
// Test conversion to BSON Decimal128
primDec, err := bson.ParseDecimal128(dec.String())
if err != nil {
t.Fatalf("Failed to parse decimal128 from %s: %v", dec.String(), err)
}
// Test conversion back to decimal
result, err := decimal.NewFromString(primDec.String())
if err != nil {
t.Fatalf("Failed to create decimal from %s: %v", primDec.String(), err)
}
if !result.Equal(dec) {
t.Errorf("Large number lost: original=%s, result=%s", dec.String(), result.String())
}
@ -248,7 +248,7 @@ func TestDecimalLargeNumbers(t *testing.T) {
// Benchmark tests
func BenchmarkMgoDecimal_ParseDecimal128(b *testing.B) {
dec := decimal.NewFromFloat(123.45)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = bson.ParseDecimal128(dec.String())
@ -257,7 +257,7 @@ func BenchmarkMgoDecimal_ParseDecimal128(b *testing.B) {
func BenchmarkMgoDecimal_DecimalFromString(b *testing.B) {
primDec, _ := bson.ParseDecimal128("123.45")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = decimal.NewFromString(primDec.String())
@ -266,10 +266,10 @@ func BenchmarkMgoDecimal_DecimalFromString(b *testing.B) {
func BenchmarkMgoDecimal_RoundTrip(b *testing.B) {
dec := decimal.NewFromFloat(123.45)
b.ResetTimer()
for i := 0; i < b.N; i++ {
primDec, _ := bson.ParseDecimal128(dec.String())
_, _ = decimal.NewFromString(primDec.String())
}
}
}

View File

@ -60,7 +60,7 @@ func (dc *DocumentDBWithCache) DeleteOne(ctx context.Context, key string, filter
listerOpts = append(listerOpts, builder)
}
}
val, err := dc.GetClient().DeleteOne(ctx, filter, listerOpts...)
if err != nil {
return 0, err
@ -102,7 +102,7 @@ func (dc *DocumentDBWithCache) FindOne(ctx context.Context, key string, v, filte
listerOpts = append(listerOpts, builder)
}
}
return dc.GetClient().FindOne(ctx, v, filter, listerOpts...)
})
}
@ -132,7 +132,7 @@ func (dc *DocumentDBWithCache) FindOneAndDelete(ctx context.Context, key string,
listerOpts = append(listerOpts, builder)
}
}
if err := dc.GetClient().FindOneAndDelete(ctx, v, filter, listerOpts...); err != nil {
return err
}
@ -171,7 +171,7 @@ func (dc *DocumentDBWithCache) FindOneAndReplace(ctx context.Context, key string
listerOpts = append(listerOpts, builder)
}
}
if err := dc.GetClient().FindOneAndReplace(ctx, v, filter, replacement, listerOpts...); err != nil {
return err
}
@ -195,7 +195,7 @@ func (dc *DocumentDBWithCache) InsertOne(ctx context.Context, key string, docume
listerOpts = append(listerOpts, builder)
}
}
res, err := dc.GetClient().Collection.InsertOne(ctx, document, listerOpts...)
if err != nil {
return nil, err
@ -236,7 +236,7 @@ func (dc *DocumentDBWithCache) UpdateByID(ctx context.Context, key string, id, u
listerOpts = append(listerOpts, builder)
}
}
res, err := dc.GetClient().Collection.UpdateByID(ctx, id, update, listerOpts...)
if err != nil {
return nil, err
@ -277,7 +277,7 @@ func (dc *DocumentDBWithCache) UpdateMany(ctx context.Context, keys []string, fi
listerOpts = append(listerOpts, builder)
}
}
res, err := dc.GetClient().Collection.UpdateMany(ctx, filter, update, listerOpts...)
if err != nil {
return nil, err
@ -318,7 +318,7 @@ func (dc *DocumentDBWithCache) UpdateOne(ctx context.Context, key string, filter
listerOpts = append(listerOpts, builder)
}
}
res, err := dc.GetClient().Collection.UpdateOne(ctx, filter, update, listerOpts...)
if err != nil {
return nil, err
@ -336,4 +336,4 @@ func (dc *DocumentDBWithCache) UpdateOne(ctx context.Context, key string, filter
// MustModelCache returns a cache cluster.
func MustModelCache(conf cache.CacheConf, opts ...cache.Option) cache.Cache {
return cache.New(conf, singleFlight, stats, mongo.ErrNoDocuments, opts...)
}
}

View File

@ -16,23 +16,23 @@ func TestDocumentDBWithCache_MustDocumentDBWithCache(t *testing.T) {
Host: "localhost:27017",
Database: "testdb",
}
collection := "testcollection"
cacheConf := cache.CacheConf{}
// This will panic if MongoDB is not available, so we need to handle it
defer func() {
if r := recover(); r != nil {
t.Logf("Expected panic in test environment: %v", r)
}
}()
db, err := MustDocumentDBWithCache(conf, collection, cacheConf, nil, nil)
if err != nil {
t.Logf("MongoDB connection failed (expected in test environment): %v", err)
return
}
if db == nil {
t.Error("Expected DocumentDBWithCache to be non-nil")
}
@ -43,39 +43,39 @@ func TestDocumentDBWithCache_CacheOperations(t *testing.T) {
Host: "localhost:27017",
Database: "testdb",
}
collection := "testcollection"
cacheConf := cache.CacheConf{}
ctx := context.Background()
db, err := MustDocumentDBWithCache(conf, collection, cacheConf, nil, nil)
if err != nil {
t.Skip("Skipping test - MongoDB not available")
return
}
// Test cache operations
key := "test-key"
value := "test-value"
// Test SetCache
err = db.SetCache(key, value)
if err != nil {
t.Errorf("Failed to set cache: %v", err)
}
// Test GetCache
var cachedValue string
err = db.GetCache(key, &cachedValue)
if err != nil {
t.Errorf("Failed to get cache: %v", err)
}
if cachedValue != value {
t.Errorf("Expected cached value %s, got %s", value, cachedValue)
}
// Test DelCache
err = db.DelCache(ctx, key)
if err != nil {
@ -88,106 +88,106 @@ func TestDocumentDBWithCache_CRUDOperations(t *testing.T) {
Host: "localhost:27017",
Database: "testdb",
}
collection := "testcollection"
cacheConf := cache.CacheConf{}
ctx := context.Background()
db, err := MustDocumentDBWithCache(conf, collection, cacheConf, nil, nil)
if err != nil {
t.Skip("Skipping test - MongoDB not available")
return
}
// Test data
testDoc := bson.M{
"name": "test",
"value": 123,
"price": decimal.NewFromFloat(99.99),
}
// Test InsertOne
result, err := db.InsertOne(ctx, collection, testDoc)
if err != nil {
t.Errorf("Failed to insert document: %v", err)
}
insertedID := result.InsertedID
if insertedID == nil {
t.Error("Expected inserted ID to be non-nil")
}
// Test FindOne
var foundDoc bson.M
err = db.FindOne(ctx, collection, bson.M{"_id": insertedID}, &foundDoc)
if err != nil {
t.Errorf("Failed to find document: %v", err)
}
if foundDoc["name"] != "test" {
t.Errorf("Expected name 'test', got %v", foundDoc["name"])
}
// Test UpdateOne
update := bson.M{"$set": bson.M{"value": 456}}
updateResult, err := db.UpdateOne(ctx, collection, bson.M{"_id": insertedID}, update)
if err != nil {
t.Errorf("Failed to update document: %v", err)
}
if updateResult.ModifiedCount != 1 {
t.Errorf("Expected 1 modified document, got %d", updateResult.ModifiedCount)
}
// Test UpdateByID
updateByID := bson.M{"$set": bson.M{"value": 789}}
updateByIDResult, err := db.UpdateByID(ctx, collection, insertedID, updateByID)
if err != nil {
t.Errorf("Failed to update document by ID: %v", err)
}
if updateByIDResult.ModifiedCount != 1 {
t.Errorf("Expected 1 modified document, got %d", updateByIDResult.ModifiedCount)
}
// Test UpdateMany
updateMany := bson.M{"$set": bson.M{"updated": true}}
updateManyResult, err := db.UpdateMany(ctx, []string{collection}, bson.M{"_id": insertedID}, updateMany)
if err != nil {
t.Errorf("Failed to update many documents: %v", err)
}
if updateManyResult.ModifiedCount != 1 {
t.Errorf("Expected 1 modified document, got %d", updateManyResult.ModifiedCount)
}
// Test FindOneAndReplace
replacement := bson.M{
"name": "replaced",
"value": 999,
"price": decimal.NewFromFloat(199.99),
}
var replacedDoc bson.M
err = db.FindOneAndReplace(ctx, collection, bson.M{"_id": insertedID}, replacement, &replacedDoc)
if err != nil {
t.Errorf("Failed to find and replace document: %v", err)
}
// Test FindOneAndDelete
var deletedDoc bson.M
err = db.FindOneAndDelete(ctx, collection, bson.M{"_id": insertedID}, &deletedDoc)
if err != nil {
t.Errorf("Failed to find and delete document: %v", err)
}
// Test DeleteOne
deleteResult, err := db.DeleteOne(ctx, collection, bson.M{"_id": insertedID})
if err != nil {
t.Errorf("Failed to delete document: %v", err)
}
if deleteResult != 0 { // Should be 0 since we already deleted it
t.Errorf("Expected 0 deleted documents, got %d", deleteResult)
}
@ -198,17 +198,17 @@ func TestDocumentDBWithCache_MustModelCache(t *testing.T) {
Host: "localhost:27017",
Database: "testdb",
}
collection := "testcollection"
cacheConf := cache.CacheConf{}
db, err := MustDocumentDBWithCache(conf, collection, cacheConf, nil, nil)
if err != nil {
t.Skip("Skipping test - MongoDB not available")
return
}
// Test that we got a valid DocumentDBWithCache
if db == nil {
t.Error("Expected DocumentDBWithCache to be non-nil")
@ -220,12 +220,12 @@ func TestDocumentDBWithCache_ErrorHandling(t *testing.T) {
invalidConf := &Conf{
Host: "invalid-host:99999",
}
collection := "testcollection"
cacheConf := cache.CacheConf{}
_, err := MustDocumentDBWithCache(invalidConf, collection, cacheConf, nil, nil)
// This should fail
if err == nil {
t.Error("Expected error with invalid host, got nil")
@ -237,24 +237,24 @@ func TestDocumentDBWithCache_ContextHandling(t *testing.T) {
Host: "localhost:27017",
Database: "testdb",
}
collection := "testcollection"
cacheConf := cache.CacheConf{}
// Test with timeout context
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
db, err := MustDocumentDBWithCache(conf, collection, cacheConf, nil, nil)
// Use ctx to avoid unused variable warning
_ = ctx
if err != nil {
t.Logf("MongoDB connection failed (expected in test environment): %v", err)
return
}
if db == nil {
t.Error("Expected DocumentDBWithCache to be non-nil")
}
@ -265,45 +265,45 @@ func TestDocumentDBWithCache_WithDecimalValues(t *testing.T) {
Host: "localhost:27017",
Database: "testdb",
}
collection := "testcollection"
cacheConf := cache.CacheConf{}
ctx := context.Background()
db, err := MustDocumentDBWithCache(conf, collection, cacheConf, nil, nil)
if err != nil {
t.Skip("Skipping test - MongoDB not available")
return
}
// Test with decimal values
testDoc := bson.M{
"name": "decimal-test",
"price": decimal.NewFromFloat(123.45),
"amount": decimal.NewFromFloat(999.99),
}
// Insert document with decimal values
result, err := db.InsertOne(ctx, collection, testDoc)
if err != nil {
t.Errorf("Failed to insert document with decimal values: %v", err)
}
insertedID := result.InsertedID
// Find document with decimal values
var foundDoc bson.M
err = db.FindOne(ctx, collection, bson.M{"_id": insertedID}, &foundDoc)
if err != nil {
t.Errorf("Failed to find document with decimal values: %v", err)
}
// Verify decimal values
if foundDoc["name"] != "decimal-test" {
t.Errorf("Expected name 'decimal-test', got %v", foundDoc["name"])
}
// Clean up
_, err = db.DeleteOne(ctx, collection, bson.M{"_id": insertedID})
if err != nil {
@ -316,18 +316,18 @@ func TestDocumentDBWithCache_WithObjectID(t *testing.T) {
Host: "localhost:27017",
Database: "testdb",
}
collection := "testcollection"
cacheConf := cache.CacheConf{}
ctx := context.Background()
db, err := MustDocumentDBWithCache(conf, collection, cacheConf, nil, nil)
if err != nil {
t.Skip("Skipping test - MongoDB not available")
return
}
// Test with ObjectID
objectID := bson.NewObjectID()
testDoc := bson.M{
@ -335,30 +335,30 @@ func TestDocumentDBWithCache_WithObjectID(t *testing.T) {
"name": "objectid-test",
"value": 123,
}
// Insert document with ObjectID
result, err := db.InsertOne(ctx, collection, testDoc)
if err != nil {
t.Errorf("Failed to insert document with ObjectID: %v", err)
}
insertedID := result.InsertedID
// Verify ObjectID
if insertedID != objectID {
t.Errorf("Expected ObjectID %v, got %v", objectID, insertedID)
}
// Find document by ObjectID
var foundDoc bson.M
err = db.FindOne(ctx, collection, bson.M{"_id": objectID}, &foundDoc)
if err != nil {
t.Errorf("Failed to find document by ObjectID: %v", err)
}
// Clean up
_, err = db.DeleteOne(ctx, collection, bson.M{"_id": objectID})
if err != nil {
t.Errorf("Failed to clean up document: %v", err)
}
}
}

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 {
SetKeysDoc := bson.D{}
for index, _ := range keys {
for index := range keys {
key := keys[index]
sort := sorts[index]
SetKeysDoc = append(SetKeysDoc, bson.E{Key: key, Value: sort})

View File

@ -27,7 +27,7 @@ func TestWithTypeCodec(t *testing.T) {
if codec.Decoder == nil {
t.Error("Expected Decoder to be set")
}
// Test WithTypeCodec function
option := WithTypeCodec(codec)
if option == nil {
@ -95,20 +95,20 @@ func TestTypeCodec_InterfaceCompliance(t *testing.T) {
func TestMgoDecimal_WithRegistry(t *testing.T) {
// Test that MgoDecimal can be used with a registry
option := SetCustomDecimalType()
// Test that the option is created
if option == nil {
t.Error("Expected option to be non-nil")
}
// Test basic decimal operations
dec := decimal.NewFromFloat(123.45)
// Test that decimal operations work
if dec.IsZero() {
t.Error("Expected decimal to be non-zero")
}
// Test string conversion
decStr := dec.String()
if decStr != "123.45" {
@ -199,18 +199,18 @@ func TestWithTypeCodec_EdgeCases(t *testing.T) {
func TestSetCustomDecimalType_MultipleCalls(t *testing.T) {
// Test calling SetCustomDecimalType multiple times
// First call
option1 := SetCustomDecimalType()
// Second call should not panic
option2 := SetCustomDecimalType()
// Options should be valid
if option1 == nil {
t.Error("Expected option1 to be non-nil")
}
if option2 == nil {
t.Error("Expected option2 to be non-nil")
}
@ -219,13 +219,12 @@ func TestSetCustomDecimalType_MultipleCalls(t *testing.T) {
func TestInitMongoOptions_ReturnType(t *testing.T) {
conf := Conf{}
opts := InitMongoOptions(conf)
// Test that the returned type is correct
if opts == nil {
t.Error("Expected options to be non-nil")
}
// Test that we can use the options (basic type check)
var _ mon.Option = opts
}

View File

@ -7,25 +7,25 @@ package domain
const (
// DefaultBcryptCost is the default cost for bcrypt password hashing
DefaultBcryptCost = 10
// MinPasswordLength is the minimum required password length
MinPasswordLength = 8
// MaxPasswordLength is the maximum allowed password length
MaxPasswordLength = 128
// DefaultVerifyCodeDigits is the default number of digits for verification codes
DefaultVerifyCodeDigits = 6
// MinVerifyCodeDigits is the minimum number of digits for verification codes
MinVerifyCodeDigits = 4
// MaxVerifyCodeDigits is the maximum number of digits for verification codes
MaxVerifyCodeDigits = 10
// DefaultCacheExpiration is the default cache expiration time in seconds
DefaultCacheExpiration = 3600
// MaxRetryAttempts is the maximum number of retry attempts for operations
MaxRetryAttempts = 3
)

View File

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

View File

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

View File

@ -5,28 +5,29 @@ import (
"time"
"backend/pkg/member/domain/member"
"go.mongodb.org/mongo-driver/v2/bson"
)
// User represents a user profile with personal information and preferences.
// It contains detailed user data that is separate from authentication credentials.
type User struct {
ID bson.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
UID string `bson:"uid"` // Unique user identifier
AvatarURL *string `bson:"avatar_url,omitempty"` // User avatar URL (optional)
FullName *string `bson:"full_name,omitempty"` // User's full name
Nickname *string `bson:"nickname,omitempty"` // User's nickname (optional)
GenderCode *int64 `bson:"gender_code,omitempty"` // Gender code
Birthdate *int64 `bson:"birthdate,omitempty"` // Birth date (format: 19930417)
Address *string `bson:"address,omitempty"` // User's address
ID bson.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
UID string `bson:"uid"` // Unique user identifier
AvatarURL *string `bson:"avatar_url,omitempty"` // User avatar URL (optional)
FullName *string `bson:"full_name,omitempty"` // User's full name
Nickname *string `bson:"nickname,omitempty"` // User's nickname (optional)
GenderCode *int64 `bson:"gender_code,omitempty"` // Gender code
Birthdate *int64 `bson:"birthdate,omitempty"` // Birth date (format: 19930417)
Address *string `bson:"address,omitempty"` // User's address
AlarmCategory member.AlarmType `bson:"alarm_category"` // Alert notification settings
UserStatus member.Status `bson:"user_status"` // User account status
PreferredLanguage string `bson:"preferred_language"` // User's preferred language
Currency string `bson:"currency"` // User's preferred currency
PhoneNumber *string `bson:"phone_number,omitempty"` // Phone number (appears after verification)
Email *string `bson:"email,omitempty"` // Email address (appears after verification)
UpdateAt *int64 `bson:"update_at,omitempty" json:"update_at,omitempty"`
CreateAt *int64 `bson:"create_at,omitempty" json:"create_at,omitempty"`
PreferredLanguage string `bson:"preferred_language"` // User's preferred language
Currency string `bson:"currency"` // User's preferred currency
PhoneNumber *string `bson:"phone_number,omitempty"` // Phone number (appears after verification)
Email *string `bson:"email,omitempty"` // Email address (appears after verification)
UpdateAt *int64 `bson:"update_at,omitempty" json:"update_at,omitempty"`
CreateAt *int64 `bson:"create_at,omitempty" json:"create_at,omitempty"`
}
// 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) {
tests := []struct {
name string
name string
password string
wantErr bool
wantErr bool
}{
{
name: "valid password",
name: "valid password",
password: "password123",
wantErr: false,
wantErr: false,
},
{
name: "password too short",
name: "password too short",
password: "123",
wantErr: true,
wantErr: true,
},
{
name: "password too long",
name: "password too long",
password: string(make([]byte, MaxPasswordLength+1)),
wantErr: true,
wantErr: true,
},
{
name: "empty password",
name: "empty password",
password: "",
wantErr: true,
wantErr: true,
},
{
name: "minimum length password",
name: "minimum length password",
password: "12345678",
wantErr: false,
wantErr: false,
},
}

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@ import (
"backend/pkg/member/domain/repository"
mgo "backend/pkg/library/mongo"
"github.com/alicebob/miniredis/v2"
"github.com/stretchr/testify/assert"
"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"
"backend/pkg/library/mongo"
"github.com/zeromicro/go-zero/core/stores/mon"
"go.mongodb.org/mongo-driver/v2/bson"
mongodriver "go.mongodb.org/mongo-driver/v2/mongo"

View File

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

View File

@ -10,13 +10,13 @@ import (
var (
// ErrNotFound is returned when a requested resource is not found
ErrNotFound = mon.ErrNotFound
// ErrInvalidObjectID is returned when an invalid MongoDB ObjectID is provided
ErrInvalidObjectID = errors.New("invalid objectId")
// ErrDuplicateKey is returned when attempting to insert a document with a duplicate key
ErrDuplicateKey = errors.New("duplicate key error")
// ErrInvalidInput is returned when input validation fails
ErrInvalidInput = errors.New("invalid input")
)

View File

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

View File

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

View File

@ -18,7 +18,7 @@ func generateVerifyCode(digits int) (string, error) {
if digits <= 0 {
digits = 6
}
// Validate digit range
if digits < 4 || digits > 10 {
return "", fmt.Errorf("%w: digits must be between 4 and 10, got %d", ErrInvalidDigits, digits)
@ -26,7 +26,7 @@ func generateVerifyCode(digits int) (string, error) {
// Calculate maximum value (10^digits - 1)
exp := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(digits)), nil)
// Generate cryptographically secure random number
randomNumber, err := rand.Int(rand.Reader, exp)
if err != nil {
@ -35,6 +35,6 @@ func generateVerifyCode(digits int) (string, error) {
// Convert to string with zero padding
verifyCode := fmt.Sprintf("%0*d", digits, randomNumber)
return verifyCode, nil
}

View File

@ -2,6 +2,7 @@ package usecase
import (
"errors"
"golang.org/x/crypto/bcrypt"
)
@ -14,11 +15,11 @@ func HashPassword(password string, cost int) (string, error) {
if password == "" {
return "", ErrInvalidPassword
}
if cost < bcrypt.MinCost || cost > bcrypt.MaxCost {
cost = bcrypt.DefaultCost
}
bytes, err := bcrypt.GenerateFromPassword([]byte(password), cost)
return string(bytes), err
}
@ -29,7 +30,7 @@ func CheckPasswordHash(password, hash string) bool {
if password == "" || hash == "" {
return false
}
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
@ -40,11 +41,11 @@ func GetHashingCost(hashedPassword []byte) int {
if len(hashedPassword) == 0 {
return 0
}
cost, err := bcrypt.Cost(hashedPassword)
if err != nil {
return 0
}
return cost
}