package service import ( "context" "log/slog" "git.lzcwy.cn/zhengchiliu/zt-closet-backend/global" "git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/dto" "git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/mysqldao" "git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/mysqlmodel" "git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/errcode" ) // QuotaService 额度业务服务。 type QuotaService struct { quotaDao *mysqldao.QuotaDao flowLogDao *mysqldao.QuotaFlowLogDao } // NewQuotaService 创建额度服务。 func NewQuotaService() *QuotaService { return &QuotaService{ quotaDao: mysqldao.NewQuotaDao(), flowLogDao: mysqldao.NewQuotaFlowLogDao(), } } // InfoGetBySelf 获取当前用户各类额度概览。 // Cos 返回永久总额度/累计已用/剩余;AI/Outfit/Closet 返回每日总额度/当日已用/当日剩余。 func (s *QuotaService) InfoGetBySelf(ctx context.Context, userID int64) (*dto.QuotaInfoGetBySelfResponse, *errcode.Error) { ctx, span := global.Tracer.Start(ctx, "svc.QuotaInfoGetBySelf") defer span.End() types := []int8{QuotaTypeCos, QuotaTypeAI, QuotaTypeOutfitShare, QuotaTypeClosetShare} items := make([]dto.QuotaItemResponse, 0, len(types)) for _, quotaType := range types { eff, err := getEffectiveQuota(ctx, userID, quotaType) if err != nil { slog.ErrorContext(ctx, "quota info: calc effective quota failed", "user_id", userID, "quota_type", quotaType, "error", err) return nil, err } items = append(items, dto.QuotaItemResponse{ QuotaType: quotaType, Total: eff.Total, Used: eff.Used, Remain: eff.Remain, }) } return &dto.QuotaInfoGetBySelfResponse{Quotas: items}, nil } // FlowList 分页查询当前用户的额度流水。 func (s *QuotaService) FlowList(ctx context.Context, userID int64, request *dto.QuotaFlowListRequest) (*dto.QuotaFlowListResponse, *errcode.Error) { ctx, span := global.Tracer.Start(ctx, "svc.QuotaFlowList") defer span.End() page, pageSize := normalizePage(request.Page, request.PageSize) list, total, err := s.flowLogDao.QuotaFlowLogListByUserID(ctx, userID, request.QuotaType, page, pageSize) if err != nil { slog.ErrorContext(ctx, "quota flow list: query failed", "user_id", userID, "error", err) return nil, errcode.DatabaseError } items := make([]dto.QuotaFlowItemResponse, 0, len(list)) for _, f := range list { items = append(items, dto.QuotaFlowItemResponse{ QuotaFlowUUID: f.QuotaFlowUUID, QuotaType: f.QuotaType, ChangeTarget: f.ChangeTarget, ChangeType: f.ChangeType, ChangeNum: f.ChangeNum, NewValue: f.NewValue, Reason: f.Reason, OrderNo: f.OrderNo, CreatedAt: f.CreatedAt, }) } return &dto.QuotaFlowListResponse{ PageResponse: dto.PageResponse{Total: total, Page: page, PageSize: pageSize}, List: items, }, nil } // ensureDefaultQuota 为新用户初始化 Cos 永久免费额度底值。 // AI/Outfit/Closet 为每日额度,不落库,由 getEffectiveQuota 动态计算,无需初始化。 // 事务内「创建 + 写流水」强一致,依赖 uk_user_quota 唯一索引实现并发安全幂等; // best-effort:失败仅告警,不阻断登录主流程。 func ensureDefaultQuota(ctx context.Context, userID int64) { quotaDao := mysqldao.NewQuotaDao() total := defaultQuotaValue(QuotaTypeCos) flow := &mysqlmodel.QuotaFlowLog{ QuotaFlowUUID: genUUID(), UserID: userID, ChangeTarget: QuotaChangeTargetAdjust, ChangeType: QuotaChangeTypeActivity, ChangeNum: total, QuotaType: QuotaTypeCos, Reason: "新用户默认上传额度", } if err := quotaDao.QuotaInitWithFlow(ctx, userID, QuotaTypeCos, total, flow); err != nil { slog.WarnContext(ctx, "init default cos quota failed", "user_id", userID, "error", err) } } // defaultQuotaValue 返回某类额度的新用户默认值: // 优先取 global.UserDefaultQuotaSetting 的配置值,配置为 0 时同样生效(表示不赠送该类免费额度); // 未配置(整段缺失或该项缺失,指针为 nil)或配置为负数时,回落内置常量。 func defaultQuotaValue(quotaType int8) int64 { cfg := global.UserDefaultQuotaSetting switch quotaType { case QuotaTypeCos: if cfg != nil && cfg.Cos != nil && *cfg.Cos >= 0 { return *cfg.Cos } return DefaultQuotaCos case QuotaTypeAI: if cfg != nil && cfg.AI != nil && *cfg.AI >= 0 { return *cfg.AI } return DefaultQuotaAI case QuotaTypeOutfitShare: if cfg != nil && cfg.OutfitShare != nil && *cfg.OutfitShare >= 0 { return *cfg.OutfitShare } return DefaultQuotaOutfitShare case QuotaTypeClosetShare: if cfg != nil && cfg.ClosetShare != nil && *cfg.ClosetShare >= 0 { return *cfg.ClosetShare } return DefaultQuotaClosetShare default: return 0 } }