From f1e51f7ff690abd03c0a747cc66390179fdd1f7a Mon Sep 17 00:00:00 2001 From: zheng020 Date: Tue, 12 May 2026 11:27:43 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=96=87=E6=A1=A3=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...6-04-07-minimax-image-generation-design.md | 0 ...026-04-08-task-management-system-design.md | 0 ...4-13-collection-asset-starbook-refactor.md | 0 ...2026-04-13-hot-reload-dev-script-design.md | 0 .../2026-04-15-economic-system-design.md | 835 +++++++++++------- .../2026-04-16-system-architecture-design.md | 0 .../2026-04-27-inspiration-flow-design.md | 0 .../specs/2026-04-27-my-assets-design.md | 0 .../初始阶段_经济系统_算法.md | 0 系统框架.md => docs/系统框架.md | 0 经济系统计算器.md => docs/经济系统计算器.md | 0 11 files changed, 519 insertions(+), 316 deletions(-) rename docs/{superpowers => }/specs/2026-04-07-minimax-image-generation-design.md (100%) rename docs/{superpowers => }/specs/2026-04-08-task-management-system-design.md (100%) rename docs/{superpowers => }/specs/2026-04-13-collection-asset-starbook-refactor.md (100%) rename docs/{superpowers => }/specs/2026-04-13-hot-reload-dev-script-design.md (100%) rename docs/{superpowers => }/specs/2026-04-15-economic-system-design.md (66%) rename docs/{superpowers => }/specs/2026-04-16-system-architecture-design.md (100%) rename docs/{superpowers => }/specs/2026-04-27-inspiration-flow-design.md (100%) rename docs/{superpowers => }/specs/2026-04-27-my-assets-design.md (100%) rename 初始阶段_经济系统_算法.md => docs/初始阶段_经济系统_算法.md (100%) rename 系统框架.md => docs/系统框架.md (100%) rename 经济系统计算器.md => docs/经济系统计算器.md (100%) diff --git a/docs/superpowers/specs/2026-04-07-minimax-image-generation-design.md b/docs/specs/2026-04-07-minimax-image-generation-design.md similarity index 100% rename from docs/superpowers/specs/2026-04-07-minimax-image-generation-design.md rename to docs/specs/2026-04-07-minimax-image-generation-design.md diff --git a/docs/superpowers/specs/2026-04-08-task-management-system-design.md b/docs/specs/2026-04-08-task-management-system-design.md similarity index 100% rename from docs/superpowers/specs/2026-04-08-task-management-system-design.md rename to docs/specs/2026-04-08-task-management-system-design.md diff --git a/docs/superpowers/specs/2026-04-13-collection-asset-starbook-refactor.md b/docs/specs/2026-04-13-collection-asset-starbook-refactor.md similarity index 100% rename from docs/superpowers/specs/2026-04-13-collection-asset-starbook-refactor.md rename to docs/specs/2026-04-13-collection-asset-starbook-refactor.md diff --git a/docs/superpowers/specs/2026-04-13-hot-reload-dev-script-design.md b/docs/specs/2026-04-13-hot-reload-dev-script-design.md similarity index 100% rename from docs/superpowers/specs/2026-04-13-hot-reload-dev-script-design.md rename to docs/specs/2026-04-13-hot-reload-dev-script-design.md diff --git a/docs/superpowers/specs/2026-04-15-economic-system-design.md b/docs/specs/2026-04-15-economic-system-design.md similarity index 66% rename from docs/superpowers/specs/2026-04-15-economic-system-design.md rename to docs/specs/2026-04-15-economic-system-design.md index e7d9612..7ad7bce 100644 --- a/docs/superpowers/specs/2026-04-15-economic-system-design.md +++ b/docs/specs/2026-04-15-economic-system-design.md @@ -1,7 +1,7 @@ # 经济系统设计文档 > **创建日期:** 2026-04-15 -> **项目:** TopFans 经济系统(水晶 / 经验 / 游戏币) +> **项目:** TopFans 经济系统(水晶 / 等级 / 游戏币) > **服务:** userService (Go Dubbo-go) + taskService (Go) + 共享 PostgreSQL --- @@ -9,9 +9,8 @@ ## 一、设计目标 1. **水晶 (Crystal)** — 记录所有收入/消耗流水,支持查询历史 -2. **经验 (Experience)** — 记录经验变化 + 自动计算等级(数据库阈值配置) +2. **等级 (Level)** — 通过「总上架时长」升级,解锁功能由配置控制 3. **游戏币 (Coin)** — 预留,与水晶结构一致 -4. **等级变化** — 记录每次升级,用于运营分析 ### 1.1 经济模型 @@ -19,11 +18,96 @@ 收入侧: 消耗侧: ├── 任务奖励(水晶) ├── 铸造藏品 ├── 展示收益(水晶) ├── 商城购物 -├── 升级奖励(水晶) ├── 应援活动道具 -├── 铸造奖励(水晶) └── 后期其他功能 +├── 点赞收益(水晶) └── 活动消耗 └── 运营发放(后台手动) ``` +### 1.2 等级成长系统 + +用户通过上架藏品累积「总上架时长」来提升等级,21级后需同时满足「总上架时长」和「搭子等级」要求。 + +**等级上限可动态配置**(存于数据库,非硬编码)。 + +#### 主等级成长表 + +| 等级 | 可上架藏品 | 升级所需「总上架时长」(小时) | 点赞押注数 | 搭子等级要求 | 升级奖励 | +|------|-----------|--------------------------|-----------|------------|---------| +| 1级 | 1 | - | - | - | - | +| 2级 | 1 | 6 | 6 | - | 可配置 | +| 3级 | 2 | 12 | 7 | - | 可配置 | +| 4级 | 2 | 18 | 8 | - | 可配置 | +| 5级 | 2 | 24 | 9 | - | 可配置 | +| 6级 | 2 | 30 | 9 | - | 可配置 | +| 7级 | 2 | 36 | 10 | - | 可配置 | +| 8级 | 2 | 42 | 11 | - | 可配置 | +| 9级 | 2 | 48 | 12 | - | 可配置 | +| 10级 | 2 | 54 | 13 | - | 可配置 | +| 11级 | 2 | 60 | 13 | - | 可配置 | +| 12级 | 2 | 66 | 13 | - | 可配置 | +| 13级 | 2 | 72 | 14 | - | 可配置 | +| 14级 | 2 | 78 | 15 | - | 可配置 | +| 15级 | 2 | 84 | 16 | - | 可配置 | +| 16级 | 2 | 90 | 16 | - | 可配置 | +| 17级 | 2 | 96 | 17 | - | 可配置 | +| 18级 | 2 | 102 | 18 | - | 可配置 | +| 19级 | 2 | 108 | 19 | - | 可配置 | +| 20级 | 2 | 114 | 20 | - | 可配置 | +| 21级+ | 待扩展 | 待扩展 | 待扩展 | 待扩展 | 可配置 | + +> **注意:** 21级后升级条件在 `level_upgrade_conditions` 表配置,搭子等级要求预留。 + +#### 等级上限配置表 (level_cap_config) + +```sql +CREATE TABLE level_cap_config ( + id BIGSERIAL PRIMARY KEY, + max_level INT NOT NULL DEFAULT 20, -- 当前等级上限 + updated_at BIGINT NOT NULL +); + +INSERT INTO level_cap_config (max_level, updated_at) VALUES (20, UNIX_MILLIS()); +``` + +#### 搭子等级阈值配置表(预留) + +```sql +CREATE TABLE dazi_level_thresholds ( + level INT PRIMARY KEY, -- 搭子等级 1-N + upgrade_condition VARCHAR(100), -- 升级条件描述(如"任务积分"、"互动次数"等) + condition_param INT DEFAULT 0, -- 条件参数(如需要完成任务数) + description VARCHAR(100) +); +``` + +#### 等级升级条件配置表 + +```sql +CREATE TABLE level_upgrade_conditions ( + level INT PRIMARY KEY, -- 主等级(如21级) + require_total_hours BIGINT NOT NULL, -- 需要的总上架时长 + require_dazi_level INT DEFAULT 0, -- 需要的搭子等级(0=不需要) + description VARCHAR(100), -- 如 "21级需搭子21级" + updated_at BIGINT NOT NULL +); + +-- 初始数据(21级示例,后续可扩展更多等级) +INSERT INTO level_upgrade_conditions (level, require_total_hours, require_dazi_level, description, updated_at) VALUES +(21, 120, 21, '21级:总时长120h + 搭子2级', UNIX_MILLIS()); +``` + +#### 用户搭子等级表(预留) + +```sql +CREATE TABLE user_dazi_level ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + star_id BIGINT NOT NULL, + dazi_level INT NOT NULL DEFAULT 1, + updated_at BIGINT NOT NULL, + UNIQUE(user_id, star_id) +); +``` + --- ## 二、整体架构 @@ -118,87 +202,152 @@ CREATE INDEX ix_coin_tx_user_star ON coin_transaction_records(user_id, star_id); CREATE INDEX ix_coin_tx_created ON coin_transaction_records(created_at DESC); ``` -### 3.3 经验变化记录表 (exp_transaction_records) - -```sql -CREATE TABLE exp_transaction_records ( - id BIGSERIAL PRIMARY KEY, - user_id BIGINT NOT NULL, - star_id BIGINT NOT NULL, - change_type VARCHAR(30) NOT NULL, -- task_reward/onboarding_reward/manual_adjust - delta BIGINT NOT NULL, -- 正数=获得(经验只增不减,溢出部分在升级时已清零) - exp_before BIGINT NOT NULL, - exp_after BIGINT NOT NULL, - level_before INT NOT NULL, -- 变化前等级 - level_after INT NOT NULL, -- 变化后等级 - level_delta INT NOT NULL, -- 正数=升级,0=无变化 - source_id VARCHAR(100), -- 关联业务ID - description VARCHAR(255), - created_at BIGINT NOT NULL -); - -CREATE INDEX ix_exp_tx_user_star ON exp_transaction_records(user_id, star_id); -CREATE INDEX ix_exp_tx_created ON exp_transaction_records(created_at DESC); -``` - -### 3.4 等级变化记录表 (level_change_records) - -```sql -CREATE TABLE level_change_records ( - id BIGSERIAL PRIMARY KEY, - user_id BIGINT NOT NULL, - star_id BIGINT NOT NULL, - level_before INT NOT NULL, - level_after INT NOT NULL, - level_delta INT NOT NULL, -- 通常为 +1,跳级时可能大于1 - trigger_type VARCHAR(30) NOT NULL, -- exp_gain/manual/admin_adjust - exp_at_change BIGINT NOT NULL, -- 触发等级变化时的经验值 - reward_claimed BOOLEAN DEFAULT false, -- 升级奖励是否已领取(预留) - source_id VARCHAR(100), -- 与触发升级的经验流水 source_id 相同(即触发升级的那笔 exp_transaction_records.source_id) - description VARCHAR(255), - created_at BIGINT NOT NULL -); - -CREATE INDEX ix_level_change_user_star ON level_change_records(user_id, star_id); -CREATE INDEX ix_level_change_created ON level_change_records(created_at DESC); - --- ============================================================ --- 后续扩展索引(如有查询需求可添加): --- 索引B: 查某用户在某时间范围的等级变化 --- CREATE INDEX ix_level_change_user_star_time --- ON level_change_records(user_id, star_id, created_at DESC); --- --- 索引C: 运营后台查某等级的所有升级记录 --- CREATE INDEX ix_level_change_level_after ON level_change_records(level_after); --- --- 索引D: 发放升级奖励(按未领取状态查) --- CREATE INDEX ix_level_change_reward_claimed ON level_change_records(reward_claimed) --- WHERE reward_claimed = false; --- ============================================================ -``` - -### 3.5 等级阈值配置表 (level_thresholds) +### 3.3 等级阈值配置表 (level_thresholds) ```sql CREATE TABLE level_thresholds ( - level INT PRIMARY KEY, -- 等级 1-10 - exp_required BIGINT NOT NULL, -- 达到该等级需要的累计经验 - crystal_reward BIGINT DEFAULT 0, -- 升级到该等级时奖励的水晶 - description VARCHAR(100) -- 如 "2级粉丝" + level INT PRIMARY KEY, -- 等级 1-20 + max_exhibition_hours BIGINT NOT NULL, -- 升级到该等级需要的累计上架时长(小时) + like_bet_count INT NOT NULL, -- 升级后解锁的点赞押注次数 + description VARCHAR(100) -- 如 "2级粉丝" ); --- 初始数据(10级满级) -INSERT INTO level_thresholds (level, exp_required, crystal_reward, description) VALUES +-- 初始数据(20级满级) +INSERT INTO level_thresholds (level, max_exhibition_hours, like_bet_count, description) VALUES (1, 0, 0, '1级新手'), -(2, 100, 10, '2级粉丝'), -(3, 300, 20, '3级真爱'), -(4, 600, 30, '4级铁粉'), -(5, 1000, 50, '5级钻石粉'), -(6, 1500, 80, '6级钻石粉'), -(7, 2100, 120, '7级钻石粉'), -(8, 2800, 180, '8级钻石粉'), -(9, 3600, 280, '9级钻石粉'), -(10, 4500, 500, '10级终极粉'); +(2, 6, 6, '2级粉丝'), +(3, 12, 7, '3级真爱'), +(4, 18, 8, '4级铁粉'), +(5, 24, 9, '5级钻石粉'), +(6, 30, 9, '6级钻石粉'), +(7, 36, 10, '7级钻石粉'), +(8, 42, 11, '8级钻石粉'), +(9, 48, 12, '9级钻石粉'), +(10, 54, 13, '10级钻石粉'), +(11, 60, 13, '11级钻石粉'), +(12, 66, 13, '12级钻石粉'), +(13, 72, 14, '13级钻石粉'), +(14, 78, 15, '14级钻石粉'), +(15, 84, 16, '15级钻石粉'), +(16, 90, 16, '16级钻石粉'), +(17, 96, 17, '17级钻石粉'), +(18, 102, 18, '18级钻石粉'), +(19, 108, 19, '19级钻石粉'), +(20, 114, 20, '20级终极粉'); +``` + +### 3.4 升级奖励配置表 (level_up_reward_config) + +```sql +CREATE TABLE level_up_reward_config ( + id BIGSERIAL PRIMARY KEY, + level INT NOT NULL, -- 等级 2-20+ + reward_type VARCHAR(50) NOT NULL, -- 奖励类型:crystal(水晶) / like_bet_count(点赞押注数) / exhibition_count(上架藏品数) / badge(勋章) / 其他 + reward_value BIGINT NOT NULL DEFAULT 0, -- 奖励值 + is_enabled BOOLEAN DEFAULT true, -- 功能开关 + updated_at BIGINT NOT NULL, + UNIQUE(level, reward_type) -- 同一等级同类型奖励唯一 +); + +-- 初始数据示例(可由运营后台动态调整) +-- reward_type: crystal=水晶, like_bet_count=点赞押注数 +INSERT INTO level_up_reward_config (level, reward_type, reward_value, is_enabled, updated_at) VALUES +(2, 'crystal', 10, true, UNIX_MILLIS()), +(2, 'like_bet_count', 1, true, UNIX_MILLIS()), +(3, 'crystal', 20, true, UNIX_MILLIS()), +(3, 'like_bet_count', 1, true, UNIX_MILLIS()), +(4, 'crystal', 30, true, UNIX_MILLIS()), +(4, 'like_bet_count', 1, true, UNIX_MILLIS()), +(5, 'crystal', 50, true, UNIX_MILLIS()), +(5, 'like_bet_count', 1, true, UNIX_MILLIS()), +(6, 'crystal', 80, true, UNIX_MILLIS()), +(7, 'crystal', 120, true, UNIX_MILLIS()), +(8, 'crystal', 180, true, UNIX_MILLIS()), +(9, 'crystal', 280, true, UNIX_MILLIS()), +(10, 'crystal', 500, true, UNIX_MILLIS()), +(11, 'crystal', 500, true, UNIX_MILLIS()), +(12, 'crystal', 500, true, UNIX_MILLIS()), +(13, 'crystal', 500, true, UNIX_MILLIS()), +(14, 'crystal', 500, true, UNIX_MILLIS()), +(15, 'crystal', 500, true, UNIX_MILLIS()), +(16, 'crystal', 500, true, UNIX_MILLIS()), +(17, 'crystal', 500, true, UNIX_MILLIS()), +(18, 'crystal', 500, true, UNIX_MILLIS()), +(19, 'crystal', 500, true, UNIX_MILLIS()), +(20, 'crystal', 500, true, UNIX_MILLIS()); +``` + +### 3.5 用户累计上架时长表 (user_exhibition_hours) + +```sql +CREATE TABLE user_exhibition_hours ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + star_id BIGINT NOT NULL, + total_exhibition_hours BIGINT NOT NULL DEFAULT 0, -- 累计上架时长(小时) + updated_at BIGINT NOT NULL, + UNIQUE(user_id, star_id) +); +``` + +### 3.6 等级上限配置表 (level_cap_config) + +> **等级上限可动态配置,非硬编码** + +```sql +CREATE TABLE level_cap_config ( + id BIGSERIAL PRIMARY KEY, + max_level INT NOT NULL DEFAULT 20, -- 当前等级上限(可动态调整) + updated_at BIGINT NOT NULL +); + +INSERT INTO level_cap_config (max_level, updated_at) VALUES (20, UNIX_MILLIS()); +``` + +### 3.7 等级升级条件配置表 (level_upgrade_conditions) + +> **用于21级及以上的升级条件配置,需同时满足总上架时长和搭子等级要求** + +```sql +CREATE TABLE level_upgrade_conditions ( + level INT PRIMARY KEY, -- 主等级(如21级) + require_total_hours BIGINT NOT NULL, -- 需要的总上架时长 + require_dazi_level INT DEFAULT 0, -- 需要的搭子等级(0=不需要) + description VARCHAR(100), -- 如 "21级需搭子21级" + updated_at BIGINT NOT NULL +); + +-- 初始数据(21级示例,后续可扩展更多等级) +INSERT INTO level_upgrade_conditions (level, require_total_hours, require_dazi_level, description, updated_at) VALUES +(21, 120, 21, '21级:总时长120h + 搭子21级', UNIX_MILLIS()); +``` + +### 3.8 搭子等级阈值配置表(预留) + +> **搭子等级升级规则待定,此表结构预留** + +```sql +CREATE TABLE dazi_level_thresholds ( + level INT PRIMARY KEY, -- 搭子等级 1-N + upgrade_condition VARCHAR(100), -- 升级条件描述(如"任务积分"、"互动次数"等) + condition_param INT DEFAULT 0, -- 条件参数(如需要完成任务数) + description VARCHAR(100) +); +``` + +### 3.9 用户搭子等级表(预留) + +> **搭子等级记录,待搭子系统开发后启用** + +```sql +CREATE TABLE user_dazi_level ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + star_id BIGINT NOT NULL, + dazi_level INT NOT NULL DEFAULT 1, + updated_at BIGINT NOT NULL, + UNIQUE(user_id, star_id) +); ``` --- @@ -214,26 +363,10 @@ INSERT INTO level_thresholds (level, exp_required, crystal_reward, description) | `mint_reward` | 铸造奖励(基础+阶梯) | + | | `exhibition_revenue` | 上架展示收益 | + | | `like_bet_revenue` | 点赞押注收益 | + | -| `level_up_bonus` | 升级奖励(由调用方主动发放,AddExperience 不自动发) | + | +| `level_up_bonus` | 升级奖励(由调用方主动查询配置后发放) | + | | `manual_adjust` | 手动调整(运营) | +/- | -### 4.2 经验 (exp_transaction_records.change_type) - -| 值 | 含义 | delta 方向 | -|---|---|---| -| `task_reward` | 任务奖励 | + | -| `onboarding_reward` | 引导阶段奖励 | + | -| `manual_adjust` | 手动调整 | + | - -### 4.3 等级变化触发类型 (level_change_records.trigger_type) - -| 值 | 含义 | -|---|---| -| `exp_gain` | 经验增长触发 | -| `manual` | 手动调整 | -| `admin_adjust` | 管理员操作 | - -### 4.4 source_id 填写规则 +### 4.2 source_id 填写规则 `source_id` 填触发这笔流水的**源头业务ID**: @@ -244,7 +377,7 @@ INSERT INTO level_thresholds (level, exp_required, crystal_reward, description) | `mint_reward` | `mint_orders.order_id`(铸造订单号) | | `exhibition_revenue` | `exhibition_revenue_records.id`(上架展示收益) | | `like_bet_revenue` | `like_bet_records.id`(点赞押注收益,记录押注者和藏品信息) | -| `level_up_bonus` | **与触发升级的经验增加 source_id 相同**(即触发升级的那笔 exp_transaction_records.source_id) | +| `level_up_bonus` | 触发升级的上架收益记录ID | | `manual_adjust` | 运营后台操作记录ID(预留) | --- @@ -343,52 +476,49 @@ func (s *userService) UpdateCrystalBalance( --- -### 5.2 AddExperience +### 5.2 AddExhibitionHours -**现有签名:** -```go -AddExperience(userID, starID int64, delta int64) (int64, error) -``` +**功能:** 用户下架藏品时,累加该藏品的实际上架时长,并检查是否触发升级。 -**新签名:** +**签名:** ```go -AddExperience( +AddExhibitionHours( userID int64, starID int64, - delta int64, - changeType string, + exhibitionHours int64, sourceID string, - description string, -) (newExp int64, newLevel int32, levelDelta int32, err error) +) (newLevel int32, levelDelta int32, crystalReward int64, err error) ``` **返回值说明:** -- `newExp` — 新的经验值(溢出升级后清零后的值) - `newLevel` — 新的等级 - `levelDelta` — 等级变化量(正数=升级,0=无变化) +- `rewards` — 升级奖励列表(类型+值,可能有多种奖励) **内部逻辑(事务内):** -1. `SELECT FOR UPDATE` 锁定 `FanProfile` 行(`user_id = ? AND star_id = ?`),**只读一次** -2. 计算 `exp_after = profile.Experience + delta` +1. `SELECT FOR UPDATE` 锁定 `FanProfile` 行(`user_id = ? AND star_id = ?`) +2. 查询/创建 `user_exhibition_hours` 记录,累加上架时长 3. 从 `level_thresholds`(含缓存)计算新等级 -4. 写入 `exp_transaction_records`(含 level_before / level_after / level_delta) -5. 更新 `FanProfile.Experience`(如等级有变,同时更新 `FanProfile.Level`) -6. **如有升级**(newLevel > profile.Level):写入 `level_change_records` -7. **返回 (newExp, newLevel, levelDelta, nil),不自动发放升级水晶奖励** +4. 如有升级(newLevel > profile.Level): + - 更新 `FanProfile.Level` + - 查询 `level_up_reward_config` 获取该等级所有奖励 + - 遍历奖励类型,逐个发放: + - `crystal` → 写入水晶流水 + 更新 `FanProfile.CrystalBalance` + - `like_bet_count` → 更新用户点赞押注次数 + - 其他类型待处理 +5. **返回 (newLevel, levelDelta, rewards, nil)** -> **注意:** 升级水晶奖励由**调用方主动查询 `level_thresholds[newLevel].crystal_reward` 后发放**,不在 AddExperience 内自动发放。 +> **注意:** 升级奖励由配置表 `level_up_reward_config` 动态决定,支持多种奖励类型组合。 --- ### 5.3 等级计算 (CalculateLevel) ```go -const MaxLevel = 10 - -// CalculateLevel 根据累计经验计算当前等级(10级满级,超出阈值不再升级) -func CalculateLevel(exp int64, thresholds map[int32]int64) int32 { - for level := MaxLevel; level >= 1; level-- { - if exp >= thresholds[level] { +// CalculateLevel 根据累计上架时长计算当前等级(等级上限从 level_cap_config 动态获取) +func CalculateLevel(totalHours int64, thresholds map[int32]int64, maxLevel int32) int32 { + for level := maxLevel; level >= 1; level-- { + if totalHours >= thresholds[level] { return level } } @@ -397,7 +527,18 @@ func CalculateLevel(exp int64, thresholds map[int32]int64) int32 { ``` - 阈值从 `level_thresholds` 表加载,带缓存(TTL 5分钟) -- 10级满级,超出阈值的经验不会导致等级变化 +- 等级上限 `maxLevel` 从 `level_cap_config` 表动态获取,非硬编码 +- 21级+的升级条件在 `level_upgrade_conditions` 表配置,需同时满足总上架时长和搭子等级 + +--- + +### 5.4 获取等级配置 (GetLevelConfig) + +**功能:** 获取指定等级的完整配置(可上架藏品数、点赞押注数)。 + +```go +GetLevelConfig(level int32) (*LevelConfig, error) +``` --- @@ -413,23 +554,6 @@ newBalance, err := s.userClient.UpdateCrystalBalance(ctx, userID, starID, def.Cr "task_reward", strconv.FormatInt(def.ID, 10), fmt.Sprintf("每日任务奖励: %s", def.Name)) - -// 发放经验 -newExp, newLevel, levelDelta, err := s.userClient.AddExperience(ctx, userID, starID, def.ExpReward, - "task_reward", - strconv.FormatInt(def.ID, 10), - fmt.Sprintf("每日任务奖励: %s", def.Name)) - -// 发放升级水晶奖励(由调用方主动查、主动发) -if levelDelta > 0 { - threshold := s.getLevelThreshold(newLevel) - if threshold != nil && threshold.CrystalReward > 0 { - s.userClient.UpdateCrystalBalance(ctx, userID, starID, threshold.CrystalReward, - "level_up_bonus", - strconv.FormatInt(def.ID, 10), // source_id 与触发升级的经验来源相同 - fmt.Sprintf("升级到%d级奖励", newLevel)) - } -} ``` ### 6.2 taskService — 引导阶段奖励 @@ -437,20 +561,9 @@ if levelDelta > 0 { **onboarding_service.go** — `ClaimStageReward`: ```go +// 发放水晶 newBalance, err := s.userClient.UpdateCrystalBalance(ctx, userID, starID, stage.CrystalReward, "onboarding_reward", sourceID, fmt.Sprintf("引导阶段%d奖励", stage)) - -newExp, newLevel, levelDelta, err := s.userClient.AddExperience(ctx, userID, starID, stage.ExpReward, - "onboarding_reward", sourceID, fmt.Sprintf("引导阶段%d奖励", stage)) - -// 发放升级水晶奖励 -if levelDelta > 0 { - threshold := s.getLevelThreshold(newLevel) - if threshold != nil && threshold.CrystalReward > 0 { - s.userClient.UpdateCrystalBalance(ctx, userID, starID, threshold.CrystalReward, - "level_up_bonus", sourceID, fmt.Sprintf("升级到%d级奖励", newLevel)) - } -} ``` ### 6.3 taskService — 展示收益 @@ -470,6 +583,18 @@ newBalance, err := s.userClient.UpdateCrystalBalance(ctx, userID, starID, crysta "exhibition_revenue", strconv.FormatInt(record.ID, 10), fmt.Sprintf("展示收益 #%d(点赞数:%d)", record.ID, likeCount)) + +// 累加上架时长并检查升级(藏品下架时调用) +newLevel, levelDelta, crystalReward, err := s.userClient.AddExhibitionHours(ctx, userID, starID, t, + strconv.FormatInt(record.ID, 10)) + +// 发放升级水晶奖励(如有升级且配置了奖励) +if levelDelta > 0 && crystalReward > 0 { + s.userClient.UpdateCrystalBalance(ctx, userID, starID, crystalReward, + "level_up_bonus", + strconv.FormatInt(record.ID, 10), + fmt.Sprintf("升级到%d级奖励", newLevel)) +} ``` #### 获取Buff百分比的伪代码 @@ -514,12 +639,6 @@ WriteCrystalRecord(tx *gorm.DB, record *model.CrystalTransactionRecord) error // CoinLedgerRepository(预留,delta 写 0) WriteCoinRecord(tx *gorm.DB, record *model.CoinTransactionRecord) error - -// ExpLedgerRepository -WriteExpRecord(tx *gorm.DB, record *model.ExpTransactionRecord) error - -// LevelChangeRepository -WriteLevelChange(tx *gorm.DB, record *model.LevelChangeRecord) error ``` > **List 查询方法暂不做,等运营后台有需求时再加。** @@ -535,6 +654,26 @@ GetAllThresholds() (map[int32]*model.LevelThreshold, error) GetThresholdByLevel(level int32) (*model.LevelThreshold, error) ``` +### 7.3 LevelUpRewardConfigRepository + +```go +// GetRewardByLevel 获取指定等级的升级奖励配置 +GetRewardByLevel(level int32) (*model.LevelUpRewardConfig, error) + +// UpdateReward 更新指定等级的升级奖励配置 +UpdateReward(level int32, reward int64, isEnabled bool) error +``` + +### 7.4 UserExhibitionHoursRepository + +```go +// GetOrCreate 获取用户累计上架时长记录(不存在则创建) +GetOrCreate(tx *gorm.DB, userID, starID int64) (*model.UserExhibitionHours, error) + +// AddHours 累加时长并返回更新后的值 +AddHours(tx *gorm.DB, userID, starID int64, hours int64) (int64, error) +``` + --- ## 八、LevelThreshold 缓存设计 @@ -582,30 +721,37 @@ func (c *levelThresholdCache) GetAll() (map[int32]*LevelThreshold, error) { ## 九、Proto 改造 -### 9.1 user.proto — AddExperienceResponse - -```protobuf -message AddExperienceResponse { - topfans.common.BaseResponse base = 1; - int64 new_experience = 2; - int32 new_level = 3; - int32 level_delta = 4; // 新增:等级变化量(正数=升级,0=无变化) -} -``` - -### 9.2 user.proto — UpdateCrystalBalanceResponse +### 9.1 user.proto — UpdateCrystalBalanceResponse 现有已返回 `new_balance`,无需改动。 -### 9.3 task.proto — ClaimDailyTaskResponse / ClaimAllDailyTasksResponse +### 9.2 user.proto — AddExhibitionHoursResponse ```protobuf -message ClaimDailyTaskResponse { +message AddExhibitionHoursResponse { topfans.common.BaseResponse base = 1; - int64 new_crystal_balance = 2; - int64 new_experience = 3; - int32 new_level = 4; - int32 level_delta = 5; // 新增 + int32 new_level = 2; // 新的等级 + int32 level_delta = 3; // 等级变化量(正数=升级,0=无变化) + int64 crystal_reward = 4; // 升级奖励水晶数(无升级时为0) +} +``` + +### 9.3 user.proto — GetLevelConfigRequest / GetLevelConfigResponse + +```protobuf +message GetLevelConfigRequest { + int32 level = 1; +} + +message GetLevelConfigResponse { + topfans.common.BaseResponse base = 1; + LevelConfig config = 2; +} + +message LevelConfig { + int32 level = 1; + int32 max_exhibition_count = 2; // 可上架藏品数 + int32 like_bet_count = 3; // 点赞押注次数 } ``` @@ -613,13 +759,12 @@ message ClaimDailyTaskResponse { ## 十、事务边界设计 -### 10.1 AddExperience 事务边界 +### 10.1 AddExhibitionHours 事务边界 ```go -func (s *userService) AddExperience( - userID int64, starID int64, delta int64, - changeType, sourceID, description string, -) (newExp int64, newLevel int32, levelDelta int32, err error) { +func (s *userService) AddExhibitionHours( + userID int64, starID int64, hours int64, sourceID string, +) (newLevel int32, levelDelta int32, crystalReward int64, err error) { err = s.db.Transaction(func(tx *gorm.DB) error { // 1. SELECT FOR UPDATE 加行锁 @@ -630,74 +775,85 @@ func (s *userService) AddExperience( return err } - // 2. 计算新经验值 - newExp = profile.Experience + delta + // 2. 查询/创建 user_exhibition_hours 记录 + var exhibitionHours model.UserExhibitionHours + err = tx.Where("user_id = ? AND star_id = ?", userID, starID). + First(&exhibitionHours).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + exhibitionHours = model.UserExhibitionHours{ + UserID: userID, + StarID: starID, + TotalExhibitionHours: 0, + UpdatedAt: time.Now().UnixMilli(), + } + } else if err != nil { + return err + } - // 3. 读取等级阈值(从缓存) + // 3. 累加上架时长 + exhibitionHours.TotalExhibitionHours += hours + exhibitionHours.UpdatedAt = time.Now().UnixMilli() + + // 4. 读取等级阈值(从缓存) thresholds, err := s.levelThresholdCache.GetAll() if err != nil { return err } - // 4. 计算新等级 - newLevel = CalculateLevel(newExp, thresholds) + // 5. 计算新等级 + newLevel = CalculateLevel(exhibitionHours.TotalExhibitionHours, thresholds) levelDelta = newLevel - profile.Level - // 5. 写入 exp_transaction_records - expRecord := &model.ExpTransactionRecord{ - UserID: userID, - StarID: starID, - ChangeType: changeType, - Delta: delta, - ExpBefore: profile.Experience, - ExpAfter: newExp, - LevelBefore: profile.Level, - LevelAfter: newLevel, - LevelDelta: levelDelta, - SourceID: sourceID, - Description: description, - CreatedAt: time.Now().UnixMilli(), + // 6. 如有升级,处理奖励 + if levelDelta > 0 { + // 查询升级奖励配置 + rewardConfig, err := s.levelUpRewardConfigRepo.GetRewardByLevel(newLevel) + if err == nil && rewardConfig.IsEnabled && rewardConfig.CrystalReward > 0 { + crystalReward = rewardConfig.CrystalReward + + // 写入水晶流水 + crystalRecord := &model.CrystalTransactionRecord{ + UserID: userID, + StarID: starID, + ChangeType: "level_up_bonus", + Delta: crystalReward, + BalanceBefore: profile.CrystalBalance, + BalanceAfter: profile.CrystalBalance + crystalReward, + SourceID: sourceID, + Description: fmt.Sprintf("升级到%d级奖励", newLevel), + CreatedAt: time.Now().UnixMilli(), + } + if err := tx.Create(crystalRecord).Error; err != nil { + return err + } + + // 更新 FanProfile 等级和水晶余额 + profile.CrystalBalance += crystalReward + } + + // 更新 FanProfile 等级 + profile.Level = newLevel } - if err := tx.Create(expRecord).Error; err != nil { + + // 7. 保存/更新 user_exhibition_hours + if err := tx.Save(&exhibitionHours).Error; err != nil { return err } - // 6. 更新 FanProfile(经验+等级) - updates := map[string]interface{}{ - "experience": newExp, - } - if newLevel != profile.Level { - updates["level"] = newLevel - } + // 8. 更新 FanProfile if err := tx.Model(&models.FanProfile{}). Where("user_id = ? AND star_id = ?", userID, starID). - Updates(updates).Error; err != nil { + Updates(map[string]interface{}{ + "level": profile.Level, + "crystal_balance": profile.CrystalBalance, + }).Error; err != nil { return err } - // 7. 如有升级,写入 level_change_records - if newLevel > profile.Level { - levelRecord := &model.LevelChangeRecord{ - UserID: userID, - StarID: starID, - LevelBefore: profile.Level, - LevelAfter: newLevel, - LevelDelta: levelDelta, - TriggerType: "exp_gain", - ExpAtChange: newExp, - SourceID: sourceID, - Description: description, - CreatedAt: time.Now().UnixMilli(), - } - if err := tx.Create(levelRecord).Error; err != nil { - return err - } - } - return nil }) - return newExp, newLevel, levelDelta, err + return newLevel, levelDelta, crystalReward, err } ``` @@ -708,53 +864,45 @@ func (s *userService) AddExperience( ``` backend/ ├── pkg/models/ -│ ├── user.go # 修改:新增 CrystalTransactionRecord / CoinTransactionRecord / -│ │ # ExpTransactionRecord / LevelChangeRecord 模型 -│ └── level_threshold.go # 新增:LevelThreshold 模型 -│ +│ ├── user.go # 修改:新增 CrystalTransactionRecord / CoinTransactionRecord 模型 +│ └── level.go # 新增:LevelThreshold / LevelUpRewardConfig / UserExhibitionHours 模型 + ├── scripts/ │ └── 20260415_economic_tables.sql # 新增:所有新建表的 DDL + level_thresholds 初始数据 -│ + ├── services/userService/ │ ├── repository/ -│ │ ├── fan_profile_repository.go # 修改:UpdateCrystalBalance / AddExperience 签名 +│ │ ├── fan_profile_repository.go # 修改:UpdateCrystalBalance 签名 │ │ ├── crystal_tx_repository.go # 新增:水晶流水写入 │ │ ├── coin_tx_repository.go # 新增:游戏币流水写入(预留) -│ │ ├── exp_tx_repository.go # 新增:经验流水写入 -│ │ ├── level_change_repository.go # 新增:等级变化写入 -│ │ └── level_threshold_repository.go # 新增:阈值查询(含缓存) +│ │ └── level_threshold_repository.go # 新增:等级阈值查询(含缓存) │ │ │ ├── service/ -│ │ └── user_service.go # 修改:AddExperience 返回 newLevel, levelDelta +│ │ └── user_service.go # 修改:AddExhibitionHours 方法 │ │ │ └── client/ -│ └── user_rpc_client.go # 修改:UpdateCrystalBalance / AddExperience 签名 -│ +│ └── user_rpc_client.go # 修改:UpdateCrystalBalance / AddExhibitionHours 签名 + ├── services/taskService/ │ ├── service/ -│ │ ├── daily_task_service.go # 修改:调用新签名 + 发放升级奖励 -│ │ ├── onboarding_service.go # 修改:调用新签名 + 发放升级奖励 -│ │ └── revenue_service.go # 修改:调用新签名 +│ │ ├── daily_task_service.go # 修改:调用新签名 +│ │ ├── onboarding_service.go # 修改:调用新签名 +│ │ └── revenue_service.go # 修改:调用新签名 + AddExhibitionHours │ │ │ └── client/ -│ └── user_rpc_client.go # 修改:UpdateCrystalBalance / AddExperience 返回值 -│ +│ └── user_rpc_client.go # 修改:UpdateCrystalBalance / AddExhibitionHours 返回值 + ├── services/assetService/ │ └── service/ │ └── mint_service.go # 修改:调用新签名 -│ + ├── proto/ -│ ├── user.proto # 修改:AddExperienceResponse 增加 new_level, level_delta -│ └── task.proto # 修改:ClaimDailyTaskResponse / ClaimAllDailyTasksResponse -│ # 增加 new_level, level_delta -│ +│ └── user.proto # 修改:AddExhibitionHoursResponse / GetLevelConfigResponse + └── pkg/proto/ - ├── user/ - │ ├── user.pb.go # 重新生成 - │ └── user.triple.go # 重新生成 - └── task/ - ├── task.pb.go # 重新生成 - └── task.triple.go # 重新生成 + └── user/ + ├── user.pb.go # 重新生成 + └── user.triple.go # 重新生成 ``` --- @@ -804,71 +952,126 @@ CREATE TABLE IF NOT EXISTS coin_transaction_records ( CREATE INDEX IF NOT EXISTS ix_coin_tx_user_star ON coin_transaction_records(user_id, star_id); CREATE INDEX IF NOT EXISTS ix_coin_tx_created ON coin_transaction_records(created_at DESC); --- 经验变化记录表 -CREATE TABLE IF NOT EXISTS exp_transaction_records ( - id BIGSERIAL PRIMARY KEY, - user_id BIGINT NOT NULL, - star_id BIGINT NOT NULL, - change_type VARCHAR(30) NOT NULL, - delta BIGINT NOT NULL, -- 正数=获得(经验只增不减,溢出部分在升级时已清零) - exp_before BIGINT NOT NULL, - exp_after BIGINT NOT NULL, - level_before INT NOT NULL, - level_after INT NOT NULL, - level_delta INT NOT NULL, -- 正数=升级,0=无变化 - source_id VARCHAR(100), - description VARCHAR(255), - created_at BIGINT NOT NULL -); - -CREATE INDEX IF NOT EXISTS ix_exp_tx_user_star ON exp_transaction_records(user_id, star_id); -CREATE INDEX IF NOT EXISTS ix_exp_tx_created ON exp_transaction_records(created_at DESC); - --- 等级变化记录表 -CREATE TABLE IF NOT EXISTS level_change_records ( - id BIGSERIAL PRIMARY KEY, - user_id BIGINT NOT NULL, - star_id BIGINT NOT NULL, - level_before INT NOT NULL, - level_after INT NOT NULL, - level_delta INT NOT NULL, - trigger_type VARCHAR(30) NOT NULL, - exp_at_change BIGINT NOT NULL, - reward_claimed BOOLEAN DEFAULT false, - source_id VARCHAR(100), - description VARCHAR(255), - created_at BIGINT NOT NULL -); - -CREATE INDEX IF NOT EXISTS ix_level_change_user_star ON level_change_records(user_id, star_id); -CREATE INDEX IF NOT EXISTS ix_level_change_created ON level_change_records(created_at DESC); - --- 后续扩展索引(如有查询需求可添加): --- CREATE INDEX ix_level_change_user_star_time ON level_change_records(user_id, star_id, created_at DESC); --- CREATE INDEX ix_level_change_level_after ON level_change_records(level_after); --- CREATE INDEX ix_level_change_reward_claimed ON level_change_records(reward_claimed) WHERE reward_claimed = false; - -- 等级阈值配置表 CREATE TABLE IF NOT EXISTS level_thresholds ( level INT PRIMARY KEY, - exp_required BIGINT NOT NULL, - crystal_reward BIGINT DEFAULT 0, + max_exhibition_hours BIGINT NOT NULL, -- 升级到该等级需要的累计上架时长(小时) + like_bet_count INT NOT NULL, -- 升级后解锁的点赞押注次数 description VARCHAR(100) ); --- 插入初始数据(10级满级) -INSERT INTO level_thresholds (level, exp_required, crystal_reward, description) VALUES +-- 插入初始数据(20级满级) +INSERT INTO level_thresholds (level, max_exhibition_hours, like_bet_count, description) VALUES (1, 0, 0, '1级新手'), -(2, 100, 10, '2级粉丝'), -(3, 300, 20, '3级真爱'), -(4, 600, 30, '4级铁粉'), -(5, 1000, 50, '5级钻石粉'), -(6, 1500, 80, '6级钻石粉'), -(7, 2100, 120, '7级钻石粉'), -(8, 2800, 180, '8级钻石粉'), -(9, 3600, 280, '9级钻石粉'), -(10, 4500, 500, '10级终极粉') +(2, 6, 6, '2级粉丝'), +(3, 12, 7, '3级真爱'), +(4, 18, 8, '4级铁粉'), +(5, 24, 9, '5级钻石粉'), +(6, 30, 9, '6级钻石粉'), +(7, 36, 10, '7级钻石粉'), +(8, 42, 11, '8级钻石粉'), +(9, 48, 12, '9级钻石粉'), +(10, 54, 13, '10级钻石粉'), +(11, 60, 13, '11级钻石粉'), +(12, 66, 13, '12级钻石粉'), +(13, 72, 14, '13级钻石粉'), +(14, 78, 15, '14级钻石粉'), +(15, 84, 16, '15级钻石粉'), +(16, 90, 16, '16级钻石粉'), +(17, 96, 17, '17级钻石粉'), +(18, 102, 18, '18级钻石粉'), +(19, 108, 19, '19级钻石粉'), +(20, 114, 20, '20级终极粉') ON CONFLICT (level) DO NOTHING; + +-- 升级奖励配置表 +CREATE TABLE IF NOT EXISTS level_up_reward_config ( + id BIGSERIAL PRIMARY KEY, + level INT NOT NULL UNIQUE, + level INT NOT NULL, + reward_type VARCHAR(50) NOT NULL, + reward_value BIGINT NOT NULL DEFAULT 0, + is_enabled BOOLEAN DEFAULT true, + updated_at BIGINT NOT NULL, + UNIQUE(level, reward_type) +); + +-- 插入初始数据示例(可由运营后台动态调整) +INSERT INTO level_up_reward_config (level, reward_type, reward_value, is_enabled, updated_at) VALUES +(2, 'crystal', 10, true, UNIX_MILLIS()), +(2, 'like_bet_count', 1, true, UNIX_MILLIS()), +(3, 'crystal', 20, true, UNIX_MILLIS()), +(3, 'like_bet_count', 1, true, UNIX_MILLIS()), +(4, 'crystal', 30, true, UNIX_MILLIS()), +(4, 'like_bet_count', 1, true, UNIX_MILLIS()), +(5, 'crystal', 50, true, UNIX_MILLIS()), +(5, 'like_bet_count', 1, true, UNIX_MILLIS()), +(6, 'crystal', 80, true, UNIX_MILLIS()), +(7, 'crystal', 120, true, UNIX_MILLIS()), +(8, 'crystal', 180, true, UNIX_MILLIS()), +(9, 'crystal', 280, true, UNIX_MILLIS()), +(10, 'crystal', 500, true, UNIX_MILLIS()), +(11, 'crystal', 500, true, UNIX_MILLIS()), +(12, 'crystal', 500, true, UNIX_MILLIS()), +(13, 'crystal', 500, true, UNIX_MILLIS()), +(14, 'crystal', 500, true, UNIX_MILLIS()), +(15, 'crystal', 500, true, UNIX_MILLIS()), +(16, 'crystal', 500, true, UNIX_MILLIS()), +(17, 'crystal', 500, true, UNIX_MILLIS()), +(18, 'crystal', 500, true, UNIX_MILLIS()), +(19, 'crystal', 500, true, UNIX_MILLIS()), +(20, 'crystal', 500, true, UNIX_MILLIS()) +ON CONFLICT (level, reward_type) DO NOTHING; + +-- 用户累计上架时长表 +CREATE TABLE IF NOT EXISTS user_exhibition_hours ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + star_id BIGINT NOT NULL, + total_exhibition_hours BIGINT NOT NULL DEFAULT 0, + updated_at BIGINT NOT NULL, + UNIQUE(user_id, star_id) +); + +-- 等级上限配置表 +CREATE TABLE IF NOT EXISTS level_cap_config ( + id BIGSERIAL PRIMARY KEY, + max_level INT NOT NULL DEFAULT 20, + updated_at BIGINT NOT NULL +); + +INSERT INTO level_cap_config (max_level, updated_at) VALUES (20, UNIX_MILLIS()); + +-- 等级升级条件配置表(21级+) +CREATE TABLE IF NOT EXISTS level_upgrade_conditions ( + level INT PRIMARY KEY, + require_total_hours BIGINT NOT NULL, + require_dazi_level INT DEFAULT 0, + description VARCHAR(100), + updated_at BIGINT NOT NULL +); + +-- 初始数据(21级示例) +INSERT INTO level_upgrade_conditions (level, require_total_hours, require_dazi_level, description, updated_at) VALUES +(21, 120, 21, '21级:总时长120h + 搭子21级', UNIX_MILLIS()); + +-- 搭子等级阈值配置表(预留) +CREATE TABLE IF NOT EXISTS dazi_level_thresholds ( + level INT PRIMARY KEY, + upgrade_condition VARCHAR(100), + condition_param INT DEFAULT 0, + description VARCHAR(100) +); + +-- 用户搭子等级表(预留) +CREATE TABLE IF NOT EXISTS user_dazi_level ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + star_id BIGINT NOT NULL, + dazi_level INT NOT NULL DEFAULT 1, + updated_at BIGINT NOT NULL, + UNIQUE(user_id, star_id) +); ``` --- diff --git a/docs/superpowers/specs/2026-04-16-system-architecture-design.md b/docs/specs/2026-04-16-system-architecture-design.md similarity index 100% rename from docs/superpowers/specs/2026-04-16-system-architecture-design.md rename to docs/specs/2026-04-16-system-architecture-design.md diff --git a/docs/superpowers/specs/2026-04-27-inspiration-flow-design.md b/docs/specs/2026-04-27-inspiration-flow-design.md similarity index 100% rename from docs/superpowers/specs/2026-04-27-inspiration-flow-design.md rename to docs/specs/2026-04-27-inspiration-flow-design.md diff --git a/docs/superpowers/specs/2026-04-27-my-assets-design.md b/docs/specs/2026-04-27-my-assets-design.md similarity index 100% rename from docs/superpowers/specs/2026-04-27-my-assets-design.md rename to docs/specs/2026-04-27-my-assets-design.md diff --git a/初始阶段_经济系统_算法.md b/docs/初始阶段_经济系统_算法.md similarity index 100% rename from 初始阶段_经济系统_算法.md rename to docs/初始阶段_经济系统_算法.md diff --git a/系统框架.md b/docs/系统框架.md similarity index 100% rename from 系统框架.md rename to docs/系统框架.md diff --git a/经济系统计算器.md b/docs/经济系统计算器.md similarity index 100% rename from 经济系统计算器.md rename to docs/经济系统计算器.md