package service import ( "time" "accounting-app/internal/models" "accounting-app/internal/repository" ) // HealthScoreService handles financial health score calculation type HealthScoreService struct { reportRepo *repository.ReportRepository accountRepo *repository.AccountRepository budgetRepo *repository.BudgetRepository } // NewHealthScoreService creates a new HealthScoreService instance func NewHealthScoreService( reportRepo *repository.ReportRepository, accountRepo *repository.AccountRepository, budgetRepo *repository.BudgetRepository, ) *HealthScoreService { return &HealthScoreService{ reportRepo: reportRepo, accountRepo: accountRepo, budgetRepo: budgetRepo, } } // HealthScoreFactor represents a factor contributing to the health score type HealthScoreFactor struct { Name string `json:"name"` Score float64 `json:"score"` // 0-100 Weight float64 `json:"weight"` // 0-1 Tip string `json:"tip"` // 改进建议 Description string `json:"description"` // 因素描述 } // HealthScoreResult represents the overall health score result type HealthScoreResult struct { Score int `json:"score"` // 0-100 Level string `json:"level"` // 优秀/良好/一般/需改善 Description string `json:"description"` // 总体描述 Factors []HealthScoreFactor `json:"factors"` } // CalculateHealthScore calculates the financial health score for a user func (s *HealthScoreService) CalculateHealthScore(userID uint) (*HealthScoreResult, error) { now := time.Now() monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) threeMonthsAgo := monthStart.AddDate(0, -3, 0) factors := []HealthScoreFactor{} totalWeight := 0.0 weightedSum := 0.0 // Factor 1: 储蓄率 (Savings Rate) - 权重 30% savingsScore, savingsTip := s.calculateSavingsRate(userID, threeMonthsAgo, now) savingsFactor := HealthScoreFactor{ Name: "储蓄率", Score: savingsScore, Weight: 0.30, Tip: savingsTip, Description: "收入中储蓄的比例", } factors = append(factors, savingsFactor) weightedSum += savingsScore * 0.30 totalWeight += 0.30 // Factor 2: 负债率 (Debt Ratio) - 权重 25% debtScore, debtTip := s.calculateDebtRatio(userID) debtFactor := HealthScoreFactor{ Name: "负债率", Score: debtScore, Weight: 0.25, Tip: debtTip, Description: "负债占总资产的比例", } factors = append(factors, debtFactor) weightedSum += debtScore * 0.25 totalWeight += 0.25 // Factor 3: 预算执行率 (Budget Compliance) - 权重 20% budgetScore, budgetTip := s.calculateBudgetCompliance(userID) budgetFactor := HealthScoreFactor{ Name: "预算执行", Score: budgetScore, Weight: 0.20, Tip: budgetTip, Description: "预算控制情况", } factors = append(factors, budgetFactor) weightedSum += budgetScore * 0.20 totalWeight += 0.20 // Factor 4: 消费稳定性 (Spending Stability) - 权重 15% stabilityScore, stabilityTip := s.calculateSpendingStability(userID, threeMonthsAgo, now) stabilityFactor := HealthScoreFactor{ Name: "消费稳定", Score: stabilityScore, Weight: 0.15, Tip: stabilityTip, Description: "月度支出波动情况", } factors = append(factors, stabilityFactor) weightedSum += stabilityScore * 0.15 totalWeight += 0.15 // Factor 5: 资产多样性 (Asset Diversity) - 权重 10% diversityScore, diversityTip := s.calculateAssetDiversity(userID) diversityFactor := HealthScoreFactor{ Name: "资产配置", Score: diversityScore, Weight: 0.10, Tip: diversityTip, Description: "资产分散程度", } factors = append(factors, diversityFactor) weightedSum += diversityScore * 0.10 totalWeight += 0.10 // Calculate final score finalScore := int(weightedSum / totalWeight) if finalScore > 100 { finalScore = 100 } if finalScore < 0 { finalScore = 0 } // Determine level and description level, description := getHealthScoreLevel(finalScore) return &HealthScoreResult{ Score: finalScore, Level: level, Description: description, Factors: factors, }, nil } // calculateSavingsRate calculates the savings rate factor func (s *HealthScoreService) calculateSavingsRate(userID uint, startDate, endDate time.Time) (float64, string) { // Get income and expense totals using existing repository method summaries, err := s.reportRepo.GetTransactionSummaryByCurrency(userID, startDate, endDate) if err != nil || len(summaries) == 0 { return 50.0, "无法获取收支数据" } var totalIncome, totalExpense float64 for _, cs := range summaries { totalIncome += cs.TotalIncome totalExpense += cs.TotalExpense } if totalIncome <= 0 { return 30.0, "暂无收入记录,建议开始记录收入" } savingsRate := (totalIncome - totalExpense) / totalIncome * 100 var score float64 var tip string switch { case savingsRate >= 30: score = 100 tip = "储蓄率非常优秀,继续保持!" case savingsRate >= 20: score = 85 tip = "储蓄率良好,可以考虑增加投资" case savingsRate >= 10: score = 70 tip = "储蓄率一般,建议适当控制支出" case savingsRate >= 0: score = 50 tip = "储蓄率偏低,需要减少非必要支出" default: score = 20 tip = "入不敷出,请尽快调整消费习惯" } return score, tip } // calculateDebtRatio calculates the debt ratio factor func (s *HealthScoreService) calculateDebtRatio(userID uint) (float64, string) { // Use GetTotalBalance which returns assets and liabilities totalAssets, totalLiabilities, err := s.accountRepo.GetTotalBalance(userID) if err != nil { return 50.0, "无法获取账户数据" } if totalAssets <= 0 && totalLiabilities <= 0 { return 70.0, "暂无资产负债记录" } // Debt ratio = Liabilities / (Assets + Liabilities) total := totalAssets + totalLiabilities if total <= 0 { return 30.0, "净资产为负,需要关注财务状况" } debtRatio := totalLiabilities / total * 100 var score float64 var tip string switch { case debtRatio <= 10: score = 100 tip = "负债率极低,财务非常健康" case debtRatio <= 30: score = 85 tip = "负债率健康,保持良好习惯" case debtRatio <= 50: score = 65 tip = "负债率中等,建议适度控制" case debtRatio <= 70: score = 40 tip = "负债率偏高,需要制定还款计划" default: score = 20 tip = "负债率过高,请优先处理债务" } return score, tip } // calculateBudgetCompliance calculates budget compliance factor func (s *HealthScoreService) calculateBudgetCompliance(userID uint) (float64, string) { now := time.Now() budgets, err := s.budgetRepo.GetActiveBudgets(userID, now) if err != nil || len(budgets) == 0 { return 60.0, "暂无预算设置,建议设置预算" } var totalBudgets int var overBudgetCount int for _, budget := range budgets { // 获取预算期间的支出 spent, err := s.budgetRepo.GetSpentAmount(&budget, budget.StartDate, now) if err != nil { continue } totalBudgets++ if spent > budget.Amount { overBudgetCount++ } } if totalBudgets == 0 { return 60.0, "暂无有效预算" } complianceRate := float64(totalBudgets-overBudgetCount) / float64(totalBudgets) * 100 var score float64 var tip string switch { case complianceRate >= 90: score = 100 tip = "预算执行非常出色!" case complianceRate >= 70: score = 80 tip = "预算执行良好,继续保持" case complianceRate >= 50: score = 60 tip = "部分预算超支,需要加强控制" default: score = 30 tip = "预算超支严重,请重新规划预算" } return score, tip } // calculateSpendingStability calculates spending stability factor func (s *HealthScoreService) calculateSpendingStability(userID uint, startDate, endDate time.Time) (float64, string) { // 获取按月的支出趋势 trendData, err := s.reportRepo.GetTrendDataByMonth(userID, startDate, endDate, nil) if err != nil || len(trendData) < 2 { return 70.0, "数据不足以分析消费稳定性" } // 计算支出的标准差 var sum, sumSq float64 n := float64(len(trendData)) for _, dp := range trendData { sum += dp.TotalExpense sumSq += dp.TotalExpense * dp.TotalExpense } mean := sum / n variance := (sumSq / n) - (mean * mean) // 变异系数 (CV) = 标准差 / 平均值 if mean <= 0 { return 70.0, "暂无支出记录" } cv := (variance / (mean * mean)) * 100 // 简化计算 var score float64 var tip string switch { case cv <= 10: score = 100 tip = "消费非常稳定,财务规划出色" case cv <= 25: score = 80 tip = "消费较为稳定,理财意识良好" case cv <= 50: score = 60 tip = "消费有一定波动,建议制定月度预算" default: score = 40 tip = "消费波动较大,需要加强支出管理" } return score, tip } // calculateAssetDiversity calculates asset diversity factor func (s *HealthScoreService) calculateAssetDiversity(userID uint) (float64, string) { accounts, err := s.accountRepo.GetAll(userID) if err != nil { return 50.0, "无法获取账户数据" } // 统计不同类型的账户数量 accountTypes := make(map[models.AccountType]int) for _, acc := range accounts { if acc.Balance > 0 && !acc.IsCredit { accountTypes[acc.Type]++ } } diversity := len(accountTypes) var score float64 var tip string switch { case diversity >= 4: score = 100 tip = "资产配置多样化,抗风险能力强" case diversity >= 3: score = 80 tip = "资产配置较好,可考虑增加投资类型" case diversity >= 2: score = 60 tip = "资产类型偏少,建议分散配置" default: score = 40 tip = "资产过于单一,建议多元化理财" } return score, tip } // getHealthScoreLevel returns the level and description based on score func getHealthScoreLevel(score int) (string, string) { switch { case score >= 90: return "优秀", "您的财务状况非常健康,继续保持良好的理财习惯!" case score >= 75: return "良好", "您的财务状况良好,还有一些提升空间。" case score >= 60: return "一般", "您的财务状况尚可,建议关注评分较低的方面。" case score >= 40: return "需改善", "您的财务状况需要改善,请查看具体建议。" default: return "警示", "您的财务状况存在风险,请尽快采取行动改善。" } }