由 Codex (gpt-5.6-sol) 实施,Claude 审核。 以前每次打开商品列表都要现调 erp/shop/all,而这条链路要先完成 货憨憨登录(CID/CST → 验证码 → OCR → 登录 → 验证 token), 任何一环失败下拉框就是空的,连店铺名都看不到。 internal/store/shop.go(新建) - shops 表在 migrations 末尾追加,未改动任何已有迁移条目 - ReplaceShops 是全量替换:同一事务内先 DELETE 再批量插入。 只做 upsert 会让货憨憨那边已删除的店铺永远留在下拉框里, 选中后拉不到任何数据 - 缓存字段是白名单,不含 accessToken、refreshToken、createUser app.go - ListShops 改为只读缓存;新增 GetCachedShops、GetShopsUpdatedAt、 RefreshShops - startup 里用协程后台刷新,不阻塞窗口显示;且仅在账号密码都已配置时 才刷新——同事第一次安装还没填账号就启动时,刷新必然失败, 一开机弹「登录失败」体验很差 - 成功发 shops:refreshed 事件,失败发 shops:refresh-failed 并带上 失败原因、缓存时间和缓存条数 前端 - 启动先读缓存立即填充下拉框,再由后台刷新更新 - 刷新失败时弹窗同时说明失败原因和缓存时间,只说「更新失败」 使用者无法判断该不该继续用 - 下拉框旁常驻显示缓存更新时间,并提供手动刷新 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LbdtsD3ohhSMy3KPoCgARq
111 lines
3.4 KiB
Go
111 lines
3.4 KiB
Go
package store
|
||
|
||
import (
|
||
"encoding/json"
|
||
"testing"
|
||
)
|
||
|
||
func 测试店铺列表() []Shop {
|
||
return []Shop{
|
||
{PlatformShopID: "shop-3", ID: "3", ShopName: "朝阳店", Region: "TW", Platform: "0", Status: "NORMAL"},
|
||
{PlatformShopID: "shop-1", ID: "1", ShopName: "白云店", Region: "MY", Platform: "0", Status: "NORMAL"},
|
||
{PlatformShopID: "shop-2", ID: "2", ShopName: "春风店", Region: "SG", Platform: "0", Status: "NORMAL"},
|
||
}
|
||
}
|
||
|
||
func Test建表后版本等于迁移条数(t *testing.T) {
|
||
s := newTestStore(t)
|
||
version, err := s.Version()
|
||
if err != nil {
|
||
t.Fatalf("读取迁移版本失败:%v", err)
|
||
}
|
||
if version != len(migrations) {
|
||
t.Fatalf("迁移版本应为 %d,实际为 %d", len(migrations), version)
|
||
}
|
||
}
|
||
|
||
func Test店铺缓存写入后按名称排序读回(t *testing.T) {
|
||
s := newTestStore(t)
|
||
if err := s.ReplaceShops(测试店铺列表(), "2026-09-02 17:30:00"); err != nil {
|
||
t.Fatalf("写入店铺缓存失败:%v", err)
|
||
}
|
||
|
||
shops, err := s.ListShops()
|
||
if err != nil {
|
||
t.Fatalf("读取店铺缓存失败:%v", err)
|
||
}
|
||
if len(shops) != 3 {
|
||
t.Fatalf("应读回 3 个店铺,实际为 %d", len(shops))
|
||
}
|
||
if shops[0].ShopName != "春风店" || shops[1].ShopName != "朝阳店" || shops[2].ShopName != "白云店" {
|
||
t.Fatalf("店铺未按名称排序:%v", []string{shops[0].ShopName, shops[1].ShopName, shops[2].ShopName})
|
||
}
|
||
if shops[0].UpdatedAt != "2026-09-02 17:30:00" {
|
||
t.Fatalf("缓存时间应写入每个店铺,实际为 %q", shops[0].UpdatedAt)
|
||
}
|
||
}
|
||
|
||
func Test店铺缓存采用全量替换(t *testing.T) {
|
||
s := newTestStore(t)
|
||
shops := 测试店铺列表()
|
||
if err := s.ReplaceShops(shops, "2026-09-02 17:30:00"); err != nil {
|
||
t.Fatalf("首次写入 3 个店铺失败:%v", err)
|
||
}
|
||
if err := s.ReplaceShops(shops[:2], "2026-09-02 17:31:00"); err != nil {
|
||
t.Fatalf("用 2 个店铺全量替换失败:%v", err)
|
||
}
|
||
|
||
got, err := s.ListShops()
|
||
if err != nil {
|
||
t.Fatalf("读取替换后的店铺失败:%v", err)
|
||
}
|
||
if len(got) != 2 {
|
||
t.Fatalf("全量替换后应只剩 2 个店铺,实际为 %d", len(got))
|
||
}
|
||
for _, shop := range got {
|
||
if shop.PlatformShopID == "shop-2" {
|
||
t.Fatalf("已从线上消失的第 3 个店铺仍留在缓存中")
|
||
}
|
||
}
|
||
}
|
||
|
||
func Test店铺缓存时间在无缓存时为空写入后可读(t *testing.T) {
|
||
s := newTestStore(t)
|
||
updatedAt, err := s.ShopsUpdatedAt()
|
||
if err != nil {
|
||
t.Fatalf("读取空缓存时间失败:%v", err)
|
||
}
|
||
if updatedAt != "" {
|
||
t.Fatalf("无缓存时更新时间应为空,实际为 %q", updatedAt)
|
||
}
|
||
|
||
const now = "2026-09-02 17:30:00"
|
||
if err := s.ReplaceShops(测试店铺列表(), now); err != nil {
|
||
t.Fatalf("写入店铺缓存失败:%v", err)
|
||
}
|
||
updatedAt, err = s.ShopsUpdatedAt()
|
||
if err != nil {
|
||
t.Fatalf("读取缓存时间失败:%v", err)
|
||
}
|
||
if updatedAt != now {
|
||
t.Fatalf("缓存时间应为 %q,实际为 %q", now, updatedAt)
|
||
}
|
||
}
|
||
|
||
func Test店铺缓存JSON不含凭据或手机号字段(t *testing.T) {
|
||
raw, err := json.Marshal(Shop{PlatformShopID: "shop-1", ShopName: "测试店铺"})
|
||
if err != nil {
|
||
t.Fatalf("序列化店铺缓存失败:%v", err)
|
||
}
|
||
|
||
var fields map[string]any
|
||
if err := json.Unmarshal(raw, &fields); err != nil {
|
||
t.Fatalf("解析店铺缓存 JSON 失败:%v", err)
|
||
}
|
||
for _, forbidden := range []string{"accessToken", "refreshToken", "createUser"} {
|
||
if _, exists := fields[forbidden]; exists {
|
||
t.Fatalf("店铺缓存 JSON 不得包含敏感字段 %q:%s", forbidden, raw)
|
||
}
|
||
}
|
||
}
|