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_STYLE=go_zero
GO_ZERO_STYLE := go_zero
GO ?= go
GOFMT ?= gofmt "-s"
GOFILES := $(shell find . -name "*.go")
LDFLAGS := -s -w
VERSION="v0.0.1"
DOCKER_REP="reg.wang/app-cloudep-member-service"
GIT_COMMIT ?= $(shell git rev-parse --short HEAD)
GOFMT ?= gofmt
GOFILES := $(shell find . -name '*.go' -not -path './generate/doc-generate/*')
GO_DOC_DIR := generate/doc-generate
GO_DOC_BIN := $(GO_DOC_DIR)/bin/go-doc
API_ENTRY := ./generate/api/gateway.api
DOC_OUT := ./docs/openapi
.PHONY: gen-api build-go-doc gen-doc
gen-api: # 使用專案 handler 模板response.Write
goctl api go -api $(API_ENTRY) -dir . -style go_zero -home generate/goctl
GOCTL ?= goctl
GOCTL_PKG := github.com/zeromicro/go-zero/tools/goctl@latest
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 文件生成器)
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
@mkdir -p $(DOC_OUT)
$(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 文件生成。
@ -12,32 +12,40 @@ y94# Portal API Gateway (PGW)
## 環境需求
| 工具 | 說明 |
|------|------|
| Go | ≥ 1.26.1(見 `go.mod` |
| [goctl](https://go-zero.dev/docs/tasks/installation/goctl) | go-zero 程式碼生成 |
| Make | 執行 `gen-api` / `gen-doc`(可選) |
| 工具 | 必要 | 說明 |
|------|:----:|------|
| Go | ✓ | ≥ 1.26.1(見 `go.mod` |
| 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
go install github.com/zeromicro/go-zero/tools/goctl@latest
```
## 快速開始
```bash
# 克隆後進入專案目錄
cd gateway
# 下載依賴
# 1. 工具goctl、goimports
make tools
# 2. 依賴
go mod download
# 依 .api 生成 handler / logic / types若已生成可跳過
# 3. 依 .api 生成程式碼可選repo 內通常已有
make gen-api
# 啟動服務
go run gateway.go -f etc/gateway.yaml
# 4. 啟動
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 gen-doc` | 生成 `docs/openapi/gateway.yaml`OpenAPI 3.0 |
| `go run gateway.go -f etc/gateway.yaml` | 啟動 Gateway |
| `go test ./...` | 執行測試 |
| `make tools` | 首次安裝 goctl、goimports |
| `make gen-api` | 由 `gateway.api` 生成 Go自訂 `generate/goctl` 模板) |
| `make gen-doc` | 編譯 go-doc 並生成 `docs/openapi/gateway.yaml` |
| `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 實作。
### 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。
- **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`。
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 節。
### 7. 錯誤碼 API 速查
### 8. 錯誤碼 API 速查
```go
import (
@ -296,7 +325,7 @@ return nil, errb.InputMissingRequired("email").WithCause(err)
Category 與 HTTP 對照見 [internal/library/errors/README.md](internal/library/errors/README.md)。
### 8. API 文件(`@` 註解)
### 9. API 文件(`@` 註解)
`.api` 中:
@ -306,7 +335,7 @@ Category 與 HTTP 對照見 [internal/library/errors/README.md](internal/library
範例見 `generate/api/normal.api`
### 9. OpenAPI 產物與 Git
### 10. OpenAPI 產物與 Git
預設 `.gitignore` 會忽略 `docs/openapi/*.yaml`。若需提交給前端,請在 `.gitignore` 註解相關規則後執行 `make gen-doc` 並加入版本控制。

View File

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

View File

@ -4,8 +4,9 @@ import (
"strconv"
"strings"
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
"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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,7 +28,7 @@ func (ve ValidationErrors) Error() string {
var sb strings.Builder
sb.WriteString("validation failed:")
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 {
sb.WriteString(";")
}
@ -57,9 +57,9 @@ func (ve ValidationErrors) ToBusinessError(scope code.Scope) *errs.Error {
}
// translateValidationErrors converts validator.ValidationErrors to our custom ValidationErrors.
func translateValidationErrors(errs validator.ValidationErrors, trans ut.Translator) ValidationErrors {
customErrors := make(ValidationErrors, 0, len(errs))
for _, err := range errs {
func translateValidationErrors(valErrs validator.ValidationErrors, trans ut.Translator) ValidationErrors {
customErrors := make(ValidationErrors, 0, len(valErrs))
for _, err := range valErrs {
customErrors = append(customErrors, ValidationError{
Field: err.Field(), // Already transformed by RegisterTagNameFunc
Message: err.Translate(trans),

View File

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

View File

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

View File

@ -54,7 +54,7 @@ func TestWrapRequestErrorPreservesBusinessError(t *testing.T) {
t.Parallel()
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")
}
}