diff --git a/.planning/codebase/ARCHITECTURE.md b/.planning/codebase/ARCHITECTURE.md new file mode 100644 index 0000000..051b3e0 --- /dev/null +++ b/.planning/codebase/ARCHITECTURE.md @@ -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* diff --git a/.planning/codebase/CONCERNS.md b/.planning/codebase/CONCERNS.md new file mode 100644 index 0000000..6f218f0 --- /dev/null +++ b/.planning/codebase/CONCERNS.md @@ -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* diff --git a/.planning/codebase/CONVENTIONS.md b/.planning/codebase/CONVENTIONS.md new file mode 100644 index 0000000..978994a --- /dev/null +++ b/.planning/codebase/CONVENTIONS.md @@ -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` 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 `Request`: `listAppProductsRequest`, `CreateActivityOrderRequest` +- Response structs named `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* diff --git a/.planning/codebase/INTEGRATIONS.md b/.planning/codebase/INTEGRATIONS.md new file mode 100644 index 0000000..9d60c11 --- /dev/null +++ b/.planning/codebase/INTEGRATIONS.md @@ -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* diff --git a/.planning/codebase/STACK.md b/.planning/codebase/STACK.md new file mode 100644 index 0000000..1214829 --- /dev/null +++ b/.planning/codebase/STACK.md @@ -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* diff --git a/.planning/codebase/STRUCTURE.md b/.planning/codebase/STRUCTURE.md new file mode 100644 index 0000000..731d330 --- /dev/null +++ b/.planning/codebase/STRUCTURE.md @@ -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* diff --git a/.planning/codebase/TESTING.md b/.planning/codebase/TESTING.md new file mode 100644 index 0000000..9cc0d4e --- /dev/null +++ b/.planning/codebase/TESTING.md @@ -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:** +- `_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*