完成新需求

This commit is contained in:
super
2026-03-20 14:26:34 +08:00
parent 43d508f527
commit 92d3fc965b
71 changed files with 3265 additions and 540 deletions

View File

@@ -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,

View File

@@ -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")

View File

@@ -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"
}
}
}
}