Files
cmsp/internal/store/video_test.go
T
QiuSWandClaude Opus 5 d1149b629d feat: 视频上传回货憨憨并关联商品,首版限单商品 (#17)
接口契约来自 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
2026-09-03 15:19:34 +08:00

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])
}
}