141 lines
4.7 KiB
Go
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
|
|
}
|
|
}
|