174 lines
6.7 KiB
Markdown
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*
|