refactor(catalog): 废弃 LEWAN_ 镜像,统一英文 SKU 与积分价,商品图按 SKU 缓存

对外目录只保留英文主 sku;迁移硬删镜像并校准 1元=100积分;发货页/凭证优先使用 local {sku}.webp。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yml2213
2026-09-14 20:00:26 +08:00
co-authored by Cursor
parent bf9614569e
commit df1a170d44
57 changed files with 1052 additions and 171 deletions
+51 -36
View File
@@ -72,7 +72,8 @@ func (h *DeliveryHandler) Submit(c *gin.Context) {
response.OK(c, result)
}
// ImageProxy 代理图片并支持本地持久化缓存,杜绝跨域污染和远端加载失败。
// ImageProxy 代理外链图片并落盘缓存(仅无本地 SKU 图时的兜底)。
// 正式商品图请使用 frontend/public/images/products/{sku}.webp(go run ./cmd/sync_product_images)。
func (h *DeliveryHandler) ImageProxy(c *gin.Context) {
rawURL := strings.TrimSpace(c.Query("url"))
if rawURL == "" || (!strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://")) {
@@ -80,33 +81,57 @@ func (h *DeliveryHandler) ImageProxy(c *gin.Context) {
return
}
// 1. 优先检查本地官方商品图片缓存
for _, base := range []string{"data/uploads/products", "../data/uploads/products", "uploads/products"} {
if strings.Contains(rawURL, "/products/") {
parts := strings.Split(rawURL, "/products/")
if len(parts) > 1 {
sub := strings.Split(parts[1], "/")
if len(sub) > 0 && sub[0] != "" {
localPath := filepath.Join(base, sub[0]+".webp")
if info, err := os.Stat(localPath); err == nil && info.Size() > 0 {
c.Header("Cache-Control", "public, max-age=2592000, immutable")
c.File(localPath)
return
}
}
sku := strings.TrimSpace(c.Query("sku"))
publicDirs := []string{
"../frontend/public/images/products",
"frontend/public/images/products",
"public/images/products",
}
uploadDirs := []string{"data/uploads/products", "../data/uploads/products", "uploads/products"}
tryLocal := func(dirs []string, name string) bool {
if name == "" {
return false
}
for _, base := range dirs {
localPath := filepath.Join(base, name+".webp")
if info, err := os.Stat(localPath); err == nil && info.Size() > 0 {
c.Header("Cache-Control", "public, max-age=2592000, immutable")
c.File(localPath)
return true
}
}
// 按 URL SHA256 哈希查本地缓存
hash := sha256.Sum256([]byte(rawURL))
hashPath := filepath.Join(base, hex.EncodeToString(hash[:])+".webp")
if info, err := os.Stat(hashPath); err == nil && info.Size() > 0 {
c.Header("Cache-Control", "public, max-age=2592000, immutable")
c.File(hashPath)
return
}
return false
}
// 2. 本地无缓存时请求远端
// 1. 优先 SKU 命名的静态缓存
if tryLocal(publicDirs, sku) {
return
}
// 2. 历史:URL 中的乐玩 UUID → 先查 public,再查 uploads
var uuidName string
if strings.Contains(rawURL, "/products/") {
parts := strings.Split(rawURL, "/products/")
if len(parts) > 1 {
sub := strings.Split(parts[1], "/")
if len(sub) > 0 && sub[0] != "" {
uuidName = sub[0]
}
}
}
if tryLocal(publicDirs, uuidName) || tryLocal(uploadDirs, uuidName) {
return
}
// 3. URL hash 缓存(uploads 运行时兜底)
hash := sha256.Sum256([]byte(rawURL))
hashName := hex.EncodeToString(hash[:])
if tryLocal(uploadDirs, hashName) {
return
}
// 4. 本地无缓存时请求远端
req, err := http.NewRequestWithContext(c.Request.Context(), http.MethodGet, rawURL, nil)
if err != nil {
response.BadRequest(c, "构建请求失败")
@@ -131,20 +156,10 @@ func (h *DeliveryHandler) ImageProxy(c *gin.Context) {
return
}
// 3. 异步/同步回写到本地磁盘缓存
// 5. 仅将未知外链写入 uploads 哈希缓存(不再按 UUID 污染正式目录)
cacheDir := "data/uploads/products"
_ = os.MkdirAll(cacheDir, 0755)
if strings.Contains(rawURL, "/products/") {
parts := strings.Split(rawURL, "/products/")
if len(parts) > 1 {
sub := strings.Split(parts[1], "/")
if len(sub) > 0 && sub[0] != "" {
_ = os.WriteFile(filepath.Join(cacheDir, sub[0]+".webp"), data, 0644)
}
}
}
hash := sha256.Sum256([]byte(rawURL))
_ = os.WriteFile(filepath.Join(cacheDir, hex.EncodeToString(hash[:])+".webp"), data, 0644)
_ = os.WriteFile(filepath.Join(cacheDir, hashName+".webp"), data, 0644)
contentType := resp.Header.Get("Content-Type")
if contentType == "" {