Files
huya-go/internal/huya/category_test.go
T
yml2213 d21b06a311 新增商品类目(限量返场/精英专享)支持与版本号 0.2.1
10.15 抓包确认:积分中心商城 PlayMallUI.queryAllocation 的商品与 webActUI.getActPrizeList(sid=2277) pid 完全一致,属于同一批商品,因此列表无需接商城链路;真号实测 sid=2277 返回 30 项(限量返场 99 共 12 项 + 精英专享 101 共 18 项),sid=2203 已返回空。\n\n- huya:解析商品 tag23/25/26/27/28(发布时间、上架/下架时间、每日时段),新增 PrizeInfo.CategoryIDs/InCategory/StartsAt/NotYetOnSale 与响应级 CategoryStats/CategorySummary/FilterByCategory,列表日志带类目统计\n- sniper:Config.Category 限定自动选品范围,未命中提示带类目\n- CLI:新增 -category / -category-only\n- GUI:Config.Category、ListPrizes 回传类目字典与统计;界面商品列表按类目分页签、每行标注类目、未上架显示上架时间与倒计时,所选类目随配置提交给监控\n- 测试:新增 internal/huya/category_test.go 与 sniper 类目用例;README/WSS_NOTES 补记结论
2026-10-15 18:23:38 +08:00

248 lines
7.3 KiB
Go

