fix: validate cover image lists before replacement

This commit is contained in:
QiuSW
2026-09-18 10:17:12 +08:00
parent 75edd0b06a
commit a8d9833e9c
4 changed files with 104 additions and 15 deletions
+3
View File
@@ -185,6 +185,9 @@ func (e ShopeeProduct) updateCover(c *gin.Context, shopeeID, coverURL, filename,
if err != nil {
return nil, fmt.Errorf("商品编辑信息准备失败:%w", err)
}
if strategy == "replace_first" && !info.HasCompleteImageList() {
return nil, fmt.Errorf("商品编辑信息缺少完整图片列表,已拒绝更新主图")
}
sourceURL := coverURL
if len(content) > 0 {
coverURL, err = client.UploadImage(ctx, filename, content, contentType)
+60 -15
View File
@@ -15,17 +15,62 @@ import (
)
type ProductEditInfo struct {
ID string `json:"id"`
ItemID string `json:"itemId"`
ItemName string `json:"itemName"`
MainImage string `json:"mainImage"`
PlatformShopID string `json:"platformShopId"`
Platform string `json:"platform"`
CategoryID string `json:"categoryId"`
Image struct {
ImageIDList []string `json:"imageIdList"`
ImageURLList []string `json:"imageUrlList"`
} `json:"image"`
ID string `json:"id"`
ItemID string `json:"itemId"`
ItemName string `json:"itemName"`
MainImage string `json:"mainImage"`
PlatformShopID string `json:"platformShopId"`
Platform string `json:"platform"`
CategoryID string `json:"categoryId"`
Image ProductEditImage `json:"image"`
imagePresent bool
}
type ProductEditImage struct {
ImageIDList []string `json:"imageIdList"`
ImageURLList []string `json:"imageUrlList"`
imageIDPresent bool
imageURLPresent bool
}
// UnmarshalJSON tracks whether Huohanhan explicitly supplied each list. A
// missing/null field must never be treated as a valid empty image list.
func (p *ProductEditInfo) UnmarshalJSON(data []byte) error {
type plain ProductEditInfo
var decoded plain
if err := json.Unmarshal(data, &decoded); err != nil {
return err
}
*p = ProductEditInfo(decoded)
var fields map[string]json.RawMessage
if err := json.Unmarshal(data, &fields); err != nil {
return err
}
rawImage, ok := fields["image"]
if !ok || bytes.Equal(bytes.TrimSpace(rawImage), []byte("null")) {
return nil
}
var imageFields map[string]json.RawMessage
if err := json.Unmarshal(rawImage, &imageFields); err != nil || imageFields == nil {
if err == nil {
err = fmt.Errorf("image 不是对象")
}
return err
}
p.imagePresent = true
if raw, ok := imageFields["imageUrlList"]; ok && !bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
p.Image.imageURLPresent = true
}
if raw, ok := imageFields["imageIdList"]; ok && !bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
p.Image.imageIDPresent = true
}
return nil
}
// HasCompleteImageList accepts a confirmed empty list but rejects missing,
// null, malformed, or mismatched lists before a cover update is submitted.
func (p ProductEditInfo) HasCompleteImageList() bool {
return p.imagePresent && p.Image.imageURLPresent && p.Image.imageIDPresent && len(p.Image.ImageURLList) == len(p.Image.ImageIDList)
}
type ProductEditTaskStatus struct {
@@ -179,7 +224,10 @@ func CoverImageListWithStrategy(info ProductEditInfo, coverURL, strategy string)
if strategy == "replace_first" {
var urls, ids []string
for i, u := range info.Image.ImageURLList {
if strings.TrimSpace(u) == "" || u == coverURL {
// Index zero is the image being replaced. Remove duplicate copies of
// the new image only from the remaining references; doing that before
// dropping index zero would incorrectly drop ref1 for [new, ref1].
if i == 0 || strings.TrimSpace(u) == "" || u == coverURL {
continue
}
urls = append(urls, u)
@@ -189,9 +237,6 @@ func CoverImageListWithStrategy(info ProductEditInfo, coverURL, strategy string)
ids = append(ids, "")
}
}
if len(urls) > 0 {
urls, ids = urls[1:], ids[1:]
}
urls, ids = append([]string{coverURL}, urls...), append([]string{""}, ids...)
return urls, ids
}
+24
View File
@@ -132,6 +132,7 @@ func Test统一替换第一张策略(t *testing.T) {
{"一张旧图", []string{"a"}, []string{"new"}},
{"八张旧图", []string{"a", "b", "c", "d", "e", "f", "g", "h"}, []string{"new", "b", "c", "d", "e", "f", "g", "h"}},
{"九张旧图", []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"}, []string{"new", "b", "c", "d", "e", "f", "g", "h", "i"}},
{"新图已在首位", []string{"new", "ref1", "ref2"}, []string{"new", "ref1", "ref2"}},
{"新图原在后面", []string{"a", "new", "b"}, []string{"new", "b"}},
}
for _, tc := range cases {
@@ -147,6 +148,29 @@ func Test统一替换第一张策略(t *testing.T) {
}
}
func Test编辑信息必须明确返回完整图片列表(t *testing.T) {
cases := []struct {
name, raw string
ok bool
}{
{"完整空列表", `{"image":{"imageUrlList":[],"imageIdList":[]}}`, true},
{"缺少 image", `{}`, false},
{"缺少 URL", `{"image":{"imageIdList":[]}}`, false},
{"URL 为 null", `{"image":{"imageUrlList":null,"imageIdList":[]}}`, false},
{"ID 为 null", `{"image":{"imageUrlList":[],"imageIdList":null}}`, false},
{"长度不等", `{"image":{"imageUrlList":["a"],"imageIdList":[]}}`, false},
}
for _, tc := range cases {
var info ProductEditInfo
if err := json.Unmarshal([]byte(tc.raw), &info); err != nil {
t.Fatalf("%s: %v", tc.name, err)
}
if got := info.HasCompleteImageList(); got != tc.ok {
t.Fatalf("%s: got %v", tc.name, got)
}
}
}
func Test按URL转存主图(t *testing.T) {
src := "http://122.228.200.167:8020/uploads/images/24720421377.jpg?v=1789627038"
bean := `["https://hhh-prod-1307856765.cos.ap-guangzhou.myqcloud.com/video/1131990643264757761.jpg"]`
+17
View File
@@ -0,0 +1,17 @@
# LOCAL-022:主图替换列表安全修复(R1、R4)
## 目标
修复 `coverStrategy=replace_first` 的参考图误删问题,并在 Huohanhan 编辑信息未返回完整图片列表时拒绝更新,防止把异常响应误当成空图片列表。
## 验收
- `[new, ref1, ref2]` 更新为 `new` 后保留 `ref1, ref2`。
- `[old, new, ref1]` 更新后只保留一份 `new`,且保留 `ref1`。
- 0/1/8/9 张旧图均保持正确数量和 URL/ID 下标对应。
- 缺少 `image`、缺少/为 null 的 `imageUrlList` 或 `imageIdList`、两表长度不等时,不提交 Huohanhan 主图更新。
- 明确返回合法双空数组时允许新图成为唯一图片。
## 状态
已完成。