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 补记结论
248 lines
7.3 KiB
Go
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)
|
|
}
|
|
}
|