Files
simple-memo/api/app/growth.go
T
sunct 3c3bf53ae4 docs(website): 添加简记memo产品原型和UI设计规范文档
- 新增「简记memo」一体化小程序产品原型设计文档
- 新增简记memo完整版UI视觉设计规范和界面细节
- 添加IDEA项目配置文件.gitignore
- 创建404页面HTML文件,包含响应式布局和错误提示
- 添加关于页面HTML文件,展示品牌介绍和团队信息
- 实现AES加解密工具函数,支持请求体加密
- 添加用户协议页面基础框架
2026-07-31 14:12:32 +08:00

1312 lines
43 KiB
Go

// 成长等级系统
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
}