接口契约来自 payloads/huohanhan_batch_save_product_video.har,两步:
POST product/material/uploadFiles multipart
files / isLocalFile=true / fileType=1 → bean[0] 是 COS 地址
POST product/batchEdit/batchUpdateShopProductVideo json
[{id, platformShopId, videoUrl}] 裸数组,只有三个字段
id 是货憨憨内部记录 ID(products.id),不是蝦皮商品 ID。
不采用 common-product/updateProductItem 那条路:它要回传整份商品共 56 个
字段 26589 字节,而 getDetail 返回的结构无法直接回传,model[].priceInfo
的值就对不上——那是价格数据,照抄会写坏真实店铺且不可逆。
batchUpdateShopProductVideo 只改 videoUrl 一个字段。
这是本项目第一个会往真实店铺写、且删不回来的功能,所以:
- 只有显式点击「上传数据」才触发,不作为任何查询流程的副作用
- 必须二次确认,列出蝦皮 ID、商品名、本地文件名与大小
- 该商品已有视频时额外红字警告会覆盖且无法恢复(接口是覆盖语义,
videoUrl 传空即删除)
- 首版一次只能一个商品,拦截在 Go 侧,前端只做提示
- 任一步非 SUCCESS 立即停止,不自动重试
关联成功后必须回读 getShopItemInfoPage 确认 records[0].video 非空,
才写 upload_status=done。接口返回 SUCCESS 不等于视频真的挂上了。
容量判断整体延后:payloads 下所有 HAR 都没有 getMaterialSize 的证据,
路径与返回结构均属推测,不凭猜测实现。本次只在确认框放一行静态提示,
待补抓包后另开工单。
测试全部用 httptest 假服务器,未向真实货憨憨发出任何请求。
真机验证尚未进行。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbdtsD3ohhSMy3KPoCgARq
64 lines
2.4 KiB
Go
64 lines
2.4 KiB
Go
package store
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestReplaceVideos按商品全量替换(t *testing.T) {
|
|
s := newTestStore(t)
|
|
first := []Video{
|
|
{SourceItem: "a", SourceURL: "https://example.invalid/a.mp4", Status: VideoStatusPending},
|
|
{SourceItem: "b", SourceURL: "https://example.invalid/b.mp4", Status: VideoStatusDownloaded},
|
|
{SourceItem: "c", SourceURL: "https://example.invalid/c.mp4", Status: VideoStatusFailed},
|
|
}
|
|
if err := s.ReplaceVideos("product-1", first, "2026-09-03 10:00:00"); err != nil {
|
|
t.Fatalf("首次写入 3 条视频失败:%v", err)
|
|
}
|
|
second := []Video{{SourceItem: "d", SourceURL: "https://example.invalid/d.mp4", Status: VideoStatusDownloaded}}
|
|
if err := s.ReplaceVideos("product-1", second, "2026-09-03 11:00:00"); err != nil {
|
|
t.Fatalf("用 1 条视频替换失败:%v", err)
|
|
}
|
|
got, err := s.ListVideos("product-1")
|
|
if err != nil {
|
|
t.Fatalf("读取替换后视频失败:%v", err)
|
|
}
|
|
if len(got) != 1 || got[0].SourceItem != "d" {
|
|
t.Fatalf("全量替换后应只剩 d,实际:%+v", got)
|
|
}
|
|
if got[0].ProductID != "product-1" || got[0].CreatedAt != "2026-09-03 11:00:00" {
|
|
t.Fatalf("商品 ID 或创建时间没有按参数写入:%+v", got[0])
|
|
}
|
|
}
|
|
|
|
func Test取第一个存在的已下载视频并写回上传状态(t *testing.T) {
|
|
s := newTestStore(t)
|
|
dir := t.TempDir()
|
|
missing := filepath.Join(dir, "missing.mp4")
|
|
available := filepath.Join(dir, "available.mp4")
|
|
if err := os.WriteFile(available, []byte("fake-mp4"), 0o600); err != nil {
|
|
t.Fatalf("准备本地视频失败:%v", err)
|
|
}
|
|
if err := s.ReplaceVideos("商品-1", []Video{
|
|
{LocalPath: missing, Status: VideoStatusDownloaded},
|
|
{LocalPath: available, Status: VideoStatusDownloaded},
|
|
}, "2026-09-03 10:00:00"); err != nil {
|
|
t.Fatalf("写入视频失败:%v", err)
|
|
}
|
|
video, found, err := s.FirstUploadableVideo("商品-1")
|
|
if err != nil || !found || video.LocalPath != available {
|
|
t.Fatalf("应跳过不存在文件并取第一个可上传视频,video=%+v found=%v err=%v", video, found, err)
|
|
}
|
|
if err := s.MarkVideoUploaded(video.ID, "https://cos.example.invalid/video.mp4"); err != nil {
|
|
t.Fatalf("写回上传状态失败:%v", err)
|
|
}
|
|
items, err := s.ListVideos("商品-1")
|
|
if err != nil {
|
|
t.Fatalf("读取视频失败:%v", err)
|
|
}
|
|
if items[1].Status != VideoStatusUploaded || items[1].RemoteURL != "https://cos.example.invalid/video.mp4" {
|
|
t.Fatalf("上传状态或远端地址没有写回:%+v", items[1])
|
|
}
|
|
}
|