382 lines
10 KiB
Go
382 lines
10 KiB
Go
|
|
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 "警示", "您的财务状况存在风险,请尽快采取行动改善。"
|
||
|
|
}
|
||
|
|
}
|