2026-03-21 16:01:32 +08:00

6.7 KiB

Coding Conventions

Analysis Date: 2026-03-21

Naming Patterns

Files (Go backend):

  • snake_case for all Go source files: activity_order_service.go, draw_config_save.go
  • Test files co-located with source: reward_snapshot_test.go next to rewards_create.go
  • Generated files suffixed with .gen.go: never edited manually
  • Package names match directory name: package activity in internal/service/activity/

Files (Vue frontend):

  • kebab-case for TypeScript API files: pay-orders.ts, order-snapshots.ts
  • kebab-case for view directories: player-manage/, shipping-orders/
  • PascalCase for Vue component filenames where applicable

Functions (Go):

  • PascalCase for exported: NewActivityOrderService, CreateActivityOrder, ListProductsForApp
  • camelCase for unexported: newRewardSnapshotTestService, shouldTriggerInstantDraw, assertAttribution
  • Constructor functions named New<Type> for service constructors: NewProduct(...), NewStore(...)
  • Handler methods return core.HandlerFunc (closure pattern): func (h *productHandler) ListProductsForApp() core.HandlerFunc

Functions (TypeScript frontend):

  • fetch prefix for API functions: fetchGetActivities, fetchGetActivityDetail
  • camelCase for all functions

Variables:

  • camelCase in Go: userID, activityID, testLogger
  • Named ID variables use int64 type consistently: userID int64, activityID int64

Types/Structs (Go):

  • PascalCase for exported: CreateActivityOrderRequest, ActivityOrderService
  • Unexported structs for implementation: activityOrderService, productHandler, context
  • Request structs named <verb><Domain>Request: listAppProductsRequest, CreateActivityOrderRequest
  • Response structs named <verb><Domain>Response: listAppProductsResponse, getAppProductDetailResponse
  • Interface types use verb-noun: ActivityOrderService, Service, Repo

Constants (Go error codes):

  • 5-digit pattern: service level (1) + module level (2) + specific error (2)
  • All-caps with CamelCase words: ServerError = 10101, ParamBindError = 10102
  • Grouped by domain in internal/code/code.go

Code Style

Formatting (Go):

  • gofmt -s via make fmt (uses standard gofmt)
  • Import grouping via go run cmd/mfmt/main.go: stdlib → local module (bindbox-game/...) → third-party
  • Line length not strictly enforced but long lines occur in handler code

Linting (Go):

  • golangci-lint run -D staticcheck via make lint
  • staticcheck disabled; other default golangci-lint checks active

Formatting (Frontend):

  • Prettier for all file types, configured via lint-staged hooks
  • ESLint with eslint-plugin-prettier/recommended
  • Single quotes enforced: quotes: ['error', 'single']
  • No semicolons: semi: ['error', 'never']
  • No var: 'no-var': 'error' — use let or const
  • @typescript-eslint/no-explicit-any disabled (any is allowed)
  • Vue multi-word component name rule disabled

Import Organization

Go — Three groups (enforced by cmd/mfmt/main.go):

  1. Standard library: "context", "net/http", "testing"
  2. Local module: "bindbox-game/internal/pkg/core", "bindbox-game/internal/repository/mysql"
  3. Third-party: "gorm.io/gorm", "github.com/gin-gonic/gin", "go.uber.org/zap"

TypeScript — Relative imports with @/ alias:

  • import request from '@/utils/http'
  • import { getActivityDetail } from './adminActivities' (relative for same-level)

Error Handling

Handler layer pattern:

if err := ctx.ShouldBindForm(req); err != nil {
    ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
    return
}

Service-to-handler errors:

if err != nil {
    ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, validation.Error(err)))
    return
}

Special string-based sentinel errors (avoid when possible, currently used in product handler):

if err.Error() == "PRODUCT_OFFSHELF" {
    ctx.AbortWithError(core.Error(http.StatusOK, 20001, "商品已下架"))
    return
}

Business error construction: Always use core.Error(httpCode, businessCode, message). Optionally chain .WithError(err) to attach stack trace or .WithAlert() for alerting.

Test error handling: Use t.Fatal(err) for setup failures, t.Fatalf(...) with format strings for assertion failures, t.Skipf(...) when preconditions fail (e.g., no live DB).

Logging

Framework: Zap-based custom logger via internal/pkg/logger (logger.CustomLogger interface)

Patterns:

  • Logger injected into handlers and services via constructor
  • Handler structs hold logger logger.CustomLogger field
  • Service structs hold logger logger.CustomLogger field
  • Use structured fields: zap.Field variadic args
  • Exported methods: Info, Error, Warn, Debug
  • In tests, use logger.NewCustomLogger(nil, logger.WithOutputInConsole())

Comments

Swagger annotations on every exported handler:

// ListProductsForApp 商品列表
// @Summary 商品列表
// @Description ...
// @Tags APP端.商品
// @Accept json
// @Produce json
// @Security LoginVerifyToken
// @Param ...
// @Success 200 {object} listAppProductsResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/products [get]

Chinese comments common for domain logic inline comments, struct field descriptions, and test assertions — bilingual codebase.

Interface private guard pattern:

// i 为了避免被其他包实现
i()

Function Design

Handler functions: Return core.HandlerFunc (closure); keep handler thin — delegate to service layer.

Service constructors: Always return interface, not concrete struct:

func NewActivityOrderService(l logger.CustomLogger, db mysql.Repo) ActivityOrderService {
    return &activityOrderService{...}
}

Service structs hold: logger, readDB *dao.Query, writeDB *dao.Query, repo mysql.Repo, plus nested service interfaces for cross-domain calls.

Context propagation: Use core.Context in handlers (not gin.Context); extract context.Context via ctx.RequestContext() for service calls.

Pagination defaults: Always default Page = 1, PageSize = 20 when not provided.

Module Design

Layer boundaries:

  • internal/api/ → handlers only, thin, call services
  • internal/service/ → business logic, call DAOs and other services
  • internal/repository/mysql/ → data access via GORM DAOs (generated)
  • internal/pkg/ → shared utilities, no business logic

Barrel files: Not used in Go. Each file exports its own types.

Repo interface: All database access goes through mysql.Repo interface (GetDbR(), GetDbW()). Always use dao.Use(db.GetDbR()) for read queries and dao.Use(db.GetDbW()) for writes.


Convention analysis: 2026-03-21