| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429 |
- import tkinter as tk
- from tkinter import ttk, filedialog, messagebox, scrolledtext
- import pandas as pd
- import os
- import threading
- import requests
- from openpyxl import load_workbook
- from openpyxl.utils import get_column_letter
- import re
- import json
- import datetime
- import hashlib
- import hmac
- from urllib.parse import quote, urlparse, parse_qsl
- import time
- import base64
- from io import BytesIO
- class AmazonImageUploader:
- """亚马逊图片上传类"""
-
- def __init__(self, config=None):
- # 默认配置
- defaults = {
- "client_id": "amzn1.application-oa2-client.9f775d35077a4c7598116e5a89b60222",
- "client_secret": "amzn1.oa2-cs.v1.6909777baa82b796555a431cd23709342e8e01434abcad1f8b59cadf048b51c3",
- "refresh_token": "Atzr|IwEBICh_fojh7qAfrePzSjOZhQxTnHRZCYOuWNvSAHGaBs1WfkjR4qjC1Dmv0uK3oyq_L-R9SRKK4cYmPwF1MQXQdqKlpzOAGItWeXnNzh-SyhHHISv0_vCt3D6hluVmIEmF2kA7ro_XF-PslD41g3Eq6bv0Qq4y4pQbkDj-9dudDuz7nku8UL-nc4EWgJCrHCJTAs8TK48Yvrfi3E35uv97tYhs6qb8D9d4dR9C7daaC49eoDqP88qyUHThQV1Kqon8xukx4AT2_wLqWNcabpADzuK-5w7D-vvFFtvzfkbmtfzoPeEdHPulG6scc59BPFTyBlGU7a3d1JSFg2ROHIlrljRm",
- "aws_access_key": "AKIA24CLQSVTW2K4S6RP",
- "aws_secret_key": "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",
- "aws_region": "us-east-1",
- "seller_id": "A1H50Z0WYYLEGQ",
- "marketplace_id": "ATVPDKIKX0DER",
- "product_type": "TABLE",
- }
- if config:
- defaults.update(config)
-
- self.client_id = defaults["client_id"]
- self.client_secret = defaults["client_secret"]
- self.refresh_token = defaults["refresh_token"]
- self.aws_access_key = defaults["aws_access_key"]
- self.aws_secret_key = defaults["aws_secret_key"]
- self.aws_region = defaults["aws_region"]
- self.seller_id = defaults["seller_id"]
- self.marketplace_id = defaults["marketplace_id"]
- self.product_type = defaults["product_type"]
-
- def update_config(self, config):
- """更新配置"""
- if "client_id" in config:
- self.client_id = config["client_id"]
- if "client_secret" in config:
- self.client_secret = config["client_secret"]
- if "refresh_token" in config:
- self.refresh_token = config["refresh_token"]
- if "aws_access_key" in config:
- self.aws_access_key = config["aws_access_key"]
- if "aws_secret_key" in config:
- self.aws_secret_key = config["aws_secret_key"]
- if "aws_region" in config:
- self.aws_region = config["aws_region"]
- if "seller_id" in config:
- self.seller_id = config["seller_id"]
- if "marketplace_id" in config:
- self.marketplace_id = config["marketplace_id"]
- if "product_type" in config:
- self.product_type = config["product_type"]
-
- def sha256_hash(self, data):
- """兼容 str 和 bytes"""
- if isinstance(data, str):
- data = data.encode("utf-8")
- return hashlib.sha256(data).hexdigest()
-
- def hmac_sha256(self, key, msg):
- """返回 bytes 类型的 digest,兼容 str 和 bytes 输入"""
- if isinstance(key, str):
- key = key.encode("utf-8")
- if isinstance(msg, str):
- msg = msg.encode("utf-8")
- return hmac.new(key, msg, hashlib.sha256).digest()
-
- def get_signature_key(self, secret_key, date_stamp, region, service):
- k_date = self.hmac_sha256(("AWS4" + secret_key).encode("utf-8"), date_stamp)
- k_region = self.hmac_sha256(k_date, region)
- k_service = self.hmac_sha256(k_region, service)
- return self.hmac_sha256(k_service, "aws4_request")
-
- def get_access_token(self):
- url = "https://api.amazon.com/auth/o2/token"
- resp = requests.post(url, data={
- "grant_type": "refresh_token",
- "refresh_token": self.refresh_token,
- "client_id": self.client_id,
- "client_secret": self.client_secret,
- })
- resp.raise_for_status()
- return resp.json()["access_token"]
-
- def spapi_request(self, method, path, query, body=None, access_token=None):
- service = "execute-api"
- host = "sellingpartnerapi-na.amazon.com"
- payload = json.dumps(body, separators=(",", ":")) if body else ""
-
- t = datetime.datetime.now(datetime.timezone.utc)
- amz_date = t.strftime("%Y%m%dT%H%M%SZ")
- date_stamp = t.strftime("%Y%m%d")
-
- canonical_headers = (
- f"host:{host}\n"
- f"x-amz-access-token:{access_token}\n"
- f"x-amz-date:{amz_date}\n"
- )
- signed_headers = "host;x-amz-access-token;x-amz-date"
- payload_hash = self.sha256_hash(payload)
-
- canonical_request = (
- f"{method}\n{path}\n{query}\n"
- f"{canonical_headers}\n{signed_headers}\n{payload_hash}"
- )
-
- algorithm = "AWS4-HMAC-SHA256"
- credential_scope = f"{date_stamp}/{self.aws_region}/{service}/aws4_request"
- string_to_sign = (
- f"{algorithm}\n{amz_date}\n{credential_scope}\n"
- f"{self.sha256_hash(canonical_request)}"
- )
-
- signing_key = self.get_signature_key(self.aws_secret_key, date_stamp, self.aws_region, service)
- signature = self.hmac_sha256(signing_key, string_to_sign).hex()
-
- auth_header = (
- f"{algorithm} Credential={self.aws_access_key}/{credential_scope}, "
- f"SignedHeaders={signed_headers}, Signature={signature}"
- )
-
- headers = {
- "Host": host,
- "x-amz-access-token": access_token,
- "x-amz-date": amz_date,
- "Authorization": auth_header,
- }
- if body:
- headers["Content-Type"] = "application/json"
-
- url = f"https://{host}{path}?{query}"
- return requests.request(method, url, headers=headers, data=payload or None)
-
- def patch_image(self, access_token, image_type, image_url, sku):
- """
- image_type 可选值:
- "main" → 主图
- "other_1" ~ "other_8" → 副图1~8
- "swatch" → 变体图
- """
- encoded_sku = quote(sku, safe="")
- path = f"/listings/2021-08-01/items/{self.seller_id}/{encoded_sku}"
- query = f"marketplaceIds={self.marketplace_id}"
-
- if image_type == "main":
- attr_path = "/attributes/main_product_image_locator"
- elif image_type.startswith("other_") and 1 <= int(image_type.split("_")[1]) <= 8:
- n = image_type.split("_")[1]
- attr_path = f"/attributes/other_product_image_locator_{n}"
- elif image_type == "swatch":
- attr_path = "/attributes/swatch_product_image_locator"
- else:
- raise ValueError(f"不支持的 image_type: {image_type}")
-
- body = {
- "productType": self.product_type,
- "patches": [
- {
- "op": "replace",
- "path": attr_path,
- "value": [
- {
- "marketplace_id": self.marketplace_id,
- "media_location": image_url
- }
- ]
- }
- ]
- }
- return self.spapi_request("PATCH", path, query, body=body, access_token=access_token)
-
- # ==================== A+ Content API 方法 ====================
- def create_upload_destination(self, access_token, content_md5, content_type="image/png"):
- """创建上传目标"""
- resource = "aplus/2020-11-01/contentDocuments"
- path = f"/uploads/2020-11-01/uploadDestinations/{quote(resource, safe='/')}"
- query = f"marketplaceIds={self.marketplace_id}&contentMD5={quote(content_md5, safe='')}&contentType={quote(content_type, safe='')}"
-
- resp = self.spapi_request("POST", path, query, access_token=access_token)
- if resp.status_code not in [200, 201]:
- raise Exception(f"创建上传目标失败: {resp.text}")
- return resp.json().get("payload", {})
-
- def upload_to_s3(self, upload_url, image_binary, content_type="image/png"):
- """上传图片到S3"""
- parsed = urlparse(upload_url)
- base_url = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
- query_params = dict(parse_qsl(parsed.query))
- files = {"File": ("image.png", image_binary, content_type)}
- resp = requests.post(base_url, data=query_params, files=files, timeout=60)
- if resp.status_code not in [200, 201, 204]:
- raise Exception(f"S3上传失败: {resp.status_code}")
- return True
-
- def create_content_document(self, access_token, content_doc):
- """创建内容文档"""
- resp = self.spapi_request(
- "POST",
- "/aplus/2020-11-01/contentDocuments",
- f"marketplaceId={self.marketplace_id}",
- body={"contentDocument": content_doc},
- access_token=access_token
- )
- if resp.status_code not in [200, 201]:
- raise Exception(f"创建内容文档失败: {resp.text}")
- return resp.json().get("contentReferenceKey")
-
- def bind_asin_to_content(self, access_token, content_key, asin):
- """绑定ASIN到内容"""
- body = {"asinSet": [asin]}
- resp = self.spapi_request(
- "POST",
- f"/aplus/2020-11-01/contentDocuments/{content_key}/asins",
- f"marketplaceId={self.marketplace_id}",
- body=body,
- access_token=access_token
- )
- if resp.status_code not in [200, 201]:
- raise Exception(f"绑定ASIN失败: {resp.text}")
- return True
-
- def submit_for_approval(self, access_token, content_key):
- """提交审核"""
- resp = self.spapi_request(
- "POST",
- f"/aplus/2020-11-01/contentDocuments/{content_key}/approvalSubmissions",
- f"marketplaceId={self.marketplace_id}",
- access_token=access_token
- )
- if resp.status_code not in [200, 201]:
- raise Exception(f"提交审核失败: {resp.text}")
- return True
- class ImageMatcherApp:
- def __init__(self, root):
- self.root = root
- self.root.title("图片匹配与上传工具 - 亚马逊版")
- self.root.geometry("950x800")
-
- # 存储文件路径
- self.template_path = tk.StringVar()
- self.image_dir = tk.StringVar()
-
- # 固定上传配置
- self.upload_url = "https://cos.port.run/uploadFile"
- self.upload_group = "soundSynthesis"
- self.upload_user = "1"
-
- # 亚马逊上传器
- self.amazon_uploader = AmazonImageUploader()
-
- # 创建UI
- self.create_widgets()
-
- # 加载保存的配置
- self.load_config()
-
- def create_widgets(self):
- # 主框架
- main_frame = ttk.Frame(self.root, padding="10")
- main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
-
- # 配置网格权重
- self.root.columnconfigure(0, weight=1)
- self.root.rowconfigure(0, weight=1)
- main_frame.columnconfigure(0, weight=1)
- main_frame.rowconfigure(1, weight=1)
-
- # 创建Notebook(选项卡)
- self.notebook = ttk.Notebook(main_frame)
- self.notebook.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10)
-
- # 选项卡1: 图片匹配与上传
- self.tab_matching = ttk.Frame(self.notebook)
- self.notebook.add(self.tab_matching, text="图片匹配上传")
- self.create_matching_tab()
-
- # 选项卡2: 亚马逊主图更换
- self.tab_amazon = ttk.Frame(self.notebook)
- self.notebook.add(self.tab_amazon, text="亚马逊主图更换")
- self.create_amazon_tab()
-
- # 选项卡3: 亚马逊详情图更换 (A+ Content)
- self.tab_detail = ttk.Frame(self.notebook)
- self.notebook.add(self.tab_detail, text="亚马逊详情图更换")
- self.create_detail_tab()
-
- # 选项卡4: 设置
- self.tab_settings = ttk.Frame(self.notebook)
- self.notebook.add(self.tab_settings, text="设置")
- self.create_settings_tab()
-
- # 状态栏
- self.status_var = tk.StringVar()
- self.status_var.set("就绪")
- status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
- status_bar.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=5)
-
- def create_matching_tab(self):
- """创建图片匹配上传选项卡"""
- tab = self.tab_matching
- tab.columnconfigure(1, weight=1)
- tab.rowconfigure(4, weight=1)
-
- # 标题
- title_label = ttk.Label(tab, text="图片匹配与上传", font=("Arial", 14, "bold"))
- title_label.grid(row=0, column=0, columnspan=3, pady=10)
-
- # 模板文件选择
- ttk.Label(tab, text="模板文件:").grid(row=1, column=0, sticky=tk.W, pady=5)
- ttk.Entry(tab, textvariable=self.template_path, width=60).grid(row=1, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
- ttk.Button(tab, text="选择模板", command=self.select_template).grid(row=1, column=2, pady=5, padx=5)
-
- # 图片目录选择
- ttk.Label(tab, text="图片目录:").grid(row=2, column=0, sticky=tk.W, pady=5)
- ttk.Entry(tab, textvariable=self.image_dir, width=60).grid(row=2, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
- ttk.Button(tab, text="选择目录", command=self.select_image_dir).grid(row=2, column=2, pady=5, padx=5)
-
- # 按钮框架
- button_frame = ttk.Frame(tab)
- button_frame.grid(row=3, column=0, columnspan=3, pady=10)
-
- ttk.Button(button_frame, text="开始处理", command=self.start_matching, width=15).pack(side=tk.LEFT, padx=5)
- ttk.Button(button_frame, text="清空日志", command=self.clear_log, width=15).pack(side=tk.LEFT, padx=5)
-
- # 日志框
- log_frame = ttk.LabelFrame(tab, text="日志", padding="5")
- log_frame.grid(row=4, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10)
- log_frame.columnconfigure(0, weight=1)
- log_frame.rowconfigure(0, weight=1)
-
- self.log_text = scrolledtext.ScrolledText(log_frame, height=15, width=80, wrap=tk.WORD)
- self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
-
- # 存储生成的文件路径
- self.generated_file_path = None
-
- def create_amazon_tab(self):
- """创建亚马逊主图更换选项卡"""
- tab = self.tab_amazon
- tab.columnconfigure(1, weight=1)
- tab.rowconfigure(5, weight=1)
-
- # 标题
- title_label = ttk.Label(tab, text="亚马逊主图/附图更换", font=("Arial", 14, "bold"))
- title_label.grid(row=0, column=0, columnspan=3, pady=10)
-
- # SKU输入
- ttk.Label(tab, text="SKU:").grid(row=1, column=0, sticky=tk.W, pady=5)
- self.sku_input = tk.StringVar(value="bar grey 03")
- ttk.Entry(tab, textvariable=self.sku_input, width=40).grid(row=1, column=1, sticky=tk.W, pady=5, padx=5)
-
- # 图片文件夹选择
- ttk.Label(tab, text="图片文件夹:").grid(row=2, column=0, sticky=tk.W, pady=5)
- self.amazon_image_dir = tk.StringVar()
- ttk.Entry(tab, textvariable=self.amazon_image_dir, width=60).grid(row=2, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
- ttk.Button(tab, text="选择文件夹", command=self.select_amazon_image_dir).grid(row=2, column=2, pady=5, padx=5)
-
- # 说明标签
- note_label = ttk.Label(
- tab,
- text="说明:文件夹中的图片将按文件名排序,第1张作为主图,第2-9张作为附图1-8\n"
- "支持格式:.jpg / .jpeg / .png",
- foreground="gray"
- )
- note_label.grid(row=3, column=0, columnspan=3, sticky=tk.W, pady=5, padx=5)
-
- # 按钮框架
- button_frame = ttk.Frame(tab)
- button_frame.grid(row=4, column=0, columnspan=3, pady=10)
-
- ttk.Button(button_frame, text="开始上传", command=self.start_amazon_upload, width=15).pack(side=tk.LEFT, padx=5)
- ttk.Button(button_frame, text="清空日志", command=self.clear_amazon_log, width=15).pack(side=tk.LEFT, padx=5)
-
- # 日志框
- log_frame = ttk.LabelFrame(tab, text="上传日志", padding="5")
- log_frame.grid(row=5, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10)
- log_frame.columnconfigure(0, weight=1)
- log_frame.rowconfigure(0, weight=1)
-
- self.amazon_log_text = scrolledtext.ScrolledText(log_frame, height=15, width=80, wrap=tk.WORD)
- self.amazon_log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
-
- # 进度标签
- self.amazon_progress_var = tk.StringVar(value="就绪")
- progress_label = ttk.Label(tab, textvariable=self.amazon_progress_var, relief=tk.SUNKEN, anchor=tk.W)
- progress_label.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
- def create_detail_tab(self):
- """创建亚马逊详情图更换选项卡 (A+ Content)"""
- tab = self.tab_detail
- tab.columnconfigure(1, weight=1)
- tab.rowconfigure(9, weight=1)
-
- # 标题
- title_label = ttk.Label(tab, text="亚马逊详情图更换 (A+ Content)", font=("Arial", 14, "bold"))
- title_label.grid(row=0, column=0, columnspan=3, pady=10)
-
- # ASIN输入
- ttk.Label(tab, text="ASIN:").grid(row=1, column=0, sticky=tk.W, pady=5)
- self.asin_input = tk.StringVar(value="B0FR1WRK4M")
- ttk.Entry(tab, textvariable=self.asin_input, width=40).grid(row=1, column=1, sticky=tk.W, pady=5, padx=5)
-
- # 标题输入
- ttk.Label(tab, text="标题:").grid(row=2, column=0, sticky=tk.W, pady=5)
- self.detail_headline = tk.StringVar(value="Premium Quality Product")
- ttk.Entry(tab, textvariable=self.detail_headline, width=60).grid(row=2, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
-
- # 描述输入
- ttk.Label(tab, text="描述:").grid(row=3, column=0, sticky=tk.W, pady=5)
- self.detail_body = tk.StringVar(value="High-quality product designed for your needs.")
- ttk.Entry(tab, textvariable=self.detail_body, width=60).grid(row=3, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
-
- # 图片文件夹选择
- ttk.Label(tab, text="图片文件夹:").grid(row=4, column=0, sticky=tk.W, pady=5)
- self.detail_image_dir = tk.StringVar()
- ttk.Entry(tab, textvariable=self.detail_image_dir, width=60).grid(row=4, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
- ttk.Button(tab, text="选择文件夹", command=self.select_detail_image_dir).grid(row=4, column=2, pady=5, padx=5)
-
- # 图片尺寸设置
- size_frame = ttk.Frame(tab)
- size_frame.grid(row=5, column=0, columnspan=3, sticky=tk.W, pady=5, padx=5)
-
- ttk.Label(size_frame, text="图片宽度(像素):").pack(side=tk.LEFT)
- self.image_width = tk.StringVar(value="970")
- ttk.Entry(size_frame, textvariable=self.image_width, width=10).pack(side=tk.LEFT, padx=5)
-
- ttk.Label(size_frame, text="图片高度(像素):").pack(side=tk.LEFT, padx=(20, 0))
- self.image_height = tk.StringVar(value="600")
- ttk.Entry(size_frame, textvariable=self.image_height, width=10).pack(side=tk.LEFT, padx=5)
-
- # 说明标签
- note_label = ttk.Label(
- tab,
- text="说明:文件夹中的所有图片将按文件名顺序上传,每张图片作为一个 A+ 模块\n"
- "支持格式:.jpg / .jpeg / .png",
- foreground="gray"
- )
- note_label.grid(row=6, column=0, columnspan=3, sticky=tk.W, pady=5, padx=5)
-
- # 按钮框架
- button_frame = ttk.Frame(tab)
- button_frame.grid(row=7, column=0, columnspan=3, pady=10)
-
- ttk.Button(button_frame, text="开始上传", command=self.start_detail_upload, width=15).pack(side=tk.LEFT, padx=5)
- ttk.Button(button_frame, text="清空日志", command=self.clear_detail_log, width=15).pack(side=tk.LEFT, padx=5)
-
- # 日志框
- log_frame = ttk.LabelFrame(tab, text="上传日志", padding="5")
- log_frame.grid(row=8, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10)
- log_frame.columnconfigure(0, weight=1)
- log_frame.rowconfigure(0, weight=1)
-
- self.detail_log_text = scrolledtext.ScrolledText(log_frame, height=15, width=80, wrap=tk.WORD)
- self.detail_log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
-
- # 进度标签
- self.detail_progress_var = tk.StringVar(value="就绪")
- progress_label = ttk.Label(tab, textvariable=self.detail_progress_var, relief=tk.SUNKEN, anchor=tk.W)
- progress_label.grid(row=9, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
-
- def create_settings_tab(self):
- """创建设置选项卡"""
- tab = self.tab_settings
- tab.columnconfigure(0, weight=1)
- tab.rowconfigure(1, weight=1)
-
- # 标题
- title_label = ttk.Label(tab, text="亚马逊 API 设置", font=("Arial", 14, "bold"))
- title_label.grid(row=0, column=0, pady=10, sticky=tk.W, padx=10)
-
- # 创建一个可滚动的Frame来容纳所有设置项
- canvas = tk.Canvas(tab)
- scrollbar = ttk.Scrollbar(tab, orient="vertical", command=canvas.yview)
- scrollable_frame = ttk.Frame(canvas)
-
- scrollable_frame.bind(
- "<Configure>",
- lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
- )
-
- canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
- canvas.configure(yscrollcommand=scrollbar.set)
-
- canvas.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=10)
- scrollbar.grid(row=1, column=1, sticky=(tk.N, tk.S))
-
- scrollable_frame.columnconfigure(1, weight=1)
-
- # 设置项
- row = 0
-
- # Client ID
- ttk.Label(scrollable_frame, text="Client ID:").grid(row=row, column=0, sticky=tk.W, pady=5, padx=5)
- self.setting_client_id = tk.StringVar()
- ttk.Entry(scrollable_frame, textvariable=self.setting_client_id, width=80).grid(row=row, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
- row += 1
-
- # Client Secret
- ttk.Label(scrollable_frame, text="Client Secret:").grid(row=row, column=0, sticky=tk.W, pady=5, padx=5)
- self.setting_client_secret = tk.StringVar()
- ttk.Entry(scrollable_frame, textvariable=self.setting_client_secret, width=80, show="*").grid(row=row, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
- row += 1
-
- # Refresh Token
- ttk.Label(scrollable_frame, text="Refresh Token:").grid(row=row, column=0, sticky=tk.W, pady=5, padx=5)
- self.setting_refresh_token = tk.StringVar()
- ttk.Entry(scrollable_frame, textvariable=self.setting_refresh_token, width=80).grid(row=row, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
- row += 1
-
- # AWS Access Key
- ttk.Label(scrollable_frame, text="AWS Access Key:").grid(row=row, column=0, sticky=tk.W, pady=5, padx=5)
- self.setting_aws_access_key = tk.StringVar()
- ttk.Entry(scrollable_frame, textvariable=self.setting_aws_access_key, width=80).grid(row=row, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
- row += 1
-
- # AWS Secret Key
- ttk.Label(scrollable_frame, text="AWS Secret Key:").grid(row=row, column=0, sticky=tk.W, pady=5, padx=5)
- self.setting_aws_secret_key = tk.StringVar()
- ttk.Entry(scrollable_frame, textvariable=self.setting_aws_secret_key, width=80, show="*").grid(row=row, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
- row += 1
-
- # AWS Region
- ttk.Label(scrollable_frame, text="AWS Region:").grid(row=row, column=0, sticky=tk.W, pady=5, padx=5)
- self.setting_aws_region = tk.StringVar()
- ttk.Entry(scrollable_frame, textvariable=self.setting_aws_region, width=30).grid(row=row, column=1, sticky=tk.W, pady=5, padx=5)
- row += 1
-
- # Seller ID
- ttk.Label(scrollable_frame, text="Seller ID:").grid(row=row, column=0, sticky=tk.W, pady=5, padx=5)
- self.setting_seller_id = tk.StringVar()
- ttk.Entry(scrollable_frame, textvariable=self.setting_seller_id, width=80).grid(row=row, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
- row += 1
-
- # Marketplace ID
- ttk.Label(scrollable_frame, text="Marketplace ID:").grid(row=row, column=0, sticky=tk.W, pady=5, padx=5)
- self.setting_marketplace_id = tk.StringVar()
- ttk.Entry(scrollable_frame, textvariable=self.setting_marketplace_id, width=80).grid(row=row, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
- row += 1
-
- # Product Type
- ttk.Label(scrollable_frame, text="Product Type:").grid(row=row, column=0, sticky=tk.W, pady=5, padx=5)
- self.setting_product_type = tk.StringVar()
- ttk.Entry(scrollable_frame, textvariable=self.setting_product_type, width=30).grid(row=row, column=1, sticky=tk.W, pady=5, padx=5)
- row += 1
-
- # 分隔线
- ttk.Separator(scrollable_frame, orient=tk.HORIZONTAL).grid(row=row, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=15)
- row += 1
-
- # 按钮框架
- button_frame = ttk.Frame(scrollable_frame)
- button_frame.grid(row=row, column=0, columnspan=2, pady=15)
-
- ttk.Button(button_frame, text="保存设置", command=self.save_settings, width=15).pack(side=tk.LEFT, padx=5)
- ttk.Button(button_frame, text="重置为默认值", command=self.reset_settings, width=15).pack(side=tk.LEFT, padx=5)
- ttk.Button(button_frame, text="测试连接", command=self.test_connection, width=15).pack(side=tk.LEFT, padx=5)
-
- row += 1
-
- # 设置日志框
- log_frame = ttk.LabelFrame(scrollable_frame, text="设置日志", padding="5")
- log_frame.grid(row=row, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10, padx=5)
- log_frame.columnconfigure(0, weight=1)
- log_frame.rowconfigure(0, weight=1)
-
- self.settings_log_text = scrolledtext.ScrolledText(log_frame, height=8, width=80, wrap=tk.WORD)
- self.settings_log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
-
- # 初始化设置值
- self.init_settings_values()
-
- def init_settings_values(self):
- """初始化设置值"""
- self.setting_client_id.set(self.amazon_uploader.client_id)
- self.setting_client_secret.set(self.amazon_uploader.client_secret)
- self.setting_refresh_token.set(self.amazon_uploader.refresh_token)
- self.setting_aws_access_key.set(self.amazon_uploader.aws_access_key)
- self.setting_aws_secret_key.set(self.amazon_uploader.aws_secret_key)
- self.setting_aws_region.set(self.amazon_uploader.aws_region)
- self.setting_seller_id.set(self.amazon_uploader.seller_id)
- self.setting_marketplace_id.set(self.amazon_uploader.marketplace_id)
- self.setting_product_type.set(self.amazon_uploader.product_type)
-
- def settings_log(self, message):
- """在设置选项卡日志框中添加消息"""
- self.settings_log_text.insert(tk.END, f"{message}\n")
- self.settings_log_text.see(tk.END)
- self.root.update_idletasks()
-
- def get_settings_config_path(self):
- """获取设置配置文件路径"""
- return os.path.join(os.path.dirname(os.path.abspath(__file__)), "amazon_settings.json")
-
- def save_settings(self):
- """保存设置"""
- config = {
- "client_id": self.setting_client_id.get(),
- "client_secret": self.setting_client_secret.get(),
- "refresh_token": self.setting_refresh_token.get(),
- "aws_access_key": self.setting_aws_access_key.get(),
- "aws_secret_key": self.setting_aws_secret_key.get(),
- "aws_region": self.setting_aws_region.get(),
- "seller_id": self.setting_seller_id.get(),
- "marketplace_id": self.setting_marketplace_id.get(),
- "product_type": self.setting_product_type.get(),
- }
- try:
- with open(self.get_settings_config_path(), 'w', encoding='utf-8') as f:
- json.dump(config, f, ensure_ascii=False, indent=2)
-
- # 更新亚马逊上传器配置
- self.amazon_uploader.update_config(config)
-
- self.settings_log("✅ 设置已保存并应用")
- messagebox.showinfo("成功", "设置已保存!")
- except Exception as e:
- self.settings_log(f"❌ 保存设置失败: {str(e)}")
- messagebox.showerror("错误", f"保存设置失败: {str(e)}")
-
- def reset_settings(self):
- """重置为默认值"""
- if not messagebox.askyesno("确认", "确定要重置为默认值吗?"):
- return
-
- defaults = {
- "client_id": "amzn1.application-oa2-client.9f775d35077a4c7598116e5a89b60222",
- "client_secret": "amzn1.oa2-cs.v1.6909777baa82b796555a431cd23709342e8e01434abcad1f8b59cadf048b51c3",
- "refresh_token": "Atzr|IwEBICh_fojh7qAfrePzSjOZhQxTnHRZCYOuWNvSAHGaBs1WfkjR4qjC1Dmv0uK3oyq_L-R9SRKK4cYmPwF1MQXQdqKlpzOAGItWeXnNzh-SyhHHISv0_vCt3D6hluVmIEmF2kA7ro_XF-PslD41g3Eq6bv0Qq4y4pQbkDj-9dudDuz7nku8UL-nc4EWgJCrHCJTAs8TK48Yvrfi3E35uv97tYhs6qb8D9d4dR9C7daaC49eoDqP88qyUHThQV1Kqon8xukx4AT2_wLqWNcabpADzuK-5w7D-vvFFtvzfkbmtfzoPeEdHPulG6scc59BPFTyBlGU7a3d1JSFg2ROHIlrljRm",
- "aws_access_key": "AKIA24CLQSVTW2K4S6RP",
- "aws_secret_key": "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",
- "aws_region": "us-east-1",
- "seller_id": "A1H50Z0WYYLEGQ",
- "marketplace_id": "ATVPDKIKX0DER",
- "product_type": "TABLE",
- }
-
- self.setting_client_id.set(defaults["client_id"])
- self.setting_client_secret.set(defaults["client_secret"])
- self.setting_refresh_token.set(defaults["refresh_token"])
- self.setting_aws_access_key.set(defaults["aws_access_key"])
- self.setting_aws_secret_key.set(defaults["aws_secret_key"])
- self.setting_aws_region.set(defaults["aws_region"])
- self.setting_seller_id.set(defaults["seller_id"])
- self.setting_marketplace_id.set(defaults["marketplace_id"])
- self.setting_product_type.set(defaults["product_type"])
-
- self.settings_log("✅ 已重置为默认值")
-
- def test_connection(self):
- """测试连接"""
- self.settings_log("🔍 测试连接...")
-
- # 先保存设置
- self.save_settings()
-
- try:
- # 测试获取Access Token
- self.settings_log(" 🔑 获取Access Token...")
- token = self.amazon_uploader.get_access_token()
- self.settings_log(f" ✅ Token获取成功 (长度: {len(token)})")
-
- # 测试SP-API连接(获取marketplace参与信息)
- self.settings_log(" 🌐 测试SP-API连接...")
- resp = self.amazon_uploader.spapi_request(
- "GET",
- "/sellers/v1/marketplaceParticipations",
- "",
- access_token=token
- )
-
- if resp.status_code == 200:
- self.settings_log(" ✅ SP-API连接成功")
- data = resp.json()
- self.settings_log(f" 参与市场数量: {len(data.get('payload', []))}")
- else:
- self.settings_log(f" ⚠️ SP-API返回状态码: {resp.status_code}")
- self.settings_log(f" 响应: {resp.text[:200]}")
-
- self.settings_log("✅ 连接测试完成")
-
- except Exception as e:
- self.settings_log(f"❌ 连接测试失败: {str(e)}")
- messagebox.showerror("连接失败", f"连接测试失败:\n{str(e)}")
-
- def load_settings(self):
- """加载设置"""
- try:
- config_path = self.get_settings_config_path()
- if os.path.exists(config_path):
- with open(config_path, 'r', encoding='utf-8') as f:
- config = json.load(f)
-
- self.setting_client_id.set(config.get("client_id", self.amazon_uploader.client_id))
- self.setting_client_secret.set(config.get("client_secret", self.amazon_uploader.client_secret))
- self.setting_refresh_token.set(config.get("refresh_token", self.amazon_uploader.refresh_token))
- self.setting_aws_access_key.set(config.get("aws_access_key", self.amazon_uploader.aws_access_key))
- self.setting_aws_secret_key.set(config.get("aws_secret_key", self.amazon_uploader.aws_secret_key))
- self.setting_aws_region.set(config.get("aws_region", self.amazon_uploader.aws_region))
- self.setting_seller_id.set(config.get("seller_id", self.amazon_uploader.seller_id))
- self.setting_marketplace_id.set(config.get("marketplace_id", self.amazon_uploader.marketplace_id))
- self.setting_product_type.set(config.get("product_type", self.amazon_uploader.product_type))
-
- # 更新上传器配置
- self.amazon_uploader.update_config(config)
- except Exception:
- pass
- # ==================== 文件/目录选择 ====================
- def select_template(self):
- filename = filedialog.askopenfilename(
- title="选择模板文件",
- filetypes=[("Excel文件", "*.xlsx *.xls"), ("所有文件", "*.*")]
- )
- if filename:
- self.template_path.set(filename)
- self.log(f"已选择模板文件: {filename}")
-
- def select_image_dir(self):
- directory = filedialog.askdirectory(title="选择图片目录")
- if directory:
- self.image_dir.set(directory)
- self.log(f"已选择图片目录: {directory}")
-
- def select_amazon_image_dir(self):
- directory = filedialog.askdirectory(title="选择图片文件夹")
- if directory:
- self.amazon_image_dir.set(directory)
- self.amazon_log(f"已选择图片文件夹: {directory}")
-
- def select_detail_image_dir(self):
- directory = filedialog.askdirectory(title="选择图片文件夹")
- if directory:
- self.detail_image_dir.set(directory)
- self.detail_log(f"已选择图片文件夹: {directory}")
-
- # ==================== 日志方法 ====================
- def log(self, message):
- """在匹配选项卡日志框中添加消息"""
- self.log_text.insert(tk.END, f"{message}\n")
- self.log_text.see(tk.END)
- self.root.update_idletasks()
-
- def clear_log(self):
- """清空匹配选项卡日志"""
- self.log_text.delete(1.0, tk.END)
-
- def amazon_log(self, message):
- """在亚马逊选项卡日志框中添加消息"""
- self.amazon_log_text.insert(tk.END, f"{message}\n")
- self.amazon_log_text.see(tk.END)
- self.root.update_idletasks()
-
- def clear_amazon_log(self):
- """清空亚马逊选项卡日志"""
- self.amazon_log_text.delete(1.0, tk.END)
-
- def detail_log(self, message):
- """在详情图选项卡日志框中添加消息"""
- self.detail_log_text.insert(tk.END, f"{message}\n")
- self.detail_log_text.see(tk.END)
- self.root.update_idletasks()
-
- def clear_detail_log(self):
- """清空详情图选项卡日志"""
- self.detail_log_text.delete(1.0, tk.END)
-
- # ==================== 配置路径 ====================
- def get_amazon_config_path(self):
- """获取主图配置路径"""
- return os.path.join(os.path.dirname(os.path.abspath(__file__)), "amazon_config.json")
-
- def get_detail_config_path(self):
- """获取详情图配置路径"""
- return os.path.join(os.path.dirname(os.path.abspath(__file__)), "amazon_detail_config.json")
-
- # ==================== 配置文件 ====================
- def load_config(self):
- """启动时自动加载配置"""
- # 加载主图配置
- try:
- config_path = self.get_amazon_config_path()
- if os.path.exists(config_path):
- with open(config_path, 'r', encoding='utf-8') as f:
- config = json.load(f)
- self.sku_input.set(config.get("sku", "bar grey 03"))
- if config.get("image_dir"):
- self.amazon_image_dir.set(config["image_dir"])
- except Exception:
- pass
-
- # 加载详情图配置
- try:
- config_path = self.get_detail_config_path()
- if os.path.exists(config_path):
- with open(config_path, 'r', encoding='utf-8') as f:
- config = json.load(f)
- self.asin_input.set(config.get("asin", "B0FR1WRK4M"))
- self.detail_headline.set(config.get("headline", "Premium Quality Product"))
- self.detail_body.set(config.get("body", "High-quality product designed for your needs."))
- self.image_width.set(config.get("image_width", "970"))
- self.image_height.set(config.get("image_height", "600"))
- if config.get("image_dir"):
- self.detail_image_dir.set(config["image_dir"])
- except Exception:
- pass
-
- # 加载设置配置
- self.load_settings()
- # ==================== 图片匹配上传 ====================
- def start_matching(self):
- """开始匹配处理"""
- if not self.template_path.get():
- messagebox.showerror("错误", "请先选择模板文件!")
- return
- if not self.image_dir.get():
- messagebox.showerror("错误", "请先选择图片目录!")
- return
-
- thread = threading.Thread(target=self.process_matching, daemon=True)
- thread.start()
-
- def process_matching(self):
- """执行匹配处理"""
- try:
- self.status_var.set("处理中...")
- self.log("=" * 50)
- self.log("开始处理...")
-
- template_path = self.template_path.get()
- image_dir = self.image_dir.get()
-
- self.log(f"读取模板文件: {template_path}")
-
- # 使用openpyxl加载工作簿,保留所有样式
- wb = load_workbook(template_path)
- ws = wb.active
-
- # 获取表头(第二行)
- headers = []
- for col in range(1, ws.max_column + 1):
- cell_value = ws.cell(row=2, column=col).value
- headers.append(str(cell_value) if cell_value else '')
-
- self.log(f"找到 {len(headers)} 列")
-
- # 查找各列的列号
- col_a_index = 1 # A列是第1列
- product_main_col = None
- image_cols = {}
-
- for idx, header in enumerate(headers, start=1):
- if header == '产品主图':
- product_main_col = idx
- elif '上架图片' in header:
- match = re.search(r'(\d+)', header)
- if match:
- num = int(match.group(1))
- image_cols[num] = idx
-
- if product_main_col is None:
- self.log("警告: 未找到'产品主图'列")
- else:
- self.log(f"找到产品主图列: 第{product_main_col}列")
-
- self.log(f"找到上架图片列: {len(image_cols)} 列")
-
- # 处理每一行(从第三行开始)
- processed_count = 0
- for row_idx in range(3, ws.max_row + 1):
- a_cell = ws.cell(row=row_idx, column=col_a_index)
- a_value = str(a_cell.value).strip() if a_cell.value else ''
-
- if not a_value or a_value == 'nan' or a_value == 'None':
- continue
-
- self.log(f"\n处理第 {row_idx} 行, A列值: {a_value}")
-
- # 处理产品主图 (图片编号1)
- if product_main_col is not None:
- img_url = self.find_and_upload_image(a_value, 1, image_dir)
- if img_url:
- ws.cell(row=row_idx, column=product_main_col, value=img_url)
- self.log(f" 产品主图上传成功: {img_url[:50]}...")
- else:
- self.log(f" 产品主图: 未找到或上传失败")
-
- # 处理上架图片1-6 (图片编号2-7)
- for i in range(1, 7):
- if i in image_cols:
- img_url = self.find_and_upload_image(a_value, i+1, image_dir)
- if img_url:
- ws.cell(row=row_idx, column=image_cols[i], value=img_url)
- self.log(f" 上架图片{i}上传成功: {img_url[:50]}...")
- else:
- self.log(f" 上架图片{i}: 未找到或上传失败")
-
- processed_count += 1
-
- # 保存结果(保留所有样式)
- output_path = self.generate_output_path(template_path)
- wb.save(output_path)
- self.generated_file_path = output_path
- self.log(f"\n处理完成!共处理 {processed_count} 行数据")
- self.log(f"结果已保存到: {output_path}")
-
- self.status_var.set(f"处理完成!共处理 {processed_count} 行")
- messagebox.showinfo("完成", f"处理完成!\n共处理 {processed_count} 行数据\n结果保存在:\n{output_path}")
-
- except Exception as e:
- error_msg = f"处理出错: {str(e)}"
- self.log(f"错误: {error_msg}")
- self.status_var.set("处理出错")
- messagebox.showerror("错误", error_msg)
- import traceback
- self.log(traceback.format_exc())
-
- def generate_output_path(self, template_path):
- """生成输出文件路径"""
- dir_path = os.path.dirname(template_path)
- base_name = os.path.basename(template_path)
- name_without_ext = os.path.splitext(base_name)[0]
- new_filename = f"{name_without_ext}_结果.xlsx"
- return os.path.join(dir_path, new_filename)
-
- def find_and_upload_image(self, a_value, num, image_dir):
- """查找图片并上传,返回URL"""
- extensions = ['.jpg', '.jpeg', '.png']
- local_file = None
-
- for ext in extensions:
- filename = f"{a_value}_{num}{ext}"
- filepath = os.path.join(image_dir, filename)
- if os.path.exists(filepath):
- local_file = filepath
- break
-
- if not local_file:
- self.log(f"找不到目录:{filepath}")
- return None
-
- try:
- full_url = f"{self.upload_url}?group={self.upload_group}&user={self.upload_user}"
-
- content_type = 'image/jpeg'
- if local_file.lower().endswith('.png'):
- content_type = 'image/png'
-
- with open(local_file, 'rb') as f:
- files = {'file': (os.path.basename(local_file), f, content_type)}
- response = requests.post(full_url, files=files, timeout=30)
-
- if response.status_code == 200:
- result = response.json()
- if result.get('err') == 0:
- return result.get('url')
- else:
- self.log(f" 上传失败: {result.get('msg', '未知错误')}")
- return None
- else:
- self.log(f" 上传请求失败,状态码: {response.status_code}")
- return None
-
- except Exception as e:
- self.log(f" 上传出错: {str(e)}")
- return None
- # ==================== 亚马逊主图更换 ====================
- def start_amazon_upload(self):
- """开始亚马逊主图上传"""
- if not self.sku_input.get().strip():
- messagebox.showerror("错误", "请先输入SKU!")
- return
-
- if not self.amazon_image_dir.get():
- messagebox.showerror("错误", "请先选择图片文件夹!")
- return
-
- # 保存配置
- self.save_amazon_config()
-
- thread = threading.Thread(target=self.process_amazon_upload, daemon=True)
- thread.start()
-
- def save_amazon_config(self):
- """保存主图配置"""
- config = {
- "sku": self.sku_input.get(),
- "image_dir": self.amazon_image_dir.get(),
- }
- try:
- with open(self.get_amazon_config_path(), 'w', encoding='utf-8') as f:
- json.dump(config, f, ensure_ascii=False, indent=2)
- except Exception:
- pass
-
- def get_image_files(self, folder):
- """获取文件夹中的图片文件,按文件名排序"""
- extensions = ['.jpg', '.jpeg', '.png']
- files = []
- if not os.path.isdir(folder):
- return files
- for f in os.listdir(folder):
- ext = os.path.splitext(f)[1].lower()
- if ext in extensions:
- files.append(os.path.join(folder, f))
- # 按文件名排序
- files.sort(key=lambda x: os.path.basename(x).lower())
- return files
-
- def upload_image_to_cos(self, local_file):
- """上传图片到图片服务器,返回URL"""
- try:
- full_url = f"{self.upload_url}?group={self.upload_group}&user={self.upload_user}"
-
- content_type = 'image/jpeg'
- if local_file.lower().endswith('.png'):
- content_type = 'image/png'
-
- with open(local_file, 'rb') as f:
- files = {'file': (os.path.basename(local_file), f, content_type)}
- response = requests.post(full_url, files=files, timeout=30)
-
- if response.status_code == 200:
- result = response.json()
- if result.get('err') == 0:
- return result.get('url')
- else:
- raise Exception(result.get('msg', '未知错误'))
- else:
- raise Exception(f"HTTP {response.status_code}")
- except Exception as e:
- raise Exception(f"上传失败: {str(e)}")
-
- def process_amazon_upload(self):
- """执行亚马逊主图上传"""
- try:
- self.amazon_progress_var.set("处理中...")
- self.amazon_log("=" * 50)
- self.amazon_log("开始亚马逊主图/附图上传...")
-
- sku = self.sku_input.get().strip()
- image_dir = self.amazon_image_dir.get()
-
- self.amazon_log(f"SKU: {sku}")
- self.amazon_log(f"图片文件夹: {image_dir}")
-
- # 获取图片文件
- image_files = self.get_image_files(image_dir)
- if not image_files:
- self.amazon_log("❌ 未找到任何图片文件")
- self.amazon_progress_var.set("上传失败")
- return
-
- self.amazon_log(f"找到 {len(image_files)} 张图片")
- for i, f in enumerate(image_files, start=1):
- self.amazon_log(f" {i}. {os.path.basename(f)}")
-
- # 限制最多9张(1主图+8附图)
- if len(image_files) > 9:
- self.amazon_log(f"⚠️ 图片数量超过9张,只使用前9张")
- image_files = image_files[:9]
-
- # 获取Access Token
- self.amazon_log("\n🔑 获取Access Token...")
- try:
- token = self.amazon_uploader.get_access_token()
- self.amazon_log("✅ Token获取成功")
- except Exception as e:
- self.amazon_log(f"❌ 获取Token失败: {str(e)}")
- self.amazon_progress_var.set("上传失败")
- return
-
- # 处理每张图片
- success_count = 0
- fail_count = 0
-
- for i, local_file in enumerate(image_files):
- filename = os.path.basename(local_file)
-
- # 确定图片类型
- if i == 0:
- image_type = "main"
- type_name = "主图"
- else:
- image_type = f"other_{i}"
- type_name = f"附图{i}"
-
- self.amazon_log(f"\n处理 {type_name}: {filename}")
-
- # 上传到图片服务器获取URL
- try:
- self.amazon_log(f" 📤 上传到图片服务器...")
- img_url = self.upload_image_to_cos(local_file)
- self.amazon_log(f" ✅ 图片服务器上传成功: {img_url[:60]}...")
- except Exception as e:
- self.amazon_log(f" ❌ 图片服务器上传失败: {str(e)}")
- fail_count += 1
- continue
-
- # 调用亚马逊API更换图片
- try:
- self.amazon_log(f" 🔄 调用亚马逊API更换{type_name}...")
- resp = self.amazon_uploader.patch_image(token, image_type, img_url, sku)
- if resp.status_code == 200:
- self.amazon_log(f" ✅ {type_name}更换成功")
- success_count += 1
- else:
- self.amazon_log(f" ❌ {type_name}更换失败: HTTP {resp.status_code}")
- try:
- error_data = resp.json()
- self.amazon_log(f" 错误详情: {json.dumps(error_data, ensure_ascii=False)[:300]}")
- except:
- self.amazon_log(f" 响应: {resp.text[:300]}")
- fail_count += 1
- except Exception as e:
- self.amazon_log(f" ❌ {type_name}更换异常: {str(e)}")
- fail_count += 1
-
- # 避免请求过快
- time.sleep(0.5)
-
- self.amazon_log(f"\n{'=' * 50}")
- self.amazon_log(f"✅ 处理完成!成功: {success_count}, 失败: {fail_count}")
- self.amazon_progress_var.set(f"上传完成!成功: {success_count}, 失败: {fail_count}")
- messagebox.showinfo("完成", f"亚马逊图片上传完成!\n成功: {success_count}\n失败: {fail_count}")
-
- except Exception as e:
- error_msg = f"处理出错: {str(e)}"
- self.amazon_log(f"❌ {error_msg}")
- self.amazon_progress_var.set("处理出错")
- messagebox.showerror("错误", error_msg)
- import traceback
- self.amazon_log(traceback.format_exc())
- # ==================== 亚马逊详情图更换 ====================
- def start_detail_upload(self):
- """开始亚马逊详情图上传 (A+ Content)"""
- if not self.asin_input.get().strip():
- messagebox.showerror("错误", "请先输入ASIN!")
- return
-
- if not self.detail_image_dir.get():
- messagebox.showerror("错误", "请先选择图片文件夹!")
- return
-
- # 保存配置
- self.save_detail_config()
-
- thread = threading.Thread(target=self.process_detail_upload, daemon=True)
- thread.start()
-
- def save_detail_config(self):
- """保存详情图配置"""
- config = {
- "asin": self.asin_input.get(),
- "headline": self.detail_headline.get(),
- "body": self.detail_body.get(),
- "image_width": self.image_width.get(),
- "image_height": self.image_height.get(),
- "image_dir": self.detail_image_dir.get(),
- }
- try:
- with open(self.get_detail_config_path(), 'w', encoding='utf-8') as f:
- json.dump(config, f, ensure_ascii=False, indent=2)
- except Exception:
- pass
-
- def process_detail_upload(self):
- """执行亚马逊详情图上传 (A+ Content)"""
- try:
- self.detail_progress_var.set("处理中...")
- self.detail_log("=" * 50)
- self.detail_log("开始亚马逊详情图上传 (A+ Content)...")
-
- asin = self.asin_input.get().strip()
- headline = self.detail_headline.get().strip()
- body_text = self.detail_body.get().strip()
- image_dir = self.detail_image_dir.get()
-
- # 获取图片尺寸
- try:
- target_width = int(self.image_width.get())
- target_height = int(self.image_height.get())
- except ValueError:
- self.detail_log("❌ 图片尺寸必须为数字")
- self.detail_progress_var.set("上传失败")
- return
-
- self.detail_log(f"ASIN: {asin}")
- self.detail_log(f"标题: {headline}")
- self.detail_log(f"描述: {body_text}")
- self.detail_log(f"图片尺寸: {target_width}x{target_height}")
- self.detail_log(f"图片文件夹: {image_dir}")
-
- # 获取图片文件
- image_files = self.get_image_files(image_dir)
- if not image_files:
- self.detail_log("❌ 未找到任何图片文件")
- self.detail_progress_var.set("上传失败")
- return
-
- self.detail_log(f"\n找到 {len(image_files)} 张图片")
- for i, f in enumerate(image_files, start=1):
- self.detail_log(f" {i}. {os.path.basename(f)}")
-
- # 尝试导入PIL
- try:
- from PIL import Image
- from io import BytesIO
- HAS_PIL = True
- except ImportError:
- HAS_PIL = False
- self.detail_log("⚠️ 未安装Pillow,将使用原始图片尺寸")
-
- # 获取Access Token
- self.detail_log("\n🔑 获取Access Token...")
- try:
- token = self.amazon_uploader.get_access_token()
- self.detail_log("✅ Token获取成功")
- except Exception as e:
- self.detail_log(f"❌ 获取Token失败: {str(e)}")
- self.detail_progress_var.set("上传失败")
- return
-
- # 处理每张图片
- success_count = 0
- fail_count = 0
- upload_ids = []
-
- for i, local_file in enumerate(image_files, start=1):
- filename = os.path.basename(local_file)
- self.detail_log(f"\n处理第 {i}/{len(image_files)} 张图片: {filename}")
-
- try:
- # 读取图片
- with open(local_file, 'rb') as f:
- image_binary = f.read()
-
- # 调整尺寸
- if HAS_PIL:
- try:
- img = Image.open(BytesIO(image_binary))
- original_size = img.size
- # 调整尺寸
- img_resized = img.resize((target_width, target_height), Image.LANCZOS)
- buffer = BytesIO()
- # 判断原图格式
- if filename.lower().endswith('.png'):
- img_resized.save(buffer, format="PNG")
- else:
- img_resized.save(buffer, format="JPEG")
- image_binary = buffer.getvalue()
- self.detail_log(f" 图片尺寸调整: {original_size} -> {target_width}x{target_height}")
- except Exception as e:
- self.detail_log(f" 图片调整失败: {str(e)},使用原图")
-
- # 计算MD5
- md5_hash = hashlib.md5(image_binary).digest()
- content_md5 = base64.b64encode(md5_hash).decode('utf-8')
-
- # 确定Content-Type
- content_type = "image/jpeg"
- if filename.lower().endswith('.png'):
- content_type = "image/png"
-
- self.detail_log(f" 图片大小: {len(image_binary)} bytes")
-
- # 创建上传目标
- upload_data = self.amazon_uploader.create_upload_destination(token, content_md5, content_type)
- upload_id = upload_data.get("uploadDestinationId")
- upload_url = upload_data.get("url")
-
- if not upload_id or not upload_url:
- self.detail_log(f" ❌ 创建上传目标失败")
- fail_count += 1
- continue
-
- self.detail_log(f" ✅ 上传目标创建成功")
-
- # 上传到S3
- self.amazon_uploader.upload_to_s3(upload_url, image_binary, content_type)
- self.detail_log(f" ✅ S3上传成功")
-
- upload_ids.append({
- "upload_id": upload_id,
- "width": target_width,
- "height": target_height,
- "alt_text": f"Product detail image {i}"
- })
-
- success_count += 1
- time.sleep(0.5)
-
- except Exception as e:
- self.detail_log(f" ❌ 处理失败: {str(e)}")
- fail_count += 1
-
- if not upload_ids:
- self.detail_log("❌ 没有成功上传的图片")
- self.detail_progress_var.set("上传失败")
- return
-
- # 构建A+ Content内容文档
- self.detail_log(f"\n📝 构建A+ Content文档 ({len(upload_ids)} 个模块)...")
-
- # 构建模块列表 - 每张图片一个模块
- content_module_list = []
- for i, upload_info in enumerate(upload_ids):
- module = {
- "contentModuleType": "STANDARD_HEADER_IMAGE_TEXT",
- "standardHeaderImageText": {
- "headline": {
- "value": headline if i == 0 else f"{headline} - {i+1}",
- "decoratorSet": []
- },
- "block": {
- "image": {
- "uploadDestinationId": upload_info["upload_id"],
- "imageCropSpecification": {
- "size": {
- "width": {"value": upload_info["width"], "units": "pixels"},
- "height": {"value": upload_info["height"], "units": "pixels"}
- },
- "offset": {
- "x": {"value": 0, "units": "pixels"},
- "y": {"value": 0, "units": "pixels"}
- }
- },
- "altText": upload_info["alt_text"]
- },
- "headline": {
- "value": f"Image {i+1}",
- "decoratorSet": []
- },
- "body": {
- "textList": [
- {
- "value": body_text if i == 0 else f"Product detail view {i+1}",
- "decoratorSet": []
- }
- ]
- }
- }
- }
- }
- content_module_list.append(module)
-
- # 构建content文档
- content_doc = {
- "name": f"Product Images for {asin}",
- "contentType": "EBC",
- "locale": "en-US",
- "contentModuleList": content_module_list
- }
-
- # 创建内容文档
- self.detail_log("📤 创建A+ Content文档...")
- try:
- content_key = self.amazon_uploader.create_content_document(token, content_doc)
- self.detail_log(f"✅ Content文档创建成功,Content Key: {content_key}")
- except Exception as e:
- self.detail_log(f"❌ 创建Content文档失败: {str(e)}")
- self.detail_progress_var.set("上传失败")
- return
-
- # 绑定ASIN
- self.detail_log(f"🔗 绑定ASIN: {asin}...")
- try:
- self.amazon_uploader.bind_asin_to_content(token, content_key, asin)
- self.detail_log(f"✅ ASIN绑定成功")
- except Exception as e:
- self.detail_log(f"❌ 绑定ASIN失败: {str(e)}")
- self.detail_progress_var.set("上传失败")
- return
-
- # 提交审核
- self.detail_log("📋 提交审核...")
- try:
- self.amazon_uploader.submit_for_approval(token, content_key)
- self.detail_log(f"✅ 审核提交成功")
- self.detail_log(f"⚠️ 审核通常需要 1-7 个工作日")
- except Exception as e:
- self.detail_log(f"❌ 提交审核失败: {str(e)}")
- self.detail_progress_var.set("上传失败")
- return
-
- self.detail_log(f"\n{'=' * 50}")
- self.detail_log(f"✅ 处理完成!成功: {success_count}, 失败: {fail_count}")
- self.detail_log(f" Content Key: {content_key}")
- self.detail_log(f" ASIN: {asin}")
- self.detail_progress_var.set(f"上传完成!成功: {success_count}, 失败: {fail_count}")
- messagebox.showinfo("完成", f"亚马逊详情图上传完成!\n成功: {success_count}\n失败: {fail_count}\n\nContent Key: {content_key}\nASIN: {asin}\n\n审核通常需要 1-7 个工作日")
-
- except Exception as e:
- error_msg = f"处理出错: {str(e)}"
- self.detail_log(f"❌ {error_msg}")
- self.detail_progress_var.set("处理出错")
- messagebox.showerror("错误", error_msg)
- import traceback
- self.detail_log(traceback.format_exc())
- def main():
- root = tk.Tk()
- app = ImageMatcherApp(root)
- root.mainloop()
- if __name__ == "__main__":
- main()
|