1071 lines
36 KiB
Go
1071 lines
36 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"strings"
|
||
"sync"
|
||
"testing"
|
||
"time"
|
||
|
||
"affiliate_dash/internal/model"
|
||
"affiliate_dash/internal/pkg/timeutil"
|
||
"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_PrepareDelivery_ZeroStockWithAssignedToken(t *testing.T) {
|
||
db := newRockTestDB(t)
|
||
client := NewRockClient("https://example.com")
|
||
rockSvc := NewRockService(db, client)
|
||
provider := NewRockProvider(rockSvc)
|
||
|
||
// 开启总开关
|
||
tr := true
|
||
_, _ = rockSvc.UpdateConfig(context.Background(), UpdateRockConfigReq{
|
||
AutoDeliveryEnabled: &tr,
|
||
})
|
||
|
||
// 当前数据库没有可用 Token (available = 0)
|
||
// 场景 1:未分配 Token 的新订单
|
||
newOrder := &model.FulfillmentOrder{
|
||
OrderNo: "ROCK-ORDER-NEW",
|
||
ProductSKU: model.RockSKUBlackTshirt,
|
||
}
|
||
stateNew, err := provider.PrepareDelivery(context.Background(), newOrder, nil)
|
||
if err != nil {
|
||
t.Fatalf("prepare new order: %v", err)
|
||
}
|
||
if stateNew.CanShip {
|
||
t.Fatalf("库存为 0 且未分配 Token 时新订单应该不可发货")
|
||
}
|
||
if !strings.Contains(stateNew.CannotShipReason, "库存补充中") {
|
||
t.Fatalf("期望库存补充中提示,实际为: %s", stateNew.CannotShipReason)
|
||
}
|
||
|
||
// 场景 2:已分配并锁定 Token 的订单 (using 或 bound 状态)
|
||
assignedOrder := &model.FulfillmentOrder{
|
||
OrderNo: "ROCK-ORDER-ASSIGNED",
|
||
ProductSKU: model.RockSKUBlackTshirt,
|
||
}
|
||
now := time.Now()
|
||
token := model.RockToken{
|
||
UpstreamTokenID: "999888",
|
||
Token: "token_assigned",
|
||
RedeemCode: "code_assigned",
|
||
Color: model.RockColorBlack,
|
||
Status: model.RockStatusBound,
|
||
AssignedOrderNo: "ROCK-ORDER-ASSIGNED",
|
||
ExpectedUID: "123456",
|
||
ClaimedAt: &now,
|
||
}
|
||
db.Create(&token)
|
||
|
||
stateAssigned, err := provider.PrepareDelivery(context.Background(), assignedOrder, nil)
|
||
if err != nil {
|
||
t.Fatalf("prepare assigned order: %v", err)
|
||
}
|
||
if !stateAssigned.CanShip {
|
||
t.Fatalf("已分配并锁定 Token 的订单即使池子空闲库存为 0 也必须允许继续提货发货: %s", stateAssigned.CannotShipReason)
|
||
}
|
||
}
|
||
|
||
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.Token.Status = "bound"
|
||
resp.Token.BoundAccount = currentBoundAccount
|
||
resp.Token.BoundAccountID = currentBoundRoleID
|
||
resp.Token.Region = "安卓-微信"
|
||
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)
|
||
}
|
||
|
||
// 3b. 绑定失败后,即使池里还有库存、买家改了 UID,也不能再领新号。
|
||
spare := model.RockToken{
|
||
UpstreamTokenID: "151154",
|
||
Token: "spare_jwt_token",
|
||
RedeemCode: "spare_redeem_code",
|
||
Color: model.RockColorBlack,
|
||
Status: model.RockStatusAvailable,
|
||
ClaimedAt: &now,
|
||
}
|
||
db.Create(&spare)
|
||
_, err = provider.Bind(context.Background(), order, "1111111111")
|
||
if err == nil || !strings.Contains(err.Error(), "联系客服") {
|
||
t.Fatalf("mismatch 后必须拒绝更换提货号,实际: %v", err)
|
||
}
|
||
var spareAfter model.RockToken
|
||
db.First(&spareAfter, spare.ID)
|
||
if spareAfter.Status != model.RockStatusAvailable || spareAfter.AssignedOrderNo != "" {
|
||
t.Fatalf("拒绝换号后备用库存应保持可用,实际 status=%s order=%s", spareAfter.Status, spareAfter.AssignedOrderNo)
|
||
}
|
||
var assignedCount int64
|
||
db.Model(&model.RockToken{}).Where("assigned_order_no = ?", order.OrderNo).Count(&assignedCount)
|
||
if assignedCount != 1 {
|
||
t.Fatalf("一个订单只能占用一个提货号,实际 %d", assignedCount)
|
||
}
|
||
// 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)
|
||
}
|
||
|
||
// 5. 重新绑定正确账号“感性的洁少”(3688825401)
|
||
currentBoundAccount = "感性的洁少"
|
||
currentBoundRoleID = "3688825401"
|
||
currentBindChangedAt = "2026-10-05 14:05:00"
|
||
|
||
bindRes2, err := provider.Bind(context.Background(), order, expectedUID)
|
||
if err != nil {
|
||
t.Fatalf("rebind error: %v", err)
|
||
}
|
||
check3, err := provider.CheckBind(context.Background(), order, bindRes2.BindUUID)
|
||
if err != nil {
|
||
t.Fatalf("check bind match: %v", err)
|
||
}
|
||
if !check3.Bound {
|
||
t.Fatalf("正确账号扫码后应判定为 bound = true")
|
||
}
|
||
if check3.RoleName != "感性的洁少" {
|
||
t.Fatalf("正确账号扫码后应正确读取角色名 感性的洁少,实际为: %s", check3.RoleName)
|
||
}
|
||
}
|
||
|
||
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.HasSuffix(r.URL.Path, "/refresh") {
|
||
var resp RockRefreshResponse
|
||
resp.DeliveryList = []RockDeliveryItem{
|
||
{FulfillID: 888999},
|
||
}
|
||
_ = json.NewEncoder(w).Encode(resp)
|
||
return
|
||
}
|
||
if strings.HasPrefix(r.URL.Path, "/api/redeem/") {
|
||
if !strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
|
||
w.WriteHeader(http.StatusUnauthorized)
|
||
_ = json.NewEncoder(w).Encode(map[string]string{"error": "请先登录"})
|
||
return
|
||
}
|
||
var req map[string]string
|
||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||
receivedSkin = req["skin"]
|
||
receivedCurrency = req["currency"]
|
||
receivedAccount = req["account"]
|
||
|
||
resp := RockRedeemResponse{
|
||
Status: "success",
|
||
Message: "领取成功",
|
||
}
|
||
resp.Token.Status = "redeemed"
|
||
resp.Token.RedeemSkin = receivedSkin
|
||
resp.Token.RedeemCurrency = receivedCurrency
|
||
_ = json.NewEncoder(w).Encode(resp)
|
||
return
|
||
}
|
||
w.WriteHeader(http.StatusOK)
|
||
}))
|
||
defer server.Close()
|
||
|
||
client := NewRockClient(server.URL)
|
||
rockSvc := NewRockService(db, client)
|
||
provider := NewRockProvider(rockSvc)
|
||
db.Model(&model.RockConfig{}).Where("id = 1").Update("bearer_token", "mock_bearer_token")
|
||
|
||
// 场景 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/user/dashboard" {
|
||
writeRockDashboardJSON(w, 999, "20.0")
|
||
return
|
||
}
|
||
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))
|
||
}
|
||
|
||
// 6. 测试关联履约订单与创建订单商户单号反查
|
||
orderNo := "FO_TEST_ROCK_999"
|
||
clientOrderNo := "SHOP_ORDER_PDD_888"
|
||
_ = db.Create(&model.FulfillmentOrder{
|
||
OrderNo: orderNo,
|
||
ClientOrderNo: clientOrderNo,
|
||
ProductSKU: "rock_black_tshirt",
|
||
ProductName: "黑色ROCK长袖T恤",
|
||
}).Error
|
||
_ = db.Model(&model.RockToken{}).Where("id = ?", list[0].ID).Update("assigned_order_no", orderNo).Error
|
||
|
||
listWithClient, _, err := rockSvc.ListTokens(context.Background(), 1, 10, "", "")
|
||
if err != nil {
|
||
t.Fatalf("list tokens with client: %v", err)
|
||
}
|
||
found := false
|
||
for _, tk := range listWithClient {
|
||
if tk.ID == list[0].ID {
|
||
found = true
|
||
if tk.ClientOrderNo != clientOrderNo {
|
||
t.Fatalf("期望商户单号为 %s,实际为 %s", clientOrderNo, tk.ClientOrderNo)
|
||
}
|
||
}
|
||
}
|
||
if !found {
|
||
t.Fatalf("未找到对应的 Token 记录")
|
||
}
|
||
|
||
// 7. 测试按商户单号关键词搜索
|
||
searchList, searchTotal, err := rockSvc.ListTokens(context.Background(), 1, 10, "", "PDD_888")
|
||
if err != nil {
|
||
t.Fatalf("search tokens by client_order_no: %v", err)
|
||
}
|
||
if searchTotal != 1 || len(searchList) != 1 {
|
||
t.Fatalf("期望按商户单号搜索出 1 条,实际为 total=%d len=%d", searchTotal, len(searchList))
|
||
}
|
||
}
|
||
|
||
func TestReleaseUnboundUsingToken(t *testing.T) {
|
||
db := newRockTestDB(t)
|
||
|
||
var boundRoleID string
|
||
var bindChangedAt string
|
||
var deliveryTime string
|
||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
if strings.HasSuffix(r.URL.Path, "/refresh") {
|
||
var resp RockRefreshResponse
|
||
if boundRoleID != "" {
|
||
resp.Token.Status = "bound"
|
||
resp.Token.BoundAccount = "已扫码角色"
|
||
resp.Token.BoundAccountID = boundRoleID
|
||
resp.Token.BindChangedAt = bindChangedAt
|
||
}
|
||
if deliveryTime != "" {
|
||
resp.DeliveryList = []RockDeliveryItem{{
|
||
Time: deliveryTime,
|
||
Role: "iOS-微信-已扫码角色",
|
||
Message: "成功发放",
|
||
}}
|
||
}
|
||
_ = json.NewEncoder(w).Encode(resp)
|
||
return
|
||
}
|
||
w.WriteHeader(http.StatusNotFound)
|
||
}))
|
||
defer server.Close()
|
||
|
||
client := NewRockClient(server.URL)
|
||
rockSvc := NewRockService(db, client)
|
||
provider := NewRockProvider(rockSvc)
|
||
bearer := "mock_bearer_token"
|
||
if _, err := rockSvc.UpdateConfig(context.Background(), UpdateRockConfigReq{BearerToken: &bearer}); err != nil {
|
||
t.Fatalf("update config: %v", err)
|
||
}
|
||
|
||
snap := BindingSnapshot{
|
||
Bound: true,
|
||
RoleName: "历史残留",
|
||
RoleID: "111",
|
||
BindChangedAt: "2026-10-01 10:00:00",
|
||
}
|
||
snapBytes, _ := json.Marshal(snap)
|
||
token := model.RockToken{
|
||
UpstreamTokenID: "88001",
|
||
Token: "jwt",
|
||
RedeemCode: "redeem-unused",
|
||
Color: model.RockColorBlack,
|
||
Status: model.RockStatusUsing,
|
||
AssignedOrderNo: "FO-RELEASE-1",
|
||
ExpectedUID: "3688825401",
|
||
InitialBoundSnapshot: string(snapBytes),
|
||
}
|
||
if err := db.Create(&token).Error; err != nil {
|
||
t.Fatalf("create token: %v", err)
|
||
}
|
||
if err := db.Create(&model.FulfillmentOrder{
|
||
OrderNo: "FO-RELEASE-1",
|
||
ProductSKU: model.RockSKUBlackTshirt,
|
||
ProductName: "黑色ROCK长袖T恤",
|
||
ResultData: fmt.Sprintf(`{"provider":"rock","rock_token_id":%d,"rock_redeem_code":"redeem-unused","expected_uid":"3688825401"}`, token.ID),
|
||
}).Error; err != nil {
|
||
t.Fatalf("create order: %v", err)
|
||
}
|
||
|
||
// 历史残留与快照一致,视为买家还没扫,允许释放。
|
||
boundRoleID = "111"
|
||
bindChangedAt = "2026-10-01 10:00:00"
|
||
if err := rockSvc.ReleaseUnboundUsingToken(context.Background(), token.ID); err != nil {
|
||
t.Fatalf("release ghost snapshot: %v", err)
|
||
}
|
||
var released model.RockToken
|
||
if err := db.First(&released, token.ID).Error; err != nil {
|
||
t.Fatalf("reload token: %v", err)
|
||
}
|
||
if released.Status != model.RockStatusAvailable || released.AssignedOrderNo != "" || released.ExpectedUID != "" {
|
||
t.Fatalf("释放后应回到可用库存,实际 status=%s order=%s uid=%s", released.Status, released.AssignedOrderNo, released.ExpectedUID)
|
||
}
|
||
var order model.FulfillmentOrder
|
||
if err := db.Where("order_no = ?", "FO-RELEASE-1").First(&order).Error; err != nil {
|
||
t.Fatalf("reload order: %v", err)
|
||
}
|
||
if resultDataNumber(order.ResultData, "rock_token_id") != 0 || !strings.Contains(order.ResultData, "rock_token_released") {
|
||
t.Fatalf("原订单仍指向已释放链接: %s", order.ResultData)
|
||
}
|
||
|
||
// 旧发货页继续轮询时,不能把已释放的链接再改成绑定。
|
||
boundRoleID = "3688825401"
|
||
bindChangedAt = "2026-10-05 21:00:00"
|
||
check, err := provider.CheckBind(context.Background(), &order, fmt.Sprintf("%d", token.ID))
|
||
if err != nil {
|
||
t.Fatalf("check bind after release: %v", err)
|
||
}
|
||
if check.Bound {
|
||
t.Fatalf("已释放链接不应再判定为绑定成功")
|
||
}
|
||
if err := db.First(&released, token.ID).Error; err != nil {
|
||
t.Fatalf("reload after check: %v", err)
|
||
}
|
||
if released.Status != model.RockStatusAvailable {
|
||
t.Fatalf("旧页面轮询后状态被改写为 %s", released.Status)
|
||
}
|
||
|
||
// 上游已经出现新扫码时拒绝释放。
|
||
using := model.RockToken{
|
||
UpstreamTokenID: "88002",
|
||
Token: "jwt2",
|
||
RedeemCode: "redeem-bound",
|
||
Color: model.RockColorBlack,
|
||
Status: model.RockStatusUsing,
|
||
AssignedOrderNo: "FO-RELEASE-2",
|
||
ExpectedUID: "3688825401",
|
||
}
|
||
if err := db.Create(&using).Error; err != nil {
|
||
t.Fatalf("create using token: %v", err)
|
||
}
|
||
boundRoleID = "3688825401"
|
||
bindChangedAt = "2026-10-05 21:10:00"
|
||
if err := rockSvc.ReleaseUnboundUsingToken(context.Background(), using.ID); err == nil || !strings.Contains(err.Error(), "已扫码绑定") {
|
||
t.Fatalf("已绑定链接必须拒绝释放,实际: %v", err)
|
||
}
|
||
var stillUsing model.RockToken
|
||
if err := db.First(&stillUsing, using.ID).Error; err != nil {
|
||
t.Fatalf("reload using: %v", err)
|
||
}
|
||
if stillUsing.Status != model.RockStatusUsing || stillUsing.AssignedOrderNo != "FO-RELEASE-2" {
|
||
t.Fatalf("拒绝释放后应保持使用中,实际 status=%s order=%s", stillUsing.Status, stillUsing.AssignedOrderNo)
|
||
}
|
||
|
||
// 上游 status=bound,但发货记录早于本次领取、绑定时间仍为空:历史残留,可以再次使用。
|
||
// 上游时间串不带偏移,统一按业务时区解析;这里的时间基线也必须用同一时区,
|
||
// 否则测试结果会随宿主机 TZ 漂移。
|
||
claimedAt := time.Date(2026, 10, 5, 20, 32, 17, 0, timeutil.Location())
|
||
stale := model.RockToken{
|
||
UpstreamTokenID: "152415",
|
||
Token: "jwt3",
|
||
RedeemCode: "dbece05764c701a3be3bb5c689926d07",
|
||
Color: model.RockColorBlack,
|
||
Status: model.RockStatusUsing,
|
||
AssignedOrderNo: "FO-RELEASE-3",
|
||
ExpectedUID: "3364503067",
|
||
ClaimedAt: &claimedAt,
|
||
InitialBoundSnapshot: `{"bound":true,"role_name":"予心纯白","role_id":"1900505843","channel":"iOS-微信","bind_changed_at":""}`,
|
||
}
|
||
if err := db.Create(&stale).Error; err != nil {
|
||
t.Fatalf("create stale token: %v", err)
|
||
}
|
||
boundRoleID = "1900505843"
|
||
bindChangedAt = ""
|
||
deliveryTime = "2026-09-27T14:38:51.23"
|
||
if err := rockSvc.ReleaseUnboundUsingToken(context.Background(), stale.ID); err != nil {
|
||
t.Fatalf("历史残留应允许释放: %v", err)
|
||
}
|
||
|
||
// 领取之后出现新的发货记录,不能释放。
|
||
fresh := model.RockToken{
|
||
UpstreamTokenID: "152420",
|
||
Token: "jwt4",
|
||
RedeemCode: "redeem-fresh-delivery",
|
||
Color: model.RockColorBlack,
|
||
Status: model.RockStatusUsing,
|
||
AssignedOrderNo: "FO-RELEASE-4",
|
||
ExpectedUID: "4744920014",
|
||
ClaimedAt: &claimedAt,
|
||
}
|
||
if err := db.Create(&fresh).Error; err != nil {
|
||
t.Fatalf("create fresh token: %v", err)
|
||
}
|
||
boundRoleID = "4744920014"
|
||
bindChangedAt = "2026-10-05 21:52:30"
|
||
deliveryTime = "2026-10-05T21:52:40.61"
|
||
if err := rockSvc.ReleaseUnboundUsingToken(context.Background(), fresh.ID); err == nil || !strings.Contains(err.Error(), "已扫码绑定") {
|
||
t.Fatalf("领取后的新发货必须拒绝释放,实际: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestRockProvider_SchemeB_UIDSelfHealing(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 strings.Contains(r.URL.Path, "/api/redeem/") {
|
||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||
"id": 1001,
|
||
"bind_url": "https://igame.qq.com/activity/test?code=redeem_scheme_b",
|
||
"redeem_code": "redeem_scheme_b",
|
||
})
|
||
return
|
||
}
|
||
if strings.Contains(r.URL.Path, "refresh") {
|
||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||
"token": map[string]interface{}{
|
||
"id": 152001,
|
||
"status": "bound",
|
||
"bind_changed_at": "2026-10-06 10:00:00",
|
||
"bound_account": "少年有点忧郁",
|
||
"bound_account_id": "3454648872",
|
||
"region": "安卓-微信",
|
||
},
|
||
"deliveryList": []interface{}{},
|
||
})
|
||
return
|
||
}
|
||
w.WriteHeader(http.StatusNotFound)
|
||
}))
|
||
defer server.Close()
|
||
|
||
client := NewRockClient(server.URL)
|
||
rockSvc := NewRockService(db, client)
|
||
provider := NewRockProvider(rockSvc)
|
||
ctx := context.Background()
|
||
|
||
now := time.Now()
|
||
token := model.RockToken{
|
||
UpstreamTokenID: "152001",
|
||
Token: "test_jwt_scheme_b",
|
||
RedeemCode: "redeem_scheme_b",
|
||
Color: model.RockColorBlack,
|
||
Status: model.RockStatusAvailable,
|
||
ClaimedAt: &now,
|
||
}
|
||
db.Create(&token)
|
||
|
||
order := &model.FulfillmentOrder{
|
||
OrderNo: "ROCK-ORDER-HEAL",
|
||
ProductSKU: model.RockSKUBlackTshirt,
|
||
}
|
||
db.Create(order)
|
||
|
||
// 1. 买家一开始输错 UID 为 3658745636,触发分配
|
||
bindRes1, err := provider.Bind(ctx, order, "3658745636")
|
||
if err != nil {
|
||
t.Fatalf("首次分配失败: %v", err)
|
||
}
|
||
if bindRes1.Bound {
|
||
t.Fatalf("尚未扫码,bound 应为 false")
|
||
}
|
||
|
||
// 2. 扫码反查,真实角色是 3454648872,发生 mismatch
|
||
check1, err := provider.CheckBind(ctx, order, bindRes1.BindUUID)
|
||
if err != nil {
|
||
t.Fatalf("check bind failed: %v", err)
|
||
}
|
||
if !check1.Mismatch {
|
||
t.Fatalf("UID 不一致必须标记为 mismatch")
|
||
}
|
||
if !strings.Contains(check1.Message, "3454648872") {
|
||
t.Fatalf("提示信息应包含引导买家确认修改为 3454648872,实际: %s", check1.Message)
|
||
}
|
||
|
||
// 3. 买家尝试换成第三个账号 9999999999,应坚决被拦截
|
||
_, err = provider.Bind(ctx, order, "9999999999")
|
||
if err == nil || !strings.Contains(err.Error(), "3454648872") {
|
||
t.Fatalf("更换为非扫码角色应被拦截,实际 err: %v", err)
|
||
}
|
||
|
||
// 4. 方案 B:买家确认更换为扫码角色 UID 3454648872
|
||
bindRes2, err := provider.Bind(ctx, order, "3454648872")
|
||
if err != nil {
|
||
t.Fatalf("确认更换为扫码角色 UID 应自愈成功,实际失败: %v", err)
|
||
}
|
||
if !bindRes2.Bound {
|
||
t.Fatalf("自愈后 bound 应为 true")
|
||
}
|
||
if bindRes2.RoleName != "少年有点忧郁" || bindRes2.GameAccount != "3454648872" {
|
||
t.Fatalf("自愈后返回的角色信息不符: %v", bindRes2)
|
||
}
|
||
|
||
// 5. 校验数据库状态与订单占号唯一性
|
||
var dbToken model.RockToken
|
||
db.First(&dbToken, token.ID)
|
||
if dbToken.Status != model.RockStatusBound {
|
||
t.Fatalf("自愈后 token 状态应为 bound,当前: %s", dbToken.Status)
|
||
}
|
||
if dbToken.ExpectedUID != "3454648872" {
|
||
t.Fatalf("自愈后 expectedUID 应为 3454648872,当前: %s", dbToken.ExpectedUID)
|
||
}
|
||
var count int64
|
||
db.Model(&model.RockToken{}).Where("assigned_order_no = ?", order.OrderNo).Count(&count)
|
||
if count != 1 {
|
||
t.Fatalf("整个流程该订单必须严格只占用 1 个 Token,实际占用: %d", count)
|
||
}
|
||
}
|
||
|
||
func TestRockService_CorrectMismatchUIDKeepsAssignment(t *testing.T) {
|
||
db := newRockTestDB(t)
|
||
now := time.Now()
|
||
token := model.RockToken{
|
||
UpstreamTokenID: "179",
|
||
Token: "token-179",
|
||
RedeemCode: "redeem-179",
|
||
Color: model.RockColorBlack,
|
||
Status: model.RockStatusMismatch,
|
||
AssignedOrderNo: "FO-KEEP-179",
|
||
ExpectedUID: "3780457044",
|
||
ActualRoleName: "德州火箭头",
|
||
ActualRoleID: "3780451044",
|
||
ActualChannel: "安卓-微信",
|
||
MismatchAt: &now,
|
||
MismatchReason: "预期UID(3780457044) 与 扫码UID(德州火箭头/3780451044) 不一致",
|
||
}
|
||
if err := db.Create(&token).Error; err != nil {
|
||
t.Fatalf("create token: %v", err)
|
||
}
|
||
order := model.FulfillmentOrder{
|
||
OrderNo: "FO-KEEP-179",
|
||
MerchantID: 2,
|
||
ProductSKU: model.RockSKUBlackTshirt,
|
||
OrderStatus: "paid",
|
||
ResultData: `{"expected_uid":"3780457044","provider":"rock"}`,
|
||
}
|
||
if err := db.Create(&order).Error; err != nil {
|
||
t.Fatalf("create order: %v", err)
|
||
}
|
||
|
||
rockSvc := NewRockService(db, NewRockClient("https://example.com"))
|
||
kept, err := rockSvc.CorrectMismatchExpectedUID(context.Background(), token.ID, "1111111111")
|
||
if err != nil {
|
||
t.Fatalf("update unmatched uid: %v", err)
|
||
}
|
||
if kept.Status != model.RockStatusMismatch || kept.AssignedOrderNo != "FO-KEEP-179" || kept.ExpectedUID != "1111111111" {
|
||
t.Fatalf("未对上扫码 UID 时应保持占用和阻断,实际 status=%s order=%s uid=%s", kept.Status, kept.AssignedOrderNo, kept.ExpectedUID)
|
||
}
|
||
|
||
bound, err := rockSvc.CorrectMismatchExpectedUID(context.Background(), token.ID, "3780451044")
|
||
if err != nil {
|
||
t.Fatalf("correct to scanned uid: %v", err)
|
||
}
|
||
if bound.Status != model.RockStatusBound || bound.AssignedOrderNo != "FO-KEEP-179" || bound.ExpectedUID != "3780451044" {
|
||
t.Fatalf("改成扫码 UID 后应解除阻断并留在原订单,实际 status=%s order=%s uid=%s", bound.Status, bound.AssignedOrderNo, bound.ExpectedUID)
|
||
}
|
||
|
||
var savedOrder model.FulfillmentOrder
|
||
if err := db.Where("order_no = ?", "FO-KEEP-179").First(&savedOrder).Error; err != nil {
|
||
t.Fatalf("reload order: %v", err)
|
||
}
|
||
if !strings.Contains(savedOrder.ResultData, "3780451044") {
|
||
t.Fatalf("订单期望 UID 应同步为扫码 UID,实际: %s", savedOrder.ResultData)
|
||
}
|
||
}
|
||
|
||
func TestRockBindLinkStillFreshUsesURLTimestamp(t *testing.T) {
|
||
now := time.Now()
|
||
stale := fmt.Sprintf("https://gp.qq.com/cp/a20241128mobile/index.html?timestamp=%d", now.Add(-rockBindLinkMaxAge-time.Second).Unix())
|
||
fresh := fmt.Sprintf("https://gp.qq.com/cp/a20241128mobile/index.html?timestamp=%d", now.Add(-time.Minute).Unix())
|
||
if rockBindLinkStillFresh(rockBindLinkCacheItem{bindURL: stale, cachedAt: now}, now) {
|
||
t.Fatal("签名已超过刷新点时,不能因为刚写入缓存就继续展示")
|
||
}
|
||
if !rockBindLinkStillFresh(rockBindLinkCacheItem{bindURL: fresh, cachedAt: now.Add(-time.Hour)}, now) {
|
||
t.Fatal("签名仍在刷新点内就应该继续展示")
|
||
}
|
||
}
|
||
|
||
func TestRockProvider_CheckBindRefreshesSameTokenLink(t *testing.T) {
|
||
db := newRockTestDB(t)
|
||
|
||
var mu sync.Mutex
|
||
redeemHits := 0
|
||
staleTS := time.Now().Add(-rockBindLinkMaxAge - time.Second).Unix()
|
||
freshTS := time.Now().Unix()
|
||
oldURL := fmt.Sprintf("https://gp.qq.com/cp/a20241128mobile/index.html?timestamp=%d&nonce=old&sign=old", staleTS)
|
||
newURL := fmt.Sprintf("https://gp.qq.com/cp/a20241128mobile/index.html?timestamp=%d&nonce=new&sign=new", freshTS)
|
||
currentURL := oldURL
|
||
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/") {
|
||
mu.Lock()
|
||
redeemHits++
|
||
bindURL := currentURL
|
||
mu.Unlock()
|
||
_ = json.NewEncoder(w).Encode(RockRedeemDetailResponse{BindURL: bindURL})
|
||
return
|
||
}
|
||
if strings.HasSuffix(r.URL.Path, "/refresh") {
|
||
_ = json.NewEncoder(w).Encode(RockRefreshResponse{})
|
||
return
|
||
}
|
||
w.WriteHeader(http.StatusNotFound)
|
||
}))
|
||
defer server.Close()
|
||
|
||
client := NewRockClient(server.URL)
|
||
rockSvc := NewRockService(db, client)
|
||
provider := NewRockProvider(rockSvc)
|
||
|
||
now := time.Now()
|
||
token := model.RockToken{
|
||
UpstreamTokenID: "177",
|
||
Token: "token-177",
|
||
RedeemCode: "7b7445735ab7432fdc9db997c7637913",
|
||
Color: model.RockColorBlack,
|
||
Status: model.RockStatusUsing,
|
||
AssignedOrderNo: "FO20261006170653afd37ce7e19",
|
||
ExpectedUID: "3763064651",
|
||
ClaimedAt: &now,
|
||
}
|
||
if err := db.Create(&token).Error; err != nil {
|
||
t.Fatalf("create token: %v", err)
|
||
}
|
||
order := &model.FulfillmentOrder{
|
||
OrderNo: token.AssignedOrderNo,
|
||
ProductSKU: model.RockSKUBlackTshirt,
|
||
ResultData: `{"expected_uid":"3763064651","provider":"rock"}`,
|
||
}
|
||
|
||
// 缓存刚写入,但链接上的签名时间已经超过刷新点,必须换新码。
|
||
rockSvc.bindLinkCache.Store(token.ID, rockBindLinkCacheItem{
|
||
bindURL: oldURL,
|
||
qrURL: "https://example.test/old.png",
|
||
cachedAt: time.Now(),
|
||
fetchedAt: time.Now().Add(-rockBindLinkRetryGap - time.Second),
|
||
})
|
||
mu.Lock()
|
||
currentURL = newURL
|
||
mu.Unlock()
|
||
|
||
check, err := provider.CheckBind(context.Background(), order, fmt.Sprintf("%d", token.ID))
|
||
if err != nil {
|
||
t.Fatalf("check bind: %v", err)
|
||
}
|
||
if check.BindURL != currentURL {
|
||
t.Fatalf("过期后应返回同一提取码的新链接,实际: %s", check.BindURL)
|
||
}
|
||
if check.BindUUID != fmt.Sprintf("%d", token.ID) || check.Bound || check.Mismatch {
|
||
t.Fatalf("刷新二维码不能换号或改绑定状态: %+v", check)
|
||
}
|
||
|
||
var saved model.RockToken
|
||
if err := db.First(&saved, token.ID).Error; err != nil {
|
||
t.Fatalf("reload token: %v", err)
|
||
}
|
||
if saved.Status != model.RockStatusUsing || saved.AssignedOrderNo != token.AssignedOrderNo {
|
||
t.Fatalf("必须留在原提货号,实际 status=%s order=%s", saved.Status, saved.AssignedOrderNo)
|
||
}
|
||
var tokenCount int64
|
||
db.Model(&model.RockToken{}).Count(&tokenCount)
|
||
if tokenCount != 1 {
|
||
t.Fatalf("不能另发提货号,实际 %d 条", tokenCount)
|
||
}
|
||
|
||
mu.Lock()
|
||
hitsAfterRefresh := redeemHits
|
||
currentURL = "https://gp.qq.com/cp/should-not-fetch"
|
||
mu.Unlock()
|
||
checkCached, err := provider.CheckBind(context.Background(), order, fmt.Sprintf("%d", token.ID))
|
||
if err != nil {
|
||
t.Fatalf("cached check: %v", err)
|
||
}
|
||
if checkCached.BindURL != newURL {
|
||
t.Fatalf("有效期内应复用刚取到的链接,实际: %s", checkCached.BindURL)
|
||
}
|
||
mu.Lock()
|
||
if redeemHits != hitsAfterRefresh {
|
||
t.Fatalf("有效期内不应再次请求 0666,实际多了 %d 次", redeemHits-hitsAfterRefresh)
|
||
}
|
||
mu.Unlock()
|
||
|
||
other := &model.FulfillmentOrder{
|
||
OrderNo: "FO-OTHER",
|
||
ProductSKU: model.RockSKUBlackTshirt,
|
||
ResultData: `{"expected_uid":"3763064651"}`,
|
||
}
|
||
checkOther, err := provider.CheckBind(context.Background(), other, fmt.Sprintf("%d", token.ID))
|
||
if err != nil {
|
||
t.Fatalf("other order check: %v", err)
|
||
}
|
||
if checkOther.BindURL != "" {
|
||
t.Fatalf("其他订单不能拿到这个提货号的二维码: %s", checkOther.BindURL)
|
||
}
|
||
}
|
||
|
||
func TestRockService_TokenRemarkAndAbnormal(t *testing.T) {
|
||
db := testdb.New(t)
|
||
client := NewRockClient("https://0666.pro")
|
||
rockSvc := NewRockService(db, client)
|
||
|
||
now := time.Now()
|
||
token := model.RockToken{
|
||
UpstreamTokenID: "TEST_REMARK_1",
|
||
Token: "jwt-remark-1",
|
||
RedeemCode: "CODE_REMARK_1",
|
||
Status: model.RockStatusMismatch,
|
||
AssignedOrderNo: "ORDER_REMARK_1",
|
||
ExpectedUID: "123456",
|
||
ActualRoleID: "654321",
|
||
ActualRoleName: "路人甲",
|
||
MismatchAt: &now,
|
||
MismatchReason: "预期UID不符",
|
||
}
|
||
if err := db.Create(&token).Error; err != nil {
|
||
t.Fatalf("create test token failed: %v", err)
|
||
}
|
||
|
||
// 1. 测试单纯更新备注
|
||
updated, err := rockSvc.UpdateTokenRemark(context.Background(), token.ID, "买家联系客服核对中", "")
|
||
if err != nil {
|
||
t.Fatalf("update remark failed: %v", err)
|
||
}
|
||
if updated.Remark != "买家联系客服核对中" || updated.Status != model.RockStatusMismatch {
|
||
t.Fatalf("更新备注不符: remark=%s status=%s", updated.Remark, updated.Status)
|
||
}
|
||
|
||
// 2. 测试快捷标记为已上报失效 (MarkTokenAbnormal)
|
||
abnormalToken, err := rockSvc.MarkTokenAbnormal(context.Background(), token.ID, "已上报0868客服 (卡密绑定错误废弃)")
|
||
if err != nil {
|
||
t.Fatalf("mark abnormal failed: %v", err)
|
||
}
|
||
if abnormalToken.Status != model.RockStatusAbnormal {
|
||
t.Fatalf("标记异常后状态应为 abnormal,实际: %s", abnormalToken.Status)
|
||
}
|
||
if abnormalToken.Remark != "已上报0868客服 (卡密绑定错误废弃)" {
|
||
t.Fatalf("标记异常后备注不符: %s", abnormalToken.Remark)
|
||
}
|
||
|
||
// 3. 搜索备注测试
|
||
list, total, err := rockSvc.ListTokens(context.Background(), 1, 10, "", "卡密绑定错误废弃")
|
||
if err != nil {
|
||
t.Fatalf("list by remark search error: %v", err)
|
||
}
|
||
if total != 1 || len(list) != 1 || list[0].ID != token.ID {
|
||
t.Fatalf("通过备注搜索未检索到目标 token, total=%d", total)
|
||
}
|
||
|
||
// 4. 从 abnormal 状态解冻重置回 available
|
||
if err := rockSvc.ResetMismatchToken(context.Background(), token.ID); err != nil {
|
||
t.Fatalf("reset abnormal token error: %v", err)
|
||
}
|
||
var resetToken model.RockToken
|
||
if err := db.First(&resetToken, token.ID).Error; err != nil {
|
||
t.Fatalf("find reset token error: %v", err)
|
||
}
|
||
if resetToken.Status != model.RockStatusAvailable {
|
||
t.Fatalf("重置后应为 available,实际: %s", resetToken.Status)
|
||
}
|
||
if resetToken.AssignedOrderNo != "" || resetToken.ExpectedUID != "" {
|
||
t.Fatalf("重置后应清空订单关联, assigned_order_no=%s", resetToken.AssignedOrderNo)
|
||
}
|
||
}
|