- 新增「简记memo」一体化小程序产品原型设计文档 - 新增简记memo完整版UI视觉设计规范和界面细节 - 添加IDEA项目配置文件.gitignore - 创建404页面HTML文件,包含响应式布局和错误提示 - 添加关于页面HTML文件,展示品牌介绍和团队信息 - 实现AES加解密工具函数,支持请求体加密 - 添加用户协议页面基础框架
1312 lines
43 KiB
Go
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
|
|
}
|