// 成长等级系统 package app import ( "context" "encoding/json" "fmt" "simple-memo/global" "simple-memo/models" "simple-memo/utils" "time" "github.com/gin-gonic/gin" "go.uber.org/zap" "gorm.io/gorm" ) // ============================================ // 1. 接口定义 // ============================================ // GrowthHandler 成长系统接口 type GrowthHandler interface { GetGrowthInfo(c *gin.Context) GetAchievements(c *gin.Context) GetGrowthRules(c *gin.Context) AddExp(c *gin.Context) AddExpInternal(userID uint, source string, sourceID string, c *gin.Context) *GrowthRewards DeductExpInternal(userID uint, source string, sourceID string, today string) int GetGrowthInfoInternal(userID uint) map[string]interface{} } // growthHandler 成长系统处理器实现 type growthHandler struct { db *gorm.DB } // NewGrowthHandler 创建成长系统处理器 func NewGrowthHandler() GrowthHandler { return &growthHandler{ db: global.DB, } } // ============================================ // 2. 配置常量 & 类型定义 // ============================================ // ======================== // 等级配置 // ======================== var dailyCap = 120 // LevelConfig 等级配置 type LevelConfig struct { Exp int Title string Icon string Color string Privileges []string } // LevelConfigs 等级配置映射 var LevelConfigs = map[int]LevelConfig{ 1: {Exp: 0, Title: "记录小白", Icon: "📝", Color: "#94A3B8", Privileges: []string{"初级称号"}}, 2: {Exp: 200, Title: "坚持新手", Icon: "💪", Color: "#22C55E", Privileges: []string{"初级称号", "更多特权敬请期待"}}, 3: {Exp: 600, Title: "习惯学徒", Icon: "🌱", Color: "#06B6D4", Privileges: []string{"中级称号", "更多特权敬请期待"}}, 4: {Exp: 1500, Title: "自律达人", Icon: "✅", Color: "#6366F1", Privileges: []string{"中级称号", "更多特权敬请期待"}}, 5: {Exp: 3500, Title: "记录大师", Icon: "🏆", Color: "#8B5CF6", Privileges: []string{"高级称号", "更多特权敬请期待"}}, 6: {Exp: 7000, Title: "时间管家", Icon: "⏰", Color: "#EC4899", Privileges: []string{"高级称号", "更多特权敬请期待"}}, 7: {Exp: 12000, Title: "生活规划师", Icon: "📋", Color: "#F97316", Privileges: []string{"稀有称号", "更多特权敬请期待"}}, 8: {Exp: 18000, Title: "效率王者", Icon: "⚡", Color: "#EAB308", Privileges: []string{"史诗称号", "更多特权敬请期待"}}, 9: {Exp: 26000, Title: "人生记录官", Icon: "📜", Color: "#EF4444", Privileges: []string{"传说称号", "更多特权敬请期待"}}, 10: {Exp: 36000, Title: "生活主宰者", Icon: "🌟", Color: "#F59E0B", Privileges: []string{"至尊称号", "专属特权敬请期待"}}, } // ======================== // 成就配置 // ======================== // Achievement 成就配置 type Achievement struct { ID string Name string Icon string Category string // streak/task/mood/bill Description string ExpReward int Target int TargetType string Rarity string } // RarityConfig 稀有度配置 type RarityConfig struct { Name string Color string Effect string } // AchievementCategory 成就分类 type AchievementCategory struct { Key string Name string Icon string } // AchievementConfigs 成就配置映射 var AchievementConfigs = map[string]Achievement{ // 坚持类 "first_record": {ID: "first_record", Name: "初次尝试", Icon: "✨", Category: "streak", Description: "完成第1天记录", ExpReward: 10, Target: 1, TargetType: "total_days", Rarity: "primary"}, "streak_3": {ID: "streak_3", Name: "3天坚持", Icon: "🌱", Category: "streak", Description: "连续记录3天", ExpReward: 20, Target: 3, TargetType: "streak", Rarity: "primary"}, "streak_7": {ID: "streak_7", Name: "一周打卡", Icon: "🌟", Category: "streak", Description: "连续记录7天", ExpReward: 50, Target: 7, TargetType: "streak", Rarity: "secondary"}, "streak_30": {ID: "streak_30", Name: "月度坚持", Icon: "📅", Category: "streak", Description: "连续记录30天", ExpReward: 200, Target: 30, TargetType: "streak", Rarity: "advanced"}, "streak_100": {ID: "streak_100", Name: "百日筑基", Icon: "💯", Category: "streak", Description: "连续记录100天", ExpReward: 500, Target: 100, TargetType: "streak", Rarity: "rare"}, "streak_180": {ID: "streak_180", Name: "半年丰碑", Icon: "🏅", Category: "streak", Description: "连续记录180天", ExpReward: 1000, Target: 180, TargetType: "streak", Rarity: "epic"}, "streak_365": {ID: "streak_365", Name: "年度传奇", Icon: "🎉", Category: "streak", Description: "连续记录365天", ExpReward: 2000, Target: 365, TargetType: "streak", Rarity: "legendary"}, // 任务类 "task_1": {ID: "task_1", Name: "任务新手", Icon: "🎯", Category: "task", Description: "完成第1个任务", ExpReward: 10, Target: 1, TargetType: "total_tasks", Rarity: "primary"}, "task_100": {ID: "task_100", Name: "任务达人", Icon: "⚡", Category: "task", Description: "累计完成100个任务", ExpReward: 200, Target: 100, TargetType: "total_tasks", Rarity: "secondary"}, "task_500": {ID: "task_500", Name: "任务大师", Icon: "🏆", Category: "task", Description: "累计完成500个任务", ExpReward: 800, Target: 500, TargetType: "total_tasks", Rarity: "rare"}, "task_1000": {ID: "task_1000", Name: "任务王者", Icon: "👑", Category: "task", Description: "累计完成1000个任务", ExpReward: 1500, Target: 1000, TargetType: "total_tasks", Rarity: "legendary"}, "perfect_day": {ID: "perfect_day", Name: "完美一天", Icon: "✨", Category: "task", Description: "单日完成所有待办", ExpReward: 30, Target: 1, TargetType: "perfect_day", Rarity: "advanced"}, "perfect_week": {ID: "perfect_week", Name: "完美一周", Icon: "💫", Category: "task", Description: "连续7天100%完成", ExpReward: 200, Target: 7, TargetType: "perfect_week", Rarity: "epic"}, // 心情类 "mood_first": {ID: "mood_first", Name: "心情记录者", Icon: "📝", Category: "mood", Description: "第1次记录心情", ExpReward: 10, Target: 1, TargetType: "total_moods", Rarity: "primary"}, "mood_30": {ID: "mood_30", Name: "月度心情家", Icon: "🌈", Category: "mood", Description: "累计记录30天心情", ExpReward: 100, Target: 30, TargetType: "total_moods", Rarity: "secondary"}, "mood_100": {ID: "mood_100", Name: "心情收藏家", Icon: "🎨", Category: "mood", Description: "累计记录100天心情", ExpReward: 300, Target: 100, TargetType: "total_moods", Rarity: "rare"}, "happy_streak": {ID: "happy_streak", Name: "阳光达人", Icon: "☀️", Category: "mood", Description: "连续10天记录开心", ExpReward: 150, Target: 10, TargetType: "happy_streak", Rarity: "advanced"}, // 记账类 "bill_1": {ID: "bill_1", Name: "记账入门", Icon: "💵", Category: "bill", Description: "第1次记账", ExpReward: 10, Target: 1, TargetType: "total_bills", Rarity: "primary"}, "bill_100": {ID: "bill_100", Name: "记账达人", Icon: "💳", Category: "bill", Description: "累计记账100笔", ExpReward: 200, Target: 100, TargetType: "total_bills", Rarity: "secondary"}, "bill_500": {ID: "bill_500", Name: "记账大师", Icon: "🏅", Category: "bill", Description: "累计记账500笔", ExpReward: 500, Target: 500, TargetType: "total_bills", Rarity: "rare"}, "bill_2000": {ID: "bill_2000", Name: "理财高手", Icon: "💎", Category: "bill", Description: "累计记账2000笔", ExpReward: 1200, Target: 2000, TargetType: "total_bills", Rarity: "legendary"}, // 复盘类 "review_first": {ID: "review_first", Name: "复盘初体验", Icon: "🔍", Category: "review", Description: "完成第1次复盘", ExpReward: 15, Target: 1, TargetType: "total_reviews", Rarity: "primary"}, "review_7": {ID: "review_7", Name: "周复盘者", Icon: "📋", Category: "review", Description: "累计复盘7次", ExpReward: 50, Target: 7, TargetType: "total_reviews", Rarity: "secondary"}, "review_30": {ID: "review_30", Name: "月度复盘师", Icon: "🧠", Category: "review", Description: "累计复盘30次", ExpReward: 150, Target: 30, TargetType: "total_reviews", Rarity: "advanced"}, "review_100": {ID: "review_100", Name: "复盘修行者", Icon: "🎓", Category: "review", Description: "累计复盘100次", ExpReward: 400, Target: 100, TargetType: "total_reviews", Rarity: "rare"}, "review_365": {ID: "review_365", Name: "年度复盘大师", Icon: "🏯", Category: "review", Description: "累计复盘365次", ExpReward: 1000, Target: 365, TargetType: "total_reviews", Rarity: "legendary"}, // 收集类 "full_collector": {ID: "full_collector", Name: "全套收藏家", Icon: "👑", Category: "collection", Description: "收集所有成就", ExpReward: 3000, Target: 1, TargetType: "full_collection", Rarity: "legendary"}, } // RarityConfigs 稀有度配置映射 var RarityConfigs = map[string]RarityConfig{ "primary": {Name: "初级", Color: "#9CA3AF", Effect: "none"}, "secondary": {Name: "中级", Color: "#10B981", Effect: "glow"}, "advanced": {Name: "高级", Color: "#3B82F6", Effect: "glow"}, "rare": {Name: "稀有", Color: "#8B5CF6", Effect: "pulse"}, "epic": {Name: "史诗", Color: "#F59E0B", Effect: "fire"}, "legendary": {Name: "传说", Color: "#EF4444", Effect: "rotate"}, } // AchievementCategories 成就分类列表 var AchievementCategories = []AchievementCategory{ {Key: "streak", Name: "坚持类", Icon: "📅"}, {Key: "task", Name: "任务类", Icon: "✅"}, {Key: "mood", Name: "心情类", Icon: "😊"}, {Key: "bill", Name: "记账类", Icon: "💰"}, {Key: "review", Name: "复盘类", Icon: "🔍"}, {Key: "collection", Name: "收集类", Icon: "👑"}, } // ======================== // 经验规则配置 // ======================== // ExpRuleConfig 经验获取规则 type ExpRuleConfig struct { Icon string Name string Desc string Exp string Detail string } // ExpRuleConfigs 经验获取规则列表 var ExpRuleConfigs = []ExpRuleConfig{ {Icon: "✅", Name: "完成任务", Desc: "完成待办任务", Exp: "5-25 EXP/个", Detail: "完成第1个任务获得5 EXP,第3个起额外+5 EXP,第5个起额外+15 EXP"}, {Icon: "💰", Name: "记账记录", Desc: "记录收支账单", Exp: "3-18 EXP/笔", Detail: "记录第1笔账单获得3 EXP,第3笔起额外+5 EXP,第5笔起额外+10 EXP"}, {Icon: "😊", Name: "记录心情", Desc: "每日记录一次心情", Exp: "5 EXP/天", Detail: "每天只能记录1次心情,多次记录不额外获得经验"}, {Icon: "🔍", Name: "每日复盘", Desc: "KPT复盘反思", Exp: "5 EXP/次", Detail: "每次复盘获得5 EXP,每日最多1次复盘"}, } // StreakBonusRule 连续打卡加成规则 type StreakBonusRule struct { Range string Bonus int Color string } // StreakBonusRules 连续打卡加成规则列表 var StreakBonusRules = []StreakBonusRule{ {Range: "1-7天", Bonus: 2, Color: "#10B981"}, {Range: "8-30天", Bonus: 5, Color: "#F59E0B"}, {Range: "31-90天", Bonus: 10, Color: "#EC4899"}, {Range: "91天以上", Bonus: 15, Color: "#8B5CF6"}, } // ======================== // 其他配置 // ======================== // GrowthTips 快速升级秘籍 var GrowthTips = []string{ "每天完成至少3个任务,可以获得更多经验", "坚持记账,每天记录3-5笔账单收益更高", "别忘记记录心情,轻松获取额外经验", "每日做一次KPT复盘,总结反思获取成长值", "保持连续打卡,连续天数越多加成越高", "完成成就可以获得额外经验奖励", } // FAQItem 常见问题项 type FAQItem struct { Q string A string } // GrowthFAQ 常见问题列表 var GrowthFAQ = []FAQItem{ {Q: "经验会清零吗?", A: "不会!你的经验值会一直累计,等级只会越来越高。"}, {Q: "断了连续打卡怎么办?", A: "别担心!断签后连续天数不会完全清零,会根据之前的连续天数保留一部分(1-7天保留0天,8-30天保留3天,31-90天保留7天,91天以上保留15天)。"}, {Q: "每天的经验有上限吗?", A: "有的,每天通过任务、记账、心情获得的最高经验是120 EXP。"}, {Q: "新用户保护期是什么?", A: "新用户前7天享受50%的额外经验加成,帮助你快速成长!"}, {Q: "成就奖励经验是一次性的吗?", A: "是的,每个成就只能领取一次经验奖励,解锁后不会再重复获得。"}, {Q: "等级特权会影响基础功能使用吗?", A: "完全不会!所有核心功能(待办、记账、心情记录、主题切换)都是免费开放的,不受等级限制。等级特权只是额外的荣誉展示和增值服务。"}, {Q: "什么是称号特权?", A: "不同等级会获得不同稀有度的称号:初级→中级→高级→稀有→史诗→传说→至尊。称号会展示在你的个人主页和成长之路页面。"}, } // ============================================ // 3. 请求/响应 结构体 // ============================================ // GetGrowthInfoReq 获取成长信息请求 type GetGrowthInfoReq struct{} // AddExpReq 增加经验请求 type AddExpReq struct { Source string `json:"source" binding:"required"` // task/bill/mood/review SourceID string `json:"source_id"` // 关联记录ID } // GetAchievementsReq 获取成就列表请求 type GetAchievementsReq struct { Category string `json:"category"` // 空则返回全部 } // ============================================ // 4. 业务模型结构体 // ============================================ // ExpSource 经验来源计算结果 type ExpSource struct { Base int TaskBonus int BillBonus int StreakBonus int ProtectBonus int } // ExpResult 经验计算最终结果 type ExpResult struct { Base int TaskExp int BillExp int MoodExp int ReviewExp int StreakBonus int ProtectBonus int Total int } // ExpBreakdown 经验明细(用于JSON存储) type ExpBreakdown struct { TaskExp int BillExp int MoodExp int ReviewExp int StreakBonus int ProtectBonus int } // GrowthRewards 成长奖励返回结构(用于业务接口集成) type GrowthRewards struct { ExpGained int `json:"expGained"` // 本次获得经验 TotalExp int `json:"totalExp"` // 总经验 LevelUp bool `json:"levelUp"` // 是否升级 OldLevel int `json:"oldLevel"` // 升级前等级 CurrentLevel int `json:"currentLevel"` // 当前等级 LevelTitle string `json:"levelTitle"` // 等级称号 LevelIcon string `json:"levelIcon"` // 等级图标 LevelColor string `json:"levelColor"` // 等级颜色 NextLevelExp int `json:"nextLevelExp"` // 下一级所需经验 NewPrivileges []string `json:"newPrivileges"` // 新解锁特权 NewAchievements []map[string]any `json:"newAchievements"` // 新获得的成就 } // ============================================ // 5. 接口实现 // ============================================ // GetGrowthInfo 获取用户成长信息 func (h *growthHandler) GetGrowthInfo(c *gin.Context) { userID := c.GetUint("userID") today := time.Now().Format("2006-01-02") growth := h.getOrCreateGrowth(userID) todayProgress := h.getTodayProgress(userID, today) levelConfig := LevelConfigs[growth.Level] nextLevelConfig := LevelConfigs[growth.Level+1] nextLevelExp := 0 if nextLevelConfig.Exp > 0 { nextLevelExp = nextLevelConfig.Exp } // 计算当前等级进度百分比 (0-100) levelProgress := 0 if nextLevelExp > 0 { currentLevelExp := LevelConfigs[growth.Level].Exp if nextLevelExp > currentLevelExp { progress := float64(growth.Experience-currentLevelExp) / float64(nextLevelExp-currentLevelExp) * 100 levelProgress = int(progress) if levelProgress > 100 { levelProgress = 100 } } } // 获取下一级特权 nextLevelPrivileges := []string{} if nextLevelConfig.Privileges != nil { nextLevelPrivileges = nextLevelConfig.Privileges } userAchievements := h.getUserAchievements(userID) achievementList := make([]map[string]interface{}, 0) unlockedMap := make(map[string]bool) for _, ua := range userAchievements { unlockedMap[ua.AchievementID] = true if config, ok := AchievementConfigs[ua.AchievementID]; ok { achievementList = append(achievementList, map[string]interface{}{ "id": config.ID, "name": config.Name, "icon": config.Icon, "rarity": config.Rarity, "category": config.Category, "unlocked": true, "unlockDate": ua.UnlockDate, }) } } totalAchievements := len(AchievementConfigs) expBreakdown := parseExpBreakdown(todayProgress.ExpBreakdown) utils.Ok(c, gin.H{ "id": growth.ID, "level": growth.Level, "title": levelConfig.Title, "titleIcon": levelConfig.Icon, "color": levelConfig.Color, "experience": growth.Experience, "currentExp": growth.Experience - levelConfig.Exp, "nextLevelExp": nextLevelExp, "totalDays": growth.TotalDays, "streakDays": growth.StreakDays, "maxStreakDays": growth.MaxStreakDays, "levelProgress": levelProgress, "todayProgress": gin.H{ "tasksCompleted": todayProgress.TaskCount, "taskExp": expBreakdown.TaskExp, "billsRecorded": todayProgress.BillCount, "billExp": expBreakdown.BillExp, "moodRecorded": todayProgress.MoodRecorded, "moodExp": expBreakdown.MoodExp, "reviewRecorded": todayProgress.ReviewRecorded, "reviewExp": expBreakdown.ReviewExp, "streakBonus": expBreakdown.StreakBonus, "protectBonus": expBreakdown.ProtectBonus, "totalExp": todayProgress.DailyExp, "maxDailyExp": dailyCap, }, "achievements": gin.H{ "total": totalAchievements, "unlocked": len(achievementList), "list": achievementList, }, "privileges": levelConfig.Privileges, "nextLevelPrivileges": nextLevelPrivileges, }) } // GetGrowthInfoInternal 内部方法,返回简化的成长信息 func (h *growthHandler) GetGrowthInfoInternal(userID uint) map[string]interface{} { growth := h.getOrCreateGrowth(userID) return map[string]interface{}{ "level": growth.Level, "experience": growth.Experience, "totalDays": growth.TotalDays, "streak_days": growth.StreakDays, "max_streak_days": growth.MaxStreakDays, } } // GetAchievements 获取成就列表 func (h *growthHandler) GetAchievements(c *gin.Context) { userID := c.GetUint("userID") var req GetAchievementsReq c.ShouldBindJSON(&req) userAchievements := h.getUserAchievements(userID) unlockedMap := make(map[string]bool) for _, ua := range userAchievements { unlockedMap[ua.AchievementID] = true } unlockedList := make([]map[string]interface{}, 0) lockedList := make([]map[string]interface{}, 0) for id, config := range AchievementConfigs { rarity := RarityConfigs[config.Rarity] achievement := map[string]interface{}{ "id": config.ID, "name": config.Name, "icon": config.Icon, "category": config.Category, "description": config.Description, "expReward": config.ExpReward, "target": config.Target, "rarity": config.Rarity, "rarityName": rarity.Name, "rarityColor": rarity.Color, } if isUnlocked, ok := unlockedMap[id]; ok && isUnlocked { achievement["unlocked"] = true unlockedList = append(unlockedList, achievement) } else { achievement["unlocked"] = false lockedList = append(lockedList, achievement) } } collectionProgress := calculateCollectionProgress(AchievementConfigs, unlockedMap) if req.Category != "" { filteredUnlocked := make([]map[string]interface{}, 0) filteredLocked := make([]map[string]interface{}, 0) for _, a := range unlockedList { if a["category"] == req.Category { filteredUnlocked = append(filteredUnlocked, a) } } for _, a := range lockedList { if a["category"] == req.Category { filteredLocked = append(filteredLocked, a) } } utils.Ok(c, gin.H{ "total": len(AchievementConfigs), "unlocked": len(unlockedList), "collectionProgress": collectionProgress, "unlockedList": filteredUnlocked, "lockedList": filteredLocked, }) return } utils.Ok(c, gin.H{ "total": len(AchievementConfigs), "unlocked": len(unlockedList), "collectionProgress": collectionProgress, "unlockedList": unlockedList, "lockedList": lockedList, }) } // AddExp 增加经验(外部调用接口) func (h *growthHandler) AddExp(c *gin.Context) { userID := c.GetUint("userID") var req AddExpReq if err := c.ShouldBindJSON(&req); err != nil { utils.Fail(c, "参数解析失败") return } h.AddExpInternal(userID, req.Source, req.SourceID, c) } // AddExpInternal 增加经验(内部调用) func (h *growthHandler) AddExpInternal(userID uint, source string, sourceID string, c *gin.Context) *GrowthRewards { ctx := context.Background() today := time.Now().Format("2006-01-02") if source == "mood" { key := fmt.Sprintf("mood_count:%d:%s", userID, today) count, _ := global.Redis.Exists(ctx, key).Result() if count > 0 { if c != nil { utils.Fail(c, "今日心情已记录") } return nil } } if source == "review" { key := fmt.Sprintf("review_count:%d:%s", userID, today) count, _ := global.Redis.Exists(ctx, key).Result() if count > 0 { if c != nil { utils.Fail(c, "今日复盘已记录") } return nil } } lockKey := fmt.Sprintf("lock:growth:%d", userID) ok, _ := global.Redis.SetNX(ctx, lockKey, "1", 2*time.Second).Result() if !ok { if c != nil { utils.Fail(c, "系统繁忙,请稍后重试") } return nil } defer global.Redis.Del(ctx, lockKey) growth := h.getOrCreateGrowth(userID) beforeExp := growth.Experience beforeLevel := growth.Level expResult := h.calculateAndUpdateExp(userID, growth, source, sourceID, today) afterExp := growth.Experience levelUp := afterExp > beforeExp && h.checkLevelUp(beforeExp, afterExp) newAchievements := h.checkAndUnlockAchievements(userID, growth) levelConfig := LevelConfigs[growth.Level] nextLevelConfig := LevelConfigs[growth.Level+1] rewards := &GrowthRewards{ ExpGained: expResult.Total, TotalExp: afterExp, LevelUp: levelUp, OldLevel: beforeLevel, CurrentLevel: growth.Level, LevelTitle: levelConfig.Title, LevelIcon: levelConfig.Icon, LevelColor: levelConfig.Color, NextLevelExp: nextLevelConfig.Exp, NewPrivileges: levelConfig.Privileges, NewAchievements: newAchievements, } if c != nil { utils.Ok(c, rewards) } fields := []zap.Field{ zap.Uint("userID", userID), zap.String("source", source), zap.Int("exp", expResult.Total), zap.Int("level", growth.Level), } if c != nil { fields = append(fields, utils.LogContextFields(c)...) } global.Logger.Info("经验增加", fields...) return rewards } // ============================================ // 6. 核心业务逻辑 // ============================================ // getOrCreateGrowth 获取或创建用户成长记录 func (h *growthHandler) getOrCreateGrowth(userID uint) *models.UserGrowth { var growth models.UserGrowth err := h.db.Where("user_id = ?", userID).First(&growth).Error if err == gorm.ErrRecordNotFound { growth = models.UserGrowth{ UserID: userID, Level: 1, Experience: 0, TotalDays: 0, StreakDays: 0, MaxStreakDays: 0, HappyStreakDays: 0, LastRecordDate: "", ProtectedDays: 7, TotalTasks: 0, TotalBills: 0, TotalMoods: 0, TotalReview: 0, } h.db.Create(&growth) } return &growth } // getTodayProgress 获取或创建今日进度 func (h *growthHandler) getTodayProgress(userID uint, today string) *models.DailyProgress { var progress models.DailyProgress err := h.db.Where("user_id = ? AND record_date = ?", userID, today).First(&progress).Error if err == gorm.ErrRecordNotFound { progress = models.DailyProgress{ UserID: userID, RecordDate: today, TaskCount: 0, BillCount: 0, MoodRecorded: false, ReviewRecorded: false, DailyExp: 0, ExpBreakdown: "{}", } h.db.Create(&progress) } return &progress } // calculateAndUpdateExp 计算并更新经验 func (h *growthHandler) calculateAndUpdateExp(userID uint, growth *models.UserGrowth, source string, sourceID string, today string) ExpResult { todayProgress := h.getTodayProgress(userID, today) expResult := ExpResult{} baseExpConfig := map[string]int{ "task": 5, "bill": 3, "mood": 5, "review": 5, } baseExp := baseExpConfig[source] expResult.Base = baseExp var streakBonus int if growth.StreakDays > 0 { if growth.StreakDays <= 7 { streakBonus = 2 } else if growth.StreakDays <= 30 { streakBonus = 5 } else if growth.StreakDays <= 90 { streakBonus = 10 } else { streakBonus = 15 } } expResult.StreakBonus = streakBonus var protectBonus int if growth.ProtectedDays > 0 { protectBonus = int(float64(baseExp) * 0.5) if growth.TotalDays < 30 { streakBonus *= 2 } } expResult.ProtectBonus = protectBonus var taskExp, billExp, moodExp, reviewExp int if source == "task" { todayProgress.TaskCount++ taskExp = baseExp if todayProgress.TaskCount >= 3 { taskExp += 5 } if todayProgress.TaskCount >= 5 { taskExp += 15 } } else if source == "bill" { todayProgress.BillCount++ billExp = baseExp if todayProgress.BillCount >= 3 { billExp += 5 } if todayProgress.BillCount >= 5 { billExp += 10 } } else if source == "mood" { moodExp = baseExp todayProgress.MoodRecorded = true todayProgress.MoodType = h.getMoodType(userID, today) if todayProgress.MoodType == "happy" { growth.HappyStreakDays++ } else { growth.HappyStreakDays = 0 } } else if source == "review" { reviewExp = baseExp todayProgress.ReviewRecorded = true } expResult.TaskExp = taskExp expResult.BillExp = billExp expResult.MoodExp = moodExp expResult.ReviewExp = reviewExp totalExp := taskExp + billExp + moodExp + reviewExp + streakBonus + protectBonus if totalExp > dailyCap { totalExp = dailyCap } expResult.Total = totalExp isFirstRecordToday := growth.LastRecordDate != today if isFirstRecordToday { yesterday := time.Now().AddDate(0, 0, -1).Format("2006-01-02") if growth.LastRecordDate == yesterday { growth.StreakDays++ } else if growth.LastRecordDate == "" || growth.LastRecordDate < yesterday { if growth.StreakDays > 0 { if growth.StreakDays <= 7 { growth.StreakDays = 0 } else if growth.StreakDays <= 30 { growth.StreakDays = 3 } else if growth.StreakDays <= 90 { growth.StreakDays = 7 } else { growth.StreakDays = 15 } } growth.StreakDays++ } growth.TotalDays++ todayProgress.TotalTasks = h.getTodayTodoCount(userID, today) } if growth.StreakDays > growth.MaxStreakDays { growth.MaxStreakDays = growth.StreakDays } growth.LastRecordDate = today growth.Experience += totalExp if growth.ProtectedDays > 0 { growth.ProtectedDays-- } if source == "task" { growth.TotalTasks++ } else if source == "bill" { growth.TotalBills++ } else if source == "mood" { growth.TotalMoods++ } else if source == "review" { growth.TotalReview++ } h.checkAndUpdateLevel(growth) h.db.Save(growth) expBreakdown := map[string]int{ "taskExp": expResult.TaskExp, "billExp": expResult.BillExp, "moodExp": expResult.MoodExp, "reviewExp": expResult.ReviewExp, "streakBonus": streakBonus, "protectBonus": protectBonus, } breakdownJson, _ := json.Marshal(expBreakdown) todayProgress.DailyExp += totalExp todayProgress.ExpBreakdown = string(breakdownJson) if source == "mood" { ctx := context.Background() key := fmt.Sprintf("mood_count:%d:%s", userID, today) global.Redis.Set(ctx, key, "1", time.Duration(utils.GetSecondsUntilTomorrowZero())) } if source == "review" { ctx := context.Background() key := fmt.Sprintf("review_count:%d:%s", userID, today) global.Redis.Set(ctx, key, "1", time.Duration(utils.GetSecondsUntilTomorrowZero())) } h.db.Save(todayProgress) log := models.ExpChangeLog{ UserID: userID, ChangeType: "add", ExpDelta: totalExp, Source: source, SourceID: sourceID, BeforeExp: growth.Experience - totalExp, AfterExp: growth.Experience, RecordDate: today, } h.db.Create(&log) return expResult } // DeductExpInternal 内部方法:扣除经验(删除时调用) func (h *growthHandler) DeductExpInternal(userID uint, source string, sourceID string, today string) int { // 查找今天的积分记录 var log models.ExpChangeLog err := h.db.Where("user_id = ? AND source = ? AND source_id = ? AND record_date = ?", userID, source, sourceID, today).First(&log).Error if err != nil { // 没有找到今天的记录,不扣除 return 0 } // 获取用户成长数据 var growth models.UserGrowth h.db.Where("user_id = ?", userID).First(&growth) if growth.ID == 0 { return 0 } // 计算要扣除的经验值 deductExp := log.ExpDelta if deductExp <= 0 { return 0 } // 确保不会扣成负数 beforeExp := growth.Experience afterExp := beforeExp - deductExp if afterExp < 0 { afterExp = 0 deductExp = beforeExp } // 更新经验值 growth.Experience = afterExp h.db.Save(&growth) // 扣除今日进度中的经验 var todayProgress models.DailyProgress h.db.Where("user_id = ? AND record_date = ?", userID, today).First(&todayProgress) if todayProgress.ID > 0 { todayProgress.DailyExp -= deductExp if todayProgress.DailyExp < 0 { todayProgress.DailyExp = 0 } h.db.Save(&todayProgress) } // 记录扣除日志 deductLog := models.ExpChangeLog{ UserID: userID, ChangeType: "deduct", ExpDelta: -deductExp, Source: source, SourceID: sourceID, BeforeExp: beforeExp, AfterExp: afterExp, RecordDate: today, } h.db.Create(&deductLog) // 删除原积分记录(避免重复扣除) h.db.Delete(&log) // 递减 Redis 计数器(复盘次数) if source == "review" { ctx := context.Background() key := fmt.Sprintf("review_count:%d:%s", userID, today) count, _ := global.Redis.Decr(ctx, key).Result() if count <= 0 { global.Redis.Del(ctx, key) } // 递减 TotalReview 计数器 if growth.TotalReview > 0 { growth.TotalReview-- h.db.Save(&growth) } // 当日复盘数归零时重置 ReviewRecorded if todayProgress.ID > 0 && count <= 0 { todayProgress.ReviewRecorded = false h.db.Save(&todayProgress) } } // 删除心情 Redis 标记(确保可重新记录) if source == "mood" { ctx := context.Background() key := fmt.Sprintf("mood_count:%d:%s", userID, today) global.Redis.Del(ctx, key) // 递减 TotalMoods 计数器 if growth.TotalMoods > 0 { growth.TotalMoods-- h.db.Save(&growth) } // 重置心情记录标记 if todayProgress.ID > 0 { todayProgress.MoodRecorded = false todayProgress.MoodType = "" h.db.Save(&todayProgress) } } return deductExp } // checkLevelUp 检查是否升级 func (h *growthHandler) checkLevelUp(beforeExp, afterExp int) bool { beforeLevel := h.getLevelByExp(beforeExp) afterLevel := h.getLevelByExp(afterExp) return afterLevel > beforeLevel } // getLevelByExp 根据经验获取等级 func (h *growthHandler) getLevelByExp(exp int) int { level := 1 for l := 10; l >= 1; l-- { if config, ok := LevelConfigs[l]; ok && exp >= config.Exp { level = l break } } return level } // checkAndUpdateLevel 检查并更新等级 func (h *growthHandler) checkAndUpdateLevel(growth *models.UserGrowth) { newLevel := h.getLevelByExp(growth.Experience) if newLevel > growth.Level { oldLevel := growth.Level growth.Level = newLevel log := models.ExpChangeLog{ UserID: growth.UserID, ChangeType: "level_up", ExpDelta: 0, Source: "level", BeforeExp: oldLevel, AfterExp: newLevel, RecordDate: time.Now().Format("2006-01-02"), } h.db.Create(&log) } } // checkAndUnlockAchievements 检查并解锁成就 func (h *growthHandler) checkAndUnlockAchievements(userID uint, growth *models.UserGrowth) []map[string]interface{} { var newAchievements []map[string]interface{} userAchievements := h.getUserAchievements(userID) unlockedMap := make(map[string]bool) for _, ua := range userAchievements { unlockedMap[ua.AchievementID] = true } for id, config := range AchievementConfigs { if unlockedMap[id] { continue } shouldUnlock := false switch config.TargetType { case "total_days": shouldUnlock = growth.TotalDays >= config.Target case "streak": shouldUnlock = growth.StreakDays >= config.Target case "total_tasks": shouldUnlock = growth.TotalTasks >= config.Target case "total_bills": shouldUnlock = growth.TotalBills >= config.Target case "total_moods": shouldUnlock = growth.TotalMoods >= config.Target case "total_reviews": shouldUnlock = growth.TotalReview >= config.Target case "full_collection": shouldUnlock = checkFullCollection(unlockedMap) case "perfect_day": shouldUnlock = h.hasPerfectDay(userID) case "perfect_week": shouldUnlock = h.hasPerfectWeek(userID) case "happy_streak": shouldUnlock = growth.HappyStreakDays >= config.Target } if shouldUnlock { ua := models.UserAchievement{ UserID: userID, AchievementID: id, UnlockDate: time.Now().Format("2006-01-02"), ExpRewarded: config.ExpReward, } h.db.Create(&ua) growth.Experience += config.ExpReward h.db.Save(growth) rarity := RarityConfigs[config.Rarity] newAchievements = append(newAchievements, map[string]interface{}{ "id": config.ID, "name": config.Name, "icon": config.Icon, "description": config.Description, "expReward": config.ExpReward, "rarity": config.Rarity, "rarityName": rarity.Name, "rarityColor": rarity.Color, "rarityEffect": rarity.Effect, "category": config.Category, }) } } return newAchievements } // checkFullCollection 检查是否收集完成所有成就(排除收集类本身) func checkFullCollection(unlockedMap map[string]bool) bool { totalAchievements := 0 totalUnlocked := 0 for id, config := range AchievementConfigs { if config.Category == "collection" { continue } totalAchievements += 1 if unlockedMap[id] { totalUnlocked += 1 } } return totalUnlocked >= totalAchievements } // getUserAchievements 获取用户已解锁成就 func (h *growthHandler) getUserAchievements(userID uint) []models.UserAchievement { var achievements []models.UserAchievement h.db.Where("user_id = ?", userID).Find(&achievements) return achievements } // parseExpBreakdown 解析经验明细JSON func parseExpBreakdown(jsonStr string) ExpBreakdown { if jsonStr == "" { return ExpBreakdown{} } var breakdown ExpBreakdown json.Unmarshal([]byte(jsonStr), &breakdown) return breakdown } // ============================================ // 7. 成长规则页面构建函数 // ============================================ // GetGrowthRules 获取成长规则 func (h *growthHandler) GetGrowthRules(c *gin.Context) { rules := gin.H{ "title": "成长规则", "subtitle": "了解如何获取经验,解锁成就与特权", "sections": []map[string]interface{}{ // 等级体系(包含特权) { "type": "level_system", "icon": "📊", "title": "等级体系", "content": "成长等级从Lv.1到Lv.10,通过累计经验值提升等级。等级越高,称号越拉风,特权越多!\n\n重要说明:所有核心功能(待办、记账、心情记录、主题切换)完全免费开放,不受等级限制!等级特权只是额外的荣誉展示和增值服务。", "levels": buildLevelList(), }, // 经验获取 { "type": "exp_rules", "icon": "⭐", "title": "经验获取", "content": "每天完成待办任务、记账、记录心情、复盘记录都可以获得经验!", "rules": buildExpRulesList(), "limit": fmt.Sprintf("每日经验上限:%d EXP", dailyCap), }, // 连续打卡加成 { "type": "streak_bonus", "icon": "🔥", "title": "连续打卡加成", "content": "连续记录天数越多,每天获得的额外加成越高!", "formula": "连续加成 = 连续天数 × 基础加成", "rules": buildStreakBonusList(), "tip": "断签后连续天数不会完全清零,而是根据断签前的连续天数保留一部分", }, // 新用户保护期 { "type": "protection", "icon": "🛡️", "title": "新用户保护期", "content": "新用户前7天享受保护期加成!", "bonus": "保护期内额外获得50%经验加成", "detail": "保护期结束后,连续天数中断时,只会减少部分天数,不会完全重置", "example": "例如:连续50天后断签,只保留15天连续(而非清零),保护期结束则完全按规则计算", }, // 成就系统 { "type": "achievement_system", "icon": "🎖️", "title": "成就系统", "content": "完成特定目标可解锁成就,获得额外经验奖励!", "categories": buildAchievementCategories(), "achievements": buildAchievementList(), "rarityDesc": "稀有度从低到高:初级 → 中级 → 高级 → 稀有 → 史诗 → 传说", }, // 快速升级秘籍 { "type": "tips", "icon": "💡", "title": "快速升级秘籍", "tips": GrowthTips, }, // 常见问题 { "type": "faq", "icon": "❓", "title": "常见问题", "questions": buildFAQList(), }, }, } utils.Ok(c, rules) } // buildLevelList 构建等级列表(包含特权信息) func buildLevelList() []map[string]interface{} { levels := make([]map[string]interface{}, 0, len(LevelConfigs)) for i := 1; i <= len(LevelConfigs); i++ { config := LevelConfigs[i] levels = append(levels, map[string]interface{}{ "level": i, "title": config.Title, "exp": config.Exp, "color": config.Color, "icon": config.Icon, "privileges": config.Privileges, }) } return levels } // buildExpRulesList 构建经验规则列表 func buildExpRulesList() []map[string]interface{} { rules := make([]map[string]interface{}, 0, len(ExpRuleConfigs)) for _, rule := range ExpRuleConfigs { rules = append(rules, map[string]interface{}{ "icon": rule.Icon, "name": rule.Name, "desc": rule.Desc, "exp": rule.Exp, "detail": rule.Detail, }) } return rules } // buildStreakBonusList 构建连续打卡加成列表 func buildStreakBonusList() []map[string]interface{} { rules := make([]map[string]interface{}, 0, len(StreakBonusRules)) for _, rule := range StreakBonusRules { rules = append(rules, map[string]interface{}{ "range": rule.Range, "bonus": fmt.Sprintf("%d EXP/天", rule.Bonus), "color": rule.Color, }) } return rules } // buildAchievementCategories 构建成就分类列表 func buildAchievementCategories() []map[string]interface{} { categories := make([]map[string]interface{}, 0, len(AchievementCategories)) for _, cat := range AchievementCategories { categories = append(categories, map[string]interface{}{ "key": cat.Key, "name": cat.Name, "icon": cat.Icon, }) } return categories } // buildAchievementList 构建成就列表 func buildAchievementList() []map[string]interface{} { achievements := make([]map[string]interface{}, 0, len(AchievementConfigs)) for _, config := range AchievementConfigs { rarity := RarityConfigs[config.Rarity] achievements = append(achievements, map[string]interface{}{ "id": config.ID, "name": config.Name, "desc": config.Description, "icon": config.Icon, "exp": config.ExpReward, "rarity": rarity.Name, "category": config.Category, }) } return achievements } // buildFAQList 构建常见问题列表 func buildFAQList() []FAQItem { return GrowthFAQ } // calculateCollectionProgress 计算收集进度 func calculateCollectionProgress(achievements map[string]Achievement, unlockedMap map[string]bool) map[string]map[string]int { progress := make(map[string]map[string]int) for _, cat := range AchievementCategories { progress[cat.Key] = make(map[string]int) progress[cat.Key]["total"] = 0 progress[cat.Key]["unlocked"] = 0 } for id, config := range achievements { if _, ok := progress[config.Category]; !ok { continue } progress[config.Category]["total"] += 1 if unlockedMap[id] { progress[config.Category]["unlocked"] += 1 } } return progress } // MockGrowthRewards 生成模拟成长奖励数据(用于前端调试) // levelUp: 是否升级 // hasAchievement: 是否获得成就 func MockGrowthRewards(levelUp bool, hasAchievement bool) *GrowthRewards { baseLevel := 2 achievements := []map[string]any{} if hasAchievement { achievements = append(achievements, map[string]any{ "id": "streak_7", "name": "一周打卡", "icon": "🌟", "description": "连续记录7天", "expReward": 50, "rarity": "secondary", "rarityName": "中级", "rarityColor": "#22C55E", "rarityEffect": "glow", "category": "streak", }) } if levelUp { baseLevel = 3 } levelConfig := LevelConfigs[baseLevel] nextLevelConfig := LevelConfigs[baseLevel+1] return &GrowthRewards{ ExpGained: 15, TotalExp: levelConfig.Exp + 50, LevelUp: levelUp, OldLevel: baseLevel - 1, CurrentLevel: baseLevel, LevelTitle: levelConfig.Title, LevelIcon: levelConfig.Icon, LevelColor: levelConfig.Color, NextLevelExp: nextLevelConfig.Exp, NewPrivileges: levelConfig.Privileges, NewAchievements: achievements, } } func (h *growthHandler) getTodayTodoCount(userID uint, today string) int { var count int64 h.db.Model(&models.Todo{}).Where("user_id = ? AND date = ?", userID, today).Count(&count) return int(count) } func (h *growthHandler) getMoodType(userID uint, today string) string { var mood models.Mood err := h.db.Where("user_id = ? AND date = ?", userID, today).First(&mood).Error if err != nil { return "" } happyEmojis := []string{"😊", "😄", "😎", "🥰", "🥳", "😇", "💪", "😘", "🤩", "😋"} for _, emoji := range happyEmojis { if mood.Emoji == emoji { return "happy" } } return "normal" } func (h *growthHandler) hasPerfectDay(userID uint) bool { var count int64 h.db.Model(&models.DailyProgress{}).Where("user_id = ? AND perfect_day = ?", userID, true).Count(&count) return count > 0 } func (h *growthHandler) hasPerfectWeek(userID uint) bool { var progressList []models.DailyProgress sevenDaysAgo := time.Now().AddDate(0, 0, -7).Format("2006-01-02") h.db.Where("user_id = ? AND record_date >= ? AND perfect_day = ?", userID, sevenDaysAgo, true).Find(&progressList) return len(progressList) >= 7 }