This commit is contained in:
王性驊 2026-05-19 21:15:18 +08:00
parent 79c12702ec
commit fb5ac4b09f
14 changed files with 278 additions and 53 deletions

145
.golangci.yml Normal file
View File

@ -0,0 +1,145 @@
# golangci-lint v2 — Gateway 專案
# 參考https://golangci-lint.run/docs/configuration/
# 預設集「standard」= errcheck + govet + ineffassign + staticcheck + unused取代已棄用的 golint
# 再啟用社群常見的品質 / 安全 / 風格 linter並關閉過度吵雜的規則。
version: "2"
run:
timeout: 5m
tests: true
modules-download-mode: readonly
go: "1.26"
output:
formats:
text:
print-linter-name: true
colors: true
linters:
default: standard
enable:
# 錯誤與 context
- bodyclose
- contextcheck
- copyloopvar
- errname
- errorlint
- noctx
# 安全
- gosec
# 風格revive 為 golint 的現代替代)
- revive
- misspell
- unconvert
- whitespace
- gocritic
- goconst
- predeclared
# 測試
- testifylint
- thelper
# HTTP / API 常見
- rowserrcheck
- sloglint
disable:
- exhaustruct
- varnamelen
- wrapcheck
- nlreturn
- wsl
- wsl_v5
- dupl
- depguard
- tagliatelle
- ireturn
- funlen
- gochecknoglobals
- gochecknoinits
settings:
gosec:
excludes:
- G104
gocritic:
enabled-tags:
- diagnostic
- style
- performance
disabled-checks:
- hugeParam
- rangeValCopy
- whyNoLint
revive:
severity: warning
rules:
- name: blank-imports
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: error-return
- name: error-naming
- name: exported
arguments:
- disableStutteringCheck
- name: increment-decrement
- name: indent-error-flow
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
- name: var-declaration
- name: var-naming
disabled: true
- name: package-comments
disabled: true
- name: comment-spacings
disabled: true
errcheck:
check-type-assertions: true
check-blank: true
staticcheck:
checks:
- all
- -ST1000
- -ST1003
- -ST1016
exclusions:
generated: lax
warn-unused: true
presets:
- comments
- common-false-positives
- std-error-handling
- legacy
rules:
- path: _test\.go
linters:
- gosec
- errcheck
- funlen
- gocyclo
- cyclop
- path: internal/types/types\.go
linters:
- revive
- gocritic
paths:
- generate/doc-generate
- docs/openapi
formatters:
enable:
- goimports
exclusions:
generated: lax
paths:
- generate/doc-generate
issues:
max-issues-per-linter: 0
max-same-issues: 0

View File

