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

114 lines
5.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Phase 1: Core P&L Functions - Context
**Gathered:** 2026-03-21
**Status:** Ready for planning
<domain>
## Phase Boundary
`internal/service/finance/` 包下新增两个函数 `QueryUserProfitLoss``QueryActivityProfitLoss`,接收可选参数(资产类型、维度 ID、时间范围返回含汇总 + 明细的 ProfitLossResult。正确处理 game-pass 互斥、退款排除、已作废库存排除、历史数据兼容等边界情况。
</domain>
<decisions>
## 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:** 两个独立的参数结构体:`UserProfitLossParams``ActivityProfitLossParams`(不共享)
- **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 查询的拆分粒度和合并策略
- 单元测试的具体用例选择
</decisions>
<specifics>
## Specific Ideas
- 订单与活动是 1:1 关系,简化了活动维度的收入归属查询(不需要 dashboard 中复杂的双层子查询)
- 复用 `finance.ClassifyOrderSpending` 做 game-pass / 现金收入分类,保持与 dashboard 计算一致
- 积分汇率存在 system_configs KV 表中,需要在查询时动态读取
</specifics>
<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>
## 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)
</deferred>
---
*Phase: 01-core-pnl-functions*
*Context gathered: 2026-03-21*