package service import ( "context" "fmt" "log" "strings" "time" "affiliate_dash/internal/model" "gorm.io/gorm" ) // PurchaseTokens 执行采购并批量入库(通用黑色 Token) func (s *RockService) PurchaseTokens(ctx context.Context, count int, isAuto bool) ([]model.RockToken, error) { s.mu.Lock() defer s.mu.Unlock() cfg, err := s.GetConfig(ctx) if err != nil { return nil, err } if cfg.BearerToken == "" { return nil, fmt.Errorf("尚未配置上游 Bearer Token,无法执行采购") } // 自动补货防抖冷却检查 now := time.Now() if isAuto && cfg.LastPurchasedAt != nil { cooldown := time.Duration(cfg.PurchaseCooldownMinutes) * time.Minute if now.Sub(*cfg.LastPurchasedAt) < cooldown { return nil, fmt.Errorf("自动采购处于冷却中(冷却时间 %d 分钟)", cfg.PurchaseCooldownMinutes) } } // 调上游 Claim 接口(统一下单黑色通用 Token) tokens, err := s.client.ClaimTokens(ctx, cfg.BearerToken, model.RockColorBlack, count) if err != nil { return nil, fmt.Errorf("调用上游采购接口失败: %w", err) } if len(tokens) == 0 { return nil, fmt.Errorf("上游未返回任何有效 Token") } var dbTokens []model.RockToken for _, t := range tokens { dbTokens = append(dbTokens, model.RockToken{ UpstreamTokenID: fmt.Sprintf("%d", t.ID), Token: t.Token, RedeemCode: t.RedeemCode, Color: model.RockColorBlack, Region: t.Region, Status: model.RockStatusAvailable, ClaimedAt: &now, }) } // 事务写入库存表 err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Create(&dbTokens).Error; err != nil { return err } // 更新最后采购时间 return tx.Model(&model.RockConfig{}).Where("id = ?", cfg.ID).Update("last_purchased_at", now).Error }) if err != nil { return nil, fmt.Errorf("入库采购 Token 失败: %w", err) } // 异步更新上游剩余积分快照 go func() { bgCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() me, meErr := s.client.GetMe(bgCtx, cfg.BearerToken) if meErr == nil && me != nil { _ = s.db.Model(&model.RockConfig{}).Where("id = 1").Update("points", me.User.Points.Float64()).Error } }() return dbTokens, nil } // CheckStockAndAutoPurchase 水位检查与自动采购触发 func (s *RockService) CheckStockAndAutoPurchase(ctx context.Context) { cfg, err := s.GetConfig(ctx) if err != nil { return } var count int64 s.db.WithContext(ctx).Model(&model.RockToken{}). Where("status = ? AND color = ?", model.RockStatusAvailable, model.RockColorBlack). Count(&count) if count <= int64(cfg.TokenWarningStock) { log.Printf("[RockService] 可用库存预警: 当前仅存 %d 个 (预警阈值 %d)", count, cfg.TokenWarningStock) if cfg.AutoPurchaseEnabled { buyCount := cfg.AutoPurchaseCount if buyCount <= 0 { buyCount = 50 } log.Printf("[RockService] 触发自动补货,采购数量: %d", buyCount) _, err := s.PurchaseTokens(ctx, buyCount, true) if err != nil { log.Printf("[RockService] 自动补货失败: %v", err) } else { log.Printf("[RockService] 自动补货成功,新增 %d 个 Token", buyCount) } } } } // ListTokens 分页查询 Token 列表 func (s *RockService) ListTokens(ctx context.Context, page, pageSize int, status, search string) ([]model.RockToken, int64, error) { if page <= 0 { page = 1 } if pageSize <= 0 { pageSize = 20 } db := s.db.WithContext(ctx).Model(&model.RockToken{}) if strings.TrimSpace(status) != "" { db = db.Where("status = ?", strings.TrimSpace(status)) } if strings.TrimSpace(search) != "" { term := "%" + strings.TrimSpace(search) + "%" db = db.Where("CAST(id AS TEXT) = ? OR upstream_token_id = ? OR upstream_token_id LIKE ? OR assigned_order_no LIKE ? OR expected_uid LIKE ? OR actual_role_name LIKE ? OR actual_role_id LIKE ? OR redeem_code LIKE ? OR remark LIKE ? OR assigned_order_no IN (SELECT order_no FROM fulfillment_orders WHERE client_order_no LIKE ?)", strings.TrimSpace(search), strings.TrimSpace(search), term, term, term, term, term, term, term, term) } var total int64 if err := db.Count(&total).Error; err != nil { return nil, 0, err } var tokens []model.RockToken err := db.Order("id DESC"). Offset((page - 1) * pageSize). Limit(pageSize). Find(&tokens).Error if err != nil { return nil, 0, err } // 批量反查关联订单的商户单号(创建订单时的 ClientOrderNo) orderNos := make([]string, 0, len(tokens)) for _, t := range tokens { if t.AssignedOrderNo != "" { orderNos = append(orderNos, t.AssignedOrderNo) } } if len(orderNos) > 0 { var orders []struct { OrderNo string `gorm:"column:order_no"` ClientOrderNo string `gorm:"column:client_order_no"` } if scanErr := s.db.WithContext(ctx).Table("fulfillment_orders"). Select("order_no, client_order_no"). Where("order_no IN ?", orderNos). Scan(&orders).Error; scanErr == nil { clientMap := make(map[string]string, len(orders)) for _, o := range orders { clientMap[o.OrderNo] = o.ClientOrderNo } for i := range tokens { if cNo, ok := clientMap[tokens[i].AssignedOrderNo]; ok && cNo != "" { tokens[i].ClientOrderNo = cNo } } } } return tokens, total, nil }