feat: add document and redis into project
This commit is contained in:
parent
4828386370
commit
d435713e3b
|
@ -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)
|
10
Makefile
10
Makefile
|
@ -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 - 顯示幫助信息"
|
||||
|
||||
|
|
Binary file not shown.
|
@ -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"
|
|
@ -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
|
2931
gateway.json
2931
gateway.json
File diff suppressed because it is too large
Load Diff
|
@ -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"`
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -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
2
go.mod
|
@ -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
4
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
"backend/internal/logic/ping"
|
||||
"backend/internal/svc"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
|
||||
"backend/internal/svc"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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...)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"backend/pkg/member/domain/repository"
|
||||
|
||||
mgo "backend/pkg/library/mongo"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
|
@ -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")
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
"backend/pkg/member/domain"
|
||||
"backend/pkg/member/domain/repository"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue