docs: map existing codebase

This commit is contained in:
win 2026-03-21 16:01:32 +08:00
parent 5b34972ee3
commit 5ede909be4
7 changed files with 1327 additions and 0 deletions

View File

@ -0,0 +1,148 @@
# Architecture
## Overview
Bindbox Game follows a **layered monolith** architecture pattern built with Go and the Gin HTTP framework. The application serves as a backend for a blind box / lottery game platform with both a WeChat Mini Program client and a Vue 3 admin panel.
## Architectural Pattern
**Layered Architecture (Handler → Service → Repository)**
```
HTTP Request
[Router] → route matching + middleware (auth, RBAC, blacklist)
[API Handler] → request parsing, validation, response formatting
[Service Layer] → business logic, orchestration
[Repository Layer] → GORM-based data access (MySQL read/write split)
MySQL (Master/Slave)
```
## Key Layers
### 1. Router Layer (`internal/router/`)
- `router.go` — Single file defining all routes via `NewHTTPMux()`
- Routes organized into groups:
- `/api/internal` — Internal service calls (X-Internal-Key auth)
- `/api/admin` — Admin panel (JWT + RBAC)
- `/api/app` — Mini Program public endpoints (no auth)
- `/api/app` (auth group) — Mini Program authenticated endpoints
- `/api/public` — Public livestream endpoints (access code auth)
- `/api/pay` — WeChat Pay callbacks (no auth)
### 2. Interceptor / Middleware Layer (`internal/router/interceptor/`)
- `admin_auth.go` — JWT token verification for admin users
- `admin_rbac.go` — Role-based access control with action-level permissions
- `app_auth.go` — App user token verification
- `blacklist.go` — Douyin user blacklist checking
- `interceptor.go` — Base interceptor struct with shared dependencies
### 3. API Handler Layer (`internal/api/`)
Organized by domain:
- `admin/` — Admin panel handlers (largest, ~30+ files)
- `activity/` — Lottery/game activity handlers
- `app/` — Store, product, banner, category handlers
- `game/` — Game ticket and minesweeper handlers
- `pay/` — Payment handlers
- `user/` — User management, orders, addresses
- `task_center/` — Task center handlers
- `common/` — Shared utilities (upload, openid)
- `public/` — Public livestream handlers
- `internal/` — Internal API handlers (Nakama integration)
### 4. Service Layer (`internal/service/`)
Business logic organized by domain:
- `activity/` — Activity CRUD, lottery processing, matching game, settlements, strategy pattern for draw types
- `admin/` — Admin user management, login
- `user/` — User management, orders, points, coupons, inventory, shipping, synthesis
- `order/` — Order processing
- `game/` — Game ticket management, minesweeper
- `douyin/` — Douyin order sync, reward dispatching
- `task_center/` — Task definitions, progress tracking, worker
- `product/` — Product management
- `finance/` — Financial operations, ledger
- `channel/` — Marketing channel management
- `title/` — User title/badge system
- `banner/`, `sysconfig/`, `common/`, `snapshot/`, `recycle/`, `synthesis/`, `livestream/`
### 5. Repository Layer (`internal/repository/mysql/`)
- `mysql.go` — Database connection management (read/write split via `Repo` interface)
- `plugin.go` — GORM plugins
- `model/*.gen.go` — Generated GORM models (do not edit)
- `dao/*.gen.go` — Generated GORM DAOs (do not edit)
- `task_center/models.go` — Task center specific models
- `test_helper.go`, `testrepo_sqlite.go` — Test infrastructure
## Entry Point
`main.go` initializes all infrastructure in order:
1. Config (Viper/TOML)
2. OpenTelemetry
3. MySQL (master + slave)
4. Logger (Zap-based with rotation)
5. Redis
6. HTTP server (Gin)
7. Background workers (settlement, expiration, order sync, dynamic config)
8. Graceful shutdown handler
## Data Flow
### Typical API Request Flow
```
Client → Gin Router → Middleware Chain → Handler → Service → Repository → MySQL
Redis (cache, locks)
```
### Background Task Flow
```
Scheduler (cron) → Service Method → Repository → MySQL
External API (WeChat, Douyin)
```
### Payment Flow
```
Client → Preorder API → WeChat Pay API → Client pays → WeChat Callback → Notify Handler → Order Service
```
## Key Design Decisions
| Decision | Rationale |
|----------|-----------|
| Read/write DB split | Performance: heavy reads go to slave, writes to master |
| GORM code generation | Consistency: models and DAOs auto-generated from schema |
| Custom `core.Context` wrapper | Standardized error handling, tracing, session management across all handlers |
| Strategy pattern for lottery | Different draw types (standard, ichiban) share interface but have different logic |
| Background workers in main process | Simplicity: no separate worker binary, uses goroutines |
| JWT with hash verification | Security: stored token hash prevents concurrent sessions |
## Cross-Cutting Concerns
- **Logging**: Zap-based with file rotation (`internal/pkg/logger/`)
- **Tracing**: OpenTelemetry integration (`internal/pkg/otel/`)
- **Error Codes**: 5-digit system in `internal/code/` (service level + module + specific)
- **Alerts**: Alert notification system (`internal/alert/`)
- **Metrics**: Prometheus metrics (`internal/metrics/`)
## External Service Boundaries
The application integrates with multiple external services through dedicated packages in `internal/pkg/`:
- WeChat Mini Program API (`wechat/`, `miniprogram/`)
- WeChat Pay v3 API (`pay/`)
- Douyin/TikTok API (`douyin/`)
- Aliyun SMS (`sms/`)
- Tencent COS (object storage)
- OpenTelemetry collector
---
*Generated: 2026-03-21*

View File

@ -0,0 +1,213 @@
# Codebase Concerns
**Analysis Date:** 2026-03-21
---
## Security Considerations
**WeChat AppSecret logged in plaintext:**
- Risk: WeChat AppSecret (OAuth credentials) are written to application logs at Info/Error level, exposing secrets in log files and any log aggregation systems.
- Files:
- `internal/service/user/login_weixin.go:52``s.logger.Info("DEBUG: LoginWeixin Config", zap.String("AppSecret", wcfg.AppSecret))`
- `internal/api/user/phone_bind.go:59``h.logger.Error("...", zap.String("app_secret", wxCfg.AppSecret))`
- `internal/api/common/openid_app.go:44``h.logger.Info("GetOpenID Config", zap.String("AppSecret", wxcfg.AppSecret))`
- Current mitigation: None. These are active, non-conditional log calls.
- Recommendations: Remove all AppSecret fields from log calls immediately. If debugging is needed, log only AppID (never AppSecret).
**Hardcoded internal API key fallback:**
- Risk: If `Internal.ApiKey` config is missing or empty, the system falls back to the hardcoded literal `"bindbox-internal-secret-2024"`. Any attacker with knowledge of this default can call all internal game settlement endpoints.
- Files: `internal/router/router.go:99`
- Current mitigation: Config key overrides the default when set.
- Recommendations: Remove the hardcoded default. Fail-closed: if `expectedKey == ""`, reject all requests with 503 rather than falling back to a known string.
**Hardcoded Nakama server key:**
- Risk: `internal/api/game/handler.go:207` contains `nakamaKey := "defaultkey"` which is the well-known Nakama default key, used when config is absent.
- Files: `internal/api/game/handler.go:207`
- Current mitigation: Config value overrides when present.
- Recommendations: Panic at startup if Nakama key is not configured in production mode.
**CORS allows all origins with credentials:**
- Risk: `AllowedOrigins: []string{"*"}` combined with `AllowCredentials: true` is an invalid CORS configuration per spec (browsers block it) and signals the intent to allow arbitrary origins was not fully thought through.
- Files: `internal/pkg/cors/cors.go:14,34`
- Current mitigation: Browsers enforce the spec restriction, partially preventing exploitation.
- Recommendations: Replace `"*"` with an explicit allowlist of trusted origins.
**pprof profiling endpoint exposed in production:**
- Risk: `core.WithEnablePProf()` is unconditionally passed in `internal/router/router.go:45`. The Go pprof endpoints at `/debug/pprof/*` expose heap dumps, goroutine stacks, CPU profiles, and memory layout — high-value information for attackers.
- Files: `internal/router/router.go:45`, `internal/pkg/core/core.go:252-258`
- Current mitigation: None — pprof is always enabled.
- Recommendations: Gate `WithEnablePProf()` behind an environment check (`ENV != "pro"`).
---
## Tech Debt
**Skipped Redis ticket validation in game token service:**
- Issue: `internal/service/game/token.go:127-136` has a commented-out `return` statement under a `// TODO: 临时跳过 Redis 验证`. When the Redis key is not found, validation is bypassed and the game token is accepted regardless. This permanently disables single-use ticket enforcement.
- Files: `internal/service/game/token.go:127-136`
- Impact: A valid JWT can be replayed indefinitely once issued; single-use semantics are broken.
- Fix approach: Restore the `return nil, fmt.Errorf("ticket not found or expired")` line. Investigate why tickets expire from Redis before use (likely TTL too short or Redis key prefix mismatch).
**Stub minesweeper game handlers:**
- Issue: `internal/api/internal/minesweeper/handler.go` contains two handlers (`VerifyTicket`, `SettleGame`) that are entirely mock implementations. Both contain `// TODO: 实际验证逻辑` and return hardcoded responses. The settle handler always returns `success: true` and a mock reward string.
- Files: `internal/api/internal/minesweeper/handler.go:46-78`
- Impact: Minesweeper game settlement and ticket verification are non-functional. Any caller receives a fake success response regardless of actual state.
- Fix approach: Implement proper Redis ticket lookup (matching the game token service), deduct tickets, and grant actual rewards on win.
**TODO counts in douyin order sync:**
- Issue: `internal/api/admin/douyin_orders_admin.go:302,337` have `GrantedCount: 0` and `RefundedCount: 0` with TODO comments noting these should return actual counts from the sync/grant functions. Admin UI shows incorrect stats (always 0) for these operations.
- Files: `internal/api/admin/douyin_orders_admin.go:302,337`
- Impact: Douyin sync reports are inaccurate. Operators cannot confirm how many prizes were actually granted or refunded per sync run.
- Fix approach: Update `SyncRefundStatus` and `GrantLivestreamPrizes` to return counts, propagate to response.
**Duplicate user handler registration with undocumented intent:**
- Issue: `internal/router/router.go:83` has an explicit `// TODO: Check if userHandler and userAppHandler are redundant or distinct.` comment. Two user handler instances exist with unclear separation of responsibility.
- Files: `internal/router/router.go:82-86`
- Impact: Risk of inconsistent behavior — changes to one handler may be expected to apply to both but don't.
- Fix approach: Audit both handler paths; consolidate or document the distinction.
---
## Known Bugs
**Debug fmt.Printf statements left in production code:**
- Symptoms: Over 35 `fmt.Printf("[DEBUG]...")` calls across production code path are unconditionally executed in all environments, writing to stdout rather than the structured Zap logger. This pollutes logs, degrades performance, and leaks business data.
- Files (representative):
- `internal/service/douyin/order_sync.go:648,710,731,800,812,824,829,840,846,850,855,859,872`
- `internal/service/sysconfig/dynamic_config.go:198`
- `internal/service/activity/activity_order_service.go:150,158,163,207,235,265`
- `internal/service/user/order_timeout.go:51,60,150`
- `internal/service/user/coupon_transfer.go:97`
- `internal/api/user/login_app.go:59`
- `internal/api/admin/lottery_admin.go:535`
- `internal/api/activity/lottery_app.go:67`
- `internal/api/activity/issue_choices_app.go:76,79,82`
- `internal/service/game/token.go:78,131,141,147,152,159`
- `internal/pkg/wechat/code2session.go:22`
- Trigger: Always — these are unconditional print calls in hot paths.
- Fix: Remove all `fmt.Printf` calls; replace necessary observability with `s.logger.Debug(...)` calls gated by log level.
**Silently discarded errors in critical paths:**
- Symptoms: Multiple writes/inserts ignore errors via `_ = h.repo.GetDbW().Exec(...)` and `_ = h.repo.GetDbR().Raw(...).Scan(...)`. Failed inventory updates, ledger entries, and coupon operations produce no error response.
- Files (representative):
- `internal/api/admin/pay_refund_admin.go:155,174,195,198,224,227,235,239,262`
- `internal/api/activity/lottery_app.go:429,639,667`
- `internal/api/activity/issues_app.go:80`
- `internal/api/admin/users_profile.go:172,188,197,222,246,249`
- `internal/api/admin/activity_commitment_admin.go:62,63,64,66,96`
- Trigger: On database errors in refund, inventory, and query flows.
- Fix: Wrap these in proper error handling; at minimum log errors; for writes in financial flows, propagate errors to callers.
---
## Performance Bottlenecks
**Dashboard admin handler is a 2,666-line monolith:**
- Problem: `internal/api/admin/dashboard_admin.go` contains the entire dashboard implementation in a single file. Many dashboard queries perform multiple full-table scans on orders, inventory, and draw_logs tables without pagination constraints.
- Files: `internal/api/admin/dashboard_admin.go`
- Cause: Dashboard endpoints aggregate across all-time data. Complex multi-table JOINs (up to 7 tables in `dashboard_spending.go`) run inline per request.
- Improvement path: Introduce materialized summaries or scheduled background computation for dashboard aggregates. Split the file into per-widget files (max 400 lines each).
**Unguarded `Find()` calls without LIMIT in handler layer:**
- Problem: Several handlers call `.Find()` on potentially unbounded result sets.
- Files:
- `internal/api/app/categories.go:44` — all active categories loaded at once
- `internal/api/activity/issue_choices_app.go:50` — all reward settings for an issue
- `internal/api/activity/draw_logs_app.go:133,146,164` — users, rewards, products hydration in loops
- `internal/api/app/product_category.go:47`
- Cause: Missing `Limit()` calls; no pagination on supporting queries.
- Improvement path: Add `.Limit(500)` guards and paginate public-facing endpoints. For in-memory hydration loops, batch-query by IN clause (already done in some places) but add bounds.
**time.Sleep in hot production paths:**
- Problem: `internal/pkg/wechat/shipping.go` uses `time.Sleep(time.Second)` and `time.Sleep(2 * time.Second)` between WeChat API calls, blocking goroutines for up to 4 seconds per operation. Under load, this exhausts the goroutine pool.
- Files: `internal/pkg/wechat/shipping.go:138,165,215,246`
- Cause: Naive retry/rate-limit handling.
- Improvement path: Replace with exponential backoff using `time.After` or `context`-aware wait, and move to a worker pool pattern.
---
## Fragile Areas
**Handler layer bypasses service layer for DB writes:**
- Files: `internal/api/admin/pay_refund_admin.go`, `internal/api/activity/lottery_app.go`, `internal/api/activity/issue_choices_app.go`
- Why fragile: 113 direct `GetDbW()` calls exist in the API handler layer. Business logic (inventory updates, ledger entries, item card resets) is scattered across handlers and services, making transactional consistency hard to enforce and audit.
- Safe modification: Any change to refund or inventory logic must trace all three locations (handler, service, scheduler). Do not add new business writes in handlers.
- Test coverage: No dedicated tests for `pay_refund_admin.go` refund flows.
**Douyin order sync with goroutine fan-out and mutex:**
- Files: `internal/service/douyin/order_sync.go`
- Why fragile: The sync loop at line 1056 spawns a goroutine per order item with a shared `sync.Mutex` for counter updates. Errors from individual goroutines are collected in a slice protected by mutex, but goroutine lifecycle is managed only through a `sync.WaitGroup`. A single panicking goroutine will be caught by the task center worker recover, but may leave the mutex in an inconsistent state.
- Safe modification: Add goroutine-level panic recovery inside the fan-out goroutine (line 1056). Do not increase fan-out concurrency without adding semaphore limiting.
- Test coverage: 1 test file for a 1,094-line service.
**Task center service is a 1,665-line file with embedded BUG FIX comments:**
- Files: `internal/service/task_center/service.go`
- Why fragile: The file contains multiple `// BUG FIX:` comments at lines 715, 809, 1558, 1581, 1609, 1629 indicating patch-on-patch fixes. The quantity resolution logic for reward payloads has been fixed three times (lines 1558, 1581, 1609, 1629) with slight variations — these must be kept consistent.
- Safe modification: When modifying reward quantity parsing, update all four sites. Do not add a fifth variant.
- Test coverage: 4 test files cover primarily list filtering and invite logic, not the reward claim path.
**Dynamic config service with global singleton and panic:**
- Files: `internal/service/sysconfig/global.go:34-37`, `internal/service/sysconfig/dynamic_config.go`
- Why fragile: `GetGlobalDynamicConfig()` panics if called before `InitGlobalDynamicConfig()`. Any package that calls this at init time or before `main.go` initialization order completes will crash the process.
- Safe modification: Always call `InitGlobalDynamicConfig()` as the first step after DB initialization in `main.go`. Do not call `GetGlobalDynamicConfig()` at package `init()` scope.
- Test coverage: No tests in `internal/service/sysconfig/`.
---
## Test Coverage Gaps
**User service layer (37 source files, 2 test files):**
- What's not tested: Login flow, coupon add/transfer, order timeout, address share, expiration task, WeChat integration wrappers.
- Files: `internal/service/user/` (35 untested files including `login_weixin.go`, `coupon_add.go`, `order_timeout.go`, `address_share.go`)
- Risk: Regressions in payment-adjacent and user lifecycle code go undetected.
- Priority: High
**Activity service (24 source files, 3 test files):**
- What's not tested: `lottery_process.go`, `activity_order_service.go`, `scheduler.go`, `matching_game.go`, most of the 24 files.
- Files: `internal/service/activity/`
- Risk: Core lottery and order creation flows have no automated test coverage. Any refactor risks silent breakage.
- Priority: High
**Admin service (6 source files, 0 test files):**
- What's not tested: All of `internal/service/admin/`.
- Files: `internal/service/admin/`
- Risk: Admin-level business logic untested.
- Priority: Medium
**Snapshot and recycle services:**
- What's not tested: `internal/service/snapshot/` (2 files, 0 tests), `internal/service/recycle/` (1 file, 0 tests).
- Risk: Snapshot replay and recycle operations silently broken.
- Priority: Medium
**Sysconfig service:**
- What's not tested: `internal/service/sysconfig/` (3 files, 0 tests) including the global singleton and dynamic config loader.
- Risk: Config loading failures or key formatting bugs go undetected.
- Priority: Medium
---
## Scaling Limits
**Matching game in-memory state:**
- Current capacity: Game state for the matching card game (`MatchingGameState`) is stored in `internal/service/activity/matching_game.go` using an in-memory `sync.Mutex`-protected struct. This is per-process state.
- Limit: Cannot scale horizontally — a second server instance has no visibility into game state of the first.
- Scaling path: Migrate game state to Redis using atomic operations or use a dedicated game state store (e.g., Nakama, which is already referenced in the codebase).
**Scheduler goroutines without stop signal:**
- Current capacity: `internal/service/activity/scheduler.go:38` and `internal/service/douyin/scheduler.go:29` spawn bare goroutines that loop forever on `time.Sleep(30 * time.Second)`.
- Limit: Cannot be stopped gracefully on shutdown — goroutines outlive the shutdown context.
- Scaling path: Thread a `context.Context` into the scheduler loop and break on `ctx.Done()`.
---
## Dependencies at Risk
**Proliferation of debug command tools in `cmd/`:**
- Risk: `cmd/` contains 9+ one-off debug/diagnostic tools (`debug_task_270`, `debug_check_coupon_22`, `fix_openid`, `exploit_verify`, `check_order`, etc.) that have hardcoded database credentials or connection strings for one-time use. These tools may be committed with active credentials and are not maintained.
- Impact: Security exposure if credentials are embedded; build confusion if these break the CI pipeline.
- Migration plan: Move all one-off tools to a `tools/` directory with a clear no-deploy policy, or delete after use.
---
*Concerns audit: 2026-03-21*

View File

@ -0,0 +1,173 @@
# 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*

View File

@ -0,0 +1,186 @@
# External Integrations
**Analysis Date:** 2026-03-21
## APIs & External Services
**WeChat Mini Program:**
- Service: WeChat Mini Program (微信小程序)
- Purpose: User authentication, phone number retrieval, QR code generation, subscribe messages, short links, URL schemes, shipping queries
- SDK/Client: Custom HTTP client in `internal/pkg/wechat/` and `internal/pkg/miniprogram/`
- Key files: `internal/pkg/wechat/code2session.go`, `internal/pkg/wechat/phone_number.go`, `internal/pkg/wechat/decrypt.go`, `internal/pkg/wechat/qrcode.go`, `internal/pkg/miniprogram/access_token.go`, `internal/pkg/miniprogram/subscribe.go`
- Auth: `configs.Wechat.AppID` / `configs.Wechat.AppSecret` (config keys: `wechat.app_id`, `wechat.app_secret`)
- Template: `configs.Wechat.LotteryResultTemplateID` for subscribe messages
**WeChat Pay:**
- Service: WeChat Pay API v3 (微信支付)
- Purpose: Payment processing for game activities
- SDK/Client: `github.com/wechatpay-apiv3/wechatpay-go v0.2.21`
- Key files: `internal/pkg/pay/wechat.go`, `internal/pkg/pay/client.go`
- Auth: Merchant ID (`WECHAT_MCHID`), API v3 key (`WECHAT_API_V3_KEY`), serial number (`WECHAT_SERIAL_NO`), RSA private key (`WECHAT_PRIVATE_KEY_PATH`)
- Supports dynamic config override from `sysconfig` service (Base64 private key stored in DB)
- Notify URL: `WECHAT_NOTIFY_URL` (callback for payment results)
**Douyin (TikTok) / 抖店:**
- Service: Douyin Mini Program + 抖店 (TikTok Shop) API
- Purpose: User auth, order synchronization, product rewards, Douyin access token
- SDK/Client: Custom HTTP client in `internal/pkg/douyin/`
- Key files: `internal/pkg/douyin/access_token.go`, `internal/pkg/douyin/code2session.go`, `internal/pkg/douyin/phonenumber.go`
- External endpoint: `https://developer.toutiao.com/api/apps/v2/token`
- Auth: `configs.Douyin.AppID` / `configs.Douyin.AppSecret` (read from dynamic sysconfig at runtime)
- Background task: `douyinsvc.StartDouyinOrderSync()` runs scheduled order sync
**Aliyun SMS (阿里云短信):**
- Service: Alibaba Cloud Dysms (短信服务)
- Purpose: SMS verification code delivery
- SDK/Client: `github.com/alibabacloud-go/dysmsapi-20170525/v4 v4.1.3` + `github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13`
- Key files: `internal/pkg/sms/aliyun.go`
- External endpoint: `dysmsapi.aliyuncs.com`
- Auth: `ALIYUN_SMS_ACCESS_KEY_ID` / `ALIYUN_SMS_ACCESS_KEY_SECRET`
- Config: `ALIYUN_SMS_SIGN_NAME`, `ALIYUN_SMS_TEMPLATE_CODE`
**Tencent COS (腾讯云对象存储):**
- Service: Tencent Cloud Object Storage
- Purpose: File uploads (images, game assets, user avatars)
- SDK/Client: `github.com/tencentyun/cos-go-sdk-v5 v0.7.37`
- Auth: `configs.COS.SecretID` / `configs.COS.SecretKey`
- Config: `configs.COS.Bucket` (e.g., `keaiya-1259195914`), `configs.COS.Region` (e.g., `ap-shanghai`), `configs.COS.BaseURL` (optional CDN URL)
## Data Storage
**Databases:**
- MySQL (primary)
- Connection: Read replica via `MYSQL_READ_ADDR` / `MYSQL_ADDR`; Write master via `MYSQL_WRITE_ADDR` / `MYSQL_ADDR`
- User: `MYSQL_USER`, Password: `MYSQL_PASS`, DB name: `MYSQL_NAME`
- Client: GORM v1.25.9 (`gorm.io/gorm`) with `gorm.io/driver/mysql v1.5.2`
- Pool: max 100 open connections, 5 idle, 2 min lifetime
- Read/write split: manual two-connection pattern (`GetDbR()` / `GetDbW()`) in `internal/repository/mysql/mysql.go`
- Generated DAOs: `internal/repository/mysql/dao/*.gen.go`
- Generated models: `internal/repository/mysql/model/*.gen.go`
- Do NOT edit `.gen.go` files directly
- SQLite (test only)
- Used in test helpers (`internal/repository/mysql/testrepo_sqlite.go`) for in-memory unit tests
- Driver: `gorm.io/driver/sqlite v1.4.3`
**File Storage:**
- Tencent COS - all uploaded files (see COS section above)
- Local filesystem for logs (`./logs/mini-chat-access.log`) with rotation via lumberjack
**Caching:**
- Redis (single-node)
- Connection: `REDIS_ADDR` (default in dev: `127.0.0.1:6379`), `REDIS_PASS`, DB index from `configs.Redis.DB`
- Client: `github.com/redis/go-redis/v9 v9.17.2`
- Singleton initialized in `internal/pkg/redis/redis.go` via `redis.Init()`
- Pool: 20 connections, dial timeout 5s, read/write timeout 3s
- Used for: activity settlement, task center worker, session management
- Test: `github.com/alicebob/miniredis/v2 v2.36.1` for in-memory Redis in tests
## Authentication & Identity
**Admin JWT:**
- Provider: Custom JWT (HS256)
- Implementation: `internal/pkg/jwtoken/jwtoken.go`
- Middleware: `internal/router/interceptor/admin_auth.go`
- Secret: `ADMIN_JWT_SECRET` env var (falls back to `configs.JWT.AdminSecret`)
- Token payload: `proposal.SessionUserInfo` (user ID, role, session info)
- Token verification: signature + user active + token hash match (prevents concurrent sessions)
**App User JWT (WeChat/Douyin users):**
- Provider: Custom JWT (HS256)
- Middleware: `internal/router/interceptor/app_auth.go`
- Secret: `configs.JWT.PatientSecret` (config key: `jwt.patient_secret`)
- Separate secret from admin tokens
**RBAC (Admin):**
- Implementation: `internal/router/interceptor/admin_rbac.go`
- Pattern: Role-based — `RequireAdminRole()` checks any role assigned; `RequireAdminAction(mark)` checks specific action permission
**Internal Service Auth:**
- Pattern: `X-Internal-Key` header check for internal API endpoints (`/api/internal/*`)
- Secret: `configs.Internal.ApiKey` (env: hardcoded fallback `bindbox-internal-secret-2024`)
- Used for Nakama game server communication
**Blacklist:**
- Implementation: `internal/router/interceptor/blacklist.go`
- Token blacklisting support (likely Redis-backed)
## Monitoring & Observability
**Distributed Tracing:**
- Service: OpenTelemetry (OTLP HTTP) — compatible with Grafana Tempo
- SDK: `go.opentelemetry.io/otel v1.39.0` + `otlptracehttp` exporter
- Implementation: `internal/pkg/otel/otel.go`, `internal/pkg/otel/middleware.go`
- Config: `configs.Otel.Enabled` (bool), `configs.Otel.Endpoint` (e.g., `tempo:4318`)
- Middleware applied in `internal/router/router.go` when enabled
- Gin middleware traces all HTTP requests
**Metrics:**
- Service: Prometheus
- SDK: `github.com/prometheus/client_golang v1.17.0`
- Implementation: `internal/metrics/` package (referenced in proposal)
**Logging:**
- Framework: Uber Zap `go.uber.org/zap v1.26.0`
- Custom wrapper: `internal/pkg/logger/logger.go`
- File rotation: `gopkg.in/natefinch/lumberjack.v2 v2.2.1`
- Log file: `./logs/mini-chat-access.log`
- Log levels: debug, info, warn, error, fatal
**Profiling:**
- pprof endpoint enabled in dev via `github.com/gin-contrib/pprof v1.4.0`
- Enabled in router: `core.WithEnablePProf()`
**Error Tracking:**
- Custom alert handler: `internal/alert/` package
- Registered via `core.WithAlertNotify(alert.NotifyHandler())` in router
## CI/CD & Deployment
**Containerization:**
- Docker multi-stage build: `Dockerfile`
- Build image: `golang:1.24-alpine`
- Runtime image: `alpine:latest`
- Port: `9991`
- Health check: `GET http://localhost:9991/system/health`
- Example image: `zfc931912343/bindbox-game:v1.10`
**Build Targets:**
- Linux (amd64): `make build-linux` → binary `bindboxgame_api`
- macOS: `make build-mac`
- Windows: `make build-win``bindboxgame_api.exe`
**CI Pipeline:**
- Not detected in codebase (no GitHub Actions / CI config files found)
## Environment Configuration
**Required env vars (production):**
- `ENV` — Environment selector (`dev`/`fat`/`uat`/`pro`)
- `MYSQL_ADDR` or `MYSQL_READ_ADDR` + `MYSQL_WRITE_ADDR`
- `MYSQL_USER`, `MYSQL_PASS`, `MYSQL_NAME`
- `REDIS_ADDR`, `REDIS_PASS`
- `WECHAT_MCHID`, `WECHAT_SERIAL_NO`, `WECHAT_API_V3_KEY`, `WECHAT_PRIVATE_KEY_PATH`, `WECHAT_NOTIFY_URL`
- `ALIYUN_SMS_ACCESS_KEY_ID`, `ALIYUN_SMS_ACCESS_KEY_SECRET`, `ALIYUN_SMS_SIGN_NAME`, `ALIYUN_SMS_TEMPLATE_CODE`
- `ADMIN_JWT_SECRET`
**Secrets location:**
- Primary: TOML config files embedded in binary (`configs/*.toml`) — note dev TOML contains real credentials (security concern)
- Override: Environment variables at runtime (preferred for production)
- WeChat Pay private key: file path or Base64 in `sysconfig` DB table (dynamic config)
## Webhooks & Callbacks
**Incoming:**
- WeChat Pay payment notification: `configs.WechatPay.NotifyURL` (`WECHAT_NOTIFY_URL`) — called by WeChat servers to deliver payment results; handled in `internal/api/pay/` package
- Douyin order notification: `configs.Douyin.NotifyURL` — callback for Douyin order events
**Outgoing:**
- Douyin access token refresh: `POST https://developer.toutiao.com/api/apps/v2/token`
- Aliyun SMS send: `POST https://dysmsapi.aliyuncs.com`
- WeChat API calls: Various WeChat Mini Program endpoints for auth, phone, subscribe messages
- Tencent COS: Object upload/download operations
---
*Integration audit: 2026-03-21*

144
.planning/codebase/STACK.md Normal file
View File

@ -0,0 +1,144 @@
# Technology Stack
**Analysis Date:** 2026-03-21
## Languages
**Primary:**
- Go 1.24.0 - Backend server, all business logic, API handlers
- TypeScript ~5.6.3 - Frontend admin panel (`web/admin/src/`)
**Secondary:**
- SQL - Database migrations (`migrations/` directory)
- TOML - Configuration files (`configs/*.toml`)
- SCSS - Frontend styles (`web/admin/src/assets/styles/`)
## Runtime
**Backend:**
- Go runtime 1.24.0 (toolchain go1.24.2)
- Docker: `golang:1.24-alpine` build stage, `alpine:latest` final stage
**Frontend:**
- Node.js >= 18.0.0
**Package Manager:**
- Go modules (`go.mod` / `go.sum`) - lockfile present
- pnpm >= 8.8.0 - frontend (`web/admin/pnpm-lock.yaml`) - lockfile present
## Frameworks
**Backend Core:**
- `github.com/gin-gonic/gin v1.9.1` - HTTP web framework
- `gorm.io/gorm v1.25.9` - ORM for MySQL
- `gorm.io/gen v0.3.26` - GORM code generation from schema
- `gorm.io/plugin/dbresolver v1.5.0` - Read/write split support
**Frontend Core:**
- Vue 3 `^3.5.21` - UI framework (`web/admin/src/`)
- Vite `^5.4.10` - Build tool and dev server
- Element Plus `^2.11.2` - UI component library
- Pinia `^3.0.3` - State management
- Vue Router `^4.5.1` - Client-side routing
- Tailwind CSS `^4.1.14` - Utility-first CSS
**Testing (Backend):**
- `github.com/stretchr/testify v1.11.1` - Assertions
- `github.com/DATA-DOG/go-sqlmock v1.5.2` - MySQL mock
- `github.com/alicebob/miniredis/v2 v2.36.1` - In-memory Redis for tests
- `gorm.io/driver/sqlite v1.4.3` - SQLite for in-memory test DB (`internal/repository/mysql/testrepo_sqlite.go`)
**Testing (Frontend):**
- Vitest `^1.0.0` - Unit test runner
- `@vue/test-utils ^2.4.0` - Vue component testing
**Build/Dev (Backend):**
- Makefile - Task runner (`Makefile`)
- `golangci-lint` - Linter (install via `make tools`)
- `go-swagger` - Swagger generation (install via `make tools`)
- `cmd/mfmt/main.go` - Custom import formatter (groups: stdlib, local, third-party)
- `cmd/gormgen/main.go` - GORM model/DAO code generator
**Build/Dev (Frontend):**
- ESLint `^9.9.1` + TypeScript ESLint `^8.3.0` - Linting
- Prettier `^3.5.3` - Code formatting
- Stylelint `^16.20.0` - CSS/SCSS linting
- Husky `^9.1.5` + lint-staged - Pre-commit hooks
- Terser `^5.36.0` - Minification
- `vite-plugin-compression ^0.5.1` - Gzip compression for production
## Key Dependencies
**Critical (Backend):**
- `github.com/spf13/viper v1.17.0` - Configuration management (TOML, env var overrides)
- `go.uber.org/zap v1.26.0` - Structured logging
- `gopkg.in/natefinch/lumberjack.v2 v2.2.1` - Log file rotation
- `github.com/golang-jwt/jwt/v5 v5.2.0` - JWT auth tokens
- `github.com/redis/go-redis/v9 v9.17.2` - Redis client (singleton)
- `github.com/go-sql-driver/mysql v1.7.1` - MySQL driver
- `github.com/bytedance/sonic v1.13.2` - High-performance JSON encoder/decoder
- `github.com/bwmarrin/snowflake v0.3.0` - Distributed ID generation
- `github.com/go-resty/resty/v2 v2.10.0` - HTTP client for external API calls
- `github.com/prometheus/client_golang v1.17.0` - Prometheus metrics
- `golang.org/x/crypto v0.44.0` - Cryptographic utilities
**Critical (Frontend):**
- Axios `^1.12.2` - HTTP client for API calls
- Echarts `^6.0.0` - Charts and data visualization
- `@vueuse/core ^13.9.0` - Vue composition utilities
- `pinia-plugin-persistedstate ^4.3.0` - Persistent state storage
- `dayjs ^1.11.19` - Date/time manipulation
- `crypto-js ^4.2.0` - Client-side cryptography
- `xlsx ^0.18.5` - Excel file generation/parsing
- `@wangeditor/editor ^5.1.23` - Rich text editor
**Infrastructure (Backend):**
- `go.opentelemetry.io/otel v1.39.0` - Distributed tracing (OTLP HTTP exporter)
- `github.com/gin-contrib/pprof v1.4.0` - Go profiling endpoint
- `github.com/swaggo/gin-swagger v1.6.0` - Swagger UI embedded in Gin
- `github.com/tealeg/xlsx v1.0.5` - Excel file generation (server-side)
- `github.com/rs/cors/wrapper/gin v0.0.0-20231013084403-73f81b45a644` - CORS middleware
## Configuration
**Backend Environment:**
- Set via `ENV` environment variable: `dev` | `fat` | `uat` | `pro` (default: `fat`)
- Config files embedded into binary at build time via `//go:embed` directives
- Config files: `configs/dev_configs.toml`, `configs/fat_configs.toml`, `configs/uat_configs.toml`, `configs/pro_configs.toml`
- TOML format parsed via Viper (`github.com/spf13/viper`)
**Environment Variable Overrides (at runtime):**
- `MYSQL_ADDR`, `MYSQL_READ_ADDR`, `MYSQL_WRITE_ADDR`, `MYSQL_USER`, `MYSQL_PASS`, `MYSQL_NAME`
- `REDIS_ADDR`, `REDIS_PASS`
- `WECHAT_MCHID`, `WECHAT_SERIAL_NO`, `WECHAT_PRIVATE_KEY_PATH`, `WECHAT_API_V3_KEY`, `WECHAT_NOTIFY_URL`, `WECHAT_PUBLIC_KEY_ID`, `WECHAT_PUBLIC_KEY_PATH`
- `ALIYUN_SMS_ACCESS_KEY_ID`, `ALIYUN_SMS_ACCESS_KEY_SECRET`, `ALIYUN_SMS_SIGN_NAME`, `ALIYUN_SMS_TEMPLATE_CODE`
- `ADMIN_JWT_SECRET` - Admin JWT signing secret override
**Frontend Environment:**
- Vite env vars: `VITE_VERSION`, `VITE_PORT`, `VITE_BASE_URL`, `VITE_API_URL`, `VITE_API_PROXY_URL`
- Dev proxy: `/api` requests forwarded to `VITE_API_PROXY_URL`
**Build:**
- Backend: `Dockerfile` (multi-stage, `golang:1.24-alpine``alpine:latest`)
- Server port: `9991` (constant in `configs/constants.go`)
- Container exposes port `9991`
## Platform Requirements
**Development:**
- Go 1.24+
- Node.js >= 18.0.0, pnpm >= 8.8.0
- MySQL instance (read/write addresses)
- Redis instance
- `golangci-lint` and `go-swagger` for linting/docs
**Production:**
- Docker (Linux/amd64 binary, CGO_ENABLED=0)
- Alpine Linux container
- MySQL with optional read replica (master-slave)
- Redis single-node
- Optional: OpenTelemetry-compatible collector (Tempo) at configured OTLP endpoint
---
*Stack analysis: 2026-03-21*