@ -1,27 +1,71 @@
# go-zero 生成風格 # go-zero 生成風格
GO_ZERO_STYLE=go_zero GO_ZERO_STYLE := go_zero
GO ?= go GO ?= go
GOFMT ?= gofmt "-s" GOFMT ?= gofmt
GOFILES := $(shell find . -name "*.go") GOFILES := $(shell find . -name '*.go' -not -path './generate/doc-generate/*')
LDFLAGS := -s -w
VERSION="v0.0.1"
DOCKER_REP="reg.wang/app-cloudep-member-service"
GIT_COMMIT ?= $(shell git rev-parse --short HEAD)
GO_DOC_DIR := generate/doc-generate GO_DOC_DIR := generate/doc-generate
GO_DOC_BIN := $(GO_DOC_DIR)/bin/go-doc GO_DOC_BIN := $(GO_DOC_DIR)/bin/go-doc
API_ENTRY := ./generate/api/gateway.api API_ENTRY := ./generate/api/gateway.api
DOC_OUT := ./docs/openapi DOC_OUT := ./docs/openapi
.PHONY: gen-api build-go-doc gen-doc GOCTL ?= goctl
gen-api: # 使用專案 handler 模板response.Write GOCTL_PKG := github.com/zeromicro/go-zero/tools/goctl@latest
goctl api go -api $(API_ENTRY) -dir . -style go_zero -home generate/goctl GOLANGCI_PKG := github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2
.DEFAULT_GOAL := help
.PHONY: help tools gen-api build-go-doc gen-doc test fmt lint lint-fix fix check run
help: ## 顯示可用指令
@echo "Gateway Makefile"
@echo ""
@echo "首次開發:"
@echo " make tools 安裝 goctl、goimports、golangci-lint寫入 \$$GOPATH/bin"
@echo " go mod download"
@echo ""
@echo "常用:"
@grep -E '^[a-zA-Z0-9_-]+:.*## ' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*## "}; {printf " make %-14s %s\n", $$1, $$2}'
tools: ## 安裝 goctl、goimports、golangci-lint需 Go且 GOPATH/bin 在 PATH
@command -v $(GOCTL) >/dev/null 2>&1 || (echo ">> installing goctl" && $(GO) install $(GOCTL_PKG))
@command -v goimports >/dev/null 2>&1 || (echo ">> installing goimports" && $(GO) install golang.org/x/tools/cmd/goimports@latest)
@command -v golangci-lint >/dev/null 2>&1 || (echo ">> installing golangci-lint" && $(GO) install $(GOLANGCI_PKG))
@echo "tools OK"
@echo " goctl: $$(goctl --version 2>/dev/null || echo missing)"
@echo " golangci-lint: $$(golangci-lint version 2>/dev/null | head -1 || echo missing)"
gen-api: tools ## 由 .api 生成 handler / logic / types自訂 handler 模板)
$(GOCTL) api go -api $(API_ENTRY) -dir . -style $(GO_ZERO_STYLE) -home generate/goctl
build-go-doc: ## 編譯 go-docOpenAPI 文件生成器) build-go-doc: ## 編譯 go-docOpenAPI 文件生成器)
cd $(GO_DOC_DIR) && GOTOOLCHAIN=go1.26.1 go build -o bin/go-doc ./cmd/go-doc @echo ">> building $(GO_DOC_BIN)"
@mkdir -p $(GO_DOC_DIR)/bin
@cd $(GO_DOC_DIR) && $(GO) build -o bin/go-doc ./cmd/go-doc
gen-doc: build-go-doc ## 從 .api 生成 OpenAPI 3.0 YAML gen-doc: build-go-doc ## 從 .api 生成 OpenAPI 3.0 YAML
@mkdir -p $(DOC_OUT) @mkdir -p $(DOC_OUT)
$(GO_DOC_BIN) -a $(API_ENTRY) -d $(DOC_OUT) -f gateway -s openapi3.0 -y $(GO_DOC_BIN) -a $(API_ENTRY) -d $(DOC_OUT) -f gateway -s openapi3.0 -y
@echo "Generated: $(DOC_OUT)/gateway.yaml" @echo "Generated: $(DOC_OUT)/gateway.yaml"
test: ## 執行測試
$(GO) test ./...
fmt: ## gofmt + goimports不含 lint
$(GOFMT) -s -w $(GOFILES)
@command -v goimports >/dev/null 2>&1 && goimports -w . || (echo "goimports not found; run: make tools" && exit 1)
lint: ## golangci-lint 靜態檢查(先 make tools
@command -v golangci-lint >/dev/null 2>&1 || (echo "golangci-lint not found; run: make tools" && exit 1)
golangci-lint run ./...
lint-fix: ## 自動修正可修的 lint / formatter 問題(見 .golangci.yml
@command -v golangci-lint >/dev/null 2>&1 || (echo "golangci-lint not found; run: make tools" && exit 1)
golangci-lint run --fix ./...
fix: fmt lint-fix lint ## 格式化 + 自動修 lint + 再檢查(提交前建議)
check: fix test ## 提交 / PR 前完整檢查fmt、lint、test
run: ## 啟動 Gatewayetc/gateway.yaml
$(GO) run gateway.go -f etc/gateway.yaml

View File

