feat: add doc generate

This commit is contained in:
王性驊 2025-10-01 17:44:41 +08:00
parent bc5ede9d5d
commit 7c309f3508
11 changed files with 689 additions and 538 deletions

178
BUGFIX_REQUIRED_FIELDS.md Normal file
View File

@ -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 文檔準確性)

View File

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

2
example/api/common.api Executable file → Normal file
View File

@ -27,7 +27,7 @@ type (
BaseReq {}
VerifyHeader {
Token string `header:"token" validate:"required"`
Authorization string `header:"authorization" validate:"required"`
}
)

0
example/api/gateway.api Executable file → Normal file
View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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