Files
zt-closet-manage-backend/internal/service/dashboard.go
T
zhengchiliu 0863234bc5 feat(backend): 对齐业务服务额度与内容治理口径
仪表盘待办与穿搭概况改为公开/私密/已封禁口径,移除恒为零的待审核统计;补齐额度凭证来源 source=4 与流水 change_type=7 枚举及订单商品类型、额度有效期快照字段;管理员发放与回收额度凭证在同一事务内写入人工调整流水,回收流水使用独立 order_no 避免撞唯一索引,并增加凭证归属校验;对齐 Quota 配置免费额度值,同步更新 README 与配置说明。
2026-09-16 02:58:56 +08:00

190 lines
5.6 KiB
Go

package service
import (
"context"
"time"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/global"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/dto"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/mysqldao"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/pkg/errcode"
)
// DashboardService 仪表盘统计服务。
type DashboardService struct {
statsDao *mysqldao.StatsDao
}
// NewDashboardService 创建仪表盘服务。
func NewDashboardService() *DashboardService {
return &DashboardService{statsDao: mysqldao.NewStatsDao()}
}
// Summary 聚合仪表盘核心指标:用户/订单/营收/待办。
func (s *DashboardService) Summary(ctx context.Context) (*dto.DashboardResponse, *errcode.Error) {
ctx, span := global.Tracer.Start(ctx, "svc.DashboardSummary")
defer span.End()
todayStart := todayStartUnix()
userStats, err := s.statsDao.UserStatsGet(ctx, todayStart)
if err != nil {
return nil, errcode.DatabaseError
}
userStatusMap, err := s.statsDao.UserStatsGetByStatus(ctx)
if err != nil {
return nil, errcode.DatabaseError
}
orderStats, err := s.statsDao.OrderStatsGet(ctx)
if err != nil {
return nil, errcode.DatabaseError
}
revenueStats, err := s.statsDao.RevenueStatsGet(ctx, todayStart)
if err != nil {
return nil, errcode.DatabaseError
}
todoStats, err := s.statsDao.TodoStatsGet(ctx)
if err != nil {
return nil, errcode.DatabaseError
}
contentStats, err := s.statsDao.ContentStatsGet(ctx)
if err != nil {
return nil, errcode.DatabaseError
}
now := nowUnix()
vipStats, err := s.statsDao.VIPStatsGet(ctx, now, now+7*86400)
if err != nil {
return nil, errcode.DatabaseError
}
goodsStats, err := s.statsDao.GoodsStatsGet(ctx)
if err != nil {
return nil, errcode.DatabaseError
}
usageStats, err := s.statsDao.UsageStatsGet(ctx, todayStatDate())
if err != nil {
return nil, errcode.DatabaseError
}
socialStats, err := s.statsDao.SocialStatsGet(ctx)
if err != nil {
return nil, errcode.DatabaseError
}
return &dto.DashboardResponse{
Users: dto.DashboardUsers{
Total: userStats.Total,
TodayNew: userStats.TodayNew,
Normal: userStatusMap[UserStatusNormal],
TempBan: userStatusMap[UserStatusTempBan],
PermBan: userStatusMap[UserStatusPermBan],
Deactivated: userStatusMap[UserStatusDeactivated],
},
Orders: dto.DashboardOrders{
Total: orderStats.Total,
Pending: orderStats.Pending,
Paid: orderStats.Paid,
Cancelled: orderStats.Cancelled,
Expired: orderStats.Expired,
Refunded: orderStats.Refunded,
},
Revenue: dto.DashboardRevenue{
TotalRevenue: revenueStats.TotalRevenue,
TodayRevenue: revenueStats.TodayRevenue,
RefundTotal: revenueStats.RefundTotal,
},
Todo: dto.DashboardTodo{
RefundPending: todoStats.RefundPending,
// 穿搭口径复用内容概况聚合结果,避免对 outfits 表重复计数。
OutfitPublicTotal: contentStats.OutfitPublic,
OutfitBanned: contentStats.OutfitBanned,
GoodsOnShelf: todoStats.GoodsOnShelf,
},
Content: dto.DashboardContent{
Outfit: dto.DashboardOutfitStats{
Total: contentStats.OutfitTotal,
Public: contentStats.OutfitPublic,
Private: contentStats.OutfitPrivate,
Banned: contentStats.OutfitBanned,
},
Clothes: dto.DashboardClothesStats{
Total: contentStats.ClothesTotal,
Processing: contentStats.ClothesProcessing,
Success: contentStats.ClothesSuccess,
Failed: contentStats.ClothesFailed,
},
Share: dto.DashboardShareStats{
Total: contentStats.ShareTotal,
Normal: contentStats.ShareNormal,
Cancelled: contentStats.ShareCancelled,
Expired: contentStats.ShareExpired,
Banned: contentStats.ShareBanned,
},
},
VIP: dto.DashboardVIP{
MemberTotal: vipStats.MemberTotal,
Monthly: vipStats.Monthly,
Yearly: vipStats.Yearly,
Trial: vipStats.Trial,
ExpiringSoon: vipStats.ExpiringSoon,
},
Goods: dto.DashboardGoods{
Total: goodsStats.Total,
OnShelf: goodsStats.OnShelf,
OffShelf: goodsStats.OffShelf,
ByType: buildGoodsTypeCounts(goodsStats.ByType),
},
Usage: dto.DashboardUsage{
AIRecommend: usageStats.AIRecommend,
ClosetShare: usageStats.ClosetShare,
OutfitShare: usageStats.OutfitShare,
},
Social: dto.DashboardSocial{
CosImageCount: socialStats.CosImageCount,
LikeCount: socialStats.LikeCount,
CollectCount: socialStats.CollectCount,
},
}, nil
}
// buildGoodsTypeCounts 商品类型数量 DAO → DTO。
func buildGoodsTypeCounts(in []mysqldao.GoodsTypeCount) []dto.GoodsTypeCount {
out := make([]dto.GoodsTypeCount, 0, len(in))
for _, v := range in {
out = append(out, dto.GoodsTypeCount{Type: v.Type, Count: v.Count})
}
return out
}
// Trends 返回近 N 天趋势(新增用户/已支付订单量/营收,按天补零对齐)。
func (s *DashboardService) Trends(ctx context.Context, days int) (*dto.TrendResponse, *errcode.Error) {
ctx, span := global.Tracer.Start(ctx, "svc.DashboardTrends")
defer span.End()
if days <= 0 {
days = 30
}
if days > 90 {
days = 90
}
startTs := todayStartUnix() - int64(days-1)*86400
stats, err := s.statsDao.TrendStatsGet(ctx, startTs)
if err != nil {
return nil, errcode.DatabaseError
}
loc := time.Now().Location()
startDay := time.Unix(startTs, 0).In(loc)
points := make([]dto.TrendPoint, 0, days)
for i := 0; i < days; i++ {
date := startDay.AddDate(0, 0, i).Format("2006-01-02")
points = append(points, dto.TrendPoint{
Date: date,
NewUsers: stats.NewUsers[date],
Orders: stats.Orders[date],
Revenue: stats.Revenue[date],
})
}
return &dto.TrendResponse{Points: points}, nil
}