fix(product-suite): validate item id before generation

This commit is contained in:
chengma
2026-07-22 18:14:50 +08:00
parent 189b5855e9
commit 01b4a05c26
8 changed files with 129 additions and 45 deletions
+51 -37
View File
@@ -14,6 +14,7 @@ from PySide6.QtCore import (
QIODevice,
QPointF,
QRect,
QRegularExpression,
QSize,
Qt,
QTimer,
@@ -29,6 +30,7 @@ from PySide6.QtGui import (
QKeySequence,
QPainter,
QPixmap,
QRegularExpressionValidator,
QWheelEvent,
)
from PySide6.QtWidgets import (
@@ -2163,6 +2165,9 @@ class ProductSuiteTab(QWidget):
self.item_id_edit = QLineEdit()
self.item_id_edit.setObjectName("suiteItemIdEdit")
self.item_id_edit.setPlaceholderText("输入商品ID")
self.item_id_edit.setValidator(
QRegularExpressionValidator(QRegularExpression("^[0-9]*$"), self.item_id_edit)
)
item_width = max(
120,
min(140, self.item_id_edit.fontMetrics().horizontalAdvance("0" * 13) + 30),
@@ -2776,7 +2781,6 @@ class ProductSuiteTab(QWidget):
if self._loading or state is None:
return
state.account_alias = str(self.account_combo.currentData() or "")
state.item_id = self.item_id_edit.text().strip()
state.prompt = self.prompt_edit.toPlainText()
state.settings = self._settings_from_controls()
@@ -2791,6 +2795,7 @@ class ProductSuiteTab(QWidget):
if index >= 0:
self.account_combo.setCurrentIndex(index)
self.item_id_edit.setText(state.item_id)
self.item_id_edit.setStyleSheet("")
self.prompt_edit.setPlainText(state.prompt)
self._set_combo_value(self.platform_combo, state.settings.get("platform"))
self._set_combo_value(self.country_combo, state.settings.get("country"))
@@ -2861,32 +2866,46 @@ class ProductSuiteTab(QWidget):
self.account_combo.setToolTip(self.account_combo.currentText())
def _on_item_finished(self):
if self._loading or self._displayed_state is None:
return
state = self._displayed_state
item_id = self.item_id_edit.text().strip()
if item_id and not item_id.isdigit():
self._commit_item_id(self._displayed_state)
def _restore_committed_item_id(self, state):
self._loading = True
try:
self.item_id_edit.setText(state.item_id)
finally:
self._loading = False
def _commit_item_id(self, state):
"""Validate and commit the visible product ID before an operation uses it."""
if self._loading or state is None:
return False
raw_item_id = self.item_id_edit.text()
item_id = image_studio.normalize_item_id_input(raw_item_id)
if item_id != raw_item_id:
self._loading = True
try:
self.item_id_edit.setText(item_id)
finally:
self._loading = False
if item_id and not image_studio.is_formal_item_id(item_id):
self.item_id_edit.setStyleSheet("border: 1px solid #9a6700;")
self._status("商品ID只能输入数字", "warning")
self._update_context_actions(state)
return
return False
self.item_id_edit.setStyleSheet("")
if self._is_draft_state(state):
if not item_id:
state.item_id = ""
self._update_context_actions(state)
return
return True
if not self._confirm(
"绑定正式商品",
"将当前临时草稿绑定到商品%s吗?\n已添加图片和生成记录会继续保留。" % item_id,
):
self._loading = True
try:
self.item_id_edit.setText(state.item_id)
finally:
self._loading = False
self._restore_committed_item_id(state)
self._update_context_actions(state)
return
return False
self._flush_prompt_save(state)
try:
project = image_studio.bind_draft_project(
@@ -2896,47 +2915,36 @@ class ProductSuiteTab(QWidget):
)
except image_studio.ImageStudioProjectConflictError as exc:
self._message("商品项目已存在", _user_error(exc))
self._loading = True
try:
self.item_id_edit.setText(state.item_id)
finally:
self._loading = False
self._restore_committed_item_id(state)
self._update_context_actions(state)
return
return False
except Exception as exc:
self._message("绑定正式商品失败", _user_error(exc))
self._loading = True
try:
self.item_id_edit.setText(state.item_id)
finally:
self._loading = False
self._restore_committed_item_id(state)
self._update_context_actions(state)
return
return False
state.item_id = project.item_id
state.project_binding_state = project.binding_state
self._set_task_title(state)
self._status("临时草稿已绑定商品%s" % project.item_id, "success")
self._update_context_actions(state)
return
return True
if state.project_id is not None and item_id != state.item_id:
if not self._confirm(
"切换商品",
"切换商品ID后,原图和生成结果会按新商品重新载入。确认继续吗?",
):
self._loading = True
try:
self.item_id_edit.setText(state.item_id)
finally:
self._loading = False
return
self._restore_committed_item_id(state)
return False
self._clear_project_binding(state)
state.item_id = item_id
if not item_id:
self._update_context_actions(state)
return
return True
if item_id and state.account_alias:
self._bind_project(state, load_existing=True)
self._update_context_actions(state)
return True
def _clear_project_binding(self, state):
self._flush_prompt_save(state)
@@ -3012,7 +3020,7 @@ class ProductSuiteTab(QWidget):
def _valid_context(self, state, *, show_message=True):
if not self._has_account_context(state, show_message=show_message):
return False
if not state.item_id or not state.item_id.isdigit():
if not image_studio.is_formal_item_id(state.item_id):
if show_message:
self._message("商品ID无效", "请输入正确的数字商品ID。")
return False
@@ -3465,6 +3473,8 @@ class ProductSuiteTab(QWidget):
state = self._displayed_state
if state is None:
return
if not self._commit_item_id(state):
return
self._save_controls_to_state(state)
state.import_created_draft = False
created_project = state.project_id is None
@@ -3662,6 +3672,8 @@ class ProductSuiteTab(QWidget):
if state.pull_running():
self._request_stop_pull(state)
return
if not self._commit_item_id(state):
return
self._save_controls_to_state(state)
if self._is_draft_state(state):
self._message(
@@ -4801,6 +4813,10 @@ class ProductSuiteTab(QWidget):
retry_job_id=None,
confirm_direct_retry=False,
):
if state is self._displayed_state:
if not self._commit_item_id(state):
return False
self._save_controls_to_state(state)
if self.subscription_preflight_callback is not None and not self.subscription_preflight_callback("生成商品套图"):
return False
if not self._ensure_generation_gateway():
@@ -4811,8 +4827,6 @@ class ProductSuiteTab(QWidget):
retry_job_id = int(retry_job_id) if retry_job_id is not None else None
retrying = retry_job_id is not None
confirm_batch = specs is None and not retrying
if state is self._displayed_state:
self._save_controls_to_state(state)
template_text = None
if specs is None:
try:
+18 -5
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import json
import os
import unicodedata
import uuid
from contextlib import contextmanager
from dataclasses import dataclass
@@ -253,15 +254,27 @@ def _db_path(path=None, config=None) -> str:
return path or appconfig.db_path(config)
def _normalize_item_id(item_id) -> str:
def normalize_item_id_input(item_id) -> str:
"""Normalize product ID input without accepting non-numeric formal IDs."""
text = str(item_id or "").strip()
return "".join(
character
for character in text
if unicodedata.category(character) != "Cf"
).strip()
def _normalize_item_id(item_id) -> str:
text = normalize_item_id_input(item_id)
if not text:
raise ImageStudioError("商品ID不能为空")
return text
def is_formal_item_id(item_id) -> bool:
return str(item_id or "").strip().isdigit()
text = normalize_item_id_input(item_id)
return bool(text) and all("0" <= character <= "9" for character in text)
def is_draft_project(project) -> bool:
@@ -404,7 +417,7 @@ def get_project(project_id, path=None, conn=None, include_deleted=False):
def get_project_by_account_item(account_alias, item_id, path=None, conn=None, include_deleted=False):
clauses = ["account_alias = ?", "item_id = ?"]
params = [str(account_alias).strip(), str(item_id).strip()]
params = [str(account_alias).strip(), normalize_item_id_input(item_id)]
if not include_deleted:
clauses.append("deleted_at IS NULL")
sql = "SELECT * FROM image_studio_projects WHERE " + " AND ".join(clauses)
@@ -431,7 +444,7 @@ def create_or_get_project(
account_name=account_name,
account_slug=account_slug,
)
item = str(item_id or _get(account, "item_id") or "").strip()
item = normalize_item_id_input(item_id or _get(account, "item_id") or "")
if not item:
raise db.DbError("AI工场项目缺少商品ID")
now = _now()
@@ -535,7 +548,7 @@ def create_draft_project(
def bind_draft_project(project_id, item_id, path=None, conn=None):
"""Bind one active draft to a formal numeric item ID without moving its files."""
item = str(item_id or "").strip()
item = normalize_item_id_input(item_id)
if not is_formal_item_id(item):
raise ImageStudioError("正式商品ID必须是数字")
with _connection(conn, path) as database: