Files
affiliate_dash/backend/internal/service/rock_stock.go
T
yml2213 80e2bbf875 fix(rock): 上游地址改为以数据库配置为准,并消除 baseURL 数据竞争
GetConfig 从不把 rock_configs.base_url 回写到客户端,main.go 又硬编码了
https://0666.pro,导致后台改上游地址「保存那一刻生效、进程重启后失效」,
数据库里的值被静默忽略且无任何提示。

- 新增 model.RockDefaultBaseURL 统一默认值来源
- 新增 RockService.ApplyConfigToClient,启动时把 DB 地址同步到客户端
- UpdateConfig 改用 client.SetBaseURL 立即生效,不再直接写字段
- RockClient.baseURL 原为裸字段,被 HTTP 请求线程与库存巡检 goroutine 并发
  读写(数据竞争),改为 sync.RWMutex 保护并新增 BaseURL/SetBaseURL
- ClaimTokens/RefreshToken/RedeemSkin 的 Origin/Referer 跟随配置地址
- 空值回落默认地址,末尾斜杠自动裁剪

为保持该提交可编译,同一次改动把 rock_service.go(1177 行)按域拆分为
8 个文件(最大 221 行):rock_service / rock_bindlink / rock_stock /
rock_assign / rock_bindcheck / rock_redeem / rock_recover / rock_snapshot。
35 个声明块全部有归属,无遗漏无重复;迁移文件 012 受 checksum 保护故未改动。

验证:TestRockServiceBaseURLFromConfig 覆盖「改地址→立即生效→重启→仍生效」;
并发用例在 -race 下通过;全量测试通过。
2026-10-06 19:42:58 +08:00

178 lines
5.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 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)
}
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
}