由 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
102 lines
3.2 KiB
Go
102 lines
3.2 KiB
Go
package store
|
|
|
|
import "fmt"
|
|
|
|
// Shop 是店铺列表缓存。字段和 shops 表一一对应。
|
|
//
|
|
// 这里只保留界面展示和后续查询商品需要的白名单字段,不得加入
|
|
// accessToken、refreshToken、createUser 等凭据或个人信息。
|
|
type Shop struct {
|
|
PlatformShopID string `json:"platformShopId"`
|
|
ID string `json:"id"`
|
|
ShopName string `json:"shopName"`
|
|
ShopAlias string `json:"shopAlias"`
|
|
Region string `json:"region"`
|
|
RegionName string `json:"regionName"`
|
|
Platform string `json:"platform"`
|
|
Status string `json:"status"`
|
|
UpdatedAt string `json:"updatedAt"`
|
|
}
|
|
|
|
// ReplaceShops 用线上店铺列表全量替换本地缓存。
|
|
//
|
|
// 必须先删后写并放在同一个事务里:店铺在货憨憨被删除或停用后,
|
|
// 缓存也要跟着消失。只做 upsert 会让已删除的店铺永远留在下拉框里,
|
|
// 使用者选中后却拉不到数据。
|
|
func (s *Store) ReplaceShops(shops []Shop, now string) error {
|
|
tx, err := s.db.Begin()
|
|
if err != nil {
|
|
return fmt.Errorf("开启店铺缓存事务失败:%w", err)
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
if _, err := tx.Exec(`DELETE FROM shops`); err != nil {
|
|
return fmt.Errorf("清空旧店铺缓存失败:%w", err)
|
|
}
|
|
|
|
stmt, err := tx.Prepare(`
|
|
INSERT INTO shops (
|
|
platform_shop_id, id, shop_name, shop_alias, region,
|
|
region_name, platform, status, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
if err != nil {
|
|
return fmt.Errorf("准备写入店铺缓存失败:%w", err)
|
|
}
|
|
defer stmt.Close()
|
|
|
|
for _, shop := range shops {
|
|
if _, err := stmt.Exec(
|
|
shop.PlatformShopID, shop.ID, shop.ShopName, shop.ShopAlias,
|
|
shop.Region, shop.RegionName, shop.Platform, shop.Status, now,
|
|
); err != nil {
|
|
return fmt.Errorf("写入店铺 %s 失败:%w", shop.ShopName, err)
|
|
}
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
return fmt.Errorf("提交店铺缓存事务失败:%w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ListShops 按店铺名排序返回本地缓存,不会联网。
|
|
func (s *Store) ListShops() ([]Shop, error) {
|
|
rows, err := s.db.Query(`
|
|
SELECT platform_shop_id, id, shop_name, shop_alias, region,
|
|
region_name, platform, status, updated_at
|
|
FROM shops
|
|
ORDER BY shop_name, platform_shop_id`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("读取店铺缓存失败:%w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
shops := make([]Shop, 0)
|
|
for rows.Next() {
|
|
var shop Shop
|
|
if err := rows.Scan(
|
|
&shop.PlatformShopID, &shop.ID, &shop.ShopName, &shop.ShopAlias,
|
|
&shop.Region, &shop.RegionName, &shop.Platform, &shop.Status,
|
|
&shop.UpdatedAt,
|
|
); err != nil {
|
|
return nil, fmt.Errorf("读取店铺缓存行失败:%w", err)
|
|
}
|
|
shops = append(shops, shop)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("遍历店铺缓存失败:%w", err)
|
|
}
|
|
return shops, nil
|
|
}
|
|
|
|
// ShopsUpdatedAt 返回店铺缓存的更新时间;没有缓存时返回空字符串。
|
|
func (s *Store) ShopsUpdatedAt() (string, error) {
|
|
var updatedAt string
|
|
if err := s.db.QueryRow(
|
|
`SELECT COALESCE(MAX(updated_at), '') FROM shops`,
|
|
).Scan(&updatedAt); err != nil {
|
|
return "", fmt.Errorf("读取店铺缓存时间失败:%w", err)
|
|
}
|
|
return updatedAt, nil
|
|
}
|