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 下通过;全量测试通过。
169 lines
5.6 KiB
Go
169 lines
5.6 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"time"
|
|
|
|
"affiliate_dash/internal/model"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// RedeemTokenForOrder 执行最终发货兑换(动态判定皮肤)
|
|
func (s *RockService) RedeemTokenForOrder(ctx context.Context, orderNo, sku, expectedUID string) (*RockRedeemResponse, error) {
|
|
cfg, err := s.GetConfig(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !cfg.AutoDeliveryEnabled {
|
|
return nil, fmt.Errorf("Rock 自动发货通道维护中,已关闭自动兑换")
|
|
}
|
|
|
|
var token model.RockToken
|
|
err = s.db.WithContext(ctx).
|
|
Where("assigned_order_no = ? AND status = ?", orderNo, model.RockStatusBound).
|
|
First(&token).Error
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, fmt.Errorf("未找到该订单已绑定的有效 Token,或当前状态不可发货")
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// 最终双重防御:核验 UID
|
|
trimmedExpected := strings.TrimSpace(expectedUID)
|
|
if trimmedExpected == "" {
|
|
trimmedExpected = strings.TrimSpace(token.ExpectedUID)
|
|
}
|
|
if trimmedExpected != "" && token.ActualRoleID != trimmedExpected {
|
|
return nil, fmt.Errorf("终审拦截:扫码UID(%s)与预期UID(%s)不符,禁止发货!", token.ActualRoleID, trimmedExpected)
|
|
}
|
|
|
|
// 根据商品 SKU 动态决定兑换的皮肤名称
|
|
targetSkin := model.RockSkinBlackTshirt
|
|
if sku == model.RockSKUBlueSportswear {
|
|
targetSkin = model.RockSkinBlueSportswear
|
|
}
|
|
|
|
// 确保买家角色名存在
|
|
actualRoleName := strings.TrimSpace(token.ActualRoleName)
|
|
if actualRoleName == "" {
|
|
if ref, refErr := s.client.RefreshToken(ctx, cfg.BearerToken, token.UpstreamTokenID); refErr == nil && ref != nil {
|
|
actualRoleName = ref.GetRoleName()
|
|
if actualRoleName != "" {
|
|
_ = s.db.WithContext(ctx).Model(&token).Update("actual_role_name", actualRoleName).Error
|
|
}
|
|
}
|
|
}
|
|
if actualRoleName == "" {
|
|
actualRoleName = token.ActualRoleID
|
|
}
|
|
|
|
// 调上游兑换发货接口(携带 Bearer Token 鉴权)
|
|
redeemResp, err := s.client.RedeemSkin(ctx, cfg.BearerToken, token.UpstreamTokenID, targetSkin, "幸运币", actualRoleName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("调用上游兑换失败: %w", err)
|
|
}
|
|
|
|
// 提取官方 fulfillId
|
|
var fulfillID int64
|
|
now := time.Now()
|
|
ref, refErr := s.client.RefreshToken(ctx, cfg.BearerToken, token.UpstreamTokenID)
|
|
if refErr == nil && len(ref.DeliveryList) > 0 {
|
|
fulfillID = ref.DeliveryList[len(ref.DeliveryList)-1].FulfillID
|
|
}
|
|
|
|
// 状态跃迁至 redeemed
|
|
_ = s.db.WithContext(ctx).Model(&token).Updates(map[string]interface{}{
|
|
"status": model.RockStatusRedeemed,
|
|
"upstream_fulfill_id": fulfillID,
|
|
"target_skin": targetSkin,
|
|
"target_currency": "幸运币",
|
|
"target_currency_quantity": 1,
|
|
"redeemed_at": now,
|
|
}).Error
|
|
|
|
return redeemResp, nil
|
|
}
|
|
|
|
// ReleaseUnboundUsingToken 把尚未扫码绑定的使用中链接释放回可用库存。
|
|
// 已绑定或上游已出现新扫码时拒绝释放,避免退款后的旧二维码被当成新库存发出去。
|
|
func (s *RockService) ReleaseUnboundUsingToken(ctx context.Context, tokenID uint) error {
|
|
var token model.RockToken
|
|
if err := s.db.WithContext(ctx).First(&token, tokenID).Error; err != nil {
|
|
return err
|
|
}
|
|
if token.Status != model.RockStatusUsing {
|
|
return fmt.Errorf("仅扫码使用中且尚未绑定的链接可以释放回库存")
|
|
}
|
|
if token.BoundAt != nil || strings.TrimSpace(token.ActualRoleID) != "" {
|
|
return fmt.Errorf("该链接已扫码绑定,不能释放回库存")
|
|
}
|
|
|
|
cfg, err := s.GetConfig(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if strings.TrimSpace(cfg.BearerToken) == "" || strings.TrimSpace(token.UpstreamTokenID) == "" {
|
|
return fmt.Errorf("缺少上游凭证,无法确认是否已扫码绑定")
|
|
}
|
|
|
|
ref, err := s.client.RefreshToken(ctx, cfg.BearerToken, token.UpstreamTokenID)
|
|
if err != nil {
|
|
return fmt.Errorf("确认上游绑定状态失败: %w", err)
|
|
}
|
|
if rockBuyerNewlyBound(&token, ref) {
|
|
return fmt.Errorf("上游已扫码绑定角色 %s(%s),不能释放", ref.GetRoleName(), ref.GetRoleID())
|
|
}
|
|
|
|
s.refreshCache.Delete(token.UpstreamTokenID)
|
|
orderNo := token.AssignedOrderNo
|
|
if err := s.db.WithContext(ctx).Model(&token).Updates(map[string]interface{}{
|
|
"status": model.RockStatusAvailable,
|
|
"assigned_order_no": "",
|
|
"expected_uid": "",
|
|
"actual_role_name": "",
|
|
"actual_role_id": "",
|
|
"actual_channel": "",
|
|
"initial_bound_snapshot": "",
|
|
"mismatch_at": nil,
|
|
"mismatch_reason": "",
|
|
"bound_at": nil,
|
|
"updated_at": time.Now(),
|
|
}).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
s.detachReleasedRockToken(ctx, orderNo, token.ID)
|
|
log.Printf("[RockService] 人工释放未绑定库存: token_id=%d order=%s", token.ID, orderNo)
|
|
return nil
|
|
}
|
|
|
|
// detachReleasedRockToken 清掉原订单上指向这条链接的记录,避免旧发货页继续占用它。
|
|
func (s *RockService) detachReleasedRockToken(ctx context.Context, orderNo string, tokenID uint) {
|
|
orderNo = strings.TrimSpace(orderNo)
|
|
if orderNo == "" || tokenID == 0 {
|
|
return
|
|
}
|
|
var order model.FulfillmentOrder
|
|
if err := s.db.WithContext(ctx).Where("order_no = ?", orderNo).First(&order).Error; err != nil {
|
|
return
|
|
}
|
|
if resultDataNumber(order.ResultData, "rock_token_id") != int64(tokenID) {
|
|
return
|
|
}
|
|
raw := resultDataMap(order.ResultData)
|
|
delete(raw, "rock_token_id")
|
|
delete(raw, "rock_redeem_code")
|
|
raw["rock_token_released"] = true
|
|
encoded, err := json.Marshal(raw)
|
|
if err != nil {
|
|
return
|
|
}
|
|
_ = s.db.WithContext(ctx).Model(&order).Update("result_data", string(encoded)).Error
|
|
}
|