完成新需求
This commit is contained in:
Binary file not shown.
703
desktop/app.py
703
desktop/app.py
@@ -250,8 +250,8 @@ class DesktopConfig:
|
||||
|
||||
|
||||
class DesktopApi:
|
||||
def __init__(self, app_base_url: str) -> None:
|
||||
self.app_base_url = app_base_url.rstrip("/")
|
||||
def __init__(self, backend_base_url: str) -> None:
|
||||
self.app_base_url = backend_base_url.rstrip("/")
|
||||
self.session = requests.Session()
|
||||
|
||||
def select_brand_xlsx_files(self) -> list[str]:
|
||||
@@ -347,17 +347,38 @@ class DesktopApi:
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
def list_txt_templates(self) -> list[dict]:
|
||||
templates, _default_template_id = build_effective_templates()
|
||||
return [
|
||||
{
|
||||
"id": template["id"],
|
||||
"label": template["label"],
|
||||
"output_filename": template["output_filename"],
|
||||
"is_default": template.get("is_default", False),
|
||||
"built_in": template.get("built_in", False),
|
||||
}
|
||||
for template in templates.values()
|
||||
]
|
||||
try:
|
||||
response = self.session.get(
|
||||
resolve_url(self.app_base_url, "/api/convert/templates"),
|
||||
timeout=60,
|
||||
)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
if not payload.get("success"):
|
||||
return []
|
||||
items = payload.get("data") or []
|
||||
return [
|
||||
{
|
||||
"id": str(item.get("templateCode") or item.get("id") or ""),
|
||||
"label": item.get("templateName") or "",
|
||||
"output_filename": item.get("outputFilename") or "",
|
||||
"is_default": bool(item.get("isDefault")),
|
||||
"built_in": bool(item.get("builtIn")),
|
||||
}
|
||||
for item in items
|
||||
]
|
||||
except Exception:
|
||||
templates, _default_template_id = build_effective_templates()
|
||||
return [
|
||||
{
|
||||
"id": template["id"],
|
||||
"label": template["label"],
|
||||
"output_filename": template["output_filename"],
|
||||
"is_default": template.get("is_default", False),
|
||||
"built_in": template.get("built_in", False),
|
||||
}
|
||||
for template in templates.values()
|
||||
]
|
||||
|
||||
def import_txt_template(self, name: str | None = None) -> dict:
|
||||
file_path = run_file_dialog(
|
||||
@@ -370,39 +391,60 @@ class DesktopApi:
|
||||
return {"success": False, "error": "用户取消"}
|
||||
|
||||
try:
|
||||
template_text = Path(file_path).read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
template_text = Path(file_path).read_text(encoding="utf-8-sig")
|
||||
try:
|
||||
template_text = Path(file_path).read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
template_text = Path(file_path).read_text(encoding="utf-8-sig")
|
||||
|
||||
response = self.session.post(
|
||||
resolve_url(self.app_base_url, "/api/convert/templates/import"),
|
||||
json={
|
||||
"templateName": (name or "").strip() or Path(file_path).stem,
|
||||
"templateContent": template_text,
|
||||
},
|
||||
timeout=120,
|
||||
)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
if not payload.get("success") or not payload.get("data"):
|
||||
return {"success": False, "error": payload.get("message") or "导入失败"}
|
||||
item = payload.get("data") or {}
|
||||
return {
|
||||
"success": True,
|
||||
"template": {
|
||||
"id": str(item.get("templateCode") or item.get("id") or ""),
|
||||
"label": item.get("templateName") or "",
|
||||
"output_filename": item.get("outputFilename") or "",
|
||||
"is_default": bool(item.get("isDefault")),
|
||||
"built_in": bool(item.get("builtIn")),
|
||||
},
|
||||
}
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
template = recognize_txt_template(template_text, (name or "").strip() or None)
|
||||
if not template:
|
||||
return {"success": False, "error": "无法识别模板"}
|
||||
|
||||
user_templates, default_template_id = load_user_templates()
|
||||
user_templates[template["id"]] = template
|
||||
save_user_templates(user_templates, default_template_id)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"template": {
|
||||
"id": template["id"],
|
||||
"label": template["label"],
|
||||
"output_filename": template["output_filename"],
|
||||
"is_default": False,
|
||||
"built_in": False,
|
||||
},
|
||||
}
|
||||
|
||||
def set_default_txt_template(self, template_id: str) -> dict:
|
||||
templates, _default_template_id = build_effective_templates()
|
||||
if template_id not in templates:
|
||||
return {"success": False, "error": "模板不存在"}
|
||||
try:
|
||||
response = self.session.post(
|
||||
resolve_url(self.app_base_url, f"/api/convert/templates/{template_id}/default"),
|
||||
timeout=60,
|
||||
)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
return {"success": bool(payload.get("success")), "error": payload.get("message") if not payload.get("success") else None}
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
user_templates, _ = load_user_templates()
|
||||
save_user_templates(user_templates, template_id)
|
||||
return {"success": True}
|
||||
def delete_txt_template(self, template_id: str) -> dict:
|
||||
try:
|
||||
response = self.session.delete(
|
||||
resolve_url(self.app_base_url, f"/api/convert/templates/{template_id}"),
|
||||
timeout=60,
|
||||
)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
return {"success": bool(payload.get("success")), "error": payload.get("message") if not payload.get("success") else None}
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
def split_excel_files(self, payload: dict) -> dict:
|
||||
paths = payload.get("paths") or []
|
||||
@@ -410,7 +452,6 @@ class DesktopApi:
|
||||
split_mode = payload.get("split_mode") or "rows_per_file"
|
||||
rows_per_file = payload.get("rows_per_file")
|
||||
parts = payload.get("parts")
|
||||
output_dir = payload.get("output_dir") or ""
|
||||
|
||||
if not isinstance(paths, list) or not paths:
|
||||
return {"success": False, "error": "未选择待拆分文件"}
|
||||
@@ -422,128 +463,171 @@ class DesktopApi:
|
||||
return {"success": False, "error": "每份条数不合法"}
|
||||
if split_mode == "parts" and (not isinstance(parts, int) or parts <= 0):
|
||||
return {"success": False, "error": "拆分份数不合法"}
|
||||
if not output_dir:
|
||||
return {"success": False, "error": "未选择输出目录"}
|
||||
|
||||
output_dir_path = Path(output_dir)
|
||||
if not output_dir_path.exists() or not output_dir_path.is_dir():
|
||||
return {"success": False, "error": "输出目录不存在"}
|
||||
|
||||
items: list[dict] = []
|
||||
success_count = 0
|
||||
failed_count = 0
|
||||
|
||||
for path in paths:
|
||||
try:
|
||||
workbook = load_workbook(path)
|
||||
sheet = workbook[workbook.sheetnames[0]]
|
||||
header_cells = next(sheet.iter_rows(min_row=1, max_row=1))
|
||||
header_map: dict[str, int] = {}
|
||||
for index, cell in enumerate(header_cells):
|
||||
value = normalize_cell_text(cell.value)
|
||||
if not value:
|
||||
continue
|
||||
value = value.split("idASIN国家状态价格变体数量", 1)[0].strip() or value
|
||||
if value in header_map:
|
||||
continue
|
||||
header_map[value] = index
|
||||
if value == "缩略图地址8":
|
||||
break
|
||||
|
||||
missing = [column for column in selected_columns if column not in header_map]
|
||||
if missing:
|
||||
workbook.close()
|
||||
items.append({
|
||||
"source_path": path,
|
||||
"success": False,
|
||||
"error": f"缺少列:{'、'.join(missing)}",
|
||||
})
|
||||
failed_count += 1
|
||||
continue
|
||||
|
||||
rows_data: list[list[str]] = []
|
||||
for row in sheet.iter_rows(min_row=2, values_only=True):
|
||||
projected = [
|
||||
normalize_cell_text(row[header_map[column]]) if header_map[column] < len(row) else ""
|
||||
for column in selected_columns
|
||||
]
|
||||
rows_data.append(projected)
|
||||
|
||||
total_rows = len(rows_data)
|
||||
if total_rows == 0:
|
||||
workbook.close()
|
||||
items.append({
|
||||
"source_path": path,
|
||||
"success": False,
|
||||
"error": "没有可拆分的数据行",
|
||||
})
|
||||
failed_count += 1
|
||||
continue
|
||||
|
||||
if split_mode == "rows_per_file":
|
||||
chunk_size = rows_per_file
|
||||
chunks = [rows_data[i : i + chunk_size] for i in range(0, total_rows, chunk_size)]
|
||||
else:
|
||||
actual_parts = min(parts, total_rows)
|
||||
base = total_rows // actual_parts
|
||||
remainder = total_rows % actual_parts
|
||||
chunks = []
|
||||
start = 0
|
||||
for idx in range(actual_parts):
|
||||
size = base + (1 if idx < remainder else 0)
|
||||
end = start + size
|
||||
chunks.append(rows_data[start:end])
|
||||
start = end
|
||||
|
||||
from openpyxl import Workbook
|
||||
|
||||
source = Path(path)
|
||||
for index, chunk in enumerate(chunks, start=1):
|
||||
output_workbook = Workbook()
|
||||
output_sheet = output_workbook.active
|
||||
output_sheet.title = sheet.title
|
||||
output_sheet.append(selected_columns)
|
||||
for row_values in chunk:
|
||||
output_sheet.append(row_values)
|
||||
|
||||
output_path = build_named_output_path(output_dir, f"{source.stem}_split_{index}.xlsx")
|
||||
output_workbook.save(output_path)
|
||||
output_workbook.close()
|
||||
|
||||
items.append({
|
||||
"source_path": path,
|
||||
"output_path": output_path,
|
||||
"success": True,
|
||||
"row_count": len(chunk),
|
||||
})
|
||||
|
||||
workbook.close()
|
||||
success_count += 1
|
||||
except Exception as exc: # noqa: BLE001
|
||||
items.append({
|
||||
"source_path": path,
|
||||
"success": False,
|
||||
"error": str(exc),
|
||||
try:
|
||||
uploaded_files = []
|
||||
for path in paths:
|
||||
with open(path, "rb") as file_obj:
|
||||
response = self.session.post(
|
||||
resolve_url(self.app_base_url, "/api/files/upload"),
|
||||
files={"file": (Path(path).name, file_obj)},
|
||||
timeout=180,
|
||||
)
|
||||
response.raise_for_status()
|
||||
upload_payload = response.json()
|
||||
if not upload_payload.get("success") or not upload_payload.get("data"):
|
||||
return {"success": False, "error": upload_payload.get("message") or "上传文件失败"}
|
||||
upload_data = upload_payload.get("data") or {}
|
||||
uploaded_files.append({
|
||||
"fileKey": upload_data.get("fileKey"),
|
||||
"originalFilename": upload_data.get("originalFilename") or Path(path).name,
|
||||
})
|
||||
failed_count += 1
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"summary": {
|
||||
"total": len(paths),
|
||||
"success_count": success_count,
|
||||
"failed_count": failed_count,
|
||||
"output_dir": str(output_dir_path),
|
||||
},
|
||||
"items": items,
|
||||
}
|
||||
run_response = self.session.post(
|
||||
resolve_url(self.app_base_url, "/api/split/run"),
|
||||
json={
|
||||
"files": uploaded_files,
|
||||
"selectedColumns": selected_columns,
|
||||
"splitMode": split_mode,
|
||||
"rowsPerFile": rows_per_file,
|
||||
"parts": parts,
|
||||
},
|
||||
timeout=600,
|
||||
)
|
||||
run_response.raise_for_status()
|
||||
run_payload = run_response.json()
|
||||
if not run_payload.get("success") or not run_payload.get("data"):
|
||||
return {"success": False, "error": run_payload.get("message") or "数据拆分失败"}
|
||||
|
||||
data = run_payload.get("data") or {}
|
||||
result_items = []
|
||||
for item in data.get("items") or []:
|
||||
if item.get("success") and item.get("downloadUrl"):
|
||||
result_items.append({
|
||||
"result_id": item.get("resultId"),
|
||||
"source_path": item.get("sourceFilename") or "",
|
||||
"output_path": item.get("downloadUrl") or "",
|
||||
"success": True,
|
||||
"row_count": item.get("rowCount"),
|
||||
})
|
||||
else:
|
||||
result_items.append({
|
||||
"source_path": item.get("sourceFilename") or "",
|
||||
"success": False,
|
||||
"error": item.get("error") or "数据拆分失败",
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"summary": {
|
||||
"total": data.get("total") or len(paths),
|
||||
"success_count": data.get("successCount") or 0,
|
||||
"failed_count": data.get("failedCount") or 0,
|
||||
},
|
||||
"items": result_items,
|
||||
}
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
def delete_history_item(self, module_type: str, result_id: int) -> dict:
|
||||
try:
|
||||
response = self.session.delete(
|
||||
resolve_url(self.app_base_url, f"/api/{module_type}/history/{result_id}"),
|
||||
timeout=60,
|
||||
)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
return {"success": bool(payload.get("success")), "error": payload.get("message") if not payload.get("success") else None}
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
def get_convert_history(self) -> dict:
|
||||
try:
|
||||
response = self.session.get(
|
||||
resolve_url(self.app_base_url, "/api/convert/history"),
|
||||
timeout=60,
|
||||
)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
if not payload.get("success") or not payload.get("data"):
|
||||
return {"success": False, "error": payload.get("message") or "获取历史失败"}
|
||||
|
||||
items = payload.get("data", {}).get("items") or []
|
||||
return {
|
||||
"success": True,
|
||||
"items": [
|
||||
{
|
||||
"result_id": item.get("resultId"),
|
||||
"source_path": item.get("sourceFilename") or "",
|
||||
"output_path": item.get("downloadUrl") or "",
|
||||
"success": bool(item.get("success", True)),
|
||||
}
|
||||
for item in items
|
||||
],
|
||||
}
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
def get_split_history(self) -> dict:
|
||||
try:
|
||||
response = self.session.get(
|
||||
resolve_url(self.app_base_url, "/api/split/history"),
|
||||
timeout=60,
|
||||
)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
if not payload.get("success") or not payload.get("data"):
|
||||
return {"success": False, "error": payload.get("message") or "获取历史失败"}
|
||||
|
||||
items = payload.get("data", {}).get("items") or []
|
||||
return {
|
||||
"success": True,
|
||||
"items": [
|
||||
{
|
||||
"result_id": item.get("resultId"),
|
||||
"source_path": item.get("sourceFilename") or "",
|
||||
"output_path": item.get("downloadUrl") or "",
|
||||
"success": bool(item.get("success", True)),
|
||||
"row_count": item.get("rowCount"),
|
||||
}
|
||||
for item in items
|
||||
],
|
||||
}
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
def get_dedupe_history(self) -> dict:
|
||||
try:
|
||||
response = self.session.get(
|
||||
resolve_url(self.app_base_url, "/api/dedupe/history"),
|
||||
timeout=60,
|
||||
)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
if not payload.get("success") or not payload.get("data"):
|
||||
return {"success": False, "error": payload.get("message") or "获取历史失败"}
|
||||
|
||||
items = payload.get("data", {}).get("items") or []
|
||||
return {
|
||||
"success": True,
|
||||
"items": [
|
||||
{
|
||||
"result_id": item.get("resultId"),
|
||||
"source_path": item.get("sourceFilename") or "",
|
||||
"output_path": item.get("downloadUrl") or "",
|
||||
"success": bool(item.get("success", True)),
|
||||
}
|
||||
for item in items
|
||||
],
|
||||
}
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
def clean_excel_files(self, payload: dict) -> dict:
|
||||
paths = payload.get("paths") or []
|
||||
selected_columns = payload.get("selected_columns") or []
|
||||
keep_integer_ids = bool(payload.get("keep_integer_ids", True))
|
||||
keep_underscore_ids = bool(payload.get("keep_underscore_ids", True))
|
||||
output_dir = payload.get("output_dir") or ""
|
||||
|
||||
if not isinstance(paths, list) or not paths:
|
||||
return {"success": False, "error": "未选择待处理文件"}
|
||||
@@ -551,213 +635,140 @@ class DesktopApi:
|
||||
return {"success": False, "error": "请至少选择一列"}
|
||||
if not keep_integer_ids and not keep_underscore_ids:
|
||||
return {"success": False, "error": "请至少选择一种 ID 保留规则"}
|
||||
if not output_dir:
|
||||
return {"success": False, "error": "未选择输出目录"}
|
||||
|
||||
output_dir_path = Path(output_dir)
|
||||
if not output_dir_path.exists() or not output_dir_path.is_dir():
|
||||
return {"success": False, "error": "输出目录不存在"}
|
||||
try:
|
||||
uploaded_files = []
|
||||
for path in paths:
|
||||
with open(path, "rb") as file_obj:
|
||||
response = self.session.post(
|
||||
resolve_url(self.app_base_url, "/api/files/upload"),
|
||||
files={"file": (Path(path).name, file_obj)},
|
||||
timeout=180,
|
||||
)
|
||||
response.raise_for_status()
|
||||
upload_payload = response.json()
|
||||
if not upload_payload.get("success") or not upload_payload.get("data"):
|
||||
return {"success": False, "error": upload_payload.get("message") or "上传文件失败"}
|
||||
upload_data = upload_payload.get("data") or {}
|
||||
uploaded_files.append({
|
||||
"fileKey": upload_data.get("fileKey"),
|
||||
"originalFilename": upload_data.get("originalFilename") or Path(path).name,
|
||||
})
|
||||
|
||||
items: list[dict] = []
|
||||
success_count = 0
|
||||
failed_count = 0
|
||||
run_response = self.session.post(
|
||||
resolve_url(self.app_base_url, "/api/dedupe/run"),
|
||||
json={
|
||||
"files": uploaded_files,
|
||||
"selectedColumns": selected_columns,
|
||||
"keepIntegerIds": keep_integer_ids,
|
||||
"keepUnderscoreIds": keep_underscore_ids,
|
||||
},
|
||||
timeout=600,
|
||||
)
|
||||
run_response.raise_for_status()
|
||||
run_payload = run_response.json()
|
||||
if not run_payload.get("success") or not run_payload.get("data"):
|
||||
return {"success": False, "error": run_payload.get("message") or "去重失败"}
|
||||
|
||||
for path in paths:
|
||||
try:
|
||||
workbook = load_workbook(path)
|
||||
sheet = workbook[workbook.sheetnames[0]]
|
||||
header_cells = next(sheet.iter_rows(min_row=1, max_row=1))
|
||||
headers: list[str] = []
|
||||
header_map: dict[str, int] = {}
|
||||
for index, cell in enumerate(header_cells):
|
||||
value = normalize_cell_text(cell.value)
|
||||
if not value:
|
||||
continue
|
||||
value = value.split("idASIN国家状态价格变体数量", 1)[0].strip() or value
|
||||
if value in header_map:
|
||||
continue
|
||||
header_map[value] = index
|
||||
headers.append(value)
|
||||
if value == "缩略图地址8":
|
||||
break
|
||||
|
||||
missing = [column for column in selected_columns if column not in header_map]
|
||||
if missing:
|
||||
workbook.close()
|
||||
items.append({
|
||||
"source_path": path,
|
||||
"success": False,
|
||||
"error": f"缺少列:{'、'.join(missing)}",
|
||||
data = run_payload.get("data") or {}
|
||||
result_items = []
|
||||
for item in data.get("items") or []:
|
||||
if item.get("success") and item.get("downloadUrl"):
|
||||
result_items.append({
|
||||
"source_path": item.get("sourceFilename") or "",
|
||||
"output_path": item.get("downloadUrl") or "",
|
||||
"success": True,
|
||||
})
|
||||
else:
|
||||
result_items.append({
|
||||
"source_path": item.get("sourceFilename") or "",
|
||||
"success": False,
|
||||
"error": item.get("error") or "去重失败",
|
||||
})
|
||||
failed_count += 1
|
||||
continue
|
||||
|
||||
from openpyxl import Workbook
|
||||
|
||||
output_workbook = Workbook()
|
||||
output_sheet = output_workbook.active
|
||||
output_sheet.title = sheet.title
|
||||
output_sheet.append(selected_columns)
|
||||
|
||||
id_column_index = header_map.get("id")
|
||||
|
||||
for row in sheet.iter_rows(min_row=2, values_only=True):
|
||||
original_values = list(row)
|
||||
if id_column_index is not None:
|
||||
id_value = original_values[id_column_index] if id_column_index < len(original_values) else None
|
||||
if not should_keep_id(id_value, keep_integer_ids, keep_underscore_ids):
|
||||
continue
|
||||
|
||||
cleaned_values = [
|
||||
normalize_cell_text(original_values[header_map[column]])
|
||||
if header_map[column] < len(original_values)
|
||||
else ""
|
||||
for column in selected_columns
|
||||
]
|
||||
output_sheet.append(cleaned_values)
|
||||
|
||||
output_path = build_output_path(output_dir, path)
|
||||
output_workbook.save(output_path)
|
||||
output_workbook.close()
|
||||
workbook.close()
|
||||
|
||||
items.append({
|
||||
"source_path": path,
|
||||
"output_path": output_path,
|
||||
"success": True,
|
||||
})
|
||||
success_count += 1
|
||||
except Exception as exc: # noqa: BLE001
|
||||
items.append({
|
||||
"source_path": path,
|
||||
"success": False,
|
||||
"error": str(exc),
|
||||
})
|
||||
failed_count += 1
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"summary": {
|
||||
"total": len(paths),
|
||||
"success_count": success_count,
|
||||
"failed_count": failed_count,
|
||||
"output_dir": str(output_dir_path),
|
||||
},
|
||||
"items": items,
|
||||
}
|
||||
return {
|
||||
"success": True,
|
||||
"summary": {
|
||||
"total": data.get("total") or len(paths),
|
||||
"success_count": data.get("successCount") or 0,
|
||||
"failed_count": data.get("failedCount") or 0,
|
||||
},
|
||||
"items": result_items,
|
||||
}
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
def convert_excel_to_uk_txt(self, payload: dict) -> dict:
|
||||
paths = payload.get("paths") or []
|
||||
output_dir = payload.get("output_dir") or ""
|
||||
template_id = payload.get("template_id") or "uk_offer"
|
||||
|
||||
if not isinstance(paths, list) or not paths:
|
||||
return {"success": False, "error": "未选择待转换文件"}
|
||||
if not output_dir:
|
||||
return {"success": False, "error": "未选择输出目录"}
|
||||
|
||||
templates, _default_template_id = build_effective_templates()
|
||||
template = templates.get(template_id)
|
||||
if not template:
|
||||
return {"success": False, "error": "模板不存在"}
|
||||
try:
|
||||
templates = self.list_txt_templates()
|
||||
selected_template = next((item for item in templates if item.get("id") == template_id), None)
|
||||
if not selected_template:
|
||||
return {"success": False, "error": "模板不存在"}
|
||||
|
||||
output_dir_path = Path(output_dir)
|
||||
if not output_dir_path.exists() or not output_dir_path.is_dir():
|
||||
return {"success": False, "error": "输出目录不存在"}
|
||||
uploaded_files = []
|
||||
for path in paths:
|
||||
with open(path, "rb") as file_obj:
|
||||
response = self.session.post(
|
||||
resolve_url(self.app_base_url, "/api/files/upload"),
|
||||
files={"file": (Path(path).name, file_obj)},
|
||||
timeout=180,
|
||||
)
|
||||
response.raise_for_status()
|
||||
upload_payload = response.json()
|
||||
if not upload_payload.get("success") or not upload_payload.get("data"):
|
||||
return {"success": False, "error": upload_payload.get("message") or "上传文件失败"}
|
||||
upload_data = upload_payload.get("data") or {}
|
||||
uploaded_files.append({
|
||||
"fileKey": upload_data.get("fileKey"),
|
||||
"originalFilename": upload_data.get("originalFilename") or Path(path).name,
|
||||
})
|
||||
|
||||
items: list[dict] = []
|
||||
success_count = 0
|
||||
failed_count = 0
|
||||
run_response = self.session.post(
|
||||
resolve_url(self.app_base_url, "/api/convert/run"),
|
||||
json={
|
||||
"files": uploaded_files,
|
||||
"templateId": str(template_id),
|
||||
},
|
||||
timeout=600,
|
||||
)
|
||||
run_response.raise_for_status()
|
||||
run_payload = run_response.json()
|
||||
if not run_payload.get("success") or not run_payload.get("data"):
|
||||
return {"success": False, "error": run_payload.get("message") or "格式转换失败"}
|
||||
|
||||
for path in paths:
|
||||
try:
|
||||
workbook = load_workbook(path, read_only=True, data_only=True)
|
||||
sheet = workbook[workbook.sheetnames[0]]
|
||||
header_row = next(sheet.iter_rows(min_row=1, max_row=1, values_only=True), None)
|
||||
if not header_row:
|
||||
workbook.close()
|
||||
items.append({"source_path": path, "success": False, "error": 'Excel 表头为空'})
|
||||
failed_count += 1
|
||||
continue
|
||||
|
||||
header_map: dict[str, int] = {}
|
||||
for index, cell in enumerate(header_row):
|
||||
value = normalize_cell_text(cell)
|
||||
if not value or value in header_map:
|
||||
continue
|
||||
header_map[value] = index
|
||||
|
||||
required = template["required_source_columns"]
|
||||
missing = [column for column in required if column not in header_map]
|
||||
if missing:
|
||||
workbook.close()
|
||||
items.append({
|
||||
"source_path": path,
|
||||
"success": False,
|
||||
"error": f"缺少列:{'、'.join(missing)}",
|
||||
data = run_payload.get("data") or {}
|
||||
result_items = []
|
||||
for item in data.get("items") or []:
|
||||
if item.get("success") and item.get("downloadUrl"):
|
||||
result_items.append({
|
||||
"source_path": item.get("sourceFilename") or "",
|
||||
"output_path": item.get("downloadUrl") or "",
|
||||
"success": True,
|
||||
})
|
||||
else:
|
||||
result_items.append({
|
||||
"source_path": item.get("sourceFilename") or "",
|
||||
"success": False,
|
||||
"error": item.get("error") or "格式转换失败",
|
||||
})
|
||||
failed_count += 1
|
||||
continue
|
||||
|
||||
rows: list[str] = []
|
||||
rows.extend(template["preamble_lines"])
|
||||
rows.append('\t'.join(template["header_columns"]))
|
||||
for _ in range(template.get("blank_header_rows_after_schema", 0)):
|
||||
rows.append('\t' * (len(template["header_columns"]) - 1))
|
||||
|
||||
batch_id = generate_snowflake_like_id()
|
||||
row_index = 1
|
||||
for row in sheet.iter_rows(min_row=2, values_only=True):
|
||||
line_values: list[str] = []
|
||||
asin = normalize_cell_text(row[header_map['ASIN']] if header_map['ASIN'] < len(row) else '')
|
||||
if not asin:
|
||||
continue
|
||||
|
||||
for column in template["header_columns"]:
|
||||
if column == 'sku':
|
||||
line_values.append(f'{batch_id}-{row_index}')
|
||||
continue
|
||||
if column in template["field_mapping"]:
|
||||
source_column = template["field_mapping"][column]
|
||||
value = normalize_cell_text(row[header_map[source_column]] if header_map[source_column] < len(row) else '')
|
||||
line_values.append(value)
|
||||
continue
|
||||
if column in template["defaults"]:
|
||||
line_values.append(str(template["defaults"][column]))
|
||||
continue
|
||||
line_values.append('')
|
||||
|
||||
rows.append('\t'.join(line_values))
|
||||
row_index += 1
|
||||
|
||||
output_path = build_named_output_path(output_dir, template["output_filename"])
|
||||
Path(output_path).write_text('\n'.join(rows), encoding='utf-8')
|
||||
workbook.close()
|
||||
|
||||
items.append({
|
||||
"source_path": path,
|
||||
"output_path": output_path,
|
||||
"success": True,
|
||||
})
|
||||
success_count += 1
|
||||
except Exception as exc: # noqa: BLE001
|
||||
items.append({
|
||||
"source_path": path,
|
||||
"success": False,
|
||||
"error": str(exc),
|
||||
})
|
||||
failed_count += 1
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"summary": {
|
||||
"total": len(paths),
|
||||
"success_count": success_count,
|
||||
"failed_count": failed_count,
|
||||
"output_dir": str(output_dir_path),
|
||||
},
|
||||
"items": items,
|
||||
}
|
||||
return {
|
||||
"success": True,
|
||||
"summary": {
|
||||
"total": data.get("total") or len(paths),
|
||||
"success_count": data.get("successCount") or 0,
|
||||
"failed_count": data.get("failedCount") or 0,
|
||||
},
|
||||
"items": result_items,
|
||||
}
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
def open_path(self, path: str) -> dict:
|
||||
try:
|
||||
@@ -939,7 +950,7 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
parser.add_argument("--port", type=int, default=0)
|
||||
parser.add_argument("--width", type=int, default=1440)
|
||||
parser.add_argument("--height", type=int, default=960)
|
||||
parser.add_argument("--title", default="南日AI")
|
||||
parser.add_argument("--title", default="数富AI")
|
||||
return parser
|
||||
|
||||
|
||||
@@ -985,7 +996,7 @@ def main() -> None:
|
||||
ensure_dev_server_ready(config.frontend_url)
|
||||
app_url = config.frontend_url
|
||||
|
||||
api = DesktopApi(app_url)
|
||||
api = DesktopApi(config.backend_url)
|
||||
window = webview.create_window(
|
||||
title=config.title,
|
||||
url=app_url,
|
||||
|
||||
@@ -64,4 +64,4 @@ Write-Host "Ensuring desktop dependencies..."
|
||||
Invoke-Python -PythonCommand $pythonCmd -Arguments @("-m", "pip", "install", "-r", ".\desktop\requirements.txt")
|
||||
|
||||
Write-Host "Starting desktop app..."
|
||||
Invoke-Python -PythonCommand $pythonCmd -Arguments @("-m", "desktop.app", "--mode", "dev", "--frontend-url", "http://127.0.0.1:5173", "--backend-url", "http://127.0.0.1:8000")
|
||||
Invoke-Python -PythonCommand $pythonCmd -Arguments @("-m", "desktop.app", "--mode", "dev", "--frontend-url", "http://127.0.0.1:5173", "--backend-url", "http://127.0.0.1:18080")
|
||||
|
||||
@@ -90,6 +90,51 @@
|
||||
"price": "价格",
|
||||
"product-id": "ASIN"
|
||||
}
|
||||
},
|
||||
"user_1773982097249": {
|
||||
"id": "user_1773982097249",
|
||||
"label": "默认",
|
||||
"output_filename": "默认.txt",
|
||||
"is_default": false,
|
||||
"built_in": false,
|
||||
"preamble_lines": [
|
||||
"TemplateType=Offer\tVersion=1.4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"
|
||||
],
|
||||
"header_columns": [
|
||||
"sku",
|
||||
"price",
|
||||
"quantity",
|
||||
"product-id",
|
||||
"product-id-type",
|
||||
"condition-type",
|
||||
"condition-note",
|
||||
"ASIN-hint",
|
||||
"title",
|
||||
"product-tax-code",
|
||||
"operation-type",
|
||||
"sale-price",
|
||||
"sale-start-date",
|
||||
"sale-end-date",
|
||||
"leadtime-to-ship",
|
||||
"launch-date",
|
||||
"is-giftwrap-available",
|
||||
"is-gift-message-available"
|
||||
],
|
||||
"blank_header_rows_after_schema": 1,
|
||||
"required_source_columns": [
|
||||
"ASIN",
|
||||
"价格"
|
||||
],
|
||||
"defaults": {
|
||||
"quantity": "100",
|
||||
"product-id-type": "ASIN",
|
||||
"condition-type": "new",
|
||||
"leadtime-to-ship": "4"
|
||||
},
|
||||
"field_mapping": {
|
||||
"price": "价格",
|
||||
"product-id": "ASIN"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user