Files
affiliate_dash/backend/internal/service/provider_rock_test.go
T
yml2213 7e5cbaf65e fix(rock): 兼容 0666.pro 接口 points 字段字符串与数值自适应解析
1. 新增 FlexibleFloat64 类型,解决上游 /api/me 响应中 points 为字符串(如 '8271.2')导致反序列化失败的问题;
2. 服务层与数据库快照存储统一调用 Float64() 保持类型严谨;
3. 单元测试补充真实上游字符串响应场景并全绿通过。
2026-10-05 15:42:00 +08:00

374 lines
11 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"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"affiliate_dash/internal/model"
"affiliate_dash/internal/testdb"
"gorm.io/gorm"
)
func newRockTestDB(t *testing.T) *gorm.DB {
t.Helper()
return testdb.New(t,
&model.Merchant{},
&model.Product{},
&model.MerchantProduct{},
&model.FulfillmentOrder{},
&model.RockConfig{},
&model.RockToken{},
)
}
func TestRockProvider_KillSwitch(t *testing.T) {
db := newRockTestDB(t)
client := NewRockClient("https://example.com")
rockSvc := NewRockService(db, client)
provider := NewRockProvider(rockSvc)
// 1. 初始化配置并关闭总开关
cfg, err := rockSvc.GetConfig(context.Background())
if err != nil {
t.Fatalf("get config: %v", err)
}
f := false
_, err = rockSvc.UpdateConfig(context.Background(), UpdateRockConfigReq{
AutoDeliveryEnabled: &f,
})
if err != nil {
t.Fatalf("update config: %v", err)
}
order := &model.FulfillmentOrder{
OrderNo: "ROCK-ORDER-001",
ProductSKU: model.RockSKUBlackTshirt,
}
// 2. PrepareDelivery 检查是否触发熔断
state, err := provider.PrepareDelivery(context.Background(), order, nil)
if err != nil {
t.Fatalf("prepare delivery: %v", err)
}
if state.CanShip {
t.Fatalf("总开关关闭时应该不可发货")
}
if !strings.Contains(state.CannotShipReason, "维护中") {
t.Fatalf("熔断提示信息不符: %s", state.CannotShipReason)
}
// 3. 恢复开启总开关
tr := true
_, _ = rockSvc.UpdateConfig(context.Background(), UpdateRockConfigReq{
AutoDeliveryEnabled: &tr,
})
_ = cfg
}
func TestRockProvider_AntiGhostAndUIDMismatch(t *testing.T) {
db := newRockTestDB(t)
// Mock 上游 0666.pro 响应
var currentBoundAccount string
var currentBoundRoleID string
var currentBindChangedAt string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if strings.HasPrefix(r.URL.Path, "/api/redeem/") {
_ = json.NewEncoder(w).Encode(RockRedeemDetailResponse{
ID: 151153,
RedeemCode: "mock_redeem_code",
Color: "black",
Status: "available",
BindURL: "https://gp.qq.com/cp/mock_bind",
})
return
}
if strings.HasSuffix(r.URL.Path, "/refresh") {
var resp RockRefreshResponse
if currentBoundRoleID != "" {
resp.Binding.Bound = true
resp.Binding.RoleName = currentBoundAccount
resp.Binding.RoleID = currentBoundRoleID
resp.Binding.Channel = "安卓-微信"
resp.Token.BindChangedAt = currentBindChangedAt
}
_ = json.NewEncoder(w).Encode(resp)
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer server.Close()
client := NewRockClient(server.URL)
rockSvc := NewRockService(db, client)
provider := NewRockProvider(rockSvc)
// 预先插入 1 个可用 Token,模拟历史残留数据
now := time.Now()
token := model.RockToken{
UpstreamTokenID: "151153",
Token: "mock_jwt_token",
RedeemCode: "mock_redeem_code",
Color: model.RockColorBlack,
Status: model.RockStatusAvailable,
ClaimedAt: &now,
}
db.Create(&token)
// 模拟场景 A:上游初始已有历史残留账号“总荒凉”(3751279035)
currentBoundAccount = "总荒凉"
currentBoundRoleID = "3751279035"
currentBindChangedAt = "2026-10-01 10:00:00"
// 买家期望 UID 为 3688825401
expectedUID := "3688825401"
order := &model.FulfillmentOrder{
OrderNo: "ROCK-ORDER-002",
ProductSKU: model.RockSKUBlackTshirt,
ResultData: fmt.Sprintf(`{"expected_uid":"%s"}`, expectedUID),
}
// 1. 买家点击生成绑定二维码
bindRes, err := provider.Bind(context.Background(), order, expectedUID)
if err != nil {
t.Fatalf("bind error: %v", err)
}
if bindRes.BindURL == "" {
t.Fatalf("bind URL 不应为空")
}
// 2. 买家尚未扫码,轮询 CheckBind:应当识别出历史残留并忽略,不报错也不发货
check1, err := provider.CheckBind(context.Background(), order, bindRes.BindUUID)
if err != nil {
t.Fatalf("check bind: %v", err)
}
if check1.Bound {
t.Fatalf("买家尚未扫码,不应判定为绑定成功(被历史残留误导)")
}
// 3. 买家扫错了号!拿了微信小号扫码,实际角色变成了“错误小号”(9999999999)
currentBoundAccount = "错误小号"
currentBoundRoleID = "9999999999"
currentBindChangedAt = "2026-10-05 14:00:00"
check2, err := provider.CheckBind(context.Background(), order, bindRes.BindUUID)
if err != nil {
t.Fatalf("check bind mismatch: %v", err)
}
if !check2.Mismatch {
t.Fatalf("UID 不匹配时必须标记 mismatch = true!")
}
if check2.Bound {
t.Fatalf("UID 不匹配时严禁 bound = true!")
}
// 检查数据库中 Token 状态是否已跃迁为 mismatch
var dbToken model.RockToken
db.First(&dbToken, token.ID)
if dbToken.Status != model.RockStatusMismatch {
t.Fatalf("Token 状态应为 mismatch,当前为 %s", dbToken.Status)
}
if !strings.Contains(dbToken.MismatchReason, "不一致") {
t.Fatalf("mismatch reason 异常: %s", dbToken.MismatchReason)
}
// 4. 客服确认买家输错,执行人工解冻重置
err = rockSvc.ResetMismatchToken(context.Background(), token.ID)
if err != nil {
t.Fatalf("reset mismatch token: %v", err)
}
db.First(&dbToken, token.ID)
if dbToken.Status != model.RockStatusAvailable {
t.Fatalf("重置后状态应为 available,当前为 %s", dbToken.Status)
}
}
func TestRockProvider_SubmitBlueAndBlack(t *testing.T) {
db := newRockTestDB(t)
var receivedSkin string
var receivedCurrency string
var receivedAccount string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if strings.HasPrefix(r.URL.Path, "/api/redeem/") {
var req map[string]string
_ = json.NewDecoder(r.Body).Decode(&req)
receivedSkin = req["skin"]
receivedCurrency = req["currency"]
receivedAccount = req["account"]
resp := RockRedeemResponse{
Success: true,
Message: "发放成功",
}
resp.Delivery.Skin = receivedSkin
resp.Delivery.Currency = receivedCurrency
resp.Delivery.Quantity = 10
resp.Delivery.FulfillID = 888999
_ = json.NewEncoder(w).Encode(resp)
return
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
client := NewRockClient(server.URL)
rockSvc := NewRockService(db, client)
provider := NewRockProvider(rockSvc)
// 场景 A:兑换蓝色运动服上衣
now := time.Now()
tokenBlue := model.RockToken{
UpstreamTokenID: "151154",
Token: "token_blue",
RedeemCode: "code_blue",
Color: model.RockColorBlack, // 通用黑色 Token
Status: model.RockStatusBound,
AssignedOrderNo: "ORDER-BLUE-001",
ExpectedUID: "4331027062",
ActualRoleName: "破浪小王子",
ActualRoleID: "4331027062",
BoundAt: &now,
}
db.Create(&tokenBlue)
orderBlue := &model.FulfillmentOrder{
OrderNo: "ORDER-BLUE-001",
ProductSKU: model.RockSKUBlueSportswear, // 蓝色运动服
ResultData: `{"expected_uid":"4331027062"}`,
}
submitRes, err := provider.Submit(context.Background(), orderBlue, "4331027062", fmt.Sprintf("%d", tokenBlue.ID))
if err != nil {
t.Fatalf("submit blue error: %v", err)
}
if receivedSkin != model.RockSkinBlueSportswear {
t.Fatalf("蓝色运动服发货应传入 %s,实际为 %s", model.RockSkinBlueSportswear, receivedSkin)
}
if receivedAccount != "破浪小王子" {
t.Fatalf("发货账号应为 破浪小王子,实际为 %s", receivedAccount)
}
if submitRes.ProviderOrderNo != "888999" {
t.Fatalf("期望官方 fulfillId 888999,实际为 %s", submitRes.ProviderOrderNo)
}
// 检查数据库记录
var dbToken model.RockToken
db.First(&dbToken, tokenBlue.ID)
if dbToken.Status != model.RockStatusRedeemed {
t.Fatalf("发货后 Token 状态应为 redeemed,当前为 %s", dbToken.Status)
}
if dbToken.UpstreamFulfillID != 888999 {
t.Fatalf("fulfillID 记录错误: %d", dbToken.UpstreamFulfillID)
}
}
func TestRockService_StatsAndPurchase(t *testing.T) {
db := newRockTestDB(t)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.URL.Path == "/api/me" {
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"user": map[string]interface{}{
"id": 10028,
"username": "王大锤",
"role": "merchant",
"points": "8271.2", // 真实上游 0666.pro 返回字符串格式
"status": "active",
},
})
return
}
if r.URL.Path == "/api/user/tokens/claim" {
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"tokens": []map[string]interface{}{
{
"id": 20001,
"token": "token_mock_1",
"color": "black",
"region": "Q/V可用",
"status": "available",
"redeem_code": "code_mock_1",
"claimed_at": "2026-10-05 14:00:00",
},
{
"id": 20002,
"token": "token_mock_2",
"color": "black",
"region": "Q/V可用",
"status": "available",
"redeem_code": "code_mock_2",
"claimed_at": "2026-10-05 14:00:00",
},
},
})
return
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
client := NewRockClient(server.URL)
rockSvc := NewRockService(db, client)
// 1. 测试 Token 连通性
me, err := rockSvc.TestConnection(context.Background(), "mock_bearer_token")
if err != nil {
t.Fatalf("test connection: %v", err)
}
if me.User.Username != "王大锤" || me.User.Points != 8271.2 {
t.Fatalf("用户信息解析不符: %+v", me.User)
}
// 2. 更新系统配置设置 BearerToken
tokenStr := "mock_bearer_token"
_, err = rockSvc.UpdateConfig(context.Background(), UpdateRockConfigReq{
BearerToken: &tokenStr,
})
if err != nil {
t.Fatalf("update config: %v", err)
}
// 3. 采购 2 个黑色通用 Token
tokens, err := rockSvc.PurchaseTokens(context.Background(), 2, false)
if err != nil {
t.Fatalf("purchase tokens: %v", err)
}
if len(tokens) != 2 {
t.Fatalf("期望采购 2 个,实际返回 %d", len(tokens))
}
// 4. 检查库存统计
stats, err := rockSvc.GetStats(context.Background())
if err != nil {
t.Fatalf("get stats: %v", err)
}
if stats.AvailableStock != 2 {
t.Fatalf("期望可用库存为 2,实际为 %d", stats.AvailableStock)
}
if stats.Username != "王大锤" {
t.Fatalf("期望采购商户名为王大锤,实际为 %s", stats.Username)
}
// 5. 分页查询列表
list, total, err := rockSvc.ListTokens(context.Background(), 1, 10, "", "")
if err != nil {
t.Fatalf("list tokens: %v", err)
}
if total != 2 || len(list) != 2 {
t.Fatalf("期望查询到 2 条,实际为 total=%d len=%d", total, len(list))
}
}