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 下通过;全量测试通过。
178 lines
5.2 KiB
Go
178 lines
5.2 KiB
Go
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
|
||
}
|