fix(status): ignore promotion edit warnings
This commit is contained in:
@@ -18,6 +18,7 @@ from .config import make_slug
|
||||
DEFAULT_BUSY_TIMEOUT_MS = 5000
|
||||
PRODUCT_STATUS_FEATURE_INTRODUCED_AT = "2026-07-18T16:34:37"
|
||||
LEGACY_PRODUCT_STATUS_DEFAULT_NOTE = "历史批次默认按架上商品处理(未实时检测)"
|
||||
LEGACY_PROMOTION_STATUS_REPAIR_NOTE = "历史商品状态修正:促销编辑提示不属于商品状态异常"
|
||||
VALID_BATCH_FIELDS = {
|
||||
"source_files_json",
|
||||
"status",
|
||||
@@ -501,6 +502,7 @@ def init_db(path=None, conn=None) -> None:
|
||||
_ensure_task_product_status_columns(database)
|
||||
_ensure_task_delete_columns(database)
|
||||
_migrate_legacy_product_status_defaults(database)
|
||||
_repair_legacy_promotion_status_defaults(database)
|
||||
_ensure_image_studio_project_suite_columns(database)
|
||||
_ensure_image_studio_project_draft_columns(database)
|
||||
_ensure_image_studio_job_recovery_columns(database)
|
||||
@@ -578,6 +580,42 @@ def _migrate_legacy_product_status_defaults(database):
|
||||
)
|
||||
|
||||
|
||||
def _repair_legacy_promotion_status_defaults(database):
|
||||
"""Repair only legacy unknown snapshots caused by a known benign warning."""
|
||||
|
||||
rows = database.execute(
|
||||
"""
|
||||
SELECT tasks.id, tasks.product_status_note
|
||||
FROM tasks
|
||||
INNER JOIN batches ON batches.id = tasks.batch_id
|
||||
WHERE tasks.product_status = 'unknown'
|
||||
AND tasks.deleted_at IS NULL
|
||||
AND batches.deleted_at IS NULL
|
||||
AND batches.created_at < ?
|
||||
""",
|
||||
(PRODUCT_STATUS_FEATURE_INTRODUCED_AT,),
|
||||
).fetchall()
|
||||
task_ids = [
|
||||
int(row["id"])
|
||||
for row in rows
|
||||
if product_status.is_promotion_edit_restriction_note(row["product_status_note"])
|
||||
]
|
||||
if not task_ids:
|
||||
return
|
||||
|
||||
placeholders = ", ".join("?" for _task_id in task_ids)
|
||||
database.execute(
|
||||
f"""
|
||||
UPDATE tasks
|
||||
SET product_status = 'normal',
|
||||
product_status_note = ?,
|
||||
updated_at = ?
|
||||
WHERE id IN ({placeholders})
|
||||
""",
|
||||
(LEGACY_PROMOTION_STATUS_REPAIR_NOTE, _now(), *task_ids),
|
||||
)
|
||||
|
||||
|
||||
def _ensure_image_studio_project_suite_columns(database):
|
||||
columns = {
|
||||
row["name"]
|
||||
|
||||
@@ -669,7 +669,7 @@ class ApplyTab(QWidget):
|
||||
if status_text:
|
||||
lines.append("当前筛选结果没有状态正常且可更新的商品。")
|
||||
lines.append(status_text)
|
||||
lines.append("请先回到①导入采集重新确认商品状态。")
|
||||
lines.append("未上架、审核中或状态未知的商品为安全起见不会更新。")
|
||||
content_error = self._update_content_error(update_plan, update_mode)
|
||||
if content_error:
|
||||
if lines:
|
||||
|
||||
+28
-4
@@ -56,6 +56,10 @@ COLLECT_SCOPE_ALL = SCOPE_ALL
|
||||
|
||||
_WHITESPACE_RE = re.compile(r"\s+")
|
||||
_NOTE_LIMIT = 2000
|
||||
_PROMOTION_EDIT_RESTRICTION_MARKERS = (
|
||||
("促銷", "無法進行編輯"),
|
||||
("促销", "无法进行编辑"),
|
||||
)
|
||||
|
||||
|
||||
def normalize_status(value) -> str:
|
||||
@@ -97,6 +101,22 @@ def normalize_text(value) -> str:
|
||||
return _WHITESPACE_RE.sub(" ", str(value or "").strip())
|
||||
|
||||
|
||||
def is_promotion_edit_restriction_alert(title, description="") -> bool:
|
||||
"""Return whether an alert only describes a promotion editing limitation."""
|
||||
|
||||
text = normalize_text(f"{title or ''} {description or ''}")
|
||||
return any(
|
||||
promotion_marker in text and edit_marker in text
|
||||
for promotion_marker, edit_marker in _PROMOTION_EDIT_RESTRICTION_MARKERS
|
||||
)
|
||||
|
||||
|
||||
def is_promotion_edit_restriction_note(note) -> bool:
|
||||
"""Recognize the persisted summary generated from a known benign alert."""
|
||||
|
||||
return is_promotion_edit_restriction_alert(note)
|
||||
|
||||
|
||||
def classify_alerts(alerts) -> dict:
|
||||
"""Classify normalized EDS warning-alert data without retaining page HTML."""
|
||||
|
||||
@@ -105,8 +125,8 @@ def classify_alerts(alerts) -> dict:
|
||||
if not alerts:
|
||||
return _snapshot(STATUS_NORMAL, "")
|
||||
|
||||
first_note = ""
|
||||
valid_alert_seen = False
|
||||
unknown_alert_note = ""
|
||||
for alert in alerts:
|
||||
if not isinstance(alert, dict):
|
||||
continue
|
||||
@@ -114,16 +134,20 @@ def classify_alerts(alerts) -> dict:
|
||||
title = normalize_text(alert.get("title"))
|
||||
description = normalize_text(alert.get("description"))
|
||||
note = _alert_note(title, description)
|
||||
if not first_note:
|
||||
first_note = note
|
||||
if title in {"審核中", "审核中"}:
|
||||
return _snapshot(STATUS_REVIEWING, note)
|
||||
if "您的商品未上架" in title:
|
||||
return _snapshot(STATUS_UNLISTED, note)
|
||||
if is_promotion_edit_restriction_alert(title, description):
|
||||
continue
|
||||
if not unknown_alert_note:
|
||||
unknown_alert_note = note
|
||||
|
||||
if not valid_alert_seen:
|
||||
return _snapshot(STATUS_UNKNOWN, "")
|
||||
return _snapshot(STATUS_UNKNOWN, first_note)
|
||||
if not unknown_alert_note:
|
||||
return _snapshot(STATUS_NORMAL, "")
|
||||
return _snapshot(STATUS_UNKNOWN, unknown_alert_note)
|
||||
|
||||
|
||||
def partition_tasks(tasks) -> dict:
|
||||
|
||||
@@ -423,9 +423,9 @@ data/images/<batch_id>/<slug>/<task_id>_<item_id>_new.<ext> # AI 生成的新
|
||||
- ①采集调用 `open_product(..., bring_to_front=False)`,不主动执行 `Page.bringToFront`;新建商品 tab 时尝试 `Target.createTarget(background=true)` 降低 Chrome 抢焦点概率,若当前 Chrome/CDP 不接受该参数则退回普通新建 tab。③更新是上传、拖拽和线上提交流程,每条任务均以前台方式打开或激活当前商品 tab,优先保障页面交互稳定;后台态失败安全恢复逻辑仅为兼容直接调用保留,正常③批量路径不依赖它。
|
||||
- 采集前和采集中途的登录检测必须区分“明确未登录”和“暂时不确定”。明确 `LOGIN_PAGE` / 登录页 URL 才整组略过该账号后续任务;`NO_SESSION_COOKIE`、检测超时或 CDP 短暂异常只记录为不确定并继续尝试采集当前商品,不得级联跳过同账号剩余任务。Cookie API 调用失败不能伪装成空 Cookie;登录检测若连到正在关闭的旧商品 target,应在本次检测预算内重新枚举并改连其他有效 Shopee 页面。
|
||||
- 若商品 ID 失效、无权限或店铺不匹配导致商品编辑页无法就绪,`open_product` 必须读取/捕获 Shopee toast,把最近错误文案写入采集失败原因和诊断日志,不能只返回泛化超时。①列表只在明确捕获商品失效类 toast 时把“阶段”显示为“商品失效”;底层 `stage` 不新增中文值。若这个失败发生在后台只读、程序自动新建的商品 tab 内,`open_product` 要关闭并执行同样的有界 target 消失确认;复用用户已有 tab 不关闭。③前台更新打开失败仍沿用既有清理路径,不引入额外等待。
|
||||
- 商品状态只读取 `.eds-alert.eds-alert--warning` 内的 `.eds-alert-title/.eds-alert-desc`,不得依赖 Vue `data-v-*`。`審核中/审核中` 归为 `reviewing`,`您的商品未上架` 归为 `unlisted`,没有 warning 为 `normal`,未识别横幅、DOM 异常或非法返回为 `unknown`。保存归一化摘要而非页面 HTML;状态探测异常只写脱敏诊断,不伪造技术采集失败。
|
||||
- 商品状态只读取 `.eds-alert.eds-alert--warning` 内的 `.eds-alert-title/.eds-alert-desc`,不得依赖 Vue `data-v-*`。`審核中/审核中` 归为 `reviewing`,`您的商品未上架` 归为 `unlisted`;已知的「促销期间部分字段无法编辑」横幅不是商品生命周期状态,单独出现时仍归为 `normal`。没有 warning 为 `normal`,其他未识别横幅、DOM 异常或非法返回为 `unknown`。保存归一化摘要而非页面 HTML;状态探测异常只写脱敏诊断,不伪造技术采集失败。
|
||||
- 每次点击采集先在主线程选择本轮范围,策略值为 `normal_only/all`,默认 `normal_only`,不跨轮记忆。两种范围都会逐条重新打开页面检测并独立保存状态,不能用旧数据库状态预过滤;`normal_only` 仅正常商品读取标题和封面,其他三类状态记为业务略过并保留旧内容,`all` 则四类状态均走现有采集流程。范围略过不是技术失败,汇总分别统计状态和略过数。
|
||||
- 不提供历史商品状态重检按钮或批量打开详情页的补检流程。历史默认迁移只在数据库初始化时执行,条件为活动批次创建时间严格早于上线时间且状态为空;已有四类状态、软删除批次、上线时点及之后的空状态均不改写。
|
||||
- 不提供历史商品状态重检按钮或批量打开详情页的补检流程。历史默认迁移只在数据库初始化时执行,条件为活动批次创建时间严格早于上线时间且状态为空;另仅修复同一范围内由已知促销编辑提示误写的 `unknown`,保留原检测时间并注明历史修正。其他已有四类状态、软删除批次、上线时点及之后的空状态均不改写。
|
||||
|
||||
- 旧封面:取第一张 itembox 的 `img.src`(CDN 链接),下载到 `data/images/<batch_id>/<slug>/<task_id>_<item_id>_old.jpg`。
|
||||
- 写 `old_title/old_cover_path`、stage=collected;批量回写 Excel 旧字段。
|
||||
@@ -517,7 +517,7 @@ data/images/<batch_id>/<slug>/<task_id>_<item_id>_new.<ext> # AI 生成的新
|
||||
| 前台激活 | ①采集和商品套图只读打开商品页时不主动 `Page.bringToFront`;新建 tab 尝试 `Target.createTarget(background=true)`,不支持时退回普通新建。③更新真实提交每条任务都以前台方式新建或激活商品 tab,并执行 `Page.bringToFront`,保障上传、图片管理器刷新和拖拽排序稳定;后台态封面恢复逻辑仅保留给兼容直接调用,不作为正常③批量路径 |
|
||||
| SPA 就绪 | 不用 load 事件;轮询“唯一商品名称输入框 + 至少一张主图 itembox + 唯一主图上传输入框”三者都在。脚本返回标题命中数、主图/上传入口状态和当前 URL 的就绪快照;超时错误必须指出具体缺失组件,不能只报泛化超时 |
|
||||
| 商品页错误 toast | Shopee 错误提示使用 `.eds-toasts` / `.eds-toast__content`,可能很快隐藏或 `display:none`。打开商品页/等待 SPA 就绪前应注入 `MutationObserver` 或等价监听,把 toast 文本、`outerHTML`、当前 URL、时间、可见状态保存到页面缓存(如 `window.__cmshopee_toasts`);等待详情页关键元素超时时,再兜底读取当前 DOM 中的 toast。明确商品失效/不存在/无权限类 toast 即使已经隐藏,也优先成为 `open_product` 失败原因并驱动①阶段列显示“商品失效”;其他普通 toast 只有仍可见且属于当前页面 URL 时,才以“页面提示(可能无关)”附加在就绪快照后。已隐藏的物流、备货、库存、价格等编辑校验提示不得覆盖真正缺失的就绪组件;网络、CDP、未登录、页面超时、风控等其他失败仍显示“失败” |
|
||||
| 商品状态警示 | 编辑页状态只读取 `.eds-alert.eds-alert--warning` 内的 `.eds-alert-title/.eds-alert-desc`,不得使用 `data-v-*`。`審核中/审核中` → `reviewing`,`您的商品未上架` → `unlisted`;无 warning → `normal`;未识别 warning、DOM 异常或非法响应 → `unknown`。多个横幅按 DOM 顺序取第一个白名单命中;只保存归一化文本摘要,不保存整段 HTML |
|
||||
| 商品状态警示 | 编辑页状态只读取 `.eds-alert.eds-alert--warning` 内的 `.eds-alert-title/.eds-alert-desc`,不得使用 `data-v-*`。`審核中/审核中` → `reviewing`,`您的商品未上架` → `unlisted`;已知促销字段编辑限制不构成商品状态;无状态横幅 → `normal`;其他未识别 warning、DOM 异常或非法响应 → `unknown`。多个横幅仍优先取审核中/未上架;只保存归一化文本摘要,不保存整段 HTML |
|
||||
|
||||
| 标题输入框 | 主定位为 `data-product-edit-field-unique-id="name"` 业务字段内唯一可见 `input.eds-input__input`,不再用标题字符数判断身份;仅当该业务字段根不存在时,才回退旧 XPath `//input[@class='eds-input__input' and string-length(@modelvalue)>24]`。主定位命中多个时明确失败,不猜测写入 |
|
||||
| 写标题 | 原生 setter + 派发 `input`/`change`;`value`==`modelvalue`==新值 |
|
||||
|
||||
+2
-2
@@ -524,7 +524,7 @@ T-523 后 GUI 已从旧 `app/gui.py` 拆为 `app/gui/` 包:`__init__.py` 负
|
||||
- 任务列表使用 `QTableView + TaskTableModel`,列为:账号、别名、商品ID、阶段。
|
||||
- 账号列优先显示匹配到的 `accounts.account_name`;未匹配账号时保留 Excel 输入账号名。
|
||||
- 别名未匹配 `accounts.alias` 时列表阶段列显示“略过”;点击「采集旧标题/旧封面」后由 `CollectWorker` 逐条写库为 `skipped`,原因 `别名未匹配账号`。
|
||||
- 「采集旧标题/旧封面」先弹 `ProductStatusScopeDialog` 范围确认框,默认按钮为“采集架上商品”(稳定策略 `normal_only`,仅继续采集检测结果为正常的商品),扩展按钮为“采集全部商品”(`all`,还包括未上架、审核中和状态未知商品);后者是警示橙色的扩大范围操作而非删除操作。对话框最小宽度 520px,三个中文选项纵向全宽显示,取消不创建 Worker。`CollectWorker` 只处理 `stage=imported` 的任务;采集前先做账号就绪预检。无账号、当前批次匹配账号未启动 CDP 端口或未登录时,返回 `blocked=True`,GUI 弹窗汇总并跳转/引导去账号管理,不进入逐条采集、不写 skipped/failed。预检通过后,已匹配任务调用 `editor.collect()` 先检测和保存页面状态;默认范围仅正常商品下载旧封面到 `image_dir/<batch_id>/<slug>/<task_id>_<item_id>_old.jpg` 并 `db.set_collected()`,其他状态 `set_product_status()` 后按范围 `mark_skipped` 且保留原旧内容;选择全部范围时四类状态均采集。别名未匹配任务仍逐条 `mark_skipped`;单条技术失败 `mark_failed(..., "collect", error)` 后继续。采集完成且本轮成功采集数量大于 0 时,自动触发当前批次旧字段回写;锁文件失败时只提示,不回滚 SQLite。商品状态功能上线时间前的活动历史批次空状态由 `init_db()` 默认迁移为 `normal`,不打开 Chrome、不创建 Worker;上线时点及之后的缺失状态仍保持未知语义。
|
||||
- 「采集旧标题/旧封面」先弹 `ProductStatusScopeDialog` 范围确认框,默认按钮为“采集架上商品”(稳定策略 `normal_only`,仅继续采集检测结果为正常的商品),扩展按钮为“采集全部商品”(`all`,还包括未上架、审核中和状态未知商品);后者是警示橙色的扩大范围操作而非删除操作。对话框最小宽度 520px,三个中文选项纵向全宽显示,取消不创建 Worker。`CollectWorker` 只处理 `stage=imported` 的任务;采集前先做账号就绪预检。无账号、当前批次匹配账号未启动 CDP 端口或未登录时,返回 `blocked=True`,GUI 弹窗汇总并跳转/引导去账号管理,不进入逐条采集、不写 skipped/failed。预检通过后,已匹配任务调用 `editor.collect()` 先检测和保存页面状态;默认范围仅正常商品下载旧封面到 `image_dir/<batch_id>/<slug>/<task_id>_<item_id>_old.jpg` 并 `db.set_collected()`,其他状态 `set_product_status()` 后按范围 `mark_skipped` 且保留原旧内容;选择全部范围时四类状态均采集。别名未匹配任务仍逐条 `mark_skipped`;单条技术失败 `mark_failed(..., "collect", error)` 后继续。采集完成且本轮成功采集数量大于 0 时,自动触发当前批次旧字段回写;锁文件失败时只提示,不回滚 SQLite。商品状态功能上线时间前的活动历史批次空状态由 `init_db()` 默认迁移为 `normal`;同一范围内仅修复由已知促销编辑提示误写的 `unknown`,不打开 Chrome、不创建 Worker;上线时点及之后的缺失状态和其他未知状态仍保持未知语义。
|
||||
- 采集打开商品页时,若本轮自动新建 tab,采集完成后会关闭该 tab,并在最多 2 秒内确认 target 已从 `/json` 消失;确认超时只写 warning/诊断,不把采集成功改成失败。若失败发生在 `open_product()` 内部且尚未返回 `cdp`,也要关闭本轮自动新建 tab;若复用用户已打开的商品页,只断开 CDP 连接不关闭页面。
|
||||
- 采集中途登录检测必须快速跳过正在销毁的旧商品 target,改连其他有效 Shopee 页面。Cookie API 调用失败返回 `LOGIN_CHECK_TARGET_UNAVAILABLE`,只有 Cookie API 成功返回空会话时才返回 `NO_SESSION_COOKIE`;两者都不按明确掉登录批量略过,显式 `LOGIN_PAGE` 仍按账号需登录处理。retry/recovered 运行日志包含当前任务 ID 和商品 ID,避免与上一条采集成功日志混淆。
|
||||
- 采集打开商品页失败时,`CollectWorker` 应把 `open_product()` 捕获到的 Shopee toast 文案写入 `run_log_events` 和 `tasks.last_error`;商品 ID 失效、无权限、店铺不匹配等场景不得只显示泛化超时。① `TaskTableModel` 的“阶段”列只在 `last_error` 明确为商品失效类错误时显示“商品失效”,否则仍按 `status=failed` 显示“失败”;底层不新增 `stage` 枚举。
|
||||
@@ -550,7 +550,7 @@ T-523 后 GUI 已从旧 `app/gui.py` 拆为 `app/gui/` 包:`__init__.py` 负
|
||||
③ 更新蝦皮当前要点(T-303b/T-401/T-402/T-403):
|
||||
|
||||
- `ApplyTab` 顶部筛选栏包含:批次、店铺、商品ID、状态、刷新。批次来自 `db.list_batches()`;店铺来自当前更新候选任务别名并优先显示匹配账号名;商品ID输入框按包含匹配 `item_id`,清空表示全部。
|
||||
- ③ 只列出已生成或可查看的更新候选任务:`stage=generated/applied`,或已有新字段且 `status=failed/skipped` 的任务;真正开始更新前先按 `product_status` 分出可进入线上更新的 `normal` 与必须排除的 `unlisted/reviewing/unknown`。商品状态功能上线前活动历史批次的空状态已启动时默认迁移为 `normal`;其它 NULL 仍归入未知,再按③「更新内容」模式分出可执行任务与缺少 `new_title` / `new_cover_path` 的预检跳过记录。
|
||||
- ③ 只列出已生成或可查看的更新候选任务:`stage=generated/applied`,或已有新字段且 `status=failed/skipped` 的任务;真正开始更新前先按 `product_status` 分出可进入线上更新的 `normal` 与必须排除的 `unlisted/reviewing/unknown`。商品状态功能上线前活动历史批次的空状态已启动时默认迁移为 `normal`;同一范围内已知促销编辑提示误写的 `unknown` 会精确修正为 `normal`,其它 NULL 和未知状态仍归入未知,再按③「更新内容」模式分出可执行任务与缺少 `new_title` / `new_cover_path` 的预检跳过记录。
|
||||
- 状态筛选支持:已生成(默认,`stage=generated` 且 `status=success/pending`)、失败、已更新、略过、全部状态。
|
||||
- 任务列表使用 `QTableView + ApplyTaskTableModel`,列为:店铺、商品ID、新标题、新封面、阶段、结果。
|
||||
- 「开始更新」只读取当前筛选结果;无任务时只提示,不弹确认、不改库;该按钮是③的主操作,视觉上强于检查、停止和回写。
|
||||
|
||||
+1
-1
@@ -74,7 +74,7 @@
|
||||
- **导入汇总栏**(导入后即时刷新,跑采集前的校验关口):显示 文件数、解析行数(原始数据量)、有效/无效行、匹配账号行数(按账号细分)、未匹配行数。未匹配/无效数字标红可点,点击在列表筛出便于定位纠错。
|
||||
- 采集:点击后先从专用范围选择框选择范围,默认「采集架上商品」(即检测结果为正常),「采集全部商品」为警示橙色描边而非删除红色,包含未上架、审核中和状态未知商品;三个选项纵向全宽显示,常见 Windows 缩放下不截断。随后为本轮匹配账号确保 Chrome 就绪(已开复用、未开启动),再检测登录;明确未登录账号的任务整组略过并汇总提示。`NO_SESSION_COOKIE`、登录检测超时或 CDP 短暂异常会重试,连续不确定时不批量略过,继续打开商品页由真实页面结果决定成功/失败。登录账号用对应 Chrome 只读打开商品页,先检测并保存商品状态;默认范围下只有正常商品才读旧标题、下载旧封面到 `data/images/<batch_id>/<slug>/<task_id>_<item_id>_old.jpg`,未上架、审核中和状态未知商品按范围略过且不覆盖已有内容。选择全部范围时四类商品均继续采集。采集不主动把商品页切到前台;程序自动新建商品页 tab 时尽量后台创建,采集结束后自动关闭;若复用用户原本打开的 tab,则不关闭。采集结束不关闭账号 Chrome,用户可自行关闭。
|
||||
- ①任务表显示账号、别名、商品 ID、阶段和商品状态。商品状态筛选位于「处理状态」筛选左侧,选项为全部商品状态、架上商品、未上架、审核中、状态未知、待检测;它与批次、店铺、商品 ID 和处理状态叠加。空状态在①显示为「待检测」,显式 `unknown` 显示为「状态未知」;历史默认的架上商品可在 tooltip 查看“未实时检测”备注。商品状态只影响列表可见范围,不改变采集、生成或更新的安全规则。
|
||||
- 商品状态功能于 `2026-07-18T16:34:37` 上线。更早创建的活动历史批次中,空状态在启动数据库时自动按「架上商品」写为 `normal`,备注「历史批次默认按架上商品处理(未实时检测)」且不写检测时间;不打开 Chrome、不访问详情页。上线时点及之后的空状态仍按未知处理,真实保存的未上架、审核中、状态未知也绝不覆盖。①不提供「重新检测商品状态」入口。
|
||||
- 商品状态功能于 `2026-07-18T16:34:37` 上线。更早创建的活动历史批次中,空状态在启动数据库时自动按「架上商品」写为 `normal`,备注「历史批次默认按架上商品处理(未实时检测)」且不写检测时间;同一范围内仅把由已知促销编辑提示误写的 `unknown` 修正为 `normal`,保留原检测时间并写明历史修正。其他真实保存的未上架、审核中、状态未知,以及上线时点及之后的空状态均不覆盖;不打开 Chrome、不访问详情页。①不提供「重新检测商品状态」入口。
|
||||
- 若商品 ID 已失效、无权限或店铺不匹配,Shopee 可能只弹出短暂错误 toast;采集失败时界面日志应显示捕获到的 toast 文案,并把 toast HTML/URL 写入本地诊断日志,避免用户手动抢复制。只有明确捕获商品失效/商品不存在/无权限类 toast 时,①列表“阶段”列显示“商品失效”;其他商品页打开失败仍显示“失败”。如果失败发生在 `open_product()` 内部,本轮自动新建的商品 tab 必须关闭,复用用户已有 tab 不关闭。
|
||||
|
||||
- 回写:采集完成后自动把旧标题/旧封面路径批量回写原 Excel;保留「回写旧数据到 Excel」作为手动重试入口(原文件被锁→提示关闭后重试/另存)。
|
||||
|
||||
+6
-2
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: T-673
|
||||
title: 促销编辑提示商品状态误判修复
|
||||
status: TODO
|
||||
status: DONE
|
||||
phase: 7
|
||||
deps: [T-667, T-672]
|
||||
created: 2026-07-20
|
||||
@@ -56,4 +56,8 @@ git diff --check
|
||||
|
||||
## 执行记录
|
||||
|
||||
- 待实现。
|
||||
- 在 `product_status.classify_alerts()` 增加繁简促销编辑提示识别:该提示单独出现时按 `normal`,同页出现「审核中」或「您的商品未上架」仍优先返回真实异常状态;其他未识别横幅、DOM 异常和非法返回仍为 `unknown`。
|
||||
- 在 `db.init_db()` 增加幂等精确修复:仅处理商品状态功能上线前、未软删除批次中由该促销提示误写的 `unknown`,修正为 `normal`、保留原检测时间并写入历史修正备注;其他未知、已确认异常、上线后批次和软删除数据均不改写。
|
||||
- ③状态拦截提示改为中文安全说明,不再引导用户使用已移除的「重新确认商品状态」入口;同步更新架构、路由和 API 文档。
|
||||
- 已对本机运行时数据库执行初始化迁移核验:批次 `20260710_105614_09b176d6` 的 13 条促销误判记录均已修正为 `normal`;只更新封面预检结果为候选 13 条、可执行 13 条、状态排除 0 条、缺少封面 0 条。未打开 Chrome、未调用 AI、未提交线上更新。
|
||||
- 验证通过:`py -3.10 -m unittest discover -s tests -p "test_product_status.py"`(7 项)、`py -3.10 -m unittest discover -s tests -p "test_db.py"`(21 项)、`py -3.10 -m unittest discover -s tests -p "test_gui.py"`(206 项)、`py -3.10 -m unittest discover -s tests`(625 项)、`py -3.10 -m ruff check app tests main.py`、`py -3.10 -m compileall app main.py`、`git diff --check`。
|
||||
|
||||
+143
-1
@@ -7,7 +7,7 @@ sys.path.insert(0, os.path.dirname(__file__))
|
||||
|
||||
from _helpers import TempDirMixin
|
||||
|
||||
from app import db
|
||||
from app import db, product_status
|
||||
|
||||
|
||||
class DbTests(TempDirMixin, unittest.TestCase):
|
||||
@@ -259,6 +259,148 @@ class DbTests(TempDirMixin, unittest.TestCase):
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_init_db_repairs_only_legacy_promotion_status_misclassification(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
db_path = os.path.join(temp_dir, "cmshopee.db")
|
||||
db.init_db(db_path)
|
||||
batch_ids = {}
|
||||
for index, name in enumerate(
|
||||
(
|
||||
"legacy-promotion",
|
||||
"legacy-unknown",
|
||||
"after-promotion",
|
||||
"legacy-unlisted",
|
||||
"deleted-promotion",
|
||||
),
|
||||
start=1,
|
||||
):
|
||||
batch_id = db.create_batch([f"{name}.xlsx"], path=db_path)
|
||||
batch_ids[name] = batch_id
|
||||
db.insert_tasks(
|
||||
batch_id,
|
||||
[
|
||||
{
|
||||
"source_file_abs": os.path.join(temp_dir, f"{name}.xlsx"),
|
||||
"source_sheet": "Sheet1",
|
||||
"source_row": 2,
|
||||
"account_name": "店铺",
|
||||
"alias": "alias",
|
||||
"item_id": str(51100639800 + index),
|
||||
}
|
||||
],
|
||||
path=db_path,
|
||||
)
|
||||
|
||||
promotion_note = (
|
||||
"标题:由於正在進行促銷,以下一些欄位無法進行編輯,"
|
||||
"數值將顯示為灰色。"
|
||||
)
|
||||
conn = db.connect(db_path)
|
||||
try:
|
||||
with conn:
|
||||
legacy_ids = [
|
||||
batch_ids["legacy-promotion"],
|
||||
batch_ids["legacy-unknown"],
|
||||
batch_ids["legacy-unlisted"],
|
||||
batch_ids["deleted-promotion"],
|
||||
]
|
||||
placeholders = ", ".join("?" for _batch_id in legacy_ids)
|
||||
conn.execute(
|
||||
f"UPDATE batches SET created_at = ? WHERE id IN ({placeholders})",
|
||||
("2026-07-18T16:34:36", *legacy_ids),
|
||||
)
|
||||
task_by_batch = {
|
||||
batch_id: db.list_tasks(batch_id=batch_id, conn=conn)[0]
|
||||
for batch_id in batch_ids.values()
|
||||
}
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE tasks
|
||||
SET product_status = 'unknown', product_status_note = ?,
|
||||
product_status_at = ?, new_cover_path = ?,
|
||||
stage = 'generated', status = 'success'
|
||||
WHERE id = ?
|
||||
""",
|
||||
(
|
||||
promotion_note,
|
||||
"2026-07-20T09:18:10",
|
||||
"new.jpg",
|
||||
task_by_batch[batch_ids["legacy-promotion"]].id,
|
||||
),
|
||||
)
|
||||
conn.execute(
|
||||
"UPDATE tasks SET product_status = 'unknown', product_status_note = ? WHERE id = ?",
|
||||
(
|
||||
"其他未知状态提示",
|
||||
task_by_batch[batch_ids["legacy-unknown"]].id,
|
||||
),
|
||||
)
|
||||
conn.execute(
|
||||
"UPDATE tasks SET product_status = 'unknown', product_status_note = ? WHERE id = ?",
|
||||
(
|
||||
promotion_note,
|
||||
task_by_batch[batch_ids["after-promotion"]].id,
|
||||
),
|
||||
)
|
||||
conn.execute(
|
||||
"UPDATE tasks SET product_status = 'unlisted', product_status_note = ? WHERE id = ?",
|
||||
(
|
||||
promotion_note,
|
||||
task_by_batch[batch_ids["legacy-unlisted"]].id,
|
||||
),
|
||||
)
|
||||
conn.execute(
|
||||
"UPDATE tasks SET product_status = 'unknown', product_status_note = ? WHERE id = ?",
|
||||
(
|
||||
promotion_note,
|
||||
task_by_batch[batch_ids["deleted-promotion"]].id,
|
||||
),
|
||||
)
|
||||
conn.execute(
|
||||
"UPDATE batches SET deleted_at = ? WHERE id = ?",
|
||||
("2026-07-19T00:00:00", batch_ids["deleted-promotion"]),
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
db.init_db(db_path)
|
||||
legacy_promotion = db.list_tasks(
|
||||
batch_id=batch_ids["legacy-promotion"], path=db_path
|
||||
)[0]
|
||||
legacy_unknown = db.list_tasks(
|
||||
batch_id=batch_ids["legacy-unknown"], path=db_path
|
||||
)[0]
|
||||
after_promotion = db.list_tasks(
|
||||
batch_id=batch_ids["after-promotion"], path=db_path
|
||||
)[0]
|
||||
legacy_unlisted = db.list_tasks(
|
||||
batch_id=batch_ids["legacy-unlisted"], path=db_path
|
||||
)[0]
|
||||
deleted_promotion = db.list_tasks(
|
||||
batch_id=batch_ids["deleted-promotion"],
|
||||
include_deleted=True,
|
||||
path=db_path,
|
||||
)[0]
|
||||
|
||||
self.assertEqual("normal", legacy_promotion.product_status)
|
||||
self.assertEqual(db.LEGACY_PROMOTION_STATUS_REPAIR_NOTE, legacy_promotion.product_status_note)
|
||||
self.assertEqual("2026-07-20T09:18:10", legacy_promotion.product_status_at)
|
||||
cover_plan = product_status.build_apply_plan([legacy_promotion], "cover")
|
||||
self.assertEqual([legacy_promotion.id], [task.id for task in cover_plan["executable"]])
|
||||
self.assertEqual("unknown", legacy_unknown.product_status)
|
||||
self.assertEqual("unknown", after_promotion.product_status)
|
||||
self.assertEqual("unlisted", legacy_unlisted.product_status)
|
||||
self.assertEqual("unknown", deleted_promotion.product_status)
|
||||
|
||||
first_updated_at = legacy_promotion.updated_at
|
||||
db.init_db(db_path)
|
||||
self.assertEqual(
|
||||
first_updated_at,
|
||||
db.get_task(legacy_promotion.id, path=db_path).updated_at,
|
||||
)
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_account_batch_task_lifecycle(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
db_path = os.path.join(temp_dir, "cmshopee.db")
|
||||
|
||||
@@ -7253,6 +7253,8 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
self.assertIn("审核中:1 条", message)
|
||||
self.assertIn("状态未知:1 条", message)
|
||||
self.assertNotIn("缺少新标题", message)
|
||||
self.assertIn("为安全起见不会更新", message)
|
||||
self.assertNotIn("重新确认商品状态", message)
|
||||
question.assert_not_called()
|
||||
run_worker.assert_not_called()
|
||||
self.assertIn("状态正常且可更新", states[-1])
|
||||
|
||||
@@ -53,6 +53,35 @@ class ProductStatusTests(unittest.TestCase):
|
||||
self.assertEqual(product_status.STATUS_UNLISTED, result["product_status"])
|
||||
self.assertIn("您的商品未上架", result["product_status_note"])
|
||||
|
||||
def test_promotion_edit_restriction_is_not_a_product_status_alert(self):
|
||||
traditional = {
|
||||
"title": "由於正在進行促銷,以下一些欄位無法進行編輯,數值將顯示為灰色。",
|
||||
"description": "",
|
||||
}
|
||||
simplified = {
|
||||
"title": "由于正在进行促销,以下一些字段无法进行编辑,数值将显示为灰色。",
|
||||
"description": "",
|
||||
}
|
||||
|
||||
for alert in (traditional, simplified):
|
||||
result = product_status.classify_alerts([alert])
|
||||
self.assertEqual(product_status.STATUS_NORMAL, result["product_status"])
|
||||
self.assertIsNone(result["product_status_note"])
|
||||
self.assertTrue(
|
||||
product_status.is_promotion_edit_restriction_note(
|
||||
f"标题:{alert['title']}"
|
||||
)
|
||||
)
|
||||
|
||||
reviewing = product_status.classify_alerts(
|
||||
[traditional, {"title": "审核中", "description": "等待审核"}]
|
||||
)
|
||||
unlisted = product_status.classify_alerts(
|
||||
[simplified, {"title": "您的商品未上架", "description": "已下架"}]
|
||||
)
|
||||
self.assertEqual(product_status.STATUS_REVIEWING, reviewing["product_status"])
|
||||
self.assertEqual(product_status.STATUS_UNLISTED, unlisted["product_status"])
|
||||
|
||||
def test_invalid_values_are_unknown_and_partitioning_is_consistent(self):
|
||||
tasks = [
|
||||
SimpleNamespace(product_status="normal"),
|
||||
|
||||
Reference in New Issue
Block a user