2026-03-21 17:09:14 +08:00

5.4 KiB
Raw Permalink Blame History

Phase 1: Core P&L Functions - Context

Gathered: 2026-03-21 Status: Ready for planning

## Phase Boundary

internal/service/finance/ 包下新增两个函数 QueryUserProfitLossQueryActivityProfitLoss,接收可选参数(资产类型、维度 ID、时间范围返回含汇总 + 明细的 ProfitLossResult。正确处理 game-pass 互斥、退款排除、已作废库存排除、历史数据兼容等边界情况。

## Implementation Decisions

Revenue Attribution Rules

  • D-01: 一个订单只对应一个活动1:1 关系),不需要比例分摊逻辑(跳过 dashboard 中的 two-level subquery 方案)
  • D-02: Game-pass 收入按 draw_count × activity_unit_price 计算,每个活动独立计算
  • D-03: 用户维度直接汇总用户所有订单,不做跨活动分摊

Function Signature Design

  • D-04: 两个独立的参数结构体:UserProfitLossParamsActivityProfitLossParams(不共享)
  • D-05: 返回 (*ProfitLossResult, error) — Go 标准模式error 时 result 为 nil
  • D-06: ProfitLossResult 包含汇总TotalResult+ 明细切片([]ProfitLossDetail,每个元素含 UserID/ActivityID 字段)
  • D-07: 参数全部可选:空 []int64 = 统计全部nil time = 不限时间AssetType=0 = 全部类型

Cost Source Mapping

  • D-08: 成本数据分布在多张表user_inventory实物/道具卡、user_points_ledger积分、user_coupon_ledger优惠券、fragment_synthesis_logs碎片Phase 2
  • D-09: 实物商品/道具卡成本以 user_inventory.value_cents 为准(单一真相源),不需要 fallback chain
  • D-10: 积分通过 system_configs 表中的固定汇率换算为金额(如 100积分 = 1元
  • D-11: 优惠券成本 = 优惠券面值discount_amount

Claude's Discretion

  • 具体 SQL 查询结构和 GORM 调用方式
  • ProfitLossDetail 内部字段的精确命名
  • fan-out 查询的拆分粒度和合并策略
  • 单元测试的具体用例选择
## Specific Ideas
  • 订单与活动是 1:1 关系,简化了活动维度的收入归属查询(不需要 dashboard 中复杂的双层子查询)
  • 复用 finance.ClassifyOrderSpending 做 game-pass / 现金收入分类,保持与 dashboard 计算一致
  • 积分汇率存在 system_configs KV 表中,需要在查询时动态读取

<canonical_refs>

Canonical References

Finance primitives (MUST reuse)

  • internal/service/finance/profit_metrics.go — ClassifyOrderSpending, IsGamePassOrder, ComputeGamePassValue, ComputePrizeCostWithMultiplier, ComputeProfit
  • internal/service/finance/profit_metrics_test.go — Existing test patterns for finance functions

Existing dashboard implementations (reference patterns, don't copy)

  • internal/api/admin/dashboard_activity.go — Activity-level P&L query pattern, cost query with item-card multiplier, game-pass value calculation
  • internal/api/admin/dashboard_user_spending.go — User-dimension spending aggregation pattern
  • internal/api/admin/users_profit_loss.go — User P&L trend, per-user details

Data models

  • internal/repository/mysql/model/*.gen.go — GORM models for orders, user_inventory, user_points_ledger, user_coupon_ledger, system_configs, activities
  • internal/repository/mysql/mysql.go — Repo interface, GetDbR()/GetDbW() split

Research findings

  • .planning/research/PITFALLS.md — 6 critical pitfalls with prevention strategies
  • .planning/research/STACK.md — Query patterns and SQLite test compatibility notes
  • .planning/research/FEATURES.md — Feature prioritization and dependency map

</canonical_refs>

<code_context>

Existing Code Insights

Reusable Assets

  • finance.ClassifyOrderSpending() — Unified spending classification (game-pass vs cash)
  • finance.IsGamePassOrder() — Three-condition game-pass detection
  • finance.ComputeGamePassValue() — draw_count × activity_price
  • finance.ComputePrizeCostWithMultiplier() — Base cost × item-card multiplier
  • finance.ComputeProfit() — profit + profit_rate calculation
  • finance.NormalizeMultiplierX1000() — Multiplier normalization

Established Patterns

  • Service constructor: New(logger, db) → Service with injected logger and repo
  • Fan-out queries: Multiple independent db.Table().Scan() calls, merge in Go via map[int64]*Result
  • Read-only routing: repo.GetDbR() for all read operations
  • CAST(AS SIGNED): Required for any SUM containing division (MySQL returns Decimal)
  • Void exclusion: user_inventory.status IN (1, 3) AND remark NOT LIKE '%void%'
  • Refund exclusion: orders.status = 2 with legacy escape: OR order_id = 0 OR order_id IS NULL

Integration Points

  • Constructor receives mysql.Repo — uses GetDbR() only
  • Constructor receives logger.CustomLogger — project-standard logger
  • Result types defined in same package — callers import finance.ProfitLossResult
  • system_configs table for points exchange rate lookup

</code_context>

## Deferred Ideas
  • Per-asset-type breakdown (Phase 2) — struct field defined here as empty slice, populated in Phase 2
  • Fragment synthesis cost integration (Phase 2) — AST-03
  • Redis caching wrapper (v2)
  • Admin API endpoints for frontend (v2)

Phase: 01-core-pnl-functions Context gathered: 2026-03-21