Files
verify/backend/internal/router/admin/finance.go
T
admin 086c5c6573 perf: 性能优化与错误修复
- 修复 N+1 查询: users/devices/agents 批量 GROUP BY 替代循环查询
- 添加分页: cards/finance/agents/devices API
- Redis 初始化根据安装配置 redis.enabled 决定是否连接
- 修复 DefaultVal 解析错误: 使用 sql.NullString 处理 NULL 值
- Dashboard 优化: 替换 ECharts 世界地图为 Chart.js 环形饼图
- 适配前端 cards 页面新 API 响应格式
- 添加数据库索引优化查询性能
- 实现可配置的数据清理定时任务
2026-05-06 19:19:02 +08:00

546 lines
15 KiB
Go

package admin
import (
"fmt"
"strconv"
"verification-platform-backend/internal/database"
"verification-platform-backend/internal/model"
"verification-platform-backend/internal/service"
"verification-platform-backend/pkg/response"
"github.com/gin-gonic/gin"
)
type FinanceRecordResponse struct {
ID uint `json:"id"`
OrderNo string `json:"order_no"`
Type string `json:"type"`
UserID uint `json:"user_id"`
AppID uint `json:"app_id"`
Amount float64 `json:"amount"`
Detail string `json:"detail"`
Status string `json:"status"`
PaymentType string `json:"payment_type"`
Remark string `json:"remark"`
CreatedAt string `json:"created_at"`
User *struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
} `json:"user,omitempty"`
}
func SetupFinanceRoutes(r *gin.RouterGroup) {
finance := r.Group("/finance")
{
finance.GET("/stats", handleGetFinanceStatistics)
finance.GET("/statistics", handleGetFinanceStatistics)
finance.GET("/recharge-records", handleGetRechargeRecords)
finance.GET("/consumption-records", handleGetConsumptionRecords)
finance.GET("/records", handleGetFinanceRecords)
finance.DELETE("/records/:id", handleDeleteFinanceRecord)
finance.POST("/records/batch-delete", handleBatchDeleteFinanceRecords)
}
}
func handleGetFinanceStatistics(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
response.Error(c, 401, "未授权")
return
}
var appIDs []uint
database.DB.Model(&model.Application{}).Where("user_id = ?", userID).Pluck("id", &appIDs)
if len(appIDs) == 0 {
response.Success(c, gin.H{
"total_income": 0,
"total_expense": 0,
"net_profit": 0,
"monthly_transactions": 0,
})
return
}
var appUserIDs []uint
database.DB.Model(&model.AppUser{}).Where("application_id IN ?", appIDs).Pluck("id", &appUserIDs)
if len(appUserIDs) == 0 {
response.Success(c, gin.H{
"total_income": 0,
"total_expense": 0,
"net_profit": 0,
"monthly_transactions": 0,
})
return
}
var totalIncome float64
var totalExpense float64
database.DB.Model(&model.RechargeRecord{}).
Where("user_id IN ? AND status = ?", appUserIDs, "success").
Select("COALESCE(SUM(amount), 0)").
Scan(&totalIncome)
database.DB.Model(&model.ConsumptionRecord{}).
Where("user_id IN ? AND status = ?", appUserIDs, "success").
Select("COALESCE(SUM(amount), 0)").
Scan(&totalExpense)
response.Success(c, gin.H{
"total_income": totalIncome,
"total_expense": totalExpense,
"net_profit": totalIncome - totalExpense,
})
}
func handleGetRechargeRecords(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
response.Error(c, 401, "未授权")
return
}
var appIDs []uint
database.DB.Model(&model.Application{}).Where("user_id = ?", userID).Pluck("id", &appIDs)
if len(appIDs) == 0 {
response.Success(c, gin.H{
"records": []model.RechargeRecord{},
"total": 0,
})
return
}
var appUserIDs []uint
database.DB.Model(&model.AppUser{}).Where("application_id IN ?", appIDs).Pluck("id", &appUserIDs)
if len(appUserIDs) == 0 {
response.Success(c, gin.H{
"records": []model.RechargeRecord{},
"total": 0,
})
return
}
var records []model.RechargeRecord
query := database.DB.Model(&model.RechargeRecord{}).Preload("AppUser").Where("user_id IN ?", appUserIDs)
search := c.Query("search")
status := c.Query("status")
startDate := c.Query("start_date")
endDate := c.Query("end_date")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
if search != "" {
query = query.Where("order_no LIKE ? OR card_code LIKE ?", "%"+search+"%", "%"+search+"%")
}
if status != "" && status != "all" {
query = query.Where("status = ?", status)
}
if startDate != "" {
query = query.Where("created_at >= ?", startDate)
}
if endDate != "" {
query = query.Where("created_at <= ?", endDate+" 23:59:59")
}
var total int64
query.Count(&total)
offset := (page - 1) * pageSize
if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&records).Error; err != nil {
response.Error(c, 500, "获取充值记录失败")
return
}
response.Success(c, gin.H{
"records": records,
"total": total,
"page": page,
"page_size": pageSize,
"total_page": (total + int64(pageSize) - 1) / int64(pageSize),
})
}
func handleGetConsumptionRecords(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
response.Error(c, 401, "未授权")
return
}
var appIDs []uint
database.DB.Model(&model.Application{}).Where("user_id = ?", userID).Pluck("id", &appIDs)
if len(appIDs) == 0 {
response.Success(c, gin.H{
"records": []model.ConsumptionRecord{},
"total": 0,
})
return
}
var appUserIDs []uint
database.DB.Model(&model.AppUser{}).Where("application_id IN ?", appIDs).Pluck("id", &appUserIDs)
if len(appUserIDs) == 0 {
response.Success(c, gin.H{
"records": []model.ConsumptionRecord{},
"total": 0,
})
return
}
var records []model.ConsumptionRecord
query := database.DB.Model(&model.ConsumptionRecord{}).Preload("AppUser").Where("user_id IN ?", appUserIDs)
search := c.Query("search")
recordType := c.Query("type")
status := c.Query("status")
startDate := c.Query("start_date")
endDate := c.Query("end_date")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
if search != "" {
query = query.Where("order_no LIKE ? OR content LIKE ?", "%"+search+"%", "%"+search+"%")
}
if recordType != "" && recordType != "all" {
query = query.Where("type = ?", recordType)
}
if status != "" && status != "all" {
query = query.Where("status = ?", status)
}
if startDate != "" {
query = query.Where("created_at >= ?", startDate)
}
if endDate != "" {
query = query.Where("created_at <= ?", endDate+" 23:59:59")
}
var total int64
query.Count(&total)
offset := (page - 1) * pageSize
if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&records).Error; err != nil {
response.Error(c, 500, "获取消费记录失败")
return
}
response.Success(c, gin.H{
"records": records,
"total": total,
"page": page,
"page_size": pageSize,
"total_page": (total + int64(pageSize) - 1) / int64(pageSize),
})
}
func handleGetFinanceRecords(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
response.Error(c, 401, "未授权")
return
}
var appIDs []uint
database.DB.Model(&model.Application{}).Where("user_id = ?", userID).Pluck("id", &appIDs)
if len(appIDs) == 0 {
response.Success(c, gin.H{
"recharge_records": []FinanceRecordResponse{},
"consumption_records": []FinanceRecordResponse{},
"total": 0,
})
return
}
var appUserIDs []uint
database.DB.Model(&model.AppUser{}).Where("application_id IN ?", appIDs).Pluck("id", &appUserIDs)
if len(appUserIDs) == 0 {
response.Success(c, gin.H{
"recharge_records": []FinanceRecordResponse{},
"consumption_records": []FinanceRecordResponse{},
"total": 0,
})
return
}
page := c.DefaultQuery("page", "1")
pageSize := c.DefaultQuery("page_size", "10")
search := c.Query("search")
recordType := c.Query("type")
status := c.Query("status")
startDate := c.Query("start_date")
endDate := c.Query("end_date")
var rechargeRecords []model.RechargeRecord
rechargeQuery := database.DB.Model(&model.RechargeRecord{}).Preload("AppUser").Where("user_id IN ?", appUserIDs)
if search != "" {
rechargeQuery = rechargeQuery.Where("order_no LIKE ? OR card_code LIKE ?", "%"+search+"%", "%"+search+"%")
}
if recordType == "recharge" || recordType == "" || recordType == "all" {
if status != "" && status != "all" {
rechargeQuery = rechargeQuery.Where("status = ?", status)
}
if startDate != "" {
rechargeQuery = rechargeQuery.Where("created_at >= ?", startDate)
}
if endDate != "" {
rechargeQuery = rechargeQuery.Where("created_at <= ?", endDate+" 23:59:59")
}
if err := rechargeQuery.Order("created_at DESC").Find(&rechargeRecords).Error; err != nil {
response.Error(c, 500, "获取充值记录失败")
return
}
}
var consumptionRecords []model.ConsumptionRecord
consumptionQuery := database.DB.Model(&model.ConsumptionRecord{}).Preload("AppUser").Where("user_id IN ?", appUserIDs)
if search != "" {
consumptionQuery = consumptionQuery.Where("order_no LIKE ? OR content LIKE ?", "%"+search+"%", "%"+search+"%")
}
if recordType == "consumption" || recordType == "" || recordType == "all" {
if status != "" && status != "all" {
consumptionQuery = consumptionQuery.Where("status = ?", status)
}
if startDate != "" {
consumptionQuery = consumptionQuery.Where("created_at >= ?", startDate)
}
if endDate != "" {
consumptionQuery = consumptionQuery.Where("created_at <= ?", endDate+" 23:59:59")
}
if err := consumptionQuery.Order("created_at DESC").Find(&consumptionRecords).Error; err != nil {
response.Error(c, 500, "获取消费记录失败")
return
}
}
var allRecords []FinanceRecordResponse
for _, r := range rechargeRecords {
var user *struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
}
if r.AppUser != nil && r.AppUser.ID != 0 {
user = &struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
}{
ID: r.AppUser.ID,
Username: r.AppUser.Username,
Email: r.AppUser.Email,
}
}
allRecords = append(allRecords, FinanceRecordResponse{
ID: r.ID,
OrderNo: r.OrderNo,
Type: "recharge",
UserID: r.UserID,
AppID: r.AppUser.ApplicationID,
Amount: r.Amount,
Detail: r.CardCode,
Status: r.Status,
PaymentType: r.PaymentType,
Remark: r.Remark,
CreatedAt: r.CreatedAt.Format("2006-01-02 15:04:05"),
User: user,
})
}
for _, r := range consumptionRecords {
var user *struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
}
if r.AppUser != nil && r.AppUser.ID != 0 {
user = &struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
}{
ID: r.AppUser.ID,
Username: r.AppUser.Username,
Email: r.AppUser.Email,
}
}
allRecords = append(allRecords, FinanceRecordResponse{
ID: r.ID,
OrderNo: r.OrderNo,
Type: "consumption",
UserID: r.UserID,
AppID: r.AppUser.ApplicationID,
Amount: r.Amount,
Detail: r.Content,
Status: r.Status,
PaymentType: r.PaymentType,
Remark: r.Remark,
CreatedAt: r.CreatedAt.Format("2006-01-02 15:04:05"),
User: user,
})
}
total := len(allRecords)
start := 0
end := total
if p, err := parseInt(page); err == nil && p > 0 {
if ps, err := parseInt(pageSize); err == nil && ps > 0 {
start = (p - 1) * ps
end = start + ps
if start > total {
start = total
}
if end > total {
end = total
}
}
}
if start > end {
start = end
}
response.Success(c, gin.H{
"records": allRecords[start:end],
"total": total,
})
}
func parseInt(s string) (int, error) {
var result int
_, err := fmt.Sscanf(s, "%d", &result)
return result, err
}
func handleDeleteFinanceRecord(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
response.Error(c, 401, "未授权")
return
}
recordID := c.Param("id")
recordType := c.Query("type")
var appIDs []uint
database.DB.Model(&model.Application{}).Where("user_id = ?", userID).Pluck("id", &appIDs)
if len(appIDs) == 0 {
response.Error(c, 404, "记录不存在")
return
}
var appUserIDs []uint
database.DB.Model(&model.AppUser{}).Where("application_id IN ?", appIDs).Pluck("id", &appUserIDs)
if len(appUserIDs) == 0 {
response.Error(c, 404, "记录不存在")
return
}
var deletedType string
var deletedAmount float64
if recordType == "recharge" {
var record model.RechargeRecord
if err := database.DB.Where("id = ? AND user_id IN ?", recordID, appUserIDs).First(&record).Error; err != nil {
response.Error(c, 404, "记录不存在")
return
}
if err := database.DB.Delete(&record).Error; err != nil {
response.Error(c, 500, "删除失败")
return
}
deletedType = "充值记录"
deletedAmount = record.Amount
} else if recordType == "consumption" {
var record model.ConsumptionRecord
if err := database.DB.Where("id = ? AND user_id IN ?", recordID, appUserIDs).First(&record).Error; err != nil {
response.Error(c, 404, "记录不存在")
return
}
if err := database.DB.Delete(&record).Error; err != nil {
response.Error(c, 500, "删除失败")
return
}
deletedType = "消费记录"
deletedAmount = record.Amount
} else {
var rechargeRecord model.RechargeRecord
var consumptionRecord model.ConsumptionRecord
if err := database.DB.Where("id = ? AND user_id IN ?", recordID, appUserIDs).First(&rechargeRecord).Error; err == nil {
database.DB.Delete(&rechargeRecord)
deletedType = "充值记录"
deletedAmount = rechargeRecord.Amount
service.LogOperation(c, "delete", "finance_record", nil, fmt.Sprintf("删除%s: %.2f", deletedType, deletedAmount), nil)
response.Success(c, nil)
return
}
if err := database.DB.Where("id = ? AND user_id IN ?", recordID, appUserIDs).First(&consumptionRecord).Error; err == nil {
database.DB.Delete(&consumptionRecord)
deletedType = "消费记录"
deletedAmount = consumptionRecord.Amount
service.LogOperation(c, "delete", "finance_record", nil, fmt.Sprintf("删除%s: %.2f", deletedType, deletedAmount), nil)
response.Success(c, nil)
return
}
response.Error(c, 404, "记录不存在")
return
}
service.LogOperation(c, "delete", "finance_record", nil, fmt.Sprintf("删除%s: %.2f", deletedType, deletedAmount), nil)
response.Success(c, nil)
}
func handleBatchDeleteFinanceRecords(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
response.Error(c, 401, "未授权")
return
}
var req struct {
IDs []uint `json:"ids"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, 400, "参数错误")
return
}
if len(req.IDs) == 0 {
response.Error(c, 400, "请选择要删除的记录")
return
}
var appIDs []uint
database.DB.Model(&model.Application{}).Where("user_id = ?", userID).Pluck("id", &appIDs)
if len(appIDs) == 0 {
response.Error(c, 404, "记录不存在")
return
}
var appUserIDs []uint
database.DB.Model(&model.AppUser{}).Where("application_id IN ?", appIDs).Pluck("id", &appUserIDs)
if len(appUserIDs) == 0 {
response.Error(c, 404, "记录不存在")
return
}
database.DB.Where("id IN ? AND user_id IN ?", req.IDs, appUserIDs).Delete(&model.RechargeRecord{})
database.DB.Where("id IN ? AND user_id IN ?", req.IDs, appUserIDs).Delete(&model.ConsumptionRecord{})
service.LogOperation(c, "batch_delete", "finance_record", nil, fmt.Sprintf("批量删除财务记录: %d条", len(req.IDs)), nil)
response.Success(c, gin.H{"deleted": len(req.IDs)})
}