View File

@ -0,0 +1,180 @@
# Directory Structure
## Top-Level Layout
```
bindbox_game/
├── main.go # Application entry point
├── go.mod / go.sum # Go module definition
├── Makefile # Build, test, lint, format commands
├── Dockerfile # Docker build config
├── CLAUDE.md # AI assistant guidance
├── configs/ # Environment-specific TOML config files
│ ├── dev_configs.toml
│ ├── fat_configs.toml
│ ├── uat_configs.toml
│ ├── pro_configs.toml
│ └── cert/ # SSL/payment certificates
├── internal/ # Core application code (Go convention)
│ ├── api/ # HTTP handlers (organized by domain)
│ ├── service/ # Business logic layer
│ ├── repository/mysql/ # Data access layer (GORM)
│ ├── router/ # HTTP routing and middleware
│ ├── pkg/ # Shared internal packages
│ ├── code/ # Error code definitions
│ ├── alert/ # Alert notification system
│ ├── metrics/ # Prometheus metrics
│ ├── proposal/ # Shared types/interfaces
│ └── dblogger/ # Database query logger
├── cmd/ # CLI tools and debug utilities
│ ├── gormgen/ # GORM model code generator
│ ├── mfmt/ # Import formatter
│ ├── douyin_sync_debug/ # Douyin sync debugging
│ ├── check_order/ # Order checking tool
│ └── ... # Various debug/diagnostic tools
├── web/admin/ # Vue 3 admin panel (separate git repo)
│ ├── src/ # Vue source code
│ ├── dist/ # Production build output
│ └── package.json # Frontend dependencies
├── migrations/ # SQL migration files (date-prefixed)
├── resources/admin/ # Embedded admin panel assets
├── build/ # Build output directory
├── deploy/ # Deployment configurations
├── docs/ # Documentation
├── logs/ # Application log files
├── scripts/ # Utility scripts
└── tools/ # Standalone analysis/debug tools
```
## Key Locations
### API Handlers (`internal/api/`)
```
api/
├── admin/ # Admin panel endpoints (~30+ files)
│ ├── activities_admin.go # Activity CRUD
│ ├── dashboard_*.go # Dashboard analytics (multiple files)
│ ├── users_admin.go # User management
│ ├── douyin_orders_admin.go # Douyin order management
│ ├── livestream_admin.go # Livestream management
│ └── ...
├── activity/ # Lottery/game activity endpoints
│ ├── lottery_app.go # Lottery join/draw
│ ├── matching_game_app.go # Matching game logic
│ └── ...
├── app/ # Store/product endpoints
│ ├── store.go # Store items
│ ├── product.go # Products
│ └── coupon_transfer.go # Coupon transfers
├── game/ # Game (minesweeper) endpoints
├── pay/ # Payment endpoints
├── user/ # User management endpoints
├── task_center/ # Task center endpoints
├── common/ # Shared handlers (upload)
├── public/ # Public livestream endpoints
└── internal/ # Internal service endpoints
```
### Service Layer (`internal/service/`)
```
service/
├── activity/ # Activity business logic
│ ├── activity.go # Service struct and constructor
│ ├── lottery_process.go # Core lottery algorithm
│ ├── matching_game.go # Matching game logic
│ ├── scheduler.go # Settlement scheduler
│ └── strategy/ # Draw strategy pattern
│ ├── strategy.go # Interface definition
│ ├── default.go # Standard lottery
│ └── ichiban.go # Ichiban-style lottery
├── admin/ # Admin user management
├── user/ # User business logic (largest service)
├── order/ # Order processing
├── game/ # Game ticket management
├── douyin/ # Douyin integration
│ ├── order_sync.go # Order synchronization
│ ├── reward_dispatcher.go # Reward granting
│ └── scheduler.go # Sync scheduler
├── task_center/ # Task center (worker pattern)
├── finance/ # Financial/ledger operations
├── product/ # Product management
├── channel/ # Marketing channels
├── title/ # User titles/badges
└── ...
```
### Shared Packages (`internal/pkg/`)
```
pkg/
├── core/ # Custom Gin context wrapper (core.Context, core.Mux)
├── logger/ # Zap-based logger with file rotation
├── redis/ # Redis client initialization
├── jwtoken/ # JWT generation and parsing
├── otel/ # OpenTelemetry integration
├── wechat/ # WeChat Mini Program helpers
├── miniprogram/ # WeChat access token, subscribe messages
├── pay/ # WeChat Pay v3 integration
├── douyin/ # Douyin API client
├── sms/ # Aliyun SMS client
├── validation/ # Input validation
├── httpclient/ # HTTP client wrapper
├── idgen/ # ID generation
├── timeutil/ # Time utility (CST layout)
├── errors/ # Error types
├── points/ # Points calculation utilities
├── notify/ # Notification helpers
├── async/ # Async task utilities
├── cryptoaes/ # AES encryption
├── cryptorsa/ # RSA encryption
├── env/ # Environment detection
├── color/ # Console color output
├── debug/ # Debug utilities
├── cors/ # CORS configuration
├── shutdown/ # Graceful shutdown
├── startup/ # Startup utilities
├── trace/ # Trace utilities
├── util/ # General utilities
├── utils/ # Additional utilities
└── jsonutil/ # JSON helpers
```
## Naming Conventions
### Files
- `*_app.go` — App (mini program) facing handler
- `*_admin.go` — Admin panel handler
- `*_test.go` — Test files (alongside source)
- `*.gen.go` — Generated code (do not edit)
- `*_helper.go` — Helper functions for a domain
### Packages
- Service constructors: `New(logger, db)` returns service struct
- Handler constructors: `New(logger, db, ...)` returns handler struct
- Method naming: `Create*`, `Modify*`, `Delete*`, `List*`, `Get*`
### Database
- Migration files: `YYYYMMDD_description.sql` (e.g., `20260207_add_column.sql`)
- Model files: `internal/repository/mysql/model/*.gen.go`
- DAO files: `internal/repository/mysql/dao/*.gen.go`
### Configuration
- Environment-specific: `{env}_configs.toml`
- Environments: `dev`, `fat`, `uat`, `pro`
## Important Notes
- `web/admin/` is a **separate git repository** (has its own `.git`)
- Generated files in `model/` and `dao/` should never be manually edited
- The `tools/` directory contains standalone Go programs for debugging and analysis
- `cmd/` contains both production tools (gormgen, mfmt) and debug utilities
---
*Generated: 2026-03-21*

