11 Commits
Author SHA1 Message Date
QiuSW 3f1b27b4e2 feat: 接入 erpgo 店铺与商品查询并原子同步 (#20) 2026-09-28 17:50:48 +08:00
QiuSWandClaude Opus 5 c6bafd968e feat: 批量上传视频,以磁盘为准并按货憨憨规则本地预检 (#19)
解除 #17 的单商品限制,改为勾选多个一次上传,逐个串行处理。

选定文件的方式彻底改了:不再查 videos 表,改为扫描
<video_dir>/<蝦皮ID>/*.mp4 按文件名升序取第一个。原因是使用者可能手工把
别处来的视频放进目录,那类文件在 videos 表里没有记录;而且库与磁盘的漂移
无法根绝(手动挪文件、换 video_dir、外部程序写入),以磁盘为事实来源更稳。

上传前用 ffprobe 按货憨憨的要求本地预检,不合规的跳过而不是白传:

  大小  <= 30 MB
  时长  10.0 <= duration <= 60.0,严格比较不取整
  格式  容器为 mp4
  像素  宽 <= 1280 且高 <= 1280

时长必须严格判定:本地已有的 49365131605_1.mp4 是 9.985011 秒,
差 0.015 秒卡在门槛下,任何取整都会把它误判为合规。
ProbeResult 相应增加 Width / Height。

每个商品四条分支,任一分支结束后继续下一个:

  远端已有视频且勾选 > 1  -> existing,跳过,不发任何写请求
  子目录没有 mp4          -> missing,跳过
  有文件但不合规          -> invalid,last_error 写明违反哪条及具体数值
  合规                    -> 上传,done / failed

覆盖策略:batchUpdateShopProductVideo 是覆盖语义且不可恢复,所以批量永不
覆盖远端已有视频,单个(勾选 == 1)保持 #17 的红字警告加二次确认后可覆盖。
判断依据是实时读取的远端状态,不用本地 video_diagnosis——后者只在
「下载数据」时刷新,可能已经过期。

这三个新状态只是上一次尝试的结果记录,不构成断点:批量只处理当次勾选的
商品,不会因为状态是 missing/invalid/existing/failed 就把商品排除在外,
每次点上传都重新扫盘、重新预检、重新读远端。这是为了避免重蹈
video_status='none' 被断点永久跳过的覆辙(见 #15)。

上传成功后按 product_id + local_path 写入或更新 videos 记录,
source_item 留空(磁盘还原不出来自哪个淘宝同款),不使用 ReplaceVideos。

按负责人明确决定,不做容量门禁,仅保留确认框中的静态提示。

测试全部用 httptest 假服务器,未向真实货憨憨发出任何请求。
真机验证尚未进行。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbdtsD3ohhSMy3KPoCgARq
2026-09-03 17:08:12 +08:00
QiuSWandClaude Opus 5 16e4f0faf8 fix: 视频记录不再提前清空,启动重置残留的 running 状态 (#18)
真机使用时发现 55066525387 的子目录里有视频文件,界面上「目录」却不可点。
查明是取视频跑到一半程序被重启,videos 表里该商品的记录已被删除、
新记录还没写回,磁盘有文件而库里无记录。

缺陷一:prepareVideoFetch 在开始下载前先做了一次
ReplaceVideos(product.ID, nil, ...)。而 work.Finalize 里本来就有一次
ReplaceVideos(product.ID, records, now),ReplaceVideos 的语义就是按
product_id 全量替换。前面那次并不能少做任何事,唯一效果是把「文件已下载
但记录已被删」的窗口从零扩大到整个下载过程,任务停止、程序重启和崩溃
都会落进去。删掉它。

原注释说明作者考虑过这个风险,但只防住了登录失效一种情况。

行为变化是期望的:取视频在下载阶段之前失败时,旧的视频记录会保留而不是
被清空。那些文件确实还在磁盘上,「这次没找到视频」由 products.video_status
表达,不该靠删 videos 记录来表达。

缺陷二:进程退出后 download_status / upload_status 仍停留在 running,
但没有任何任务在跑,是个不会自愈的假状态。启动时在同一事务内把这两个字段
的 running 重置为 pending,只动 running,不碰 video_status 和 videos 表。
重置失败只记 Warn,不中断启动。

按负责人要求未对 cmsp.db 做任何手工订正。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbdtsD3ohhSMy3KPoCgARq
2026-09-03 16:46:40 +08:00
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
QiuSWandClaude Opus 5 6cc334ab92 fix: 取视频链路与 Python 参考实现对齐,识别风控降级 (#15)
淘宝会在登录仍然有效的情况下停止下发视频资源:不跳登录页、不出验证码,
详情页里就是没有 mp4 地址。原实现识别不了,把它当成「这个同款没视频」,
继续跑满 20 个同款加重风控,最后写 video_status='none',
而断点规则会永久跳过 none,导致账号恢复后这批商品也不再重试。

与参考实现对齐:

- 详情页等待 4 秒改为 8 秒,提为 detail_wait_seconds
- 每个同款详情页前做一次 my_itaobao 深度守卫,在两次详情页之间插入
  正常页面访问;守卫等待提为 guard_wait_seconds
- 详情页后补一次 Cookie 级快速守卫,复用 judgeLogin
- 淘宝首页导航等待 3 秒改为 6 秒。签名请求在该页面上下文里 fetch,
  页面没加载完就发可能带不上 Cookie
- 下载失败重试,5xx 与连接错误重试 download_retries 次,
  4xx 和 ffprobe 校验失败不重试
- 每次下载尝试各有独立的 180 秒超时。此前超时套在整个重试循环外面,
  大视频首次跑到一半失败后剩余预算不足,重试等于不生效

新增风控降级识别:连续 risk_empty_threshold 个同款打开成功但取不到视频时,
判为疑似风控,中断整批任务并保持商品 pending,绝不写入 none。
停止原因通过 stopReason 枚举传给前端,不依赖中文文本分支。

数据订正提供显式按钮,不写进 migrations。刻意不做日期过滤:
synced_at 记录的是商品数据何时从货憨憨拉取,与 video_status 何时被写成
none 无关,每次「下载数据」都会把它刷成当天。某商品是否需要重做的长期
答案来自货憨憨每次全量拉取覆盖的 video_diagnosis,不来自本地时间戳。

淘宝与 Chrome 相关逻辑由假实现覆盖,真机验证尚未进行。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbdtsD3ohhSMy3KPoCgARq
2026-09-03 14:29:03 +08:00
QiuSWandClaude Opus 5 73f5688ab1 feat: 单商品全链路,提取详情视频并下载到本地 (#13)
由 Codex (gpt-5.6-sol) 实施,Claude 审核。R4a,仍只处理单个商品。

internal/taobao/detail.go
- 从同款详情页 HTML 正则提取 mp4 地址,还原 / 与 \/ 转义
- 只保留 cloud.video.taobao.com 与 cloudvideocdn 的地址
- 去重按(主机、路径、查询串)三元组,忽略 http/https 差异

internal/downloader
- 先写 .part,ffprobe 校验通过后才改名为正式文件
- ffprobe 找不到时报明确中文错误,不静默跳过校验——跳过等于允许
  损坏文件流入不可逆的上传环节
- 请求带 Referer(同款商品页)与浏览器 UA,否则淘宝 CDN 可能拒绝
- 目标文件已存在且非空时跳过

internal/store/video.go
- 视频仓储,按商品全量替换
- source_item 记录实际采用的淘宝同款商品 ID(Q3 的落库要求)

同款选择按 Q3 结论:按图搜顺序依次尝试,第一个有视频的即采用,
不做人工确认;详情页之间按配置随机等待降低风控概率;
登录失效立即停止,不继续尝试下一个同款。

app.go 的 OpenFolder 用 EvalSymlinks + filepath.Rel 校验目标必须在
配置的 VideoDir 之内,避免成为可打开任意目录的入口。

审核补测(跑完删除,未入库):下载中途断开、HTTP 403/404/500、
空响应三种情况均不得留下正式文件或 .part,实测全部通过;
Referer 与 UA 确实发出。

审核期修正:Codex 又用 window.go.main.App 绕过生成绑定三处
(FetchVideosForProduct、OpenFolder、GetVideoSummary),已重新生成
绑定并改为正常 import。这是第三个工单出现同一问题。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbdtsD3ohhSMy3KPoCgARq
2026-09-03 10:04:20 +08:00
QiuSWandClaude Opus 5 c8ce79f886 fix: 修正历史脏值并改进店铺刷新入口 (#10 #11)
负责人反馈「下拉选择缺少视频筛不出数据」,查库发现 5305 条商品的
video_diagnosis 全是 unknown,product_diagnoses 表 0 条。

根因是我中途取消 Codex 造成的:第一版按三态实现时,负责人的
wails dev 正开着,Codex 一存文件就自动重编译重启,把带
DEFAULT 'unknown' 的迁移真正执行进了数据库。随后我回滚了源码,
但数据库已升到 schema_version=10,那几条迁移不会再跑,
于是库里留下当前代码不认识的 unknown。

三处修改:

1. 追加自愈迁移,把非 missing/ok 的历史值统一收敛为 ok。
   已在负责人真实数据库上验证:5305 条 unknown 全部变为 ok,
   版本升到 11。下次「下载数据」会写入真实诊断值。

2. 去掉「刷新店铺」按钮,改为展开店铺下拉时按需刷新。
   带 5 分钟间隔,避免每次点下拉都要等一次货憨憨登录;
   自动刷新失败不弹窗,下拉框继续用缓存,旁边的「更新于 …」
   会显示旧时间,看得出数据不是最新的。启动时的刷新失败仍然弹窗,
   那是需要立刻知道的。

3. 修掉三个绕过 Wails 生成绑定的 window.go.main.App 调用。
   Codex 这样写是因为 frontend/wailsjs 在禁改清单里,做法自洽,
   但绕过类型包装且会被后来者照抄。已重新生成绑定并改为正常 import。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbdtsD3ohhSMy3KPoCgARq
2026-09-02 21:13:12 +08:00
QiuSWandClaude Opus 5 393c942762 feat: 解析商品质量诊断并支持按「缺少视频」筛选 (#11)
由 Codex (gpt-5.6-sol) 实施,Claude 审核。

货憨憨 getPage 响应里带有商品质量诊断,其中「缺少视频」直接标出了
哪些商品才需要去淘宝找视频,能把工作量从全店商品缩小到真正需要处理的那部分。
此前这个字段被完全丢弃。

字段名是 diagnosisInfo(不是 diagnoses),结构比表面看到的多一层:
{itemId, qualityLevel, diagnoses:[{field, diagnosisResults:[{type, solution}]}]}

判定规则(负责人 2026-09-02 决定,见工单评论):
- missing:诊断明确报了「缺少视频」
- ok:其余全部情况,含 diagnosisInfo 为 null
只有明确报缺少视频才算缺少,其它一律当作有视频。

500 条样本里 293 条 diagnosisInfo 为 null,按此规则归入 ok。
我曾建议保留 unknown 三态以区分「尚未诊断」,负责人已知悉并选择两态,
措辞为「初定」;顾虑与重新评估条件记录在工单 #11 评论中。

- internal/store:products 加 video_diagnosis、quality_level 两列;
  新增 product_diagnoses 表保存全部 6 种诊断类型(缺少尺寸表、缺少标准变体、
  缺少品牌信息、所需属性过少、缺少视频、合格级属性数量不足)。
  只存「缺少视频」的话,另外 5 种将来要用就得重新全量拉一遍,而采集成本为零。
  迁移全部在 migrations 末尾追加,未改动任何已有条目。
- internal/huohanhan:ProductRecord.DiagnosisInfo 用指针以区分 null;
  转换时按上述规则算出 video_diagnosis。
- 前端:工具栏加诊断筛选(全部 / 缺少视频 / 有视频);
  「视频」列改为显示诊断结果。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbdtsD3ohhSMy3KPoCgARq
2026-09-02 21:02:58 +08:00
QiuSWandClaude Opus 5 d34846e26e feat: 店铺列表缓存到 SQLite 并在启动时后台刷新 (#10)
由 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
2026-09-02 18:00:11 +08:00
QiuSWandClaude Opus 5 51977e4dc1 fix: 开发模式下配置和数据库写错位置 (#8)
负责人在设置页保存了货憨憨账号,但项目根目录的 config.yaml 仍是空的,
以为保存没生效。实际是写到了 build\bin\config.yaml。

原因:DefaultPath 用 os.Executable() 定位。打包后双击 exe 时这是对的,
但 wails dev 跑的是 build\bin\cmsp-dev.exe,配置和数据库就落在那里,
开发的人在项目根目录怎么找都找不到。

改为按「exe 目录已有 → 工作目录已有 → 新建在 exe 目录」的顺序查找,
配置文件和数据库共用同一套规则(config.ResolveDataPath)。
这样打包后仍在 exe 旁边,开发模式则命中项目根目录那一份。

补两个测试:已存在的文件优先命中;目录不算文件。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbdtsD3ohhSMy3KPoCgARq
2026-09-02 17:20:44 +08:00
QiuSWandClaude Opus 5 2cefa67001 feat: 初始化 Wails v2 + Vue3 骨架与 SQLite 存储 (#4)
建立可运行、可构建的 Go 骨架,供后续 R1~R5 在其上叠加。

后端:
- main.go / app.go:Wails v2 窗口与 14 个前端可调方法。Wails 专有 API
  只出现在这两个文件,internal/ 对 Wails 零依赖,便于将来迁移 v3
- internal/config:config.yaml 的读写、校验、脱敏。保存时按固定模板
  渲染以保留注释,不用 yaml.Marshal
- internal/store:SQLite(modernc.org/sqlite,无需 CGO)。建表迁移、
  商品仓储、KV。重复同步不会覆盖本地下载与上传状态
- internal/logx:9 条脱敏规则,覆盖淘宝 token、Cookie、Authorization、
  密码和图片 base64;并发安全的内存日志缓冲

前端(Vue 3 + Naive UI,按已确认原型 v4):
- Sidebar:176px 可折叠,仅商品列表与参数设置两项,底部是登录状态的
  唯一显示位置
- ProductListView:两行工具栏(四项筛选 + 重置搜索 / 四个操作按钮)、
  商品表格、分页
- SettingsView:四张卡片,路径用文件与目录选择框
- LogWindow:运行日志子窗口,按级别过滤、自动滚动、导出

配置改用 YAML:分 huohanhan / taobao / download 三段。taobao 段刻意
没有账号密码字段,并有单元测试挡住后续加入。

构建相关:
- .gitignore 只忽略 build/bin/,不忽略 build/。Wails 把 build/ 当源码
  目录(图标、manifest),忽略掉会让别人克隆后构建失败
- .gitignore 忽略 config.yaml、payloads/ 和 *.har(含真实手机号与凭据)

未实现的下载数据、下载视频、上传数据三个方法返回明确的「还没实现」,
不静默失败。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbdtsD3ohhSMy3KPoCgARq
2026-09-02 15:59:54 +08:00