feat: 任务列表显示店铺名和执行用时 (#136)
This commit is contained in:
+13
-1
@@ -4,7 +4,7 @@
|
|||||||
``MIGRATIONS`` 末尾增加版本,不能修改已经发布的迁移。
|
``MIGRATIONS`` 末尾增加版本,不能修改已经发布的迁移。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
SCHEMA_VERSION = 4
|
SCHEMA_VERSION = 5
|
||||||
|
|
||||||
|
|
||||||
MIGRATION_1 = (
|
MIGRATION_1 = (
|
||||||
@@ -152,10 +152,22 @@ MIGRATION_4 = (
|
|||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
MIGRATION_5 = (
|
||||||
|
"ALTER TABLE pdd_tasks ADD COLUMN shop_name TEXT",
|
||||||
|
"""
|
||||||
|
UPDATE pdd_tasks
|
||||||
|
SET shop_name = TRIM(json_extract(pdd_data, '$.shop_name'))
|
||||||
|
WHERE pdd_data IS NOT NULL AND json_valid(pdd_data)
|
||||||
|
AND json_type(pdd_data, '$.shop_name') = 'text'
|
||||||
|
AND TRIM(json_extract(pdd_data, '$.shop_name')) <> ''
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
MIGRATIONS = {
|
MIGRATIONS = {
|
||||||
1: MIGRATION_1,
|
1: MIGRATION_1,
|
||||||
2: MIGRATION_2,
|
2: MIGRATION_2,
|
||||||
3: MIGRATION_3,
|
3: MIGRATION_3,
|
||||||
4: MIGRATION_4,
|
4: MIGRATION_4,
|
||||||
|
5: MIGRATION_5,
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-12
@@ -67,12 +67,15 @@ class TaskRow:
|
|||||||
task_type: str
|
task_type: str
|
||||||
goods_id: str = ""
|
goods_id: str = ""
|
||||||
title: str = ""
|
title: str = ""
|
||||||
|
shop_name: str = ""
|
||||||
color: str = ""
|
color: str = ""
|
||||||
size: str = ""
|
size: str = ""
|
||||||
price_cents: Optional[int] = None
|
price_cents: Optional[int] = None
|
||||||
price_is_starting: bool = False
|
price_is_starting: bool = False
|
||||||
quantity: Optional[int] = None
|
quantity: Optional[int] = None
|
||||||
status: str = "待执行"
|
status: str = "待执行"
|
||||||
|
latest_run_status: str = ""
|
||||||
|
duration_seconds: Optional[int] = None
|
||||||
updated_at: str = ""
|
updated_at: str = ""
|
||||||
|
|
||||||
|
|
||||||
@@ -86,11 +89,13 @@ class TaskTableModel(QAbstractTableModel):
|
|||||||
"选择",
|
"选择",
|
||||||
"任务类型",
|
"任务类型",
|
||||||
"商品标题",
|
"商品标题",
|
||||||
|
"店铺名",
|
||||||
"颜色",
|
"颜色",
|
||||||
"尺码",
|
"尺码",
|
||||||
"价格",
|
"价格",
|
||||||
"数量",
|
"数量",
|
||||||
"状态",
|
"状态",
|
||||||
|
"用时",
|
||||||
"更新时间",
|
"更新时间",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -133,9 +138,9 @@ class TaskTableModel(QAbstractTableModel):
|
|||||||
return self._display_value(task, column)
|
return self._display_value(task, column)
|
||||||
|
|
||||||
if role == Qt.TextAlignmentRole:
|
if role == Qt.TextAlignmentRole:
|
||||||
if column in (0, 1, 7):
|
if column in (0, 1, 8, 9):
|
||||||
return Qt.AlignCenter
|
return Qt.AlignCenter
|
||||||
if column in (5, 6):
|
if column in (6, 7):
|
||||||
return Qt.AlignRight | Qt.AlignVCenter
|
return Qt.AlignRight | Qt.AlignVCenter
|
||||||
return Qt.AlignLeft | Qt.AlignVCenter
|
return Qt.AlignLeft | Qt.AlignVCenter
|
||||||
|
|
||||||
@@ -367,19 +372,27 @@ class TaskTableModel(QAbstractTableModel):
|
|||||||
if column == 2:
|
if column == 2:
|
||||||
return task.title or "尚未获取标题"
|
return task.title or "尚未获取标题"
|
||||||
if column == 3:
|
if column == 3:
|
||||||
return task.color or "—"
|
return task.shop_name or "—"
|
||||||
if column == 4:
|
if column == 4:
|
||||||
return task.size or "—"
|
return task.color or "—"
|
||||||
if column == 5:
|
if column == 5:
|
||||||
|
return task.size or "—"
|
||||||
|
if column == 6:
|
||||||
if task.price_cents is None:
|
if task.price_cents is None:
|
||||||
return "—"
|
return "—"
|
||||||
suffix = " 起" if task.price_is_starting else ""
|
suffix = " 起" if task.price_is_starting else ""
|
||||||
return f"¥{task.price_cents / 100:.2f}{suffix}"
|
return f"¥{task.price_cents / 100:.2f}{suffix}"
|
||||||
if column == 6:
|
|
||||||
return "—" if task.quantity is None else str(task.quantity)
|
|
||||||
if column == 7:
|
if column == 7:
|
||||||
return task.status
|
return "—" if task.quantity is None else str(task.quantity)
|
||||||
if column == 8:
|
if column == 8:
|
||||||
|
return task.status
|
||||||
|
if column == 9:
|
||||||
|
if task.latest_run_status == "running":
|
||||||
|
return "进行中"
|
||||||
|
if task.duration_seconds is None:
|
||||||
|
return "—"
|
||||||
|
return f"{max(0, task.duration_seconds)} 秒"
|
||||||
|
if column == 10:
|
||||||
return task.updated_at or "—"
|
return task.updated_at or "—"
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@@ -567,11 +580,13 @@ class PDDTaskPage(QWidget):
|
|||||||
self.taskTable.setColumnWidth(0, 64)
|
self.taskTable.setColumnWidth(0, 64)
|
||||||
self.taskTable.setColumnWidth(1, 82)
|
self.taskTable.setColumnWidth(1, 82)
|
||||||
self.taskTable.setColumnWidth(3, 108)
|
self.taskTable.setColumnWidth(3, 108)
|
||||||
self.taskTable.setColumnWidth(4, 82)
|
self.taskTable.setColumnWidth(4, 108)
|
||||||
self.taskTable.setColumnWidth(5, 96)
|
self.taskTable.setColumnWidth(5, 82)
|
||||||
self.taskTable.setColumnWidth(6, 70)
|
self.taskTable.setColumnWidth(6, 96)
|
||||||
self.taskTable.setColumnWidth(7, 116)
|
self.taskTable.setColumnWidth(7, 70)
|
||||||
self.taskTable.setColumnWidth(8, 156)
|
self.taskTable.setColumnWidth(8, 116)
|
||||||
|
self.taskTable.setColumnWidth(9, 82)
|
||||||
|
self.taskTable.setColumnWidth(10, 156)
|
||||||
self.taskTable.viewport().installEventFilter(self)
|
self.taskTable.viewport().installEventFilter(self)
|
||||||
|
|
||||||
self.emptyStateCard = CardWidget(self)
|
self.emptyStateCard = CardWidget(self)
|
||||||
|
|||||||
@@ -1709,10 +1709,15 @@ def summary_to_row(summary: TaskSummary) -> TaskRow:
|
|||||||
task_type=TASK_TYPE_TEXT[summary.task_type],
|
task_type=TASK_TYPE_TEXT[summary.task_type],
|
||||||
goods_id=summary.goods_id or "",
|
goods_id=summary.goods_id or "",
|
||||||
title=summary.title or "",
|
title=summary.title or "",
|
||||||
|
shop_name=summary.shop_name or "",
|
||||||
color=summary.target_color or "",
|
color=summary.target_color or "",
|
||||||
size=summary.target_size or "",
|
size=summary.target_size or "",
|
||||||
price_cents=summary.price_cent,
|
price_cents=summary.price_cent,
|
||||||
quantity=summary.quantity,
|
quantity=summary.quantity,
|
||||||
status=TASK_STATUS_TEXT[summary.status],
|
status=TASK_STATUS_TEXT[summary.status],
|
||||||
|
latest_run_status=(
|
||||||
|
summary.latest_run_status.value if summary.latest_run_status else ""
|
||||||
|
),
|
||||||
|
duration_seconds=summary.duration_seconds,
|
||||||
updated_at=summary.updated_at,
|
updated_at=summary.updated_at,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -138,6 +138,9 @@ class TaskSummary:
|
|||||||
quantity: Optional[int]
|
quantity: Optional[int]
|
||||||
status: TaskStatus
|
status: TaskStatus
|
||||||
updated_at: str
|
updated_at: str
|
||||||
|
shop_name: Optional[str] = None
|
||||||
|
latest_run_status: Optional[RunStatus] = None
|
||||||
|
duration_seconds: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|||||||
@@ -129,11 +129,20 @@ class TaskRepository:
|
|||||||
connection = open_database(self._db_path)
|
connection = open_database(self._db_path)
|
||||||
try:
|
try:
|
||||||
rows = connection.execute(
|
rows = connection.execute(
|
||||||
"SELECT id, remote_task_id, task_type, goods_id, title,"
|
"SELECT pdd_tasks.id, remote_task_id, task_type, goods_id, title,"
|
||||||
" target_color, target_size, price_cent, quantity, status, updated_at"
|
" shop_name, target_color, target_size, price_cent, quantity,"
|
||||||
|
" status, pdd_tasks.updated_at, latest_run.run_status AS latest_run_status,"
|
||||||
|
" CASE WHEN latest_run.run_status = 'running' THEN NULL"
|
||||||
|
" WHEN latest_run.started_at IS NOT NULL AND latest_run.finished_at IS NOT NULL"
|
||||||
|
" THEN MAX(0, CAST(strftime('%s', latest_run.finished_at)"
|
||||||
|
" - strftime('%s', latest_run.started_at) AS INTEGER))"
|
||||||
|
" ELSE NULL END AS duration_seconds"
|
||||||
" FROM pdd_tasks"
|
" FROM pdd_tasks"
|
||||||
|
" LEFT JOIN task_runs AS latest_run ON latest_run.id = ("
|
||||||
|
" SELECT id FROM task_runs WHERE task_id = pdd_tasks.id"
|
||||||
|
" ORDER BY attempt_no DESC LIMIT 1)"
|
||||||
f"{where_sql}"
|
f"{where_sql}"
|
||||||
" ORDER BY updated_at DESC, id DESC"
|
" ORDER BY pdd_tasks.updated_at DESC, pdd_tasks.id DESC"
|
||||||
" LIMIT ? OFFSET ?",
|
" LIMIT ? OFFSET ?",
|
||||||
parameters,
|
parameters,
|
||||||
).fetchall()
|
).fetchall()
|
||||||
@@ -1098,13 +1107,14 @@ class TaskRepository:
|
|||||||
task_cursor = connection.execute(
|
task_cursor = connection.execute(
|
||||||
"UPDATE pdd_tasks SET status = 'result_pending',"
|
"UPDATE pdd_tasks SET status = 'result_pending',"
|
||||||
" current_step = 'purchase_order_matched_pending_report',"
|
" current_step = 'purchase_order_matched_pending_report',"
|
||||||
" pdd_data = ?,"
|
" pdd_data = ?, shop_name = ?,"
|
||||||
" price_cent = ?, last_error_code = NULL,"
|
" price_cent = ?, last_error_code = NULL,"
|
||||||
" last_error_message = NULL, finished_at = ?, updated_at = ?"
|
" last_error_message = NULL, finished_at = ?, updated_at = ?"
|
||||||
" WHERE id = ? AND status = 'manual_review'"
|
" WHERE id = ? AND status = 'manual_review'"
|
||||||
" AND current_step = 'reconcile_purchase'",
|
" AND current_step = 'reconcile_purchase'",
|
||||||
(
|
(
|
||||||
result_json,
|
result_json,
|
||||||
|
self._summary_shop_name(pdd_data),
|
||||||
self._purchase_result_price(pdd_data),
|
self._purchase_result_price(pdd_data),
|
||||||
now,
|
now,
|
||||||
now,
|
now,
|
||||||
@@ -1188,11 +1198,12 @@ class TaskRepository:
|
|||||||
)
|
)
|
||||||
connection.execute(
|
connection.execute(
|
||||||
"UPDATE pdd_tasks SET status = 'result_pending',"
|
"UPDATE pdd_tasks SET status = 'result_pending',"
|
||||||
" current_step = 'submit_result', pdd_data = ?,"
|
" current_step = 'submit_result', pdd_data = ?, shop_name = ?,"
|
||||||
" price_cent = ?, finished_at = ?, updated_at = ?"
|
" price_cent = ?, finished_at = ?, updated_at = ?"
|
||||||
" WHERE id = ?",
|
" WHERE id = ?",
|
||||||
(
|
(
|
||||||
result_json,
|
result_json,
|
||||||
|
self._summary_shop_name(pdd_data),
|
||||||
self._purchase_result_price(pdd_data),
|
self._purchase_result_price(pdd_data),
|
||||||
now,
|
now,
|
||||||
now,
|
now,
|
||||||
@@ -1373,11 +1384,13 @@ class TaskRepository:
|
|||||||
connection.execute(
|
connection.execute(
|
||||||
"UPDATE pdd_tasks SET status = 'result_pending',"
|
"UPDATE pdd_tasks SET status = 'result_pending',"
|
||||||
" current_step = 'submit_result', pdd_data = ?, goods_id = ?,"
|
" current_step = 'submit_result', pdd_data = ?, goods_id = ?,"
|
||||||
" title = ?, price_cent = ?, finished_at = ?, updated_at = ?"
|
" title = ?, shop_name = ?, price_cent = ?,"
|
||||||
|
" finished_at = ?, updated_at = ?"
|
||||||
" WHERE id = ?",
|
" WHERE id = ?",
|
||||||
(
|
(
|
||||||
result_json,
|
result_json,
|
||||||
pdd_data.get("goods_id"), pdd_data.get("title"),
|
pdd_data.get("goods_id"), pdd_data.get("title"),
|
||||||
|
self._summary_shop_name(pdd_data),
|
||||||
self._summary_price(pdd_data), now, now, task["id"],
|
self._summary_price(pdd_data), now, now, task["id"],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -1834,14 +1847,31 @@ class TaskRepository:
|
|||||||
task_type=TaskType(row["task_type"]),
|
task_type=TaskType(row["task_type"]),
|
||||||
goods_id=row["goods_id"],
|
goods_id=row["goods_id"],
|
||||||
title=row["title"],
|
title=row["title"],
|
||||||
|
shop_name=row["shop_name"],
|
||||||
target_color=row["target_color"],
|
target_color=row["target_color"],
|
||||||
target_size=row["target_size"],
|
target_size=row["target_size"],
|
||||||
price_cent=row["price_cent"],
|
price_cent=row["price_cent"],
|
||||||
quantity=row["quantity"],
|
quantity=row["quantity"],
|
||||||
status=TaskStatus(row["status"]),
|
status=TaskStatus(row["status"]),
|
||||||
|
latest_run_status=(
|
||||||
|
RunStatus(row["latest_run_status"])
|
||||||
|
if row["latest_run_status"] is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
duration_seconds=row["duration_seconds"],
|
||||||
updated_at=row["updated_at"],
|
updated_at=row["updated_at"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _summary_shop_name(pdd_data: Dict[str, object]) -> Optional[str]:
|
||||||
|
"""提取列表需要的店铺名,空值不写入摘要列。"""
|
||||||
|
|
||||||
|
value = pdd_data.get("shop_name")
|
||||||
|
if not isinstance(value, str):
|
||||||
|
return None
|
||||||
|
value = value.strip()
|
||||||
|
return value or None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _to_detail(row: sqlite3.Row) -> TaskDetail:
|
def _to_detail(row: sqlite3.Row) -> TaskDetail:
|
||||||
admin_payload = TaskRepository._load_json_object(row["admin_payload"])
|
admin_payload = TaskRepository._load_json_object(row["admin_payload"])
|
||||||
|
|||||||
+38
-5
@@ -6,7 +6,7 @@ import unittest
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from src.db import DatabaseVersionError, initialize_database, open_database
|
from src.db import DatabaseVersionError, initialize_database, open_database
|
||||||
from src.db_schema import MIGRATION_1, MIGRATION_2, MIGRATION_3
|
from src.db_schema import MIGRATION_1, MIGRATION_2, MIGRATION_3, MIGRATION_4
|
||||||
|
|
||||||
|
|
||||||
EXPECTED_TABLES = {
|
EXPECTED_TABLES = {
|
||||||
@@ -64,7 +64,7 @@ class DatabaseInitializationTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertTrue(EXPECTED_TABLES.issubset(tables))
|
self.assertTrue(EXPECTED_TABLES.issubset(tables))
|
||||||
self.assertTrue(EXPECTED_INDEXES.issubset(indexes))
|
self.assertTrue(EXPECTED_INDEXES.issubset(indexes))
|
||||||
self.assertEqual(version, 4)
|
self.assertEqual(version, 5)
|
||||||
|
|
||||||
def test_v1_database_is_upgraded_without_losing_task_runs(self) -> None:
|
def test_v1_database_is_upgraded_without_losing_task_runs(self) -> None:
|
||||||
connection = open_database(self.db_path)
|
connection = open_database(self.db_path)
|
||||||
@@ -107,7 +107,7 @@ class DatabaseInitializationTests(unittest.TestCase):
|
|||||||
connection.close()
|
connection.close()
|
||||||
self.assertIn("result_data", columns)
|
self.assertIn("result_data", columns)
|
||||||
self.assertEqual(attempt_id, "ATTEMPT-OLD")
|
self.assertEqual(attempt_id, "ATTEMPT-OLD")
|
||||||
self.assertEqual(version, 4)
|
self.assertEqual(version, 5)
|
||||||
|
|
||||||
def test_initialize_can_run_twice_without_losing_data(self) -> None:
|
def test_initialize_can_run_twice_without_losing_data(self) -> None:
|
||||||
initialize_database(self.db_path)
|
initialize_database(self.db_path)
|
||||||
@@ -170,7 +170,7 @@ class DatabaseInitializationTests(unittest.TestCase):
|
|||||||
finally:
|
finally:
|
||||||
connection.close()
|
connection.close()
|
||||||
self.assertEqual(row[0], "dry_run")
|
self.assertEqual(row[0], "dry_run")
|
||||||
self.assertEqual(version, 4)
|
self.assertEqual(version, 5)
|
||||||
|
|
||||||
def test_v3_database_adds_soft_remove_column_without_losing_tasks(self) -> None:
|
def test_v3_database_adds_soft_remove_column_without_losing_tasks(self) -> None:
|
||||||
connection = open_database(self.db_path)
|
connection = open_database(self.db_path)
|
||||||
@@ -212,7 +212,40 @@ class DatabaseInitializationTests(unittest.TestCase):
|
|||||||
connection.close()
|
connection.close()
|
||||||
self.assertIn("removed_at", columns)
|
self.assertIn("removed_at", columns)
|
||||||
self.assertEqual(tuple(row), ("COL-V3", None))
|
self.assertEqual(tuple(row), ("COL-V3", None))
|
||||||
self.assertEqual(version, 4)
|
self.assertEqual(version, 5)
|
||||||
|
|
||||||
|
def test_v4_database_backfills_shop_name_from_valid_pdd_data(self) -> None:
|
||||||
|
connection = open_database(self.db_path)
|
||||||
|
try:
|
||||||
|
with connection:
|
||||||
|
for statement in MIGRATION_1 + MIGRATION_2 + MIGRATION_3 + MIGRATION_4:
|
||||||
|
connection.execute(statement)
|
||||||
|
connection.execute("PRAGMA user_version = 4")
|
||||||
|
connection.execute(
|
||||||
|
"INSERT INTO pdd_tasks"
|
||||||
|
" (remote_task_id, task_type, goods_url, status, pdd_data,"
|
||||||
|
" received_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
(
|
||||||
|
"COL-V4", "collect", "https://example.test/goods",
|
||||||
|
"succeeded", '{"shop_name":" 测试店铺 "}',
|
||||||
|
"2026-08-10T00:00:00Z", "2026-08-10T00:00:00Z",
|
||||||
|
"2026-08-10T00:00:00Z",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
initialize_database(self.db_path)
|
||||||
|
connection = open_database(self.db_path)
|
||||||
|
try:
|
||||||
|
row = connection.execute(
|
||||||
|
"SELECT shop_name FROM pdd_tasks WHERE remote_task_id = 'COL-V4'"
|
||||||
|
).fetchone()
|
||||||
|
version = connection.execute("PRAGMA user_version").fetchone()[0]
|
||||||
|
finally:
|
||||||
|
connection.close()
|
||||||
|
self.assertEqual(row[0], "测试店铺")
|
||||||
|
self.assertEqual(version, 5)
|
||||||
|
|
||||||
def test_new_connection_uses_required_pragmas(self) -> None:
|
def test_new_connection_uses_required_pragmas(self) -> None:
|
||||||
initialize_database(self.db_path)
|
initialize_database(self.db_path)
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ class TaskTableModelCheckTests(unittest.TestCase):
|
|||||||
page.detailRequested.connect(received.append)
|
page.detailRequested.connect(received.append)
|
||||||
|
|
||||||
self.assertNotIn("操作", page.taskModel.HEADERS)
|
self.assertNotIn("操作", page.taskModel.HEADERS)
|
||||||
self.assertEqual(page.taskModel.columnCount(), 9)
|
self.assertEqual(page.taskModel.columnCount(), 11)
|
||||||
page._on_table_activated(page.taskModel.index(0, CHECK_COLUMN))
|
page._on_table_activated(page.taskModel.index(0, CHECK_COLUMN))
|
||||||
page._on_table_activated(page.taskModel.index(0, 2))
|
page._on_table_activated(page.taskModel.index(0, 2))
|
||||||
|
|
||||||
|
|||||||
@@ -258,6 +258,7 @@ class TaskRepositoryTests(unittest.TestCase):
|
|||||||
{
|
{
|
||||||
"goods_id": "10001",
|
"goods_id": "10001",
|
||||||
"title": "测试商品",
|
"title": "测试商品",
|
||||||
|
"shop_name": "测试店铺",
|
||||||
"price_granularity": "color",
|
"price_granularity": "color",
|
||||||
"dimensions": [],
|
"dimensions": [],
|
||||||
"skus": [{"price_cent": 990}],
|
"skus": [{"price_cent": 990}],
|
||||||
@@ -269,12 +270,38 @@ class TaskRepositoryTests(unittest.TestCase):
|
|||||||
TaskStatus.RESULT_PENDING,
|
TaskStatus.RESULT_PENDING,
|
||||||
)
|
)
|
||||||
self.assertEqual(event.status, OutboxStatus.PENDING)
|
self.assertEqual(event.status, OutboxStatus.PENDING)
|
||||||
|
summary = self.repository.list_tasks()[0]
|
||||||
|
self.assertEqual(summary.shop_name, "测试店铺")
|
||||||
|
self.assertEqual(summary.latest_run_status, RunStatus.SUCCEEDED)
|
||||||
self.repository.mark_outbox_sent(event.id)
|
self.repository.mark_outbox_sent(event.id)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.repository.get_task("TASK-RUN").status,
|
self.repository.get_task("TASK-RUN").status,
|
||||||
TaskStatus.SUCCEEDED,
|
TaskStatus.SUCCEEDED,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_list_uses_latest_run_duration_and_clamps_negative_value(self):
|
||||||
|
self.repository.add_claimed_task(self._task("TASK-DURATION"))
|
||||||
|
started = self.repository.start_collect_run("TASK-DURATION", "USB-001")
|
||||||
|
connection = open_database(self.db_path)
|
||||||
|
try:
|
||||||
|
with connection:
|
||||||
|
connection.execute(
|
||||||
|
"UPDATE task_runs SET run_status = 'failed',"
|
||||||
|
" started_at = ?, finished_at = ? WHERE attempt_id = ?",
|
||||||
|
(
|
||||||
|
"2026-08-11T08:00:10Z",
|
||||||
|
"2026-08-11T08:00:05Z",
|
||||||
|
started.attempt_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
summary = self.repository.list_tasks()[0]
|
||||||
|
|
||||||
|
self.assertEqual(summary.latest_run_status, RunStatus.FAILED)
|
||||||
|
self.assertEqual(summary.duration_seconds, 0)
|
||||||
|
|
||||||
def test_latest_result_outbox_excludes_newer_failure_event(self):
|
def test_latest_result_outbox_excludes_newer_failure_event(self):
|
||||||
self.repository.add_claimed_task(self._task("TASK-REPORT"))
|
self.repository.add_claimed_task(self._task("TASK-REPORT"))
|
||||||
first = self.repository.start_collect_run("TASK-REPORT", "USB-001")
|
first = self.repository.start_collect_run("TASK-REPORT", "USB-001")
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ CREATE TABLE pdd_tasks (
|
|||||||
goods_id TEXT,
|
goods_id TEXT,
|
||||||
goods_url TEXT NOT NULL,
|
goods_url TEXT NOT NULL,
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
shop_name TEXT,
|
||||||
target_color TEXT,
|
target_color TEXT,
|
||||||
target_size TEXT,
|
target_size TEXT,
|
||||||
price_cent INTEGER CHECK (price_cent IS NULL OR price_cent >= 0),
|
price_cent INTEGER CHECK (price_cent IS NULL OR price_cent >= 0),
|
||||||
@@ -175,6 +176,7 @@ CREATE INDEX idx_pdd_tasks_visible_list
|
|||||||
- `remote_task_id`:跨 Admin、Client、日志、提交和归档使用的稳定任务编号。Mock 任务也必须分配稳定编号。
|
- `remote_task_id`:跨 Admin、Client、日志、提交和归档使用的稳定任务编号。Mock 任务也必须分配稳定编号。
|
||||||
- `execution_mode`:Admin 领取时确定的不可变执行模式;历史记录默认为 `dry_run`,Client 不得把任务自行提升为 `live`。
|
- `execution_mode`:Admin 领取时确定的不可变执行模式;历史记录默认为 `dry_run`,Client 不得把任务自行提升为 `live`。
|
||||||
- `price_cent`:列表摘要价格;采集任务通常为最低可用 SKU 价格,详细价格以 `pdd_data.skus` 为准。
|
- `price_cent`:列表摘要价格;采集任务通常为最低可用 SKU 价格,详细价格以 `pdd_data.skus` 为准。
|
||||||
|
- `shop_name`:列表使用的店铺名摘要;保存结果时从 `pdd_data.shop_name` 同步,避免列表查询解析完整 JSON。
|
||||||
- `status`:**本机执行状态**,只由本机的执行流程和 Outbox 提交响应驱动。Admin 那边把任务标成什么,本地不知道也不需要知道(见 §3.1)。
|
- `status`:**本机执行状态**,只由本机的执行流程和 Outbox 提交响应驱动。Admin 那边把任务标成什么,本地不知道也不需要知道(见 §3.1)。
|
||||||
- `current_step`:当前安全步骤,例如 `open_goods`、`collect_skus`、`select_options`、`placing_order`、`reconcile_order`。
|
- `current_step`:当前安全步骤,例如 `open_goods`、`collect_skus`、`select_options`、`placing_order`、`reconcile_order`。
|
||||||
- `admin_payload`:`claim` 时收到的原始 Admin 任务 JSON,用于审计和向前兼容。
|
- `admin_payload`:`claim` 时收到的原始 Admin 任务 JSON,用于审计和向前兼容。
|
||||||
@@ -212,15 +214,21 @@ CREATE INDEX idx_pdd_tasks_visible_list
|
|||||||
列表默认查询:
|
列表默认查询:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
SELECT id, remote_task_id, task_type, title, target_color, target_size,
|
SELECT pdd_tasks.id, remote_task_id, task_type, title, shop_name,
|
||||||
price_cent, quantity, status, updated_at
|
target_color, target_size, price_cent, quantity, status,
|
||||||
|
latest_run.run_status, latest_run.started_at, latest_run.finished_at,
|
||||||
|
pdd_tasks.updated_at
|
||||||
FROM pdd_tasks
|
FROM pdd_tasks
|
||||||
|
LEFT JOIN task_runs AS latest_run ON /* 每个任务 attempt_no 最大的一条执行记录 */
|
||||||
WHERE removed_at IS NULL
|
WHERE removed_at IS NULL
|
||||||
AND /* 本地搜索与筛选条件 */
|
AND /* 本地搜索与筛选条件 */
|
||||||
ORDER BY updated_at DESC, id DESC
|
ORDER BY updated_at DESC, id DESC
|
||||||
LIMIT :limit OFFSET :offset;
|
LIMIT :limit OFFSET :offset;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
列表必须在同一条 SQL 中读取最新执行记录,不能逐行查询。已结束执行的用时按
|
||||||
|
`finished_at - started_at` 取整秒,异常负数按 0 秒显示;正在执行显示“进行中”。
|
||||||
|
|
||||||
批量软删除必须在一个事务内先校验全部勾选任务,再统一写入 `removed_at`。
|
批量软删除必须在一个事务内先校验全部勾选任务,再统一写入 `removed_at`。
|
||||||
只允许 `succeeded`、`failed`、`cancelled`,且所有 Outbox 都已发送、所有执行记录的
|
只允许 `succeeded`、`failed`、`cancelled`,且所有 Outbox 都已发送、所有执行记录的
|
||||||
`irreversible_action_at` 都为空。任一任务不满足时整体失败,不能只隐藏其中一部分。
|
`irreversible_action_at` 都为空。任一任务不满足时整体失败,不能只隐藏其中一部分。
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
│ PDD 任务 │
|
│ PDD 任务 │
|
||||||
│ [自动获取] [类型▼] [状态▼] [关键词...] [搜索] [刷新] [全选] [反选] [已选0条] [重新上报] [重新采集] [重新采购] [删除] │
|
│ [自动获取] [类型▼] [状态▼] [关键词...] [搜索] [刷新] [全选] [反选] [已选0条] [重新上报] [重新采集] [重新采购] [删除] │
|
||||||
├─────────────────────────────────────────────────────────────┤
|
├─────────────────────────────────────────────────────────────┤
|
||||||
│ 选择 │ 类型 │ 商品标题 │ 颜色 │ 尺码 │ 价格 │ 数量 │ 状态 │ 更新时间 │
|
│ 选择 │ 类型 │ 商品标题 │ 店铺名 │ 颜色 │ 尺码 │ 价格 │ 数量 │ 状态 │ 用时 │ 更新时间 │
|
||||||
│ │
|
│ │
|
||||||
│ 任务数据表格 │
|
│ 任务数据表格 │
|
||||||
│ │
|
│ │
|
||||||
@@ -127,11 +127,13 @@
|
|||||||
| 选择 | 居中 | 行首复选框;占位空行不可勾选,身份使用 `remote_task_id` |
|
| 选择 | 居中 | 行首复选框;占位空行不可勾选,身份使用 `remote_task_id` |
|
||||||
| 任务类型 | 居中 | “采集”或“采购”,颜色只作辅助 |
|
| 任务类型 | 居中 | “采集”或“采购”,颜色只作辅助 |
|
||||||
| 商品标题 | 左对齐、可伸展 | 空值显示“尚未获取标题”,截断时提供完整工具提示 |
|
| 商品标题 | 左对齐、可伸展 | 空值显示“尚未获取标题”,截断时提供完整工具提示 |
|
||||||
|
| 店铺名 | 左对齐 | 采集到的店铺名;无值显示 `—` |
|
||||||
| 颜色 | 左对齐 | 无值显示 `—` |
|
| 颜色 | 左对齐 | 无值显示 `—` |
|
||||||
| 尺码 | 左对齐 | 无值显示 `—` |
|
| 尺码 | 左对齐 | 无值显示 `—` |
|
||||||
| 价格 | 右对齐 | 格式为 `¥39.90`;采集摘要可显示 `¥39.90 起` |
|
| 价格 | 右对齐 | 格式为 `¥39.90`;采集摘要可显示 `¥39.90 起` |
|
||||||
| 数量 | 右对齐 | 采购数量;采集显示 `—` |
|
| 数量 | 右对齐 | 采购数量;采集显示 `—` |
|
||||||
| 状态 | 居中 | 中文状态文字和状态图标/标记 |
|
| 状态 | 居中 | 中文状态文字和状态图标/标记 |
|
||||||
|
| 用时 | 居中 | 已结束显示整数秒,执行中显示“进行中”,尚未执行显示 `—` |
|
||||||
| 更新时间 | 左对齐 | 本地时区,精确到秒或按产品确认格式 |
|
| 更新时间 | 左对齐 | 本地时区,精确到秒或按产品确认格式 |
|
||||||
|
|
||||||
### 5.2 数据行为
|
### 5.2 数据行为
|
||||||
|
|||||||
Reference in New Issue
Block a user