Files
2026-09-16 00:23:49 +08:00

141 lines
4.7 KiB
Go

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