@ -1,4 +1,4 @@
y94# Portal API Gateway (PGW) # Portal API Gateway (PGW)
基於 [go-zero](https://github.com/zeromicro/go-zero) 的 API Gateway提供統一 HTTP JSON 回應、8 碼業務錯誤碼,以及由 `.api` 定義驅動的程式碼與 OpenAPI 3.0 文件生成。 基於 [go-zero](https://github.com/zeromicro/go-zero) 的 API Gateway提供統一 HTTP JSON 回應、8 碼業務錯誤碼,以及由 `.api` 定義驅動的程式碼與 OpenAPI 3.0 文件生成。
@ -12,32 +12,40 @@ y94# Portal API Gateway (PGW)
## 環境需求 ## 環境需求
| 工具 | 說明 | | 工具 | 必要 | 說明 |
|------|------| |------|:----:|------|
| Go | ≥ 1.26.1(見 `go.mod` | | Go | ✓ | ≥ 1.26.1(見 `go.mod` |
| [goctl](https://go-zero.dev/docs/tasks/installation/goctl) | go-zero 程式碼生成 | | Make | ✓ | 執行 `gen-api` / `gen-doc` 等 |
| Make | 執行 `gen-api` / `gen-doc`(可選) | | [goctl](https://go-zero.dev/docs/tasks/installation/goctl) | ✓ | `make gen-api` 用;可 `make tools` 安裝 |
| goimports | 建議 | `make fmt` 用;`make tools` 安裝 |
| golangci-lint | 建議 | `make lint` 用;`make tools` 安裝v2`.golangci.yml` |
| jq | 選用 | README 範例 `curl \| jq` |
安裝 goctl若尚未安裝 請確認 **`$(go env GOPATH)/bin` 在 PATH**,否則 `go install``goctl` 找不到。
## 快速開始(首次)
```bash ```bash
go install github.com/zeromicro/go-zero/tools/goctl@latest
```
## 快速開始
```bash
# 克隆後進入專案目錄
cd gateway cd gateway
# 下載依賴 # 1. 工具goctl、goimports
make tools
# 2. 依賴
go mod download go mod download
# 依 .api 生成 handler / logic / types若已生成可跳過 # 3. 依 .api 生成程式碼可選repo 內通常已有
make gen-api make gen-api
# 啟動服務 # 4. 啟動
go run gateway.go -f etc/gateway.yaml make run
# 或go run gateway.go -f etc/gateway.yaml
```
產生 OpenAPI會先編譯 `generate/doc-generate` 內的 go-doc
```bash
make gen-doc # 輸出 docs/openapi/gateway.yaml
``` ```
健康檢查: 健康檢查:
@ -58,12 +66,20 @@ curl -s http://127.0.0.1:8888/api/v1/health | jq
## 常用指令 ## 常用指令
執行 `make``make help` 可看完整列表。
| 指令 | 說明 | | 指令 | 說明 |
|------|------| |------|------|
| `make gen-api` | 由 `generate/api/gateway.api` 生成 Go 程式碼(使用 `generate/goctl` 模板) | | `make tools` | 首次安裝 goctl、goimports |
| `make gen-doc` | 生成 `docs/openapi/gateway.yaml`OpenAPI 3.0 | | `make gen-api` | 由 `gateway.api` 生成 Go自訂 `generate/goctl` 模板) |
| `go run gateway.go -f etc/gateway.yaml` | 啟動 Gateway | | `make gen-doc` | 編譯 go-doc 並生成 `docs/openapi/gateway.yaml` |
| `go test ./...` | 執行測試 | | `make test` | 執行測試 |
| `make fmt` | gofmt + goimports |
| `make lint` | golangci-lint 靜態檢查(必須 0 issues |
| `make lint-fix` | 自動修正可修的 lint / import 問題 |
| `make fix` | `fmt` + `lint-fix` + `lint`(提交前建議) |
| `make check` | `fix` + `test`PR / AI 完成前必跑) |
| `make run` | 啟動 Gateway |
## 專案結構 ## 專案結構
@ -257,7 +273,20 @@ if err != nil {
**禁止:** repository 回傳裸 `fmt.Errorf` 給上層logic 再包一層無語意錯誤logic 直接操作 Mongo / repository 實作。 **禁止:** repository 回傳裸 `fmt.Errorf` 給上層logic 再包一層無語意錯誤logic 直接操作 Mongo / repository 實作。
### 5. 請求驗證(`library/validate` ### 5. Lint 與自動修正
設定檔:`.golangci.yml`golangci-lint v2`standard` + revive / gosec / errorlint 等)。
```bash
make lint-fix # 自動修goimports、部分 linter auto-fix
make lint # 僅檢查
make fix # fmt + lint-fix + lint
make check # fix + test建議每次改 Go 後執行)
```
無法自動修的須手改AI / 協作者應以 `make check` 通過為完成條件(見 `.cursor/rules/golangci-lint.mdc`)。
### 6. 請求驗證(`library/validate`
- `svc.ServiceContext.Validator`:啟動時以 `validate.NewWithDefaultEN()` 建立,可傳入 `validate.Option` 註冊自訂 tag。 - `svc.ServiceContext.Validator`:啟動時以 `validate.NewWithDefaultEN()` 建立,可傳入 `validate.Option` 註冊自訂 tag。
- **Handler 模板**`generate/goctl/api/handler.tpl`)在 `httpx.Parse` 之後自動 `ValidateAll(&req)`,失敗經 `WrapRequestError`**400** `InputInvalidFormat`Logic 通常不必再驗證。 - **Handler 模板**`generate/goctl/api/handler.tpl`)在 `httpx.Parse` 之後自動 `ValidateAll(&req)`,失敗經 `WrapRequestError`**400** `InputInvalidFormat`Logic 通常不必再驗證。
@ -270,7 +299,7 @@ if err := l.svcCtx.Validator.ValidateAll(req); err != nil {
} }
``` ```
### 6. 新增業務模組檢查清單 ### 7. 新增業務模組檢查清單
1. 建立 `internal/model/{module}/`,依上方目錄放置 `entity`、`enum`、`repository`、`usecase`。 1. 建立 `internal/model/{module}/`,依上方目錄放置 `entity`、`enum`、`repository`、`usecase`。
2. 在 `internal/svc/service_context.go` 組裝 repository → usecase掛上介面欄位。 2. 在 `internal/svc/service_context.go` 組裝 repository → usecase掛上介面欄位。
@ -280,7 +309,7 @@ if err := l.svcCtx.Validator.ValidateAll(req); err != nil {
完整步驟見 [docs/model.md](docs/model.md) 第 11 節。 完整步驟見 [docs/model.md](docs/model.md) 第 11 節。
### 7. 錯誤碼 API 速查 ### 8. 錯誤碼 API 速查
```go ```go
import ( import (
@ -296,7 +325,7 @@ return nil, errb.InputMissingRequired("email").WithCause(err)
Category 與 HTTP 對照見 [internal/library/errors/README.md](internal/library/errors/README.md)。 Category 與 HTTP 對照見 [internal/library/errors/README.md](internal/library/errors/README.md)。
### 8. API 文件(`@` 註解) ### 9. API 文件(`@` 註解)
`.api` 中: `.api` 中:
@ -306,7 +335,7 @@ Category 與 HTTP 對照見 [internal/library/errors/README.md](internal/library
範例見 `generate/api/normal.api` 範例見 `generate/api/normal.api`
### 9. OpenAPI 產物與 Git ### 10. OpenAPI 產物與 Git
預設 `.gitignore` 會忽略 `docs/openapi/*.yaml`。若需提交給前端,請在 `.gitignore` 註解相關規則後執行 `make gen-doc` 並加入版本控制。 預設 `.gitignore` 會忽略 `docs/openapi/*.yaml`。若需提交給前端,請在 `.gitignore` 註解相關規則後執行 `make gen-doc` 並加入版本控制。

View File

@ -7,9 +7,10 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"go-doc/internal/util"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/zeromicro/go-zero/tools/goctl/pkg/parser/api/parser" "github.com/zeromicro/go-zero/tools/goctl/pkg/parser/api/parser"
"go-doc/internal/util"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )

View File

@ -4,8 +4,9 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
"go-doc/internal/util" "go-doc/internal/util"
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
) )
func rangeValueFromOptions(options []string) (minimum *float64, maximum *float64, exclusiveMinimum bool, exclusiveMaximum bool) { func rangeValueFromOptions(options []string) (minimum *float64, maximum *float64, exclusiveMinimum bool, exclusiveMaximum bool) {

View File

@ -5,9 +5,10 @@ import (
"path" "path"
"strings" "strings"
"go-doc/internal/util"
"github.com/go-openapi/spec" "github.com/go-openapi/spec"
apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec" apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec"
"go-doc/internal/util"
) )
func spec2Paths(ctx Context, srv apiSpec.Service) *spec.Paths { func spec2Paths(ctx Context, srv apiSpec.Service) *spec.Paths {

View File

@ -7,6 +7,7 @@ import (
"strconv" "strconv"
"gateway/internal/library/errors/code" "gateway/internal/library/errors/code"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
) )

View File

@ -7,6 +7,7 @@ import (
errs "gateway/internal/library/errors" errs "gateway/internal/library/errors"
"gateway/internal/library/errors/code" "gateway/internal/library/errors/code"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
) )

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"gateway/internal/library/errors/code" "gateway/internal/library/errors/code"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
) )

View File

@ -4,6 +4,7 @@ import (
"net/http" "net/http"
"gateway/internal/library/errors/code" "gateway/internal/library/errors/code"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
) )