package huya
import (
"testing"
"time"
"huya-go/internal/taf"
)
// testPrize 只写入断言用到的字段,字段顺序必须与真实响应一致(tag 递增)。
type testPrize struct {
id int64
name string
score int64
num int64
percent string
tags string
startTime int64
endTime int64
}
func (p testPrize) WriteTo(w *taf.Writer) error {
if err := w.WriteInt64(0, p.id); err != nil {
return err
}
if err := w.WriteString(2, p.name); err != nil {
return err
}
if err := w.WriteInt64(3, p.score); err != nil {
return err
}
if err := w.WriteInt64(10, p.num); err != nil {
return err
}
if err := w.WriteInt32(11, 1); err != nil {
return err
}
if err := w.WriteString(12, p.percent); err != nil {
return err
}
if err := w.WriteInt64(13, p.score); err != nil {
return err
}
if err := w.WriteString(14, p.tags); err != nil {
return err
}
if err := w.WriteInt64(25, p.startTime); err != nil {
return err
}
if err := w.WriteInt64(26, p.endTime); err != nil {
return err
}
if err := w.WriteString(27, "00:00:00"); err != nil {
return err
}
return w.WriteString(28, "23:59:59")
}
type testTag struct {
id int64
name string
weight int32
}
func (t testTag) WriteTo(w *taf.Writer) error {
if err := w.WriteInt64(0, t.id); err != nil {
return err
}
if err := w.WriteString(1, t.name); err != nil {
return err
}
return w.WriteInt32(2, int64(t.weight))
}
// decodePrizeList 拼一个和线上同构的 getActPrizeList 响应再解码回来。
func decodePrizeList(t *testing.T, tags, prizes []taf.Marshaler) GetActPrizeListResp {
t.Helper()
w := taf.NewWriter()
if err := w.WriteStructBegin(0); err != nil {
t.Fatalf("写入结构体失败: %v", err)
}
if err := w.WriteInt32(0, 200); err != nil {
t.Fatalf("写入 status 失败: %v", err)
}
if err := w.WriteString(1, "请求成功"); err != nil {
t.Fatalf("写入 msg 失败: %v", err)
}
if err := w.WriteListStruct(2, prizes); err != nil {
t.Fatalf("写入奖品列表失败: %v", err)
}
if err := w.WriteInt32(3, 20575); err != nil {
t.Fatalf("写入 moduleId 失败: %v", err)
}
if err := w.WriteListStruct(4, tags); err != nil {
t.Fatalf("写入类目字典失败: %v", err)
}
if err := w.WriteStructEnd(); err != nil {
t.Fatalf("写入结构体结尾失败: %v", err)
}
reader := taf.NewReader(w.Bytes())
if _, typ, err := reader.ReadHead(); err != nil || typ != taf.TypeStructBegin {
t.Fatalf("响应头解析失败: typ=%v err=%v", typ, err)
}
var resp GetActPrizeListResp
if err := resp.ReadFrom(reader); err != nil {
t.Fatalf("解析响应失败: %v", err)
}
return resp
}
func samplePrizeList(t *testing.T) GetActPrizeListResp {
t.Helper()
// 时间取 2026-10-16 17:00:00(限量返场实际开抢时间)与 2026-10-23 23:59:59。
return decodePrizeList(t,
[]taf.Marshaler{
testTag{id: 101, name: "精英专享", weight: 2},
testTag{id: 99, name: "限量返场", weight: 1},
},
[]taf.Marshaler{
testPrize{id: 13415, name: "套装-雪山精英", score: 600, num: 50000, percent: "93", tags: "101"},
testPrize{id: 13433, name: "怪诞乔克服装礼包", score: 8000, num: 6001, percent: "100", tags: "99", startTime: 1792141200, endTime: 1792771200},
})
}
func TestPrizeListCategoryHelpers(t *testing.T) {
resp := samplePrizeList(t)
if len(resp.Prizes) != 2 {
t.Fatalf("奖品数 = %d, 期望 2", len(resp.Prizes))
}
limited := resp.Prizes[1]
if got := limited.CategoryIDs(); len(got) != 1 || got[0] != 99 {
t.Fatalf("CategoryIDs = %v, 期望 [99]", got)
}
if !limited.InCategory(99) || limited.InCategory(101) {
t.Fatalf("InCategory 判定异常: tags=%s", limited.Tags)
}
start := limited.StartsAt()
if start.IsZero() || start.Unix() != 1792141200 {
t.Fatalf("StartsAt = %v, 期望 epoch 1792141200", start)
}
if end := limited.EndsAt(); end.Unix() != 1792771200 {
t.Fatalf("EndsAt = %d, 期望 epoch 1792771200", end.Unix())
}
if !limited.NotYetOnSale(start.Add(-time.Minute)) {
t.Fatal("上架前应判定为未开售")
}
if limited.NotYetOnSale(start.Add(time.Minute)) {
t.Fatal("上架后不应判定为未开售")
}
if resp.Prizes[0].NotYetOnSale(time.Now().Add(-100 * time.Hour)) {
t.Fatal("未下发上架时间的商品不应判定为未开售")
}
if name := resp.CategoryName(99); name != "限量返场" {
t.Fatalf("CategoryName(99) = %q", name)
}
cats := resp.CategoriesOf(limited)
if len(cats) != 1 || cats[0].Name != "限量返场" {
t.Fatalf("CategoriesOf = %v", cats)
}
}
func TestPrizeListCategoryStatsAndSummary(t *testing.T) {
resp := samplePrizeList(t)
stats := resp.CategoryStats()
if len(stats) != 2 {
t.Fatalf("类目数 = %d, 期望 2", len(stats))
}
if stats[0].Name != "精英专享" || stats[0].Count != 1 || stats[0].ID != 101 {
t.Fatalf("精英专享统计异常: %+v", stats[0])
}
if stats[1].Name != "限量返场" || stats[1].Count != 1 || stats[1].ID != 99 {
t.Fatalf("限量返场统计异常: %+v", stats[1])
}
// 剩余库存按 percent*num 估算:50000*93% = 46500、6001*100% = 6001。
if stats[0].Left != 46500 {
t.Fatalf("精英专享剩余 = %d, 期望 46500", stats[0].Left)
}
if stats[1].Left != 6001 {
t.Fatalf("限量返场剩余 = %d, 期望 6001", stats[1].Left)
}
want := "精英专享(101)=1项/限量返场(99)=1项"
if got := resp.CategorySummary(); got != want {
t.Fatalf("CategorySummary = %q, 期望 %q", got, want)
}
}
func TestFilterByCategory(t *testing.T) {
resp := samplePrizeList(t)
cases := []struct {
name string
category string
wantIDs []int64
}{
{name: "不限类目", category: "", wantIDs: []int64{13415, 13433}},
{name: "按类目名精确匹配", category: "限量返场", wantIDs: []int64{13433}},
{name: "按类目名带空格", category: " 精英专享 ", wantIDs: []int64{13415}},
{name: "按类目 id", category: "99", wantIDs: []int64{13433}},
{name: "按类目名模糊匹配", category: "返场", wantIDs: []int64{13433}},
{name: "字典里没有的类目", category: "不存在的类目", wantIDs: nil},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
filtered := resp.FilterByCategory(tc.category)
if len(filtered) != len(tc.wantIDs) {
t.Fatalf("命中 %d 项, 期望 %d 项", len(filtered), len(tc.wantIDs))
}
for i, want := range tc.wantIDs {
if filtered[i].ID != want {
t.Fatalf("第 %d 项 pid = %d, 期望 %d", i, filtered[i].ID, want)
}
}
})
}
}
func TestFilterByCategoryFallsBackToRawTags(t *testing.T) {
// 服务端新增类目但没下发字典时,用原始 tag14 文本兜底,避免把商品全过滤掉。
resp := decodePrizeList(t,
nil,
[]taf.Marshaler{testPrize{id: 1, name: "新类目商品", tags: "777"}},
)
if got := resp.FilterByCategory("777"); len(got) != 1 {
t.Fatalf("兜底匹配命中 %d 项, 期望 1 项", len(got))
}
if stats := resp.CategoryStats(); len(stats) != 0 {
t.Fatalf("无字典时不应产出类目统计, 实际 %v", stats)
}
if summary := resp.CategorySummary(); summary != "未下发类目(共1项)" {
t.Fatalf("CategorySummary = %q", summary)
}
}
func TestCategoryIDsIgnoresInvalidValues(t *testing.T) {
prize := PrizeInfo{Tags: "99, 101,abc,"}
ids := prize.CategoryIDs()
if len(ids) != 2 || ids[0] != 99 || ids[1] != 101 {
t.Fatalf("CategoryIDs = %v", ids)
}
if empty := (PrizeInfo{}).CategoryIDs(); empty != nil {
t.Fatalf("空 tags 应返回 nil, 实际 %v", empty)
}
}