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

174 lines
6.7 KiB
Markdown

# 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:**
```go
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
```
**Service-to-handler errors:**
```go
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):**
```go
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:**
```go
// 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:**
```go
// 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:
```go
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*