View File

@ -0,0 +1,283 @@
# Testing Patterns
**Analysis Date:** 2026-03-21
## Test Framework
**Runner:**
- Go standard `testing` package
- Config: `Makefile` target `test`
**Assertion Libraries:**
- `github.com/stretchr/testify v1.11.1``assert` package (preferred in newer tests)
- Standard `t.Fatal`, `t.Fatalf`, `t.Errorf` (used in most tests, older style)
**Mocking Libraries:**
- `github.com/DATA-DOG/go-sqlmock v1.5.2` — SQL-level DB mocking
- `github.com/alicebob/miniredis/v2 v2.36.1` — in-process Redis mock server
- Manual mock structs implementing interfaces (for `core.Context`, logger)
**Run Commands:**
```bash
make test # Run all tests: go test -v --cover ./internal/...
go test -v ./internal/service/... # Test specific package
go test -v -run TestFunctionName ./... # Run single test
```
**Coverage output:** `coverage.out` (present in repo root)
## Test File Organization
**Location:**
- Co-located with source files — `foo_test.go` sits in the same directory as `foo.go`
- No separate `tests/` directory for Go unit/integration tests
**Naming:**
- `<subject>_test.go` matching the function/feature under test
- Package declaration: same package as source (`package activity`) for white-box tests, or `package game_test` for black-box tests (rare)
**Structure:**
```
internal/service/activity/
├── activity_order_service.go
├── concurrency_test.go # integration (real DB)
├── reward_snapshot_test.go # integration (SQLite in-memory)
├── sanitize_test.go # unit (no DB)
internal/service/game/
├── token.go
├── token_test.go # uses miniredis + SQLite
internal/service/user/
├── error_test.go # uses go-sqlmock
├── request_shipping_batch_test.go
internal/api/app/
├── store_test.go # HTTP handler integration test
```
## Test Structure
**Suite Organization (table-driven tests):**
```go
func TestShouldTriggerInstantDraw(t *testing.T) {
testCases := []struct {
name string
orderStatus int32
drawMode string
shouldTrigger bool
}{
{"已支付+即时开奖", 2, "instant", true},
{"已支付+定时开奖", 2, "scheduled", false},
{"未支付+即时开奖", 1, "instant", false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := shouldTriggerInstantDraw(tc.orderStatus, tc.drawMode)
if result != tc.shouldTrigger {
t.Errorf("期望触发=%v实际触发=%v", tc.shouldTrigger, result)
}
})
}
}
```
**Patterns:**
- Setup: create repo/DB then initialize service via constructor
- Teardown: SQLite in-memory DBs are ephemeral (no cleanup needed); miniredis uses `defer mr.Close()`
- Assertion: `t.Fatalf` for unrecoverable setup failures; `t.Errorf` for assertion failures
- Skipping: `t.Skipf` when preconditions absent (e.g., real DB unavailable in concurrency tests)
## Mocking
**In-memory SQLite (primary DB mock):**
```go
repo, err := mysql.NewSQLiteRepoForTest() // internal/repository/mysql/testrepo_sqlite.go
if err != nil {
t.Fatal(err)
}
db := repo.GetDbW()
// Manually create tables with SQLite-compatible DDL
db.Exec(`CREATE TABLE products (id INTEGER PRIMARY KEY AUTOINCREMENT, ...)`)
```
`NewSQLiteRepoForTest()``gorm.Open(sqlite.Open(":memory:"), ...)` → returns `Repo` interface.
**TestRepo wrapping real DB (for integration tests with live MySQL):**
```go
db, err := gorm.Open(drivermysql.Open(dsn), &gorm.Config{})
repo := mysql.NewTestRepo(db) // internal/repository/mysql/test_helper.go
```
**go-sqlmock (SQL-level mocking):**
```go
db, mock, err := sqlmock.New()
gormDB, _ := gorm.Open(gormmysql.New(gormmysql.Config{
Conn: db,
SkipInitializeWithVersion: true,
}), &gorm.Config{})
mock.ExpectQuery("SELECT .* FROM `system_item_cards`").
WithArgs(100, sqlmock.AnyArg()).
WillReturnRows(sqlmock.NewRows([]string{"id", "status"}).AddRow(100, 0))
```
**miniredis (Redis mock):**
```go
mr, err := miniredis.Run()
defer mr.Close()
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
```
**Manual interface mock (for `core.Context`):**
```go
type mockContext struct {
core.Context // embed to satisfy interface
ctx context.Context
}
func (m *mockContext) RequestContext() core.StdContext { return core.StdContext{Context: m.ctx} }
func (m *mockContext) ShouldBindJSON(obj interface{}) error { return nil }
func (m *mockContext) AbortWithError(err core.BusinessError) {}
// ... implement all interface methods with no-op stubs
```
**MockLogger pattern:**
```go
type MockLogger struct {
logger.CustomLogger
}
func (l *MockLogger) Info(msg string, fields ...zap.Field) {}
func (l *MockLogger) Error(msg string, fields ...zap.Field) {}
func (l *MockLogger) Warn(msg string, fields ...zap.Field) {}
func (l *MockLogger) Debug(msg string, fields ...zap.Field) {}
```
**What to Mock:**
- DB connection (use SQLite in-memory or go-sqlmock)
- Redis (use miniredis)
- External API clients (use interface injection)
- Logger (use MockLogger embedding `logger.CustomLogger`)
- `core.Context` (use manual mock struct)
**What NOT to Mock:**
- Business logic within services under test
- DAO query building when testing DB query behavior
## Fixtures and Factories
**Test Data (DDL + seed SQL directly in test):**
```go
// Create table
repo.GetDbW().Exec(`CREATE TABLE orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
status INTEGER NOT NULL DEFAULT 1,
...
)`)
// Seed data
db.Exec("INSERT INTO orders (id, user_id, status, source_type, total_amount, created_at) VALUES (101, ?, 2, 0, 100, ?)", userID, o1Time)
```
**Shared table initialization helpers:**
```go
func initTestTables(t *testing.T, db *gorm.DB) { ... } // in task_center package tests
func ensureExtraTablesForServiceTest(t *testing.T, db *gorm.DB) { ... }
```
**Test helper functions marked with `t.Helper()`:**
```go
func assertAttribution(t *testing.T, got map[int64]activityAttribution, activityID, wantChannelID int64, wantChannelCode string) {
t.Helper()
...
}
```
**Location:**
- No centralized fixtures directory. Each test file creates its own data inline.
- Shared helpers defined within the same package test files.
## Coverage
**Requirements:** `make test` runs with `--cover` flag but no enforced minimum threshold.
**View Coverage:**
```bash
go test -v --cover ./internal/... # prints coverage % per package
```
Coverage output file: `/Users/win/2025/AICoding/bindbox/bindbox_game/coverage.out`
## Test Types
**Unit Tests (pure logic, no DB):**
- Scope: individual pure functions — JSON utilities, string parsing, coupon discount math
- Pattern: call function, assert return value
- Examples: `internal/service/product/product_test.go` (TestNormalizeJSON, TestSplitImages), `internal/service/order/discount_test.go` (TestApplyCouponDiscount), `internal/service/activity/sanitize_test.go`
**Integration Tests (SQLite in-memory DB):**
- Scope: service methods requiring DB reads/writes, HTTP handler end-to-end via `net/http/httptest`
- Pattern: `NewSQLiteRepoForTest()` → create DDL → seed data → call service/handler → assert DB state or response
- Examples: `internal/service/task_center/service_test.go`, `internal/api/app/store_test.go`, `internal/service/activity/reward_snapshot_test.go`
**Integration Tests (Real MySQL — skipped when unavailable):**
- Scope: concurrency/race condition testing requiring real DB transactions
- Pattern: hardcoded DSN, `t.Skipf` on connection failure
- Examples: `internal/service/activity/concurrency_test.go`
**E2E Tests:** Not present in the current codebase.
## Common Patterns
**HTTP Handler Testing:**
```go
mux, _ := core.New(lg)
mux.Group("/api/app").GET("/store/items", NewStore(lg, repo).ListStoreItemsForApp())
rr := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/app/store/items?kind=product&page=1&page_size=10", bytes.NewBufferString(""))
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("code=%d body=%s", rr.Code, rr.Body.String())
}
var rsp map[string]interface{}
json.Unmarshal([]byte(rr.Body.String()), &rsp)
```
**Async/Concurrency Testing:**
```go
var wg sync.WaitGroup
var mu sync.Mutex
successCount := 0
for i := 0; i < concurrency; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
// ... concurrent operation
mu.Lock()
defer mu.Unlock()
successCount++
}(i)
}
wg.Wait()
// assert final DB state
```
**Error Path Testing:**
```go
err := svc.AddItemCard(context.Background(), 1, 100, 1)
assert.Error(t, err)
assert.Equal(t, "record not found", err.Error())
```
**Testify assertion style (preferred in newer tests):**
```go
assert.NoError(t, err)
assert.NotEmpty(t, token)
assert.Equal(t, userID, claims.UserID)
assert.Contains(t, err.Error(), "invalid ticket format")
```
---
*Testing analysis: 2026-03-21*