fix(product-suite): validate item id before generation
This commit is contained in:
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user