086c5c6573
- 修复 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 响应格式 - 添加数据库索引优化查询性能 - 实现可配置的数据清理定时任务
546 lines
15 KiB
Go
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)})
|
|
}
|