20 Commits
Author SHA1 Message Date
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 0fe24f70f4 fix: 回读不能只看 video 字段,会把成功的上传误判为失败 (#17)
真机验证时 55066525387 报「回读商品视频失败:货憨憨尚未显示已关联的视频」,
但视频其实已经上传成功。两个写接口都返回了 SUCCESS,是回读判据写错了。

video 字段装的是已经在 Shopee 上生效的视频,不是刚设置的那个。货憨憨推送
到 Shopee 是异步的,保存成功后 video 必然还是空的,视频这时在 tempVideoUrl
和 videoUploadIdStr 里。证据是 payloads/huohanhan_save_product_info.har 的
第 3 个请求——一次保存成功后立刻发起的 getDetail,那里 video=[] 而
tempVideoUrl 有值。这条证据本来就在仓库里,之前定判据时没有用上。

HasShopProductVideo 改为 CheckShopProductVideo,返回四个字段:

  videoFailReason 非空                    → 失败,把原因写进 last_error
  video 非空                              → 已在 Shopee 生效
  tempVideoUrl 或 videoUploadIdStr 非空   → 已提交,等待同步,判为成功
  四者皆空                                → 确实没设上,失败

upload_status=done 的语义随之明确为「已提交并回读确认」,
而不是「Shopee 已可见」——后者是异步结果,本地无法立刻断言,
日志会分别提示这两种情况。

确认框的覆盖警告改用 Confirmed():等待同步中的视频同样会被覆盖,
只看 video 会漏掉这种情况,导致该警告不显示。

同时订正 55066525387 被误判写成的 failed 状态。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbdtsD3ohhSMy3KPoCgARq
2026-09-03 16:02:27 +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 ce729cd1c8 fix: 数据目录跟随配置文件位置,避免视频被 build -clean 删掉
DefaultVideoDir 原先按 exe 所在目录算。wails dev 跑的是
build\bin\cmsp-dev.exe,视频就下载进了 build\bin\运行数据\视频,
而 `wails build -clean` 会清空该目录——已下载的视频会被一起删掉。

改为新增 DataRoot(),规则是「配置文件在哪,数据就在哪」:

  打包后双击 exe   config.yaml 在 exe 旁边   数据也在 exe 旁边
  开发模式         config.yaml 在项目根目录   数据也在项目根目录

与 config.yaml、cmsp.db 已有的查找规则保持一致。
补测试断言视频目录不得落在 build\bin 下。

同时把 .gitignore 补上「浏览器运行状态.json」:该文件含 PID、
调试端口和本机 profile 路径,属于本机运行状态,不该进仓库。

负责人本机的既有数据已一并迁移:5 个视频文件与浏览器状态文件从
build\bin 移到项目根目录,config.yaml 的 video_dir 与数据库中
4 条 videos.local_path 同步更新,迁移后无失效路径。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbdtsD3ohhSMy3KPoCgARq
2026-09-03 11:14:15 +08:00
QiuSWandClaude Opus 5 f79208cc5c feat: 批量取视频,队列、进度与全局停止门 (#14)
由 Codex (gpt-5.6-sol) 实施,Claude 审核。MVP2 最后一块。

internal/task/task.go
- 串行阶段(Prepare)一次一个商品,全程持有同一个 CDP:
  登录深度检查 → 图搜 → 逐个同款提取视频地址。
  并发只作用于下载阶段(纯 HTTP + ffprobe,不碰 CDP)。
  这样既避免多协程踩同一个浏览器页面,也不抬高淘宝风控概率。
- 登录失效走独立的 ErrLoginRequired,触发后立即终止整批,
  不再启动后续商品,不自动重试也不自动重新登录
- 其它错误属于单商品失败,记录后继续处理下一个
- 断点复用商品自身的 download_status,不另建断点文件或断点表,
  因此不会出现断点与数据库不一致

app.go 抽出单商品核心供两处复用,未复制逻辑。

审核补测(跑完删除,未入库),五项全部通过:
- 登录失效发生在第 3 个商品时,后续 D/E/F 一个都没被处理
- Concurrency 设为 4 时 Prepare 峰值仍为 1,串行确实生效
- 下载阶段峰值恰为 2,并发上限真的起作用且确实并行
- 连续 5 个普通失败仍跑完全部,状态为 completed
- 停止后可以重新启动,不会被永久锁死

硬性验收项全部通过:前端无 window.go 等绕过生成绑定的写法
(前三个工单连续出现,本次未再出现);internal 未 import wails;
无新增依赖。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbdtsD3ohhSMy3KPoCgARq
2026-09-03 10:58:55 +08:00
QiuSWandClaude Opus 5 e71559e2f6 feat: 视频按蝦皮商品 ID 分子目录存放,文件名以蝦皮商品 ID 为前缀
原先所有视频平铺在视频目录下,后续别的程序无法按商品区分。改为:

  <视频目录>/<蝦皮商品ID>/<蝦皮商品ID>_<序号>.mp4
  例:运行数据\视频\40583431295\40583431295_1.mp4

- 子目录名和文件名前缀都用蝦皮商品 ID(itemId),不是货憨憨的内部
  记录 ID,因为后续程序对接的是 Shopee 侧的标识
- 文件名同时带前缀,是为了文件被单独挪走或和别的商品混在一起时
  仍能认出归属
- 视频来自哪个淘宝同款不再体现在文件名里,该信息记录在
  videos.source_item,需要回溯来源时查库

安全处理:目录名直接来自货憨憨返回的商品 ID,正常是纯数字但不能假定。
新增 SafeDirName 只保留字母数字与减号下划线;并加固 safeID,
让 ".." 这类全部字符被替换后的输入回落为 unknown,而不是生成
"__" 这种无意义目录名。有测试覆盖路径穿越、Windows 非法字符等输入。

GetVideoSummary 的目录改为取实际文件所在目录而非拼接,
这样分目录之前下载的旧文件(平铺在根目录)点「目录」也能正确打开。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbdtsD3ohhSMy3KPoCgARq
2026-09-03 10:37:27 +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 e8db874c04 feat: 单个商品以图搜同款 (#12)
由 Codex (gpt-5.6-sol) 实施,Claude 审核。R3a,先验证签名算法是否仍有效。

签名与参考实现字节级一致(审核时用固定随机值和时间戳对拍):

  pcSign      44 字节    一致
  params    1151 字节    一致
  requestText 1390 字节  一致
  MTOP sign   32 字节    一致
  token 提取             一致

Go 特有的陷阱已避开:签名是对 JSON 字符串做哈希,键顺序会改变结果。
Python 的 dict 按插入顺序序列化,而 Go 的 encoding/json 对 map 按键名
字母序排序,用 map 会产出完全不同的 JSON 导致签名必错,且淘宝只返回
含糊的业务错误码,极难定位。实现用 struct 按声明顺序输出,并用
json.Encoder + SetEscapeHTML(false) 对齐 Python 的 ensure_ascii=False。
四个价格字段用空指针输出真正的 null。有专门测试断言键顺序与字节,
防止后人改回 map。

internal/taobao/cdp.go 新增 CookieValue,仅用于读取 _m_h5_tk 计算签名。
AGENTS.md 明确允许该用途;全项目仅 imagesearch.go 调用一次,用完即弃,
不落库、不写日志、不返回前端。

以图搜前先做登录深度检查,未通过即停止,不发起 MTOP 请求,
避免抬高风控概率。

前端:表格操作列新增「搜同款」,结果以弹窗展示缩略图、标题、价格、
店铺、销量,点标题用系统浏览器打开淘宝商品页。

本工单不含批量、队列、并发与落库,那些属于 R3b。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbdtsD3ohhSMy3KPoCgARq
2026-09-03 09:39:58 +08:00
QiuSWandClaude Opus 5 e7c3c6849c fix: 中文 profile 路径导致 Chrome 归属校验永远失败 (#9)
负责人点「打开淘宝登录」报错:
「Chrome进程不属于配置的专属用户数据目录,已将其关闭」

根因是编码不一致。processCommandLine 用 PowerShell 读取进程命令行,
PowerShell 的标准输出使用系统 ANSI 代码页(简体中文 Windows 是 GBK),
而 Go 字符串是 UTF-8。专属 profile 路径含中文
(电商视频自动下载工具\淘宝浏览器\默认账号),两边字节不同,
strings.Index 永远匹配不上,于是 Chrome 刚启动就被判为不属于本 profile
并被杀掉。

单元测试没能发现,因为测试里比较的两个字符串都是 UTF-8。

改为让 PowerShell 先把命令行转成 UTF-8 的 Base64,Go 侧再解码。
不用 [Console]::OutputEncoding=UTF8 是因为那依赖控制台状态;
Base64 只含 ASCII,任何区域设置下都可靠。

实测三种方式:
  原样读取        含中文路径 false   ← 修复前
  设 UTF8 编码    含中文路径 true
  Base64 传输     含中文路径 true    ← 采用

已用真实运行中的 Chrome 进程验证:归属判断返回 true。
补一条防回归测试,覆盖含中文的 profile 以及编码不一致的情形。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbdtsD3ohhSMy3KPoCgARq
2026-09-03 09:22:53 +08:00
QiuSWandClaude Opus 5 656b7a8c49 feat: 专属 Chrome 管理与淘宝登录检查 (#9)
由 Codex (gpt-5.6-sol) 实施,Claude 审核。MVP2 第一块。

internal/taobao/chrome.go
- EnsureBrowser 按四项条件判断能否复用已有实例:PID 存在、进程命令行
  属于本程序专属 profile、CDP 端口可访问、存在 page 类型目标
- 启动前遍历整个端口段,发现已有 Chrome 但状态文件丢失时报错而不是
  再起一个,避免同一 profile 出现两个实例损坏登录数据
- CloseBrowser 必须先校验 PID 归属才允许 taskkill,防止误杀使用者
  自己的 Chrome

internal/taobao/cdp.go
- 基于 gorilla/websocket 的最小 CDP 客户端(该依赖原本就在模块图里,
  由 Wails 引入,此处仅从 indirect 提为直接依赖)
- CookieNames 的结构体只声明 Name 和 Domain,Cookie 的值根本不会
  进入内存,比"取到但不使用"更彻底

internal/taobao/login.go
- 两层判定:四个必需 Cookie 齐全,且我的淘宝页面标题与正文不含
  六个阻断词
- 昵称只作展示,不参与判定。这是对参考 Python 实现的一处有意偏离:
  参考实现要求解析出昵称才算有效,但该解析依赖淘宝页面文案,
  淘宝一改版就会永远解析不到,导致登录永远判为无效、全局停止门
  永久触发、工具彻底不可用。放宽的代价可控:误判为已登录时后续
  MTOP 请求会失败,那条链路本就有 token 失效处理。

app.go 只在点击按钮时启动 Chrome,startup 不自动启动。

前端:设置页两个按钮接真实方法并显示检查结果;侧栏淘宝状态灯
监听 taobao:status 事件。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbdtsD3ohhSMy3KPoCgARq
2026-09-03 09:13:40 +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 d5e32f4d2b fix: 下载数据点击后无反馈,且大店铺会被分页上限截断 (#8)
负责人反馈「点击下载数据没反应」。用真实登录态做只读诊断后确认
后端正常:ListShops 返回 19 个店铺,getPage 返回 total=4304。
问题有两个,都在前端反馈和分页参数上。

1. 前端没有任何运行状态
   downloadProductData 是个纯 await,期间按钮不禁用、不转圈、不提示。
   商品多的店铺要跑几十秒到几分钟,看起来就像点了没用。
   改为:未选店铺时先给出提示不发请求;开始时提示「进度见运行日志」;
   按钮显示 loading 并互斥禁用;失败用 error 而不是 warning。
   下载视频、上传数据两个按钮同样处理。

2. 每页 20 条导致大店铺超出 200 页上限被截断
   实测同一店铺不同 size 的表现:
     size=20   216 页  506ms/页   约 108 秒
     size=200   22 页  836ms/页   约 18 秒
     size=500    9 页  1.48s/页   约 13 秒
   4304 个商品按 20 条一页是 216 页,超过 maximumProductPages=200,
   会在 4000 条处截断并只记一条警告。
   改为每页 200 条:耗时降到约六分之一,且上限对应 4 万商品,
   远超实际店铺规模。选 200 而非 500 是响应体积与速度的平衡。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbdtsD3ohhSMy3KPoCgARq
2026-09-02 17:43:25 +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 5524fe958c feat: 店铺列表与商品数据下载 (#8)
由 Codex (gpt-5.6-sol) 实施,Claude 审核。

实现以真实抓包为准,纠正了 Python 参考实现与接口文档的三处错误:

1. erp/shop/all 请求体是 userId=(空值),不是文档写的 type=1
2. product/shop/getPage 用 form 编码,不是 JSON
3. 两个接口的响应都没有 {type, message, code, bean} 包裹:
   shop/all 返回裸数组,getPage 直接返回分页对象

internal/huohanhan/shop.go
- ListShops 解析裸数组,只保留 platform=="0" 的 Shopee 店铺,
  店铺名去首尾空格后按名称排序
- Shop 结构体只声明界面需要的 8 个字段。货憨憨返回的
  accessToken、refreshToken 是 Shopee OAuth 凭据,createUser 是手机号,
  一律不解析、不记录、不存储、不返回前端;有测试用反射与 JSON
  序列化双重断言

internal/huohanhan/product.go
- GetProductPage 发送完整表单,自定义 UnmarshalJSON 兼容
  total/size/current/pages 既可能是数字也可能是字符串
- DownloadAllProducts 按页拉全量,带 200 页上限保护防止 pages
  异常导致死循环,达上限记警告而非静默截断

app.go
- ListShops、DownloadProductData 换成真实实现
- 未选店铺时返回「请先选择店铺」。该账号有 19 个店铺,
  单店铺 1256 个商品 63 页,默认拉全部会非常慢

前端
- 店铺加载失败不再静默吞掉,改为可见提示
- 「下载数据」传当前店铺 ID,完成后刷新列表

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbdtsD3ohhSMy3KPoCgARq
2026-09-02 17:16:41 +08:00
QiuSWandClaude Opus 5 d257752441 feat: 货憨憨登录与认证复用 (#7)
由 Codex (gpt-5.6-sol) 实施,Claude 审核。

internal/huohanhan/auth.go
- 六步登录:取 CID/CST 作 Basic 认证 → getCltConf 取 clientId →
  下载验证码 → OCR 识别 → 提交登录 → app-version/info 在线验证
- OCR 响应递归查找 4~8 位验证码,优先 text/result/data/content/captcha/code
- 错误分支:invalid_verify_code 换图重试(上限可配,默认 3);
  invalid_credentials 与 disabled_credentials 直接报中文错误不重试
- 认证复用改用 SQLite kv 表 + sync.Mutex,不照搬 Python 版的 Redis
- GetValidAuth 按「内存 → SQLite 在线验证 → 重新登录」顺序取认证

internal/huohanhan/client.go
- 统一请求方法,自动带 Authorization 与 cookies
- 认证失败时清状态、重登、最多重试一次;超时、500、普通业务错误不重试

app.go
- 新增 TestHuohanhanLogin,供设置页「测试连接」调用

测试全部使用 httptest 假服务,不需要真实账号即可运行。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbdtsD3ohhSMy3KPoCgARq
2026-09-02 16:45:02 +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