View File

@ -28,7 +28,7 @@ func (ve ValidationErrors) Error() string {
var sb strings.Builder var sb strings.Builder
sb.WriteString("validation failed:") sb.WriteString("validation failed:")
for i, e := range ve { for i, e := range ve {
sb.WriteString(fmt.Sprintf(" field='%s', message='%s'", e.Field, e.Message)) fmt.Fprintf(&sb, " field='%s', message='%s'", e.Field, e.Message)
if i < len(ve)-1 { if i < len(ve)-1 {
sb.WriteString(";") sb.WriteString(";")
} }
@ -57,9 +57,9 @@ func (ve ValidationErrors) ToBusinessError(scope code.Scope) *errs.Error {
} }
// translateValidationErrors converts validator.ValidationErrors to our custom ValidationErrors. // translateValidationErrors converts validator.ValidationErrors to our custom ValidationErrors.
func translateValidationErrors(errs validator.ValidationErrors, trans ut.Translator) ValidationErrors { func translateValidationErrors(valErrs validator.ValidationErrors, trans ut.Translator) ValidationErrors {
customErrors := make(ValidationErrors, 0, len(errs)) customErrors := make(ValidationErrors, 0, len(valErrs))
for _, err := range errs { for _, err := range valErrs {
customErrors = append(customErrors, ValidationError{ customErrors = append(customErrors, ValidationError{
Field: err.Field(), // Already transformed by RegisterTagNameFunc Field: err.Field(), // Already transformed by RegisterTagNameFunc
Message: err.Translate(trans), Message: err.Translate(trans),

View File

@ -12,7 +12,7 @@ import (
entranslations "github.com/go-playground/validator/v10/translations/en" entranslations "github.com/go-playground/validator/v10/translations/en"
) )
// Validate ... // Validate validates structs using go-playground/validator tags and custom options.
type Validate interface { type Validate interface {
ValidateAll(obj any) error // 驗證整個結構體 ValidateAll(obj any) error // 驗證整個結構體
BindToValidator(opts ...Option) error // 綁定客製化驗證規則 BindToValidator(opts ...Option) error // 綁定客製化驗證規則

View File

@ -1,13 +1,13 @@
package validate package validate
import ( import (
"errors"
"fmt" "fmt"
"testing"
ut "github.com/go-playground/universal-translator" ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"testing"
) )
// Test struct for validation scenarios // Test struct for validation scenarios
@ -58,7 +58,7 @@ func TestValidateAll_DefaultTranslationFailure(t *testing.T) {
require.Error(t, err) require.Error(t, err)
var validationErrs ValidationErrors var validationErrs ValidationErrors
require.True(t, errors.As(err, &validationErrs), "Error should be of type ValidationErrors") require.ErrorAs(t, err, &validationErrs)
require.Len(t, validationErrs, 2) require.Len(t, validationErrs, 2)
// Check errors (order might vary) // Check errors (order might vary)
@ -103,7 +103,7 @@ func TestValidateAll_CustomValidationWithTranslation(t *testing.T) {
require.Error(t, err) require.Error(t, err)
var validationErrs ValidationErrors var validationErrs ValidationErrors
require.True(t, errors.As(err, &validationErrs)) require.ErrorAs(t, err, &validationErrs)
require.Len(t, validationErrs, 1) require.Len(t, validationErrs, 1)
assert.Equal(t, "user_status", validationErrs[0].Field) assert.Equal(t, "user_status", validationErrs[0].Field)

View File

@ -54,7 +54,7 @@ func TestWrapRequestErrorPreservesBusinessError(t *testing.T) {
t.Parallel() t.Parallel()
orig := errs.For(code.Facade).ResNotFound("x") orig := errs.For(code.Facade).ResNotFound("x")
if response.WrapRequestError(orig) != orig { if wrapped := response.WrapRequestError(orig); !errors.Is(wrapped, orig) {
t.Fatal("business error should not be wrapped") t.Fatal("business error should not be wrapped")
} }
} }