feat: add doc generate
This commit is contained in:
parent
bc5ede9d5d
commit
7c309f3508
|
@ -0,0 +1,178 @@
|
|||
# Bug Fix: Required Fields 判定錯誤
|
||||
|
||||
## 🐛 問題描述
|
||||
|
||||
生成的 OpenAPI/Swagger 文檔中,所有結構體字段都被錯誤地標記為 `required`,即使這些字段:
|
||||
- 是**指針類型** (`*string`, `*int`, `*int64`)
|
||||
- 在 JSON 標籤中包含 **`omitempty`**
|
||||
- 在 validate 標籤中包含 **`omitempty`**
|
||||
|
||||
### 問題示例
|
||||
|
||||
```go
|
||||
type CreateUserInfoReq {
|
||||
UID string `json:"uid" validate:"required"` // 必填
|
||||
Avatar *string `json:"avatar,omitempty"` // 應該可選
|
||||
NickName *string `json:"nick_name,omitempty"` // 應該可選
|
||||
Language string `json:"language" validate:"required"` // 必填
|
||||
}
|
||||
```
|
||||
|
||||
**修復前生成的 required 列表:**
|
||||
```json
|
||||
"required": ["uid", "avatar", "nick_name", "language"]
|
||||
```
|
||||
|
||||
**修復後正確的 required 列表:**
|
||||
```json
|
||||
"required": ["uid", "language"]
|
||||
```
|
||||
|
||||
## 🔍 根本原因
|
||||
|
||||
`internal/swagger/swagger.go` 中的 `isRequired()` 函數只檢查 JSON 標籤中的 `optional` 標記,但沒有檢查:
|
||||
1. 字段類型是否為指針(指針類型在 Go 中表示可選)
|
||||
2. JSON 標籤是否包含 `omitempty`
|
||||
|
||||
## ✅ 修復方案
|
||||
|
||||
### 修改的文件
|
||||
- `internal/swagger/swagger.go`
|
||||
|
||||
### 修改內容
|
||||
|
||||
#### 1. 修改 `rangeMemberAndDo` 函數簽名
|
||||
```go
|
||||
// 修改前
|
||||
required := isRequired(ctx, tags)
|
||||
|
||||
// 修改後
|
||||
required := isRequired(ctx, tags, field)
|
||||
```
|
||||
|
||||
#### 2. 修改 `isRequired` 函數
|
||||
```go
|
||||
// 修改前
|
||||
func isRequired(ctx Context, tags *apiSpec.Tags) bool {
|
||||
tag, err := tags.Get(tagJson)
|
||||
if err == nil {
|
||||
return !isOptional(ctx, tag.Options)
|
||||
}
|
||||
// ...
|
||||
return false
|
||||
}
|
||||
|
||||
// 修改後
|
||||
func isRequired(ctx Context, tags *apiSpec.Tags, member apiSpec.Member) bool {
|
||||
// Check if the field type is a pointer - pointer types are optional
|
||||
if _, isPointer := member.Type.(apiSpec.PointerType); isPointer {
|
||||
return false
|
||||
}
|
||||
|
||||
tag, err := tags.Get(tagJson)
|
||||
if err == nil {
|
||||
return !isOptional(ctx, tag.Options)
|
||||
}
|
||||
// ...
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 修改 `isOptional` 函數
|
||||
```go
|
||||
// 修改前
|
||||
func isOptional(_ Context, options []string) bool {
|
||||
for _, option := range options {
|
||||
if option == optionalFlag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 修改後
|
||||
func isOptional(_ Context, options []string) bool {
|
||||
for _, option := range options {
|
||||
if option == optionalFlag || option == "omitempty" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 驗證結果
|
||||
|
||||
使用 `example/api/gateway.api` 進行測試:
|
||||
|
||||
| 結構體 | 修復前 | 修復後 | 狀態 |
|
||||
|--------|--------|--------|------|
|
||||
| CreateUserInfoReq | 13 個必填 | 5 個必填 | ✅ |
|
||||
| UpdateUserInfoReq | 11 個必填 | 1 個必填 | ✅ |
|
||||
| ListUserInfoReq | 6 個必填 | 2 個必填 | ✅ |
|
||||
| GetUserInfoReq | 2 個必填 | 0 個必填 | ✅ |
|
||||
| BindingUserReq | 3 個必填 | 3 個必填 | ✅ |
|
||||
|
||||
### CreateUserInfoReq 詳細對比
|
||||
|
||||
**必填字段(非指針類型):**
|
||||
- `uid` (string)
|
||||
- `alarm_type` (int)
|
||||
- `status` (int)
|
||||
- `language` (string)
|
||||
- `currency` (string)
|
||||
|
||||
**可選字段(指針類型):**
|
||||
- `avatar` (*string)
|
||||
- `nick_name` (*string)
|
||||
- `full_name` (*string)
|
||||
- `gender` (*int64)
|
||||
- `birthdate` (*int64)
|
||||
- `phone_number` (*string)
|
||||
- `email` (*string)
|
||||
- `address` (*string)
|
||||
|
||||
## 🚀 影響範圍
|
||||
|
||||
此修復影響所有使用 go-doc 生成的 OpenAPI/Swagger 文檔:
|
||||
- ✅ Swagger 2.0 格式
|
||||
- ✅ OpenAPI 3.0 格式
|
||||
- ✅ JSON 輸出
|
||||
- ✅ YAML 輸出
|
||||
|
||||
## 📝 開發者注意事項
|
||||
|
||||
在 Go 語言和 API 設計中,以下三種方式都表示字段為可選:
|
||||
1. **指針類型** - `*string`, `*int`, `*int64` 等
|
||||
2. **JSON 標籤的 `omitempty`** - `json:"field,omitempty"`
|
||||
3. **Validate 標籤的 `omitempty`** - `validate:"omitempty,..."`
|
||||
|
||||
修復後的 go-doc 工具會正確識別這些模式。
|
||||
|
||||
## 🔗 相關文件
|
||||
|
||||
- 修改文件:`internal/swagger/swagger.go`
|
||||
- 測試文件:`example/api/gateway.api`
|
||||
- 生成文件:`example/test_output/gateway.json`
|
||||
- 驗證腳本:可運行 `make gen-gateway` 重新生成並驗證
|
||||
|
||||
## ✅ 測試通過
|
||||
|
||||
```bash
|
||||
# 重新構建
|
||||
make build
|
||||
|
||||
# 生成 gateway API 文檔
|
||||
./bin/go-doc --api example/api/gateway.api \
|
||||
--dir example/test_output \
|
||||
--filename gateway \
|
||||
--spec-version openapi3.0
|
||||
|
||||
# 所有結構體的 required 字段現在都正確了!
|
||||
```
|
||||
|
||||
---
|
||||
**修復日期:** 2025-10-01
|
||||
**修復版本:** 1.2.1
|
||||
**問題類型:** Bug Fix
|
||||
**嚴重程度:** High (影響 API 文檔準確性)
|
14
Makefile
14
Makefile
|
@ -36,8 +36,8 @@ install: ## Install the binary to $GOPATH/bin
|
|||
|
||||
run: build ## Build and run with example API file
|
||||
@echo "Running $(BINARY_NAME)..."
|
||||
@$(BUILD_DIR)/$(BINARY_NAME) -a example/multiple.api -d example/test_output -f example_cn_openapi3 -s openapi3.0
|
||||
@echo "✅ Generated: example/test_output/example.json"
|
||||
@$(BUILD_DIR)/$(BINARY_NAME) -a example/example_cn.api -d example/test_output -f example -s openapi3.0
|
||||
@echo "✅ Generated: example/test_output/gateway.json"
|
||||
|
||||
fmt: ## Format code
|
||||
@echo "Formatting code..."
|
||||
|
@ -84,3 +84,13 @@ example: build ## Generate example swagger and openapi files
|
|||
@$(BUILD_DIR)/$(BINARY_NAME) -a example/example_cn.api -d example/test_output -f example_cn_openapi3 -s openapi3.0
|
||||
@echo "✅ Examples generated in example/test_output/"
|
||||
|
||||
gen-gateway: build ## Generate Gateway API documentation (OpenAPI 3.0)
|
||||
@echo "Generating Gateway API documentation..."
|
||||
@mkdir -p example/test_output
|
||||
@$(BUILD_DIR)/$(BINARY_NAME) -a example/api/gateway.api -d example/test_output -f gateway -s openapi3.0
|
||||
@echo "✅ Generated: example/test_output/gateway.json"
|
||||
@echo ""
|
||||
@echo "📊 驗證結果:"
|
||||
@echo " - Total endpoints: $$(jq '.paths | length' example/test_output/gateway.json)"
|
||||
@echo " - Total schemas: $$(jq '.components.schemas // {} | length' example/test_output/gateway.json 2>/dev/null || echo '0')"
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ type (
|
|||
BaseReq {}
|
||||
|
||||
VerifyHeader {
|
||||
Token string `header:"token" validate:"required"`
|
||||
Authorization string `header:"authorization" validate:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -1,555 +1,359 @@
|
|||
syntax = "v1"
|
||||
|
||||
|
||||
// ================ 請求/響應結構 ================
|
||||
type (
|
||||
// 創建用戶帳號請求
|
||||
CreateUserAccountReq {
|
||||
LoginID string `json:"login_id" validate:"required,min=3,max=50"`
|
||||
Platform int `json:"platform" validate:"required,oneof=1 2 3 4"`
|
||||
Token string `json:"token" validate:"required,min=8,max=128"`
|
||||
// --- 1. 註冊 / 登入 ---
|
||||
|
||||
// CredentialsRegisterPayload 傳統帳號密碼註冊的資料
|
||||
CredentialsRegisterPayload {
|
||||
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"`
|
||||
// PlatformRegisterPayload 第三方平台註冊的資料
|
||||
PlatformRegisterPayload {
|
||||
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 註冊請求 (整合了兩種方式)
|
||||
RegisterReq {
|
||||
AuthMethod string `json:"auth_method" validate:"required,oneof=credentials platform"`
|
||||
LoginID string `json:"login_id" validate:"required,min=3,max=50"` // 信箱或手機號碼
|
||||
Credentials *CredentialsRegisterPayload `json:"credentials,optional"` // AuthMethod 為 'credentials' 時使用
|
||||
Platform *PlatformRegisterPayload `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"
|
||||
}
|
||||
|
||||
// 獲取帳號資訊響應
|
||||
GetAccountInfoResp {
|
||||
Data CreateUserAccountReq `json:"data"`
|
||||
// --- 2. 密碼重設流程 ---
|
||||
|
||||
// RequestPasswordResetReq 請求發送「忘記密碼」的驗證碼
|
||||
RequestPasswordResetReq {
|
||||
Identifier string `json:"identifier" validate:"required"` // 使用者帳號 (信箱或手機)
|
||||
AccountType string `json:"account_type" validate:"required,oneof=email phone"`
|
||||
}
|
||||
|
||||
// 更新用戶資料請求
|
||||
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"`
|
||||
// VerifyCodeReq 驗證碼校驗 (通用)
|
||||
VerifyCodeReq {
|
||||
Identifier string `json:"identifier" validate:"required"`
|
||||
VerifyCode string `json:"verify_code" validate:"required,len=6"`
|
||||
}
|
||||
|
||||
// 獲取 UID 請求
|
||||
GetUIDByAccountReq {
|
||||
Account string `json:"account" validate:"required,min=3,max=50"`
|
||||
// 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 響應
|
||||
GetUIDByAccountResp {
|
||||
UID string `json:"uid"`
|
||||
Account string `json:"account"`
|
||||
// --- 4. 權杖刷新 ---
|
||||
// RefreshTokenReq 更新 AccessToken
|
||||
RefreshTokenReq {
|
||||
RefreshToken string `json:"refresh_token" validate:"required"`
|
||||
}
|
||||
|
||||
// 更新密碼請求
|
||||
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 路由 ================
|
||||
|
||||
// =================================================================
|
||||
// Service: Member (公開 API - 無需登入)
|
||||
// Group: account
|
||||
// =================================================================
|
||||
|
||||
@server(
|
||||
group: account
|
||||
prefix: /api/v1/account
|
||||
schemes: https
|
||||
timeout: 10s
|
||||
)
|
||||
service gateway {
|
||||
// ===================================
|
||||
// 1. 註冊 (Register)
|
||||
// ===================================
|
||||
@doc(
|
||||
summary: "創建用戶帳號"
|
||||
description: "創建新的用戶帳號,支援多平台登入"
|
||||
summary: "註冊新帳號"
|
||||
description: "使用傳統帳號密碼或第三方平台進行註冊。成功後直接返回登入後的 Token 資訊。"
|
||||
)
|
||||
/*
|
||||
@respdoc-200 (OKResp) // 創建成功
|
||||
@respdoc-400 (
|
||||
40001: (ErrorResp) 帳號格式錯誤
|
||||
40002: (ErrorResp) 密碼強度不足
|
||||
40003: (ErrorResp) 平台類型無效
|
||||
) // 客戶端錯誤
|
||||
@respdoc-409 (ErrorResp) // 帳號已存在
|
||||
@respdoc-422 (ErrorResp) // 請求格式正確但語義錯誤
|
||||
@respdoc-500 (ErrorResp) // 服務器錯誤
|
||||
*/
|
||||
@handler CreateUserAccount
|
||||
post /create (CreateUserAccountReq) returns (OKResp)
|
||||
/*
|
||||
@respdoc-200 (LoginResp) // 註冊成功,並返回 Token
|
||||
@respdoc-400 (
|
||||
40001: (ErrorResp) "請求參數格式錯誤"
|
||||
40002: (ErrorResp) "密碼與確認密碼不一致"
|
||||
40003: (ErrorResp) "無效的認證方式或平台"
|
||||
40004: (ErrorResp) "無效的平台 Token"
|
||||
)
|
||||
@respdoc-409 (ErrorResp) // 409 Conflict: 代表資源衝突,這裡表示帳號已存在
|
||||
@respdoc-500 (ErrorResp) // 伺服器內部錯誤
|
||||
*/
|
||||
@handler register
|
||||
post /register (RegisterReq) returns (LoginResp)
|
||||
|
||||
|
||||
// ===================================
|
||||
// 2. 登入 (Login / Create Session)
|
||||
// ===================================
|
||||
@doc(
|
||||
summary: "使用者登入"
|
||||
description: "使用傳統帳號密碼或第三方平台 Token 進行登入,以創建一個新的會話(Session)。"
|
||||
)
|
||||
/*
|
||||
@respdoc-200 (LoginResp) // 登入成功
|
||||
@respdoc-400 (ErrorResp) "請求參數格式錯誤"
|
||||
@respdoc-401 (
|
||||
40101: (ErrorResp) "帳號或密碼錯誤"
|
||||
40102: (ErrorResp) "無效的平台 Token"
|
||||
) // 401 Unauthorized: 代表身份驗證失敗
|
||||
@respdoc-500 (ErrorResp) // 伺服器內部錯誤
|
||||
*/
|
||||
@handler login
|
||||
post /sessions/login (RegisterReq) returns (LoginResp)
|
||||
|
||||
// ===================================
|
||||
// 3. 權杖刷新 (Token Refresh)
|
||||
// ===================================
|
||||
@doc(
|
||||
summary: "刷新 Access Token"
|
||||
description: "使用有效的 Refresh Token 來獲取一組新的 Access Token 和 Refresh Token。"
|
||||
)
|
||||
/*
|
||||
@respdoc-200 (RefreshTokenResp) // 刷新成功
|
||||
@respdoc-400 (ErrorResp) "請求參數格式錯誤"
|
||||
@respdoc-401 (ErrorResp) "無效或已過期的 Refresh Token" // 401 Unauthorized
|
||||
@respdoc-500 (ErrorResp) // 伺服器內部錯誤
|
||||
*/
|
||||
@handler refreshToken
|
||||
post /sessions/refresh (RefreshTokenReq) returns (RefreshTokenResp)
|
||||
|
||||
// ===================================
|
||||
// 4. 密碼重設 (Password Reset)
|
||||
// ===================================
|
||||
@doc(
|
||||
summary: "請求發送密碼重設驗證碼(忘記密碼)"
|
||||
description: "為指定的 email 或 phone 發送一個一次性的密碼重設驗證碼。三分鐘內對同一帳號只能請求一次。"
|
||||
)
|
||||
/*
|
||||
@respdoc-201 () // 請求成功 (為安全起見,即使帳號不存在也應返回成功)
|
||||
@respdoc-400 (ErrorResp) "請求參數格式錯誤"
|
||||
@respdoc-429 (ErrorResp) // 429 Too Many Requests: 代表請求過於頻繁
|
||||
@respdoc-500 (ErrorResp) // 伺服器內部錯誤
|
||||
*/
|
||||
@handler requestPasswordReset
|
||||
post /password-resets/request (RequestPasswordResetReq) returns ()
|
||||
|
||||
|
||||
@doc(
|
||||
summary: "獲取帳號資訊"
|
||||
description: "根據帳號獲取用戶的帳號資訊"
|
||||
summary: "校驗密碼重設驗證碼"
|
||||
description: "在實際重設密碼前,先驗證使用者輸入的驗證碼是否正確。這一步可以讓前端在使用者進入下一步前就得到反饋。"
|
||||
)
|
||||
/*
|
||||
@respdoc-200 (GetAccountInfoResp) // 獲取成功
|
||||
@respdoc-400 (
|
||||
40001: (ErrorResp) 帳號格式錯誤
|
||||
40004: (ErrorResp) 參數驗證失敗
|
||||
) // 客戶端錯誤
|
||||
@respdoc-404 (ErrorResp) // 帳號不存在
|
||||
@respdoc-500 (ErrorResp) // 服務器錯誤
|
||||
*/
|
||||
@handler GetUserAccountInfo
|
||||
post /info (GetUIDByAccountReq) returns (GetAccountInfoResp)
|
||||
/*
|
||||
@respdoc-200 (OKResp) // 驗證碼正確
|
||||
@respdoc-400 (
|
||||
40001: (ErrorResp) "請求參數格式錯誤"
|
||||
40002: (ErrorResp) "驗證碼無效或已過期"
|
||||
)
|
||||
@respdoc-500 (ErrorResp) // 伺服器內部錯誤
|
||||
*/
|
||||
@handler verifyPasswordResetCode
|
||||
post /password-resets/verify (VerifyCodeReq) returns (OKResp)
|
||||
|
||||
@doc(
|
||||
summary: "更新用戶密碼"
|
||||
description: "更新指定帳號的密碼"
|
||||
summary: "執行密碼重設"
|
||||
description: "使用有效的驗證碼來設定新的密碼。"
|
||||
)
|
||||
/*
|
||||
@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)
|
||||
|
||||
@doc(
|
||||
summary: "獲取用戶 UID"
|
||||
description: "根據帳號獲取對應的用戶 UID"
|
||||
)
|
||||
/*
|
||||
@respdoc-200 (GetUIDByAccountResp) // 獲取成功
|
||||
@respdoc-400 (
|
||||
40001: (ErrorResp) 帳號格式錯誤
|
||||
40004: (ErrorResp) 參數驗證失敗
|
||||
) // 客戶端錯誤
|
||||
@respdoc-404 (ErrorResp) // 帳號不存在
|
||||
@respdoc-500 (ErrorResp) // 服務器錯誤
|
||||
*/
|
||||
@handler GetUIDByAccount
|
||||
post /uid (GetUIDByAccountReq) returns (GetUIDByAccountResp)
|
||||
|
||||
@doc(
|
||||
summary: "綁定帳號"
|
||||
description: "將帳號綁定到指定的用戶 UID"
|
||||
)
|
||||
/*
|
||||
@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)
|
||||
|
||||
@doc(
|
||||
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-201 () // 密碼重設成功
|
||||
@respdoc-400 (
|
||||
40001: (ErrorResp) "請求參數格式錯誤"
|
||||
40002: (ErrorResp) "密碼與確認密碼不一致"
|
||||
40003: (ErrorResp) "驗證碼無效或已過期"
|
||||
)
|
||||
@respdoc-500 (ErrorResp) // 伺服器內部錯誤
|
||||
*/
|
||||
@handler resetPassword
|
||||
put /password-resets (ResetPasswordReq) returns ()
|
||||
}
|
||||
|
||||
|
||||
// ================ 請求/響應結構 ================
|
||||
type (
|
||||
// --- 會員資訊 ---
|
||||
|
||||
// UserInfoResp 用於獲取會員資訊的標準響應結構 (合併 GetUserInfo 和 UserInfo)
|
||||
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"` // 地址
|
||||
AlarmCategory string `json:"alarm_category"` // 告警狀態
|
||||
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"` // 角色
|
||||
}
|
||||
|
||||
// UpdateUserInfoReq 更新會員資訊的請求結構 (原 BindingUserInfo)
|
||||
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"` // 載具
|
||||
VerifyHeader
|
||||
}
|
||||
|
||||
// --- 修改密碼 ---
|
||||
|
||||
// UpdatePasswordReq 修改密碼的請求 (原 ModifyPasswdReq)
|
||||
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"`
|
||||
VerifyHeader
|
||||
}
|
||||
|
||||
// --- 通用驗證碼 ---
|
||||
|
||||
// RequestVerificationCodeReq 請求發送驗證碼
|
||||
RequestVerificationCodeReq {
|
||||
VerifyHeader
|
||||
// 驗證目的:'email_verification' 或 'phone_verification'
|
||||
Purpose string `json:"purpose" validate:"required,oneof=email_verification phone_verification"`
|
||||
}
|
||||
|
||||
// SubmitVerificationCodeReq 提交驗證碼以完成驗證
|
||||
SubmitVerificationCodeReq {
|
||||
Purpose string `json:"purpose" validate:"required,oneof=email_verification phone_verification"`
|
||||
VerifyCode string `json:"verify_code" validate:"required,len=6"`
|
||||
VerifyHeader
|
||||
}
|
||||
)
|
||||
|
||||
// =================================================================
|
||||
// Service: Gateway (授權 API - 需要登入)
|
||||
// Group: user
|
||||
// Middleware: AuthMiddleware (JWT 驗證)
|
||||
// =================================================================
|
||||
|
||||
@server(
|
||||
group: user
|
||||
prefix: /api/v1/user
|
||||
schemes: https
|
||||
timeout: 10s
|
||||
middleware: AuthMiddleware
|
||||
)
|
||||
service gateway {
|
||||
// ===================================
|
||||
// 1. 會員資訊 (User Profile)
|
||||
// ===================================
|
||||
@doc(
|
||||
summary: "取得當前登入的會員資訊"
|
||||
)
|
||||
/*
|
||||
@respdoc-200 (UserInfoResp) // 成功獲取
|
||||
@respdoc-401 (ErrorResp) "未授權或 Token 無效"
|
||||
@respdoc-404 (ErrorResp) "找不到使用者"
|
||||
@respdoc-500 (ErrorResp) // 伺服器內部錯誤
|
||||
*/
|
||||
@handler getUserInfo
|
||||
|
||||
get /me (VerifyHeader) 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)
|
||||
|
||||
// ===================================
|
||||
// 2. 修改密碼 (Password Update)
|
||||
// ===================================
|
||||
@doc(
|
||||
summary: "修改當前登入使用者的密碼"
|
||||
description: "必須提供當前密碼以進行驗證。"
|
||||
)
|
||||
/*
|
||||
@respdoc-200 (OKResp) // 密碼修改成功
|
||||
@respdoc-400 (
|
||||
40001: (ErrorResp) "請求參數格式錯誤"
|
||||
40002: (ErrorResp) "新密碼與確認密碼不一致"
|
||||
)
|
||||
@respdoc-401 (ErrorResp) "未授權或 Token 無效"
|
||||
@respdoc-403 (ErrorResp) "當前密碼不正確" // 403 Forbidden: 認證成功,但無權執行此操作
|
||||
@respdoc-500 (ErrorResp) // 伺服器內部錯誤
|
||||
*/
|
||||
@handler updatePassword
|
||||
put /me/password (UpdatePasswordReq) returns (OKResp)
|
||||
|
||||
// ===================================
|
||||
// 3. 驗證碼流程 (Verification Flow)
|
||||
// ===================================
|
||||
@doc(
|
||||
summary: "請求發送驗證碼"
|
||||
description: "用於驗證手機或 Email。根據傳入的 `purpose` 發送對應的驗證碼。同一個目的在短時間內不能重複發送。"
|
||||
)
|
||||
/*
|
||||
@respdoc-200 (OKResp) // 請求已受理
|
||||
@respdoc-400 (ErrorResp) "請求參數格式錯誤"
|
||||
@respdoc-401 (ErrorResp) "未授權或 Token 無效"
|
||||
@respdoc-429 (ErrorResp) // 429 Too Many Requests: 請求過於頻繁
|
||||
@respdoc-500 (ErrorResp) // 伺服器內部錯誤
|
||||
*/
|
||||
@handler requestVerificationCode
|
||||
post /me/verifications (RequestVerificationCodeReq) returns (OKResp)
|
||||
|
||||
@doc(
|
||||
summary: "提交驗證碼以完成驗證"
|
||||
description: "提交收到的驗證碼,以完成特定目的的驗證,例如綁定手機或 Email。"
|
||||
)
|
||||
/*
|
||||
@respdoc-200 (OKResp) // 驗證成功
|
||||
@respdoc-400 (
|
||||
40001: (ErrorResp) "請求參數格式錯誤"
|
||||
40002: (ErrorResp) "驗證碼無效或已過期"
|
||||
)
|
||||
@respdoc-401 (ErrorResp) "未授權或 Token 無效"
|
||||
@respdoc-500 (ErrorResp) // 伺服器內部錯誤
|
||||
*/
|
||||
@handler submitVerificationCode
|
||||
put /me/verifications (SubmitVerificationCodeReq) returns (OKResp)
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
syntax = "v1"
|
||||
|
||||
info (
|
||||
title: "Test All Parameter Types"
|
||||
version: "v1"
|
||||
)
|
||||
|
||||
type (
|
||||
// 同時包含所有類型的參數
|
||||
AllParamsReq {
|
||||
// Header 參數
|
||||
Authorization string `header:"Authorization" validate:"required"`
|
||||
XRequestID string `header:"X-Request-ID,optional"`
|
||||
|
||||
// Path 參數
|
||||
UserID string `path:"userId" validate:"required"`
|
||||
ItemID string `path:"itemId" validate:"required"`
|
||||
|
||||
// Query 參數 (form)
|
||||
Page int `form:"page,optional"`
|
||||
PageSize int `form:"pageSize,optional"`
|
||||
Filter string `form:"filter,optional"`
|
||||
|
||||
// JSON Body 參數
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description string `json:"description,optional"`
|
||||
Tags []string `json:"tags,optional"`
|
||||
}
|
||||
|
||||
Response {
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
prefix: /api/v1
|
||||
)
|
||||
service test {
|
||||
// 測試所有參數類型同時存在
|
||||
@handler UpdateWithAllParams
|
||||
put /users/:userId/items/:itemId (AllParamsReq) returns (Response)
|
||||
|
||||
// 測試只有 path + query
|
||||
@handler GetWithPathQuery
|
||||
get /users/:userId/items/:itemId (AllParamsReq) returns (Response)
|
||||
|
||||
// 測試 path + header + body (POST)
|
||||
@handler CreateWithPathHeaderBody
|
||||
post /users/:userId/items (AllParamsReq) returns (Response)
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
syntax = "v1"
|
||||
|
||||
info (
|
||||
title: "Test Header with Body"
|
||||
version: "v1"
|
||||
)
|
||||
|
||||
type (
|
||||
HeaderDef {
|
||||
Token string `header:"Authorization" validate:"required"`
|
||||
}
|
||||
|
||||
// 方式1: 嵌入 header 結構體
|
||||
UpdateReqWithEmbed {
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
HeaderDef
|
||||
}
|
||||
|
||||
// 方式2: 分開定義
|
||||
UpdateReqSeparate {
|
||||
Token string `header:"Authorization" validate:"required"`
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
}
|
||||
|
||||
Response {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
prefix: /api/v1
|
||||
)
|
||||
service test {
|
||||
@handler TestWithEmbed
|
||||
put /with-embed (UpdateReqWithEmbed) returns (Response)
|
||||
|
||||
@handler TestSeparate
|
||||
put /separate (UpdateReqSeparate) returns (Response)
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
syntax = "v1"
|
||||
|
||||
info (
|
||||
title: "Test Header and Path Parameters"
|
||||
version: "v1"
|
||||
)
|
||||
|
||||
type (
|
||||
HeaderReq {
|
||||
Token string `header:"Authorization" validate:"required"`
|
||||
}
|
||||
|
||||
PathReq {
|
||||
ID string `path:"id" validate:"required"`
|
||||
}
|
||||
|
||||
QueryReq {
|
||||
Name string `form:"name,optional"`
|
||||
}
|
||||
|
||||
BodyReq {
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
Response {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
CombinedReq {
|
||||
Token string `header:"Authorization" validate:"required"`
|
||||
ID string `path:"id" validate:"required"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
prefix: /api/v1
|
||||
)
|
||||
service test {
|
||||
// Test header parameter
|
||||
@handler TestHeader
|
||||
get /header (HeaderReq) returns (Response)
|
||||
|
||||
// Test path parameter
|
||||
@handler TestPath
|
||||
get /path/:id (PathReq) returns (Response)
|
||||
|
||||
// Test query parameter
|
||||
@handler TestQuery
|
||||
get /query (QueryReq) returns (Response)
|
||||
|
||||
// Test body parameter
|
||||
@handler TestBody
|
||||
post /body (BodyReq) returns (Response)
|
||||
|
||||
// Test combined
|
||||
@handler TestCombined
|
||||
post /combined/:id (CombinedReq) returns (Response)
|
||||
}
|
|
@ -50,16 +50,16 @@ const (
|
|||
propertyKeyTags = "tags"
|
||||
propertyKeySummary = "summary"
|
||||
propertyKeyGroup = "group"
|
||||
propertyKeyOperationId = "operationId"
|
||||
propertyKeyDeprecated = "deprecated"
|
||||
propertyKeyPrefix = "prefix"
|
||||
propertyKeyAuthType = "authType"
|
||||
propertyKeyHost = "host"
|
||||
propertyKeyBasePath = "basePath"
|
||||
propertyKeyWrapCodeMsg = "wrapCodeMsg"
|
||||
propertyKeyBizCodeEnumDescription = "bizCodeEnumDescription"
|
||||
// propertyKeyOperationId = "operationId"
|
||||
propertyKeyDeprecated = "deprecated"
|
||||
propertyKeyPrefix = "prefix"
|
||||
propertyKeyAuthType = "authType"
|
||||
propertyKeyHost = "host"
|
||||
propertyKeyBasePath = "basePath"
|
||||
propertyKeyWrapCodeMsg = "wrapCodeMsg"
|
||||
propertyKeyBizCodeEnumDescription = "bizCodeEnumDescription"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultValueOfPropertyUseDefinition = false
|
||||
defaultValueOfPropertyUseDefinition = true
|
||||
)
|
||||
|
|
|
@ -9,21 +9,24 @@ import (
|
|||
)
|
||||
|
||||
func isPostJson(ctx Context, method string, tp apiSpec.Type) (string, bool) {
|
||||
if !strings.EqualFold(method, http.MethodPost) {
|
||||
// Check if this is a method that supports request body (POST, PUT, PATCH)
|
||||
if !strings.EqualFold(method, http.MethodPost) &&
|
||||
!strings.EqualFold(method, http.MethodPut) &&
|
||||
!strings.EqualFold(method, http.MethodPatch) {
|
||||
return "", false
|
||||
}
|
||||
structType, ok := tp.(apiSpec.DefineStruct)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
var isPostJson bool
|
||||
var hasJsonField bool
|
||||
rangeMemberAndDo(ctx, structType, func(tag *apiSpec.Tags, required bool, member apiSpec.Member) {
|
||||
jsonTag, _ := tag.Get(tagJson)
|
||||
if !isPostJson {
|
||||
isPostJson = jsonTag != nil
|
||||
if !hasJsonField {
|
||||
hasJsonField = jsonTag != nil
|
||||
}
|
||||
})
|
||||
return structType.RawName, isPostJson
|
||||
return structType.RawName, hasJsonField
|
||||
}
|
||||
|
||||
func parametersFromType(ctx Context, method string, tp apiSpec.Type) []spec.Parameter {
|
||||
|
|
|
@ -211,12 +211,17 @@ func rangeMemberAndDo(ctx Context, structType apiSpec.Type, do func(tag *apiSpec
|
|||
|
||||
for _, field := range members {
|
||||
tags, _ := apiSpec.Parse(field.Tag)
|
||||
required := isRequired(ctx, tags)
|
||||
required := isRequired(ctx, tags, field)
|
||||
do(tags, required, field)
|
||||
}
|
||||
}
|
||||
|
||||
func isRequired(ctx Context, tags *apiSpec.Tags) bool {
|
||||
func isRequired(ctx Context, tags *apiSpec.Tags, member apiSpec.Member) bool {
|
||||
// Check if the field type is a pointer - pointer types are optional
|
||||
if _, isPointer := member.Type.(apiSpec.PointerType); isPointer {
|
||||
return false
|
||||
}
|
||||
|
||||
tag, err := tags.Get(tagJson)
|
||||
if err == nil {
|
||||
return !isOptional(ctx, tag.Options)
|
||||
|
@ -234,7 +239,7 @@ func isRequired(ctx Context, tags *apiSpec.Tags) bool {
|
|||
|
||||
func isOptional(_ Context, options []string) bool {
|
||||
for _, option := range options {
|
||||
if option == optionalFlag {
|
||||
if option == optionalFlag || option == "omitempty" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue