/* Copyright 2025 linux.do Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package dashboard import ( "context" "time" "github.com/linux-do/credit/internal/db" "github.com/linux-do/credit/internal/model" "github.com/shopspring/decimal" ) // getDateRange 计算查询的时间范围 func getDateRange(days int) (startDate, endDate time.Time) { now := time.Now() todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) return todayStart.AddDate(0, 0, -(days - 1)), todayStart.AddDate(0, 0, 1) } // dailyAmountResult 每日金额查询结果 type dailyAmountResult struct { Date time.Time Amount decimal.Decimal } // queryDailyAmounts 查询每日金额 // isIncome: true=收入(payee), false=支出(payer) func queryDailyAmounts(ctx context.Context, userID uint64, isIncome bool, startDate, endDate time.Time) (map[string]decimal.Decimal, error) { var results []dailyAmountResult var err error if isIncome { // 收入查询:payee_user_id = user // 包括:普通收款、红包领取(red_envelope_receive)、红包退款(red_envelope_refund) err = db.DB(ctx).Model(&model.Order{}). Select("DATE_TRUNC('day', created_at) as date, SUM(amount) as amount"). Where("payee_user_id = ?", userID). Where("status = ?", model.OrderStatusSuccess). Where("created_at >= ? AND created_at < ?", startDate, endDate). Group("DATE_TRUNC('day', created_at)"). Scan(&results).Error } else { // 支出查询:payer_user_id = user,但排除 red_envelope_receive // red_envelope_receive 的 payer_user_id 是红包创建者,但创建者的支出已在 red_envelope_send 时计算 err = db.DB(ctx).Model(&model.Order{}). Select("DATE_TRUNC('day', created_at) as date, SUM(amount) as amount"). Where("payer_user_id = ?", userID). Where("status = ?", model.OrderStatusSuccess). Where("type != ?", model.OrderTypeRedEnvelopeReceive). Where("created_at >= ? AND created_at < ?", startDate, endDate). Group("DATE_TRUNC('day', created_at)"). Scan(&results).Error } if err != nil { return nil, err } // 转换为 map statsMap := make(map[string]decimal.Decimal) for _, r := range results { dateStr := r.Date.Format("2006-01-02") statsMap[dateStr] = r.Amount } return statsMap, nil } // mergeDailyStats 合并收入和支出统计,填充无数据的日期 func mergeDailyStats(startDate time.Time, days int, incomeMap, expenseMap map[string]decimal.Decimal) []DailyStatsItem { result := make([]DailyStatsItem, days) for i := range result { dateStr := startDate.AddDate(0, 0, i).Format("2006-01-02") result[i] = DailyStatsItem{ Date: dateStr, Income: incomeMap[dateStr], Expense: expenseMap[dateStr], } } return result } // userBalanceStatsResult 用户余额统计查询结果 type userBalanceStatsResult struct { TotalCount int64 TotalAmount decimal.Decimal AvgAmount decimal.Decimal MinAmount decimal.Decimal MaxAmount decimal.Decimal StdDev decimal.Decimal } // calculateUserBalanceStats 计算用户余额统计数据 func calculateUserBalanceStats(ctx context.Context) (*userBalanceStatsResult, error) { var result userBalanceStatsResult err := db.DB(ctx).Model(&model.User{}). Select(` COUNT(*) as total_count, COALESCE(SUM(available_balance), 0) as total_amount, COALESCE(AVG(available_balance), 0) as avg_amount, COALESCE(MIN(available_balance), 0) as min_amount, COALESCE(MAX(available_balance), 0) as max_amount, COALESCE(STDDEV_SAMP(available_balance), 0) as std_dev `). Scan(&result).Error if err != nil { return nil, err } return &result, nil } // calculateMedian 计算用户余额中位数 func calculateMedian(ctx context.Context) (decimal.Decimal, error) { var median decimal.Decimal err := db.DB(ctx).Raw(` SELECT COALESCE( PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY available_balance), 0 ) as median FROM users `).Scan(&median).Error if err != nil { return decimal.Zero, err } return median, nil }