Files
OpenFlare/internal/apps/dashboard/logic.go
T
2026-06-08 20:34:27 +08:00

150 lines
4.4 KiB
Go

/*
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
}