|
|
@@ -11,8 +11,10 @@ import json
|
|
|
import datetime
|
|
|
import hashlib
|
|
|
import hmac
|
|
|
-from urllib.parse import quote
|
|
|
+from urllib.parse import quote, urlparse, parse_qsl
|
|
|
import time
|
|
|
+import base64
|
|
|
+from io import BytesIO
|
|
|
|
|
|
|
|
|
class AmazonImageUploader:
|
|
|
@@ -138,13 +140,75 @@ class AmazonImageUploader:
|
|
|
]
|
|
|
}
|
|
|
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("950x750")
|
|
|
+ self.root.geometry("950x800")
|
|
|
|
|
|
# 存储文件路径
|
|
|
self.template_path = tk.StringVar()
|
|
|
@@ -189,6 +253,11 @@ class ImageMatcherApp:
|
|
|
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()
|
|
|
+
|
|
|
# 状态栏
|
|
|
self.status_var = tk.StringVar()
|
|
|
self.status_var.set("就绪")
|
|
|
@@ -287,6 +356,73 @@ class ImageMatcherApp:
|
|
|
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=7, 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(8, 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)
|
|
|
+
|
|
|
+ # 模板文件选择
|
|
|
+ ttk.Label(tab, text="模板文件:").grid(row=1, column=0, sticky=tk.W, pady=5)
|
|
|
+ self.detail_template_path = tk.StringVar()
|
|
|
+ ttk.Entry(tab, textvariable=self.detail_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_detail_template).grid(row=1, column=2, pady=5, padx=5)
|
|
|
+
|
|
|
+ # ASIN输入
|
|
|
+ ttk.Label(tab, text="ASIN (手动输入):").grid(row=2, 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=2, column=1, sticky=tk.W, pady=5, padx=5)
|
|
|
+
|
|
|
+ # 图片URL列选择(详情图)
|
|
|
+ ttk.Label(tab, text="详情图列名(逗号分隔):").grid(row=3, column=0, sticky=tk.W, pady=5)
|
|
|
+ self.detail_image_columns = tk.StringVar(value="详情图URL1,详情图URL2,详情图URL3")
|
|
|
+ ttk.Entry(tab, textvariable=self.detail_image_columns, 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.image_width = tk.StringVar(value="970")
|
|
|
+ ttk.Entry(tab, textvariable=self.image_width, width=15).grid(row=4, column=1, sticky=tk.W, pady=5, padx=5)
|
|
|
+
|
|
|
+ ttk.Label(tab, text="图片高度(像素):").grid(row=5, column=0, sticky=tk.W, pady=5)
|
|
|
+ self.image_height = tk.StringVar(value="600")
|
|
|
+ ttk.Entry(tab, textvariable=self.image_height, width=15).grid(row=5, column=1, sticky=tk.W, pady=5, padx=5)
|
|
|
+
|
|
|
+ # 标题和描述设置
|
|
|
+ ttk.Label(tab, text="模块标题:").grid(row=6, column=0, sticky=tk.W, pady=5)
|
|
|
+ self.module_headline = tk.StringVar(value="Premium Quality Product")
|
|
|
+ ttk.Entry(tab, textvariable=self.module_headline, width=50).grid(row=6, column=1, sticky=tk.W, pady=5, padx=5)
|
|
|
+
|
|
|
+ ttk.Label(tab, text="模块描述:").grid(row=7, column=0, sticky=tk.W, pady=5)
|
|
|
+ self.module_body = tk.StringVar(value="High-quality product designed for your needs.")
|
|
|
+ ttk.Entry(tab, textvariable=self.module_body, width=50).grid(row=7, column=1, sticky=tk.W, pady=5, padx=5)
|
|
|
+
|
|
|
+ # 按钮框架
|
|
|
+ button_frame = ttk.Frame(tab)
|
|
|
+ button_frame.grid(row=8, column=0, columnspan=3, pady=10)
|
|
|
+
|
|
|
+ ttk.Button(button_frame, text="保存配置", command=self.save_detail_config, width=12).pack(side=tk.LEFT, padx=5)
|
|
|
+ ttk.Button(button_frame, text="加载配置", command=self.load_detail_config, width=12).pack(side=tk.LEFT, padx=5)
|
|
|
+ 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=9, 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=10, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
|
|
|
|
|
|
def select_template(self):
|
|
|
filename = filedialog.askopenfilename(
|
|
|
@@ -311,6 +447,15 @@ class ImageMatcherApp:
|
|
|
if filename:
|
|
|
self.amazon_template_path.set(filename)
|
|
|
self.amazon_log(f"已选择模板文件: {filename}")
|
|
|
+
|
|
|
+ def select_detail_template(self):
|
|
|
+ filename = filedialog.askopenfilename(
|
|
|
+ title="选择模板文件",
|
|
|
+ filetypes=[("Excel文件", "*.xlsx *.xls"), ("所有文件", "*.*")]
|
|
|
+ )
|
|
|
+ if filename:
|
|
|
+ self.detail_template_path.set(filename)
|
|
|
+ self.detail_log(f"已选择模板文件: {filename}")
|
|
|
|
|
|
def log(self, message):
|
|
|
"""在匹配选项卡日志框中添加消息"""
|
|
|
@@ -331,10 +476,24 @@ class ImageMatcherApp:
|
|
|
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_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 save_amazon_config(self):
|
|
|
"""保存亚马逊配置"""
|
|
|
@@ -368,6 +527,45 @@ class ImageMatcherApp:
|
|
|
self.amazon_log("⚠️ 未找到配置文件,使用默认配置")
|
|
|
except Exception as e:
|
|
|
self.amazon_log(f"❌ 加载配置失败: {str(e)}")
|
|
|
+
|
|
|
+ def save_detail_config(self):
|
|
|
+ """保存详情图配置"""
|
|
|
+ config = {
|
|
|
+ "asin": self.asin_input.get(),
|
|
|
+ "detail_image_columns": self.detail_image_columns.get(),
|
|
|
+ "template_path": self.detail_template_path.get(),
|
|
|
+ "image_width": self.image_width.get(),
|
|
|
+ "image_height": self.image_height.get(),
|
|
|
+ "module_headline": self.module_headline.get(),
|
|
|
+ "module_body": self.module_body.get()
|
|
|
+ }
|
|
|
+ try:
|
|
|
+ with open(self.get_detail_config_path(), 'w', encoding='utf-8') as f:
|
|
|
+ json.dump(config, f, ensure_ascii=False, indent=2)
|
|
|
+ self.detail_log("✅ 配置已保存")
|
|
|
+ except Exception as e:
|
|
|
+ self.detail_log(f"❌ 保存配置失败: {str(e)}")
|
|
|
+
|
|
|
+ def load_detail_config(self):
|
|
|
+ """加载详情图配置"""
|
|
|
+ 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_image_columns.set(config.get("detail_image_columns", "详情图URL1,详情图URL2,详情图URL3"))
|
|
|
+ self.image_width.set(config.get("image_width", "970"))
|
|
|
+ self.image_height.set(config.get("image_height", "600"))
|
|
|
+ self.module_headline.set(config.get("module_headline", "Premium Quality Product"))
|
|
|
+ self.module_body.set(config.get("module_body", "High-quality product designed for your needs."))
|
|
|
+ if config.get("template_path"):
|
|
|
+ self.detail_template_path.set(config.get("template_path"))
|
|
|
+ self.detail_log("✅ 配置已加载")
|
|
|
+ else:
|
|
|
+ self.detail_log("⚠️ 未找到配置文件,使用默认配置")
|
|
|
+ except Exception as e:
|
|
|
+ self.detail_log(f"❌ 加载配置失败: {str(e)}")
|
|
|
|
|
|
def load_config(self):
|
|
|
"""启动时自动加载亚马逊配置"""
|
|
|
@@ -383,6 +581,22 @@ class ImageMatcherApp:
|
|
|
self.amazon_template_path.set(config.get("template_path"))
|
|
|
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_image_columns.set(config.get("detail_image_columns", "详情图URL1,详情图URL2,详情图URL3"))
|
|
|
+ self.image_width.set(config.get("image_width", "970"))
|
|
|
+ self.image_height.set(config.get("image_height", "600"))
|
|
|
+ self.module_headline.set(config.get("module_headline", "Premium Quality Product"))
|
|
|
+ self.module_body.set(config.get("module_body", "High-quality product designed for your needs."))
|
|
|
+ if config.get("template_path"):
|
|
|
+ self.detail_template_path.set(config.get("template_path"))
|
|
|
+ except Exception:
|
|
|
+ pass
|
|
|
|
|
|
def start_matching(self):
|
|
|
"""开始匹配处理"""
|
|
|
@@ -668,6 +882,275 @@ class ImageMatcherApp:
|
|
|
import traceback
|
|
|
self.amazon_log(traceback.format_exc())
|
|
|
|
|
|
+ def start_detail_upload(self):
|
|
|
+ """开始亚马逊详情图上传 (A+ Content)"""
|
|
|
+ if not self.detail_template_path.get():
|
|
|
+ messagebox.showerror("错误", "请先选择模板文件!")
|
|
|
+ return
|
|
|
+
|
|
|
+ if not self.asin_input.get().strip():
|
|
|
+ messagebox.showerror("错误", "请先输入ASIN!")
|
|
|
+ return
|
|
|
+
|
|
|
+ thread = threading.Thread(target=self.process_detail_upload, daemon=True)
|
|
|
+ thread.start()
|
|
|
+
|
|
|
+ def process_detail_upload(self):
|
|
|
+ """执行亚马逊详情图上传 (A+ Content)"""
|
|
|
+ try:
|
|
|
+ self.detail_progress_var.set("处理中...")
|
|
|
+ self.detail_log("=" * 50)
|
|
|
+ self.detail_log("开始亚马逊详情图上传 (A+ Content)...")
|
|
|
+
|
|
|
+ template_path = self.detail_template_path.get()
|
|
|
+ asin = self.asin_input.get().strip()
|
|
|
+ detail_cols_str = self.detail_image_columns.get()
|
|
|
+ detail_col_names = [col.strip() for col in detail_cols_str.split(',') if col.strip()]
|
|
|
+
|
|
|
+ # 获取图片尺寸
|
|
|
+ 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
|
|
|
+
|
|
|
+ module_headline = self.module_headline.get().strip()
|
|
|
+ module_body = self.module_body.get().strip()
|
|
|
+
|
|
|
+ self.detail_log(f"读取模板文件: {template_path}")
|
|
|
+ self.detail_log(f"使用的ASIN: {asin}")
|
|
|
+ self.detail_log(f"图片尺寸: {target_width}x{target_height}")
|
|
|
+
|
|
|
+ # 读取Excel
|
|
|
+ df = pd.read_excel(template_path)
|
|
|
+ self.detail_log(f"找到 {len(df)} 行数据")
|
|
|
+
|
|
|
+ # 检查详情图列
|
|
|
+ existing_detail_cols = [col for col in detail_col_names if col in df.columns]
|
|
|
+ if not existing_detail_cols:
|
|
|
+ self.detail_log("❌ 未找到任何详情图列")
|
|
|
+ self.detail_progress_var.set("上传失败")
|
|
|
+ return
|
|
|
+
|
|
|
+ self.detail_log(f"详情图列: {existing_detail_cols}")
|
|
|
+
|
|
|
+ # 尝试导入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("🔑 获取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
|
|
|
+ total_rows = min(len(df), 1) # 每次只处理一行(A+ Content每次只能绑定一个ASIN)
|
|
|
+
|
|
|
+ # 只处理第一行
|
|
|
+ row = df.iloc[0]
|
|
|
+
|
|
|
+ # 收集所有图片URL
|
|
|
+ image_urls = []
|
|
|
+ for col_name in existing_detail_cols:
|
|
|
+ img_url = str(row[col_name]).strip()
|
|
|
+ if img_url and img_url != 'nan' and img_url != 'None' and img_url.startswith('http'):
|
|
|
+ image_urls.append(img_url)
|
|
|
+
|
|
|
+ if not image_urls:
|
|
|
+ self.detail_log("❌ 未找到有效的图片URL")
|
|
|
+ self.detail_progress_var.set("上传失败")
|
|
|
+ return
|
|
|
+
|
|
|
+ self.detail_log(f"找到 {len(image_urls)} 张详情图")
|
|
|
+
|
|
|
+ # 处理每张图片
|
|
|
+ upload_ids = []
|
|
|
+ for i, img_url in enumerate(image_urls, start=1):
|
|
|
+ self.detail_log(f"\n处理第 {i} 张图片: {img_url[:60]}...")
|
|
|
+
|
|
|
+ try:
|
|
|
+ # 下载图片
|
|
|
+ img_resp = requests.get(img_url, timeout=30)
|
|
|
+ img_resp.raise_for_status()
|
|
|
+
|
|
|
+ image_binary = img_resp.content
|
|
|
+
|
|
|
+ # 调整尺寸
|
|
|
+ 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 img_url.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 img_url.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("\n📝 构建A+ Content文档...")
|
|
|
+
|
|
|
+ # 构建模块列表 - 每张图片一个模块
|
|
|
+ content_module_list = []
|
|
|
+ for i, upload_info in enumerate(upload_ids):
|
|
|
+ module = {
|
|
|
+ "contentModuleType": "STANDARD_HEADER_IMAGE_TEXT",
|
|
|
+ "standardHeaderImageText": {
|
|
|
+ "headline": {
|
|
|
+ "value": module_headline if i == 0 else f"{module_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": module_body 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✅ 处理完成!成功: {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()
|