自动匹配上架图片.py 53 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162
  1. import tkinter as tk
  2. from tkinter import ttk, filedialog, messagebox, scrolledtext
  3. import pandas as pd
  4. import os
  5. import threading
  6. import requests
  7. from openpyxl import load_workbook
  8. from openpyxl.utils import get_column_letter
  9. import re
  10. import json
  11. import datetime
  12. import hashlib
  13. import hmac
  14. from urllib.parse import quote, urlparse, parse_qsl
  15. import time
  16. import base64
  17. from io import BytesIO
  18. class AmazonImageUploader:
  19. """亚马逊图片上传类"""
  20. def __init__(self):
  21. self.client_id = "amzn1.application-oa2-client.9f775d35077a4c7598116e5a89b60222"
  22. self.client_secret = "amzn1.oa2-cs.v1.6909777baa82b796555a431cd23709342e8e01434abcad1f8b59cadf048b51c3"
  23. self.refresh_token = "Atzr|IwEBICh_fojh7qAfrePzSjOZhQxTnHRZCYOuWNvSAHGaBs1WfkjR4qjC1Dmv0uK3oyq_L-R9SRKK4cYmPwF1MQXQdqKlpzOAGItWeXnNzh-SyhHHISv0_vCt3D6hluVmIEmF2kA7ro_XF-PslD41g3Eq6bv0Qq4y4pQbkDj-9dudDuz7nku8UL-nc4EWgJCrHCJTAs8TK48Yvrfi3E35uv97tYhs6qb8D9d4dR9C7daaC49eoDqP88qyUHThQV1Kqon8xukx4AT2_wLqWNcabpADzuK-5w7D-vvFFtvzfkbmtfzoPeEdHPulG6scc59BPFTyBlGU7a3d1JSFg2ROHIlrljRm"
  24. self.aws_access_key = "AKIA24CLQSVTW2K4S6RP"
  25. self.aws_secret_key = "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"
  26. self.aws_region = "us-east-1"
  27. self.seller_id = "A1H50Z0WYYLEGQ"
  28. self.marketplace_id = "ATVPDKIKX0DER"
  29. self.product_type = "TABLE"
  30. def sha256_hash(self, data):
  31. return hashlib.sha256(data.encode("utf-8")).hexdigest()
  32. def hmac_sha256(self, key, msg):
  33. return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
  34. def get_signature_key(self, secret_key, date_stamp, region, service):
  35. k_date = self.hmac_sha256(("AWS4" + secret_key).encode("utf-8"), date_stamp)
  36. k_region = self.hmac_sha256(k_date, region)
  37. k_service = self.hmac_sha256(k_region, service)
  38. return self.hmac_sha256(k_service, "aws4_request")
  39. def get_access_token(self):
  40. url = "https://api.amazon.com/auth/o2/token"
  41. resp = requests.post(url, data={
  42. "grant_type": "refresh_token",
  43. "refresh_token": self.refresh_token,
  44. "client_id": self.client_id,
  45. "client_secret": self.client_secret,
  46. })
  47. resp.raise_for_status()
  48. return resp.json()["access_token"]
  49. def spapi_request(self, method, path, query, body=None, access_token=None):
  50. service = "execute-api"
  51. host = "sellingpartnerapi-na.amazon.com"
  52. payload = json.dumps(body, separators=(",", ":")) if body else ""
  53. t = datetime.datetime.now(datetime.timezone.utc)
  54. amz_date = t.strftime("%Y%m%dT%H%M%SZ")
  55. date_stamp = t.strftime("%Y%m%d")
  56. canonical_headers = (
  57. f"host:{host}\n"
  58. f"x-amz-access-token:{access_token}\n"
  59. f"x-amz-date:{amz_date}\n"
  60. )
  61. signed_headers = "host;x-amz-access-token;x-amz-date"
  62. payload_hash = self.sha256_hash(payload)
  63. canonical_request = (
  64. f"{method}\n{path}\n{query}\n"
  65. f"{canonical_headers}\n{signed_headers}\n{payload_hash}"
  66. )
  67. algorithm = "AWS4-HMAC-SHA256"
  68. credential_scope = f"{date_stamp}/{self.aws_region}/{service}/aws4_request"
  69. string_to_sign = (
  70. f"{algorithm}\n{amz_date}\n{credential_scope}\n"
  71. f"{self.sha256_hash(canonical_request)}"
  72. )
  73. signing_key = self.get_signature_key(self.aws_secret_key, date_stamp, self.aws_region, service)
  74. signature = self.hmac_sha256(signing_key, string_to_sign.encode("utf-8")).hexdigest()
  75. auth_header = (
  76. f"{algorithm} Credential={self.aws_access_key}/{credential_scope}, "
  77. f"SignedHeaders={signed_headers}, Signature={signature}"
  78. )
  79. headers = {
  80. "Host": host,
  81. "x-amz-access-token": access_token,
  82. "x-amz-date": amz_date,
  83. "Authorization": auth_header,
  84. }
  85. if body:
  86. headers["Content-Type"] = "application/json"
  87. url = f"https://{host}{path}?{query}"
  88. return requests.request(method, url, headers=headers, data=payload or None)
  89. def patch_image(self, access_token, image_type, image_url, sku):
  90. """
  91. image_type 可选值:
  92. "main" → 主图
  93. "other_1" ~ "other_8" → 副图1~8
  94. "swatch" → 变体图
  95. """
  96. encoded_sku = quote(sku, safe="")
  97. path = f"/listings/2021-08-01/items/{self.seller_id}/{encoded_sku}"
  98. query = f"marketplaceIds={self.marketplace_id}"
  99. if image_type == "main":
  100. attr_path = "/attributes/main_product_image_locator"
  101. elif image_type.startswith("other_") and 1 <= int(image_type.split("_")[1]) <= 8:
  102. n = image_type.split("_")[1]
  103. attr_path = f"/attributes/other_product_image_locator_{n}"
  104. elif image_type == "swatch":
  105. attr_path = "/attributes/swatch_product_image_locator"
  106. else:
  107. raise ValueError(f"不支持的 image_type: {image_type}")
  108. body = {
  109. "productType": self.product_type,
  110. "patches": [
  111. {
  112. "op": "replace",
  113. "path": attr_path,
  114. "value": [
  115. {
  116. "marketplace_id": self.marketplace_id,
  117. "media_location": image_url
  118. }
  119. ]
  120. }
  121. ]
  122. }
  123. return self.spapi_request("PATCH", path, query, body=body, access_token=access_token)
  124. # ==================== A+ Content API 方法 ====================
  125. def create_upload_destination(self, access_token, content_md5, content_type="image/png"):
  126. """创建上传目标"""
  127. resource = "aplus/2020-11-01/contentDocuments"
  128. path = f"/uploads/2020-11-01/uploadDestinations/{quote(resource, safe='/')}"
  129. query = f"marketplaceIds={self.marketplace_id}&contentMD5={quote(content_md5, safe='')}&contentType={quote(content_type, safe='')}"
  130. resp = self.spapi_request("POST", path, query, access_token=access_token)
  131. if resp.status_code not in [200, 201]:
  132. raise Exception(f"创建上传目标失败: {resp.text}")
  133. return resp.json().get("payload", {})
  134. def upload_to_s3(self, upload_url, image_binary, content_type="image/png"):
  135. """上传图片到S3"""
  136. parsed = urlparse(upload_url)
  137. base_url = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
  138. query_params = dict(parse_qsl(parsed.query))
  139. files = {"File": ("image.png", image_binary, content_type)}
  140. resp = requests.post(base_url, data=query_params, files=files, timeout=60)
  141. if resp.status_code not in [200, 201, 204]:
  142. raise Exception(f"S3上传失败: {resp.status_code}")
  143. return True
  144. def create_content_document(self, access_token, content_doc):
  145. """创建内容文档"""
  146. resp = self.spapi_request(
  147. "POST",
  148. "/aplus/2020-11-01/contentDocuments",
  149. f"marketplaceId={self.marketplace_id}",
  150. body={"contentDocument": content_doc},
  151. access_token=access_token
  152. )
  153. if resp.status_code not in [200, 201]:
  154. raise Exception(f"创建内容文档失败: {resp.text}")
  155. return resp.json().get("contentReferenceKey")
  156. def bind_asin_to_content(self, access_token, content_key, asin):
  157. """绑定ASIN到内容"""
  158. body = {"asinSet": [asin]}
  159. resp = self.spapi_request(
  160. "POST",
  161. f"/aplus/2020-11-01/contentDocuments/{content_key}/asins",
  162. f"marketplaceId={self.marketplace_id}",
  163. body=body,
  164. access_token=access_token
  165. )
  166. if resp.status_code not in [200, 201]:
  167. raise Exception(f"绑定ASIN失败: {resp.text}")
  168. return True
  169. def submit_for_approval(self, access_token, content_key):
  170. """提交审核"""
  171. resp = self.spapi_request(
  172. "POST",
  173. f"/aplus/2020-11-01/contentDocuments/{content_key}/approvalSubmissions",
  174. f"marketplaceId={self.marketplace_id}",
  175. access_token=access_token
  176. )
  177. if resp.status_code not in [200, 201]:
  178. raise Exception(f"提交审核失败: {resp.text}")
  179. return True
  180. class ImageMatcherApp:
  181. def __init__(self, root):
  182. self.root = root
  183. self.root.title("图片匹配与上传工具 - 亚马逊版")
  184. self.root.geometry("950x800")
  185. # 存储文件路径
  186. self.template_path = tk.StringVar()
  187. self.image_dir = tk.StringVar()
  188. # 固定上传配置
  189. self.upload_url = "https://cos.port.run/uploadFile"
  190. self.upload_group = "soundSynthesis"
  191. self.upload_user = "1"
  192. # 亚马逊上传器
  193. self.amazon_uploader = AmazonImageUploader()
  194. # 创建UI
  195. self.create_widgets()
  196. # 加载保存的配置
  197. self.load_config()
  198. def create_widgets(self):
  199. # 主框架
  200. main_frame = ttk.Frame(self.root, padding="10")
  201. main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  202. # 配置网格权重
  203. self.root.columnconfigure(0, weight=1)
  204. self.root.rowconfigure(0, weight=1)
  205. main_frame.columnconfigure(0, weight=1)
  206. main_frame.rowconfigure(1, weight=1)
  207. # 创建Notebook(选项卡)
  208. self.notebook = ttk.Notebook(main_frame)
  209. self.notebook.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10)
  210. # 选项卡1: 图片匹配与上传
  211. self.tab_matching = ttk.Frame(self.notebook)
  212. self.notebook.add(self.tab_matching, text="图片匹配上传")
  213. self.create_matching_tab()
  214. # 选项卡2: 亚马逊主图更换
  215. self.tab_amazon = ttk.Frame(self.notebook)
  216. self.notebook.add(self.tab_amazon, text="亚马逊主图更换")
  217. self.create_amazon_tab()
  218. # 选项卡3: 亚马逊详情图更换 (A+ Content)
  219. self.tab_detail = ttk.Frame(self.notebook)
  220. self.notebook.add(self.tab_detail, text="亚马逊详情图更换")
  221. self.create_detail_tab()
  222. # 状态栏
  223. self.status_var = tk.StringVar()
  224. self.status_var.set("就绪")
  225. status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
  226. status_bar.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=5)
  227. def create_matching_tab(self):
  228. """创建图片匹配上传选项卡"""
  229. tab = self.tab_matching
  230. tab.columnconfigure(1, weight=1)
  231. tab.rowconfigure(4, weight=1)
  232. # 标题
  233. title_label = ttk.Label(tab, text="图片匹配与上传", font=("Arial", 14, "bold"))
  234. title_label.grid(row=0, column=0, columnspan=3, pady=10)
  235. # 模板文件选择
  236. ttk.Label(tab, text="模板文件:").grid(row=1, column=0, sticky=tk.W, pady=5)
  237. ttk.Entry(tab, textvariable=self.template_path, width=60).grid(row=1, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
  238. ttk.Button(tab, text="选择模板", command=self.select_template).grid(row=1, column=2, pady=5, padx=5)
  239. # 图片目录选择
  240. ttk.Label(tab, text="图片目录:").grid(row=2, column=0, sticky=tk.W, pady=5)
  241. ttk.Entry(tab, textvariable=self.image_dir, width=60).grid(row=2, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
  242. ttk.Button(tab, text="选择目录", command=self.select_image_dir).grid(row=2, column=2, pady=5, padx=5)
  243. # 按钮框架
  244. button_frame = ttk.Frame(tab)
  245. button_frame.grid(row=3, column=0, columnspan=3, pady=10)
  246. ttk.Button(button_frame, text="开始处理", command=self.start_matching, width=15).pack(side=tk.LEFT, padx=5)
  247. ttk.Button(button_frame, text="清空日志", command=self.clear_log, width=15).pack(side=tk.LEFT, padx=5)
  248. # 日志框
  249. log_frame = ttk.LabelFrame(tab, text="日志", padding="5")
  250. log_frame.grid(row=4, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10)
  251. log_frame.columnconfigure(0, weight=1)
  252. log_frame.rowconfigure(0, weight=1)
  253. self.log_text = scrolledtext.ScrolledText(log_frame, height=15, width=80, wrap=tk.WORD)
  254. self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  255. # 存储生成的文件路径
  256. self.generated_file_path = None
  257. def create_amazon_tab(self):
  258. """创建亚马逊主图更换选项卡"""
  259. tab = self.tab_amazon
  260. tab.columnconfigure(1, weight=1)
  261. tab.rowconfigure(6, weight=1)
  262. # 标题
  263. title_label = ttk.Label(tab, text="亚马逊主图/附图批量更换", font=("Arial", 14, "bold"))
  264. title_label.grid(row=0, column=0, columnspan=3, pady=10)
  265. # 模板文件选择
  266. ttk.Label(tab, text="模板文件:").grid(row=1, column=0, sticky=tk.W, pady=5)
  267. self.amazon_template_path = tk.StringVar()
  268. ttk.Entry(tab, textvariable=self.amazon_template_path, width=60).grid(row=1, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
  269. ttk.Button(tab, text="选择模板", command=self.select_amazon_template).grid(row=1, column=2, pady=5, padx=5)
  270. # SKU输入(手动输入,不是从表格读取)
  271. ttk.Label(tab, text="SKU (手动输入):").grid(row=2, column=0, sticky=tk.W, pady=5)
  272. self.sku_input = tk.StringVar(value="bar grey 03")
  273. ttk.Entry(tab, textvariable=self.sku_input, width=40).grid(row=2, column=1, sticky=tk.W, pady=5, padx=5)
  274. # 主图列选择
  275. ttk.Label(tab, text="主图列名:").grid(row=3, column=0, sticky=tk.W, pady=5)
  276. self.main_image_column = tk.StringVar(value="产品主图")
  277. ttk.Entry(tab, textvariable=self.main_image_column, width=30).grid(row=3, column=1, sticky=tk.W, pady=5, padx=5)
  278. # 附图列选择
  279. ttk.Label(tab, text="附图列名(逗号分隔):").grid(row=4, column=0, sticky=tk.W, pady=5)
  280. self.other_image_columns = tk.StringVar(value="上架图片1,上架图片2,上架图片3,上架图片4,上架图片5,上架图片6,上架图片7,上架图片8")
  281. ttk.Entry(tab, textvariable=self.other_image_columns, width=60).grid(row=4, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
  282. # 按钮框架
  283. button_frame = ttk.Frame(tab)
  284. button_frame.grid(row=5, column=0, columnspan=3, pady=10)
  285. ttk.Button(button_frame, text="保存配置", command=self.save_amazon_config, width=12).pack(side=tk.LEFT, padx=5)
  286. ttk.Button(button_frame, text="加载配置", command=self.load_amazon_config, width=12).pack(side=tk.LEFT, padx=5)
  287. ttk.Button(button_frame, text="开始上传", command=self.start_amazon_upload, width=15).pack(side=tk.LEFT, padx=5)
  288. ttk.Button(button_frame, text="清空日志", command=self.clear_amazon_log, width=15).pack(side=tk.LEFT, padx=5)
  289. # 日志框
  290. log_frame = ttk.LabelFrame(tab, text="上传日志", padding="5")
  291. log_frame.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10)
  292. log_frame.columnconfigure(0, weight=1)
  293. log_frame.rowconfigure(0, weight=1)
  294. self.amazon_log_text = scrolledtext.ScrolledText(log_frame, height=15, width=80, wrap=tk.WORD)
  295. self.amazon_log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  296. # 进度标签
  297. self.amazon_progress_var = tk.StringVar(value="就绪")
  298. progress_label = ttk.Label(tab, textvariable=self.amazon_progress_var, relief=tk.SUNKEN, anchor=tk.W)
  299. progress_label.grid(row=7, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
  300. def create_detail_tab(self):
  301. """创建亚马逊详情图更换选项卡 (A+ Content)"""
  302. tab = self.tab_detail
  303. tab.columnconfigure(1, weight=1)
  304. tab.rowconfigure(8, weight=1)
  305. # 标题
  306. title_label = ttk.Label(tab, text="亚马逊详情图更换 (A+ Content)", font=("Arial", 14, "bold"))
  307. title_label.grid(row=0, column=0, columnspan=3, pady=10)
  308. # 模板文件选择
  309. ttk.Label(tab, text="模板文件:").grid(row=1, column=0, sticky=tk.W, pady=5)
  310. self.detail_template_path = tk.StringVar()
  311. ttk.Entry(tab, textvariable=self.detail_template_path, width=60).grid(row=1, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
  312. ttk.Button(tab, text="选择模板", command=self.select_detail_template).grid(row=1, column=2, pady=5, padx=5)
  313. # ASIN输入
  314. ttk.Label(tab, text="ASIN (手动输入):").grid(row=2, column=0, sticky=tk.W, pady=5)
  315. self.asin_input = tk.StringVar(value="B0FR1WRK4M")
  316. ttk.Entry(tab, textvariable=self.asin_input, width=40).grid(row=2, column=1, sticky=tk.W, pady=5, padx=5)
  317. # 图片URL列选择(详情图)
  318. ttk.Label(tab, text="详情图列名(逗号分隔):").grid(row=3, column=0, sticky=tk.W, pady=5)
  319. self.detail_image_columns = tk.StringVar(value="详情图URL1,详情图URL2,详情图URL3")
  320. ttk.Entry(tab, textvariable=self.detail_image_columns, width=60).grid(row=3, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
  321. # 图片尺寸设置
  322. ttk.Label(tab, text="图片宽度(像素):").grid(row=4, column=0, sticky=tk.W, pady=5)
  323. self.image_width = tk.StringVar(value="970")
  324. ttk.Entry(tab, textvariable=self.image_width, width=15).grid(row=4, column=1, sticky=tk.W, pady=5, padx=5)
  325. ttk.Label(tab, text="图片高度(像素):").grid(row=5, column=0, sticky=tk.W, pady=5)
  326. self.image_height = tk.StringVar(value="600")
  327. ttk.Entry(tab, textvariable=self.image_height, width=15).grid(row=5, column=1, sticky=tk.W, pady=5, padx=5)
  328. # 标题和描述设置
  329. ttk.Label(tab, text="模块标题:").grid(row=6, column=0, sticky=tk.W, pady=5)
  330. self.module_headline = tk.StringVar(value="Premium Quality Product")
  331. ttk.Entry(tab, textvariable=self.module_headline, width=50).grid(row=6, column=1, sticky=tk.W, pady=5, padx=5)
  332. ttk.Label(tab, text="模块描述:").grid(row=7, column=0, sticky=tk.W, pady=5)
  333. self.module_body = tk.StringVar(value="High-quality product designed for your needs.")
  334. ttk.Entry(tab, textvariable=self.module_body, width=50).grid(row=7, column=1, sticky=tk.W, pady=5, padx=5)
  335. # 按钮框架
  336. button_frame = ttk.Frame(tab)
  337. button_frame.grid(row=8, column=0, columnspan=3, pady=10)
  338. ttk.Button(button_frame, text="保存配置", command=self.save_detail_config, width=12).pack(side=tk.LEFT, padx=5)
  339. ttk.Button(button_frame, text="加载配置", command=self.load_detail_config, width=12).pack(side=tk.LEFT, padx=5)
  340. ttk.Button(button_frame, text="开始上传", command=self.start_detail_upload, width=15).pack(side=tk.LEFT, padx=5)
  341. ttk.Button(button_frame, text="清空日志", command=self.clear_detail_log, width=15).pack(side=tk.LEFT, padx=5)
  342. # 日志框
  343. log_frame = ttk.LabelFrame(tab, text="上传日志", padding="5")
  344. log_frame.grid(row=9, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10)
  345. log_frame.columnconfigure(0, weight=1)
  346. log_frame.rowconfigure(0, weight=1)
  347. self.detail_log_text = scrolledtext.ScrolledText(log_frame, height=15, width=80, wrap=tk.WORD)
  348. self.detail_log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  349. # 进度标签
  350. self.detail_progress_var = tk.StringVar(value="就绪")
  351. progress_label = ttk.Label(tab, textvariable=self.detail_progress_var, relief=tk.SUNKEN, anchor=tk.W)
  352. progress_label.grid(row=10, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
  353. def select_template(self):
  354. filename = filedialog.askopenfilename(
  355. title="选择模板文件",
  356. filetypes=[("Excel文件", "*.xlsx *.xls"), ("所有文件", "*.*")]
  357. )
  358. if filename:
  359. self.template_path.set(filename)
  360. self.log(f"已选择模板文件: {filename}")
  361. def select_image_dir(self):
  362. directory = filedialog.askdirectory(title="选择图片目录")
  363. if directory:
  364. self.image_dir.set(directory)
  365. self.log(f"已选择图片目录: {directory}")
  366. def select_amazon_template(self):
  367. filename = filedialog.askopenfilename(
  368. title="选择模板文件",
  369. filetypes=[("Excel文件", "*.xlsx *.xls"), ("所有文件", "*.*")]
  370. )
  371. if filename:
  372. self.amazon_template_path.set(filename)
  373. self.amazon_log(f"已选择模板文件: {filename}")
  374. def select_detail_template(self):
  375. filename = filedialog.askopenfilename(
  376. title="选择模板文件",
  377. filetypes=[("Excel文件", "*.xlsx *.xls"), ("所有文件", "*.*")]
  378. )
  379. if filename:
  380. self.detail_template_path.set(filename)
  381. self.detail_log(f"已选择模板文件: {filename}")
  382. def log(self, message):
  383. """在匹配选项卡日志框中添加消息"""
  384. self.log_text.insert(tk.END, f"{message}\n")
  385. self.log_text.see(tk.END)
  386. self.root.update_idletasks()
  387. def clear_log(self):
  388. """清空匹配选项卡日志"""
  389. self.log_text.delete(1.0, tk.END)
  390. def amazon_log(self, message):
  391. """在亚马逊选项卡日志框中添加消息"""
  392. self.amazon_log_text.insert(tk.END, f"{message}\n")
  393. self.amazon_log_text.see(tk.END)
  394. self.root.update_idletasks()
  395. def clear_amazon_log(self):
  396. """清空亚马逊选项卡日志"""
  397. self.amazon_log_text.delete(1.0, tk.END)
  398. def detail_log(self, message):
  399. """在详情图选项卡日志框中添加消息"""
  400. self.detail_log_text.insert(tk.END, f"{message}\n")
  401. self.detail_log_text.see(tk.END)
  402. self.root.update_idletasks()
  403. def clear_detail_log(self):
  404. """清空详情图选项卡日志"""
  405. self.detail_log_text.delete(1.0, tk.END)
  406. def get_config_path(self):
  407. """获取配置文件路径"""
  408. return os.path.join(os.path.dirname(os.path.abspath(__file__)), "amazon_config.json")
  409. def get_detail_config_path(self):
  410. """获取详情图配置文件路径"""
  411. return os.path.join(os.path.dirname(os.path.abspath(__file__)), "amazon_detail_config.json")
  412. def save_amazon_config(self):
  413. """保存亚马逊配置"""
  414. config = {
  415. "sku": self.sku_input.get(),
  416. "main_image_column": self.main_image_column.get(),
  417. "other_image_columns": self.other_image_columns.get(),
  418. "template_path": self.amazon_template_path.get()
  419. }
  420. try:
  421. with open(self.get_config_path(), 'w', encoding='utf-8') as f:
  422. json.dump(config, f, ensure_ascii=False, indent=2)
  423. self.amazon_log("✅ 配置已保存")
  424. except Exception as e:
  425. self.amazon_log(f"❌ 保存配置失败: {str(e)}")
  426. def load_amazon_config(self):
  427. """加载亚马逊配置"""
  428. try:
  429. config_path = self.get_config_path()
  430. if os.path.exists(config_path):
  431. with open(config_path, 'r', encoding='utf-8') as f:
  432. config = json.load(f)
  433. self.sku_input.set(config.get("sku", "bar grey 03"))
  434. self.main_image_column.set(config.get("main_image_column", "产品主图"))
  435. self.other_image_columns.set(config.get("other_image_columns", "上架图片1,上架图片2,上架图片3,上架图片4,上架图片5,上架图片6,上架图片7,上架图片8"))
  436. if config.get("template_path"):
  437. self.amazon_template_path.set(config.get("template_path"))
  438. self.amazon_log("✅ 配置已加载")
  439. else:
  440. self.amazon_log("⚠️ 未找到配置文件,使用默认配置")
  441. except Exception as e:
  442. self.amazon_log(f"❌ 加载配置失败: {str(e)}")
  443. def save_detail_config(self):
  444. """保存详情图配置"""
  445. config = {
  446. "asin": self.asin_input.get(),
  447. "detail_image_columns": self.detail_image_columns.get(),
  448. "template_path": self.detail_template_path.get(),
  449. "image_width": self.image_width.get(),
  450. "image_height": self.image_height.get(),
  451. "module_headline": self.module_headline.get(),
  452. "module_body": self.module_body.get()
  453. }
  454. try:
  455. with open(self.get_detail_config_path(), 'w', encoding='utf-8') as f:
  456. json.dump(config, f, ensure_ascii=False, indent=2)
  457. self.detail_log("✅ 配置已保存")
  458. except Exception as e:
  459. self.detail_log(f"❌ 保存配置失败: {str(e)}")
  460. def load_detail_config(self):
  461. """加载详情图配置"""
  462. try:
  463. config_path = self.get_detail_config_path()
  464. if os.path.exists(config_path):
  465. with open(config_path, 'r', encoding='utf-8') as f:
  466. config = json.load(f)
  467. self.asin_input.set(config.get("asin", "B0FR1WRK4M"))
  468. self.detail_image_columns.set(config.get("detail_image_columns", "详情图URL1,详情图URL2,详情图URL3"))
  469. self.image_width.set(config.get("image_width", "970"))
  470. self.image_height.set(config.get("image_height", "600"))
  471. self.module_headline.set(config.get("module_headline", "Premium Quality Product"))
  472. self.module_body.set(config.get("module_body", "High-quality product designed for your needs."))
  473. if config.get("template_path"):
  474. self.detail_template_path.set(config.get("template_path"))
  475. self.detail_log("✅ 配置已加载")
  476. else:
  477. self.detail_log("⚠️ 未找到配置文件,使用默认配置")
  478. except Exception as e:
  479. self.detail_log(f"❌ 加载配置失败: {str(e)}")
  480. def load_config(self):
  481. """启动时自动加载亚马逊配置"""
  482. try:
  483. config_path = self.get_config_path()
  484. if os.path.exists(config_path):
  485. with open(config_path, 'r', encoding='utf-8') as f:
  486. config = json.load(f)
  487. self.sku_input.set(config.get("sku", "bar grey 03"))
  488. self.main_image_column.set(config.get("main_image_column", "产品主图"))
  489. self.other_image_columns.set(config.get("other_image_columns", "上架图片1,上架图片2,上架图片3,上架图片4,上架图片5,上架图片6,上架图片7,上架图片8"))
  490. if config.get("template_path"):
  491. self.amazon_template_path.set(config.get("template_path"))
  492. except Exception:
  493. pass
  494. # 加载详情图配置
  495. try:
  496. config_path = self.get_detail_config_path()
  497. if os.path.exists(config_path):
  498. with open(config_path, 'r', encoding='utf-8') as f:
  499. config = json.load(f)
  500. self.asin_input.set(config.get("asin", "B0FR1WRK4M"))
  501. self.detail_image_columns.set(config.get("detail_image_columns", "详情图URL1,详情图URL2,详情图URL3"))
  502. self.image_width.set(config.get("image_width", "970"))
  503. self.image_height.set(config.get("image_height", "600"))
  504. self.module_headline.set(config.get("module_headline", "Premium Quality Product"))
  505. self.module_body.set(config.get("module_body", "High-quality product designed for your needs."))
  506. if config.get("template_path"):
  507. self.detail_template_path.set(config.get("template_path"))
  508. except Exception:
  509. pass
  510. def start_matching(self):
  511. """开始匹配处理"""
  512. if not self.template_path.get():
  513. messagebox.showerror("错误", "请先选择模板文件!")
  514. return
  515. if not self.image_dir.get():
  516. messagebox.showerror("错误", "请先选择图片目录!")
  517. return
  518. thread = threading.Thread(target=self.process_matching, daemon=True)
  519. thread.start()
  520. def process_matching(self):
  521. """执行匹配处理"""
  522. try:
  523. self.status_var.set("处理中...")
  524. self.log("=" * 50)
  525. self.log("开始处理...")
  526. template_path = self.template_path.get()
  527. image_dir = self.image_dir.get()
  528. self.log(f"读取模板文件: {template_path}")
  529. # 使用openpyxl加载工作簿,保留所有样式
  530. wb = load_workbook(template_path)
  531. ws = wb.active
  532. # 获取表头(第二行)
  533. headers = []
  534. for col in range(1, ws.max_column + 1):
  535. cell_value = ws.cell(row=2, column=col).value
  536. headers.append(str(cell_value) if cell_value else '')
  537. self.log(f"找到 {len(headers)} 列")
  538. # 查找各列的列号
  539. col_a_index = 1 # A列是第1列
  540. product_main_col = None
  541. image_cols = {}
  542. for idx, header in enumerate(headers, start=1):
  543. if header == '产品主图':
  544. product_main_col = idx
  545. elif '上架图片' in header:
  546. match = re.search(r'(\d+)', header)
  547. if match:
  548. num = int(match.group(1))
  549. image_cols[num] = idx
  550. if product_main_col is None:
  551. self.log("警告: 未找到'产品主图'列")
  552. else:
  553. self.log(f"找到产品主图列: 第{product_main_col}列")
  554. self.log(f"找到上架图片列: {len(image_cols)} 列")
  555. # 处理每一行(从第三行开始)
  556. processed_count = 0
  557. for row_idx in range(3, ws.max_row + 1):
  558. a_cell = ws.cell(row=row_idx, column=col_a_index)
  559. a_value = str(a_cell.value).strip() if a_cell.value else ''
  560. if not a_value or a_value == 'nan' or a_value == 'None':
  561. continue
  562. self.log(f"\n处理第 {row_idx} 行, A列值: {a_value}")
  563. # 处理产品主图 (图片编号1)
  564. if product_main_col is not None:
  565. img_url = self.find_and_upload_image(a_value, 1, image_dir)
  566. if img_url:
  567. ws.cell(row=row_idx, column=product_main_col, value=img_url)
  568. self.log(f" 产品主图上传成功: {img_url[:50]}...")
  569. else:
  570. self.log(f" 产品主图: 未找到或上传失败")
  571. # 处理上架图片1-6 (图片编号2-7)
  572. for i in range(1, 7):
  573. if i in image_cols:
  574. img_url = self.find_and_upload_image(a_value, i+1, image_dir)
  575. if img_url:
  576. ws.cell(row=row_idx, column=image_cols[i], value=img_url)
  577. self.log(f" 上架图片{i}上传成功: {img_url[:50]}...")
  578. else:
  579. self.log(f" 上架图片{i}: 未找到或上传失败")
  580. processed_count += 1
  581. # 保存结果(保留所有样式)
  582. output_path = self.generate_output_path(template_path)
  583. wb.save(output_path)
  584. self.generated_file_path = output_path
  585. self.log(f"\n处理完成!共处理 {processed_count} 行数据")
  586. self.log(f"结果已保存到: {output_path}")
  587. self.status_var.set(f"处理完成!共处理 {processed_count} 行")
  588. messagebox.showinfo("完成", f"处理完成!\n共处理 {processed_count} 行数据\n结果保存在:\n{output_path}")
  589. except Exception as e:
  590. error_msg = f"处理出错: {str(e)}"
  591. self.log(f"错误: {error_msg}")
  592. self.status_var.set("处理出错")
  593. messagebox.showerror("错误", error_msg)
  594. import traceback
  595. self.log(traceback.format_exc())
  596. def generate_output_path(self, template_path):
  597. """生成输出文件路径"""
  598. dir_path = os.path.dirname(template_path)
  599. base_name = os.path.basename(template_path)
  600. name_without_ext = os.path.splitext(base_name)[0]
  601. new_filename = f"{name_without_ext}_结果.xlsx"
  602. return os.path.join(dir_path, new_filename)
  603. def find_and_upload_image(self, a_value, num, image_dir):
  604. """查找图片并上传,返回URL"""
  605. extensions = ['.jpg', '.jpeg', '.png']
  606. local_file = None
  607. for ext in extensions:
  608. filename = f"{a_value}_{num}{ext}"
  609. filepath = os.path.join(image_dir, filename)
  610. if os.path.exists(filepath):
  611. local_file = filepath
  612. break
  613. if not local_file:
  614. return None
  615. try:
  616. full_url = f"{self.upload_url}?group={self.upload_group}&user={self.upload_user}"
  617. content_type = 'image/jpeg'
  618. if local_file.lower().endswith('.png'):
  619. content_type = 'image/png'
  620. with open(local_file, 'rb') as f:
  621. files = {'file': (os.path.basename(local_file), f, content_type)}
  622. response = requests.post(full_url, files=files, timeout=30)
  623. if response.status_code == 200:
  624. result = response.json()
  625. if result.get('err') == 0:
  626. return result.get('url')
  627. else:
  628. self.log(f" 上传失败: {result.get('msg', '未知错误')}")
  629. return None
  630. else:
  631. self.log(f" 上传请求失败,状态码: {response.status_code}")
  632. return None
  633. except Exception as e:
  634. self.log(f" 上传出错: {str(e)}")
  635. return None
  636. def start_amazon_upload(self):
  637. """开始亚马逊图片上传"""
  638. if not self.amazon_template_path.get():
  639. messagebox.showerror("错误", "请先选择模板文件!")
  640. return
  641. if not self.sku_input.get().strip():
  642. messagebox.showerror("错误", "请先输入SKU!")
  643. return
  644. thread = threading.Thread(target=self.process_amazon_upload, daemon=True)
  645. thread.start()
  646. def process_amazon_upload(self):
  647. """执行亚马逊图片上传"""
  648. try:
  649. self.amazon_progress_var.set("处理中...")
  650. self.amazon_log("=" * 50)
  651. self.amazon_log("开始亚马逊图片上传...")
  652. template_path = self.amazon_template_path.get()
  653. sku = self.sku_input.get().strip()
  654. main_img_col_name = self.main_image_column.get()
  655. other_cols_str = self.other_image_columns.get()
  656. other_col_names = [col.strip() for col in other_cols_str.split(',') if col.strip()]
  657. self.amazon_log(f"读取模板文件: {template_path}")
  658. self.amazon_log(f"使用的SKU: {sku}")
  659. # 读取Excel
  660. df = pd.read_excel(template_path)
  661. self.amazon_log(f"找到 {len(df)} 行数据")
  662. # 检查必要的列
  663. if main_img_col_name not in df.columns:
  664. self.amazon_log(f"❌ 未找到主图列: {main_img_col_name}")
  665. self.amazon_progress_var.set("上传失败")
  666. return
  667. # 检查附图列
  668. existing_other_cols = [col for col in other_col_names if col in df.columns]
  669. if not existing_other_cols:
  670. self.amazon_log("⚠️ 未找到任何附图列")
  671. self.amazon_log(f"主图列: {main_img_col_name}")
  672. self.amazon_log(f"附图列: {existing_other_cols}")
  673. # 获取Access Token
  674. self.amazon_log("🔑 获取Access Token...")
  675. try:
  676. token = self.amazon_uploader.get_access_token()
  677. self.amazon_log("✅ Token获取成功")
  678. except Exception as e:
  679. self.amazon_log(f"❌ 获取Token失败: {str(e)}")
  680. self.amazon_progress_var.set("上传失败")
  681. return
  682. # 处理每一行
  683. success_count = 0
  684. fail_count = 0
  685. total_rows = len(df)
  686. for idx, row in df.iterrows():
  687. self.amazon_log(f"\n处理第 {idx+1}/{total_rows} 行")
  688. # 处理主图
  689. main_img_url = str(row[main_img_col_name]).strip()
  690. if main_img_url and main_img_url != 'nan' and main_img_url != 'None':
  691. if main_img_url.startswith('http'):
  692. try:
  693. resp = self.amazon_uploader.patch_image(token, "main", main_img_url, sku)
  694. if resp.status_code == 200:
  695. self.amazon_log(f" ✅ 主图上传成功")
  696. success_count += 1
  697. else:
  698. self.amazon_log(f" ❌ 主图上传失败: {resp.status_code}")
  699. fail_count += 1
  700. # 打印错误详情
  701. try:
  702. error_data = resp.json()
  703. self.amazon_log(f" 错误详情: {json.dumps(error_data, ensure_ascii=False)[:200]}")
  704. except:
  705. pass
  706. except Exception as e:
  707. self.amazon_log(f" ❌ 主图上传异常: {str(e)}")
  708. fail_count += 1
  709. else:
  710. self.amazon_log(f" ⚠️ 主图URL无效: {main_img_url[:50]}...")
  711. else:
  712. self.amazon_log(f" ⚠️ 主图为空,跳过")
  713. # 处理附图
  714. for i, col_name in enumerate(existing_other_cols, start=1):
  715. img_url = str(row[col_name]).strip()
  716. if img_url and img_url != 'nan' and img_url != 'None':
  717. if img_url.startswith('http'):
  718. try:
  719. image_type = f"other_{i}"
  720. resp = self.amazon_uploader.patch_image(token, image_type, img_url, sku)
  721. if resp.status_code == 200:
  722. self.amazon_log(f" ✅ 附图{i}上传成功")
  723. success_count += 1
  724. else:
  725. self.amazon_log(f" ❌ 附图{i}上传失败: {resp.status_code}")
  726. fail_count += 1
  727. except Exception as e:
  728. self.amazon_log(f" ❌ 附图{i}上传异常: {str(e)}")
  729. fail_count += 1
  730. else:
  731. self.amazon_log(f" ⚠️ 附图{i} URL无效: {img_url[:50]}...")
  732. else:
  733. self.amazon_log(f" ⚠️ 附图{i}为空,跳过")
  734. # 避免请求过快
  735. time.sleep(0.5)
  736. self.amazon_log(f"\n✅ 处理完成!成功: {success_count}, 失败: {fail_count}")
  737. self.amazon_progress_var.set(f"上传完成!成功: {success_count}, 失败: {fail_count}")
  738. messagebox.showinfo("完成", f"亚马逊图片上传完成!\n成功: {success_count}\n失败: {fail_count}")
  739. except Exception as e:
  740. error_msg = f"处理出错: {str(e)}"
  741. self.amazon_log(f"❌ {error_msg}")
  742. self.amazon_progress_var.set("处理出错")
  743. messagebox.showerror("错误", error_msg)
  744. import traceback
  745. self.amazon_log(traceback.format_exc())
  746. def start_detail_upload(self):
  747. """开始亚马逊详情图上传 (A+ Content)"""
  748. if not self.detail_template_path.get():
  749. messagebox.showerror("错误", "请先选择模板文件!")
  750. return
  751. if not self.asin_input.get().strip():
  752. messagebox.showerror("错误", "请先输入ASIN!")
  753. return
  754. thread = threading.Thread(target=self.process_detail_upload, daemon=True)
  755. thread.start()
  756. def process_detail_upload(self):
  757. """执行亚马逊详情图上传 (A+ Content)"""
  758. try:
  759. self.detail_progress_var.set("处理中...")
  760. self.detail_log("=" * 50)
  761. self.detail_log("开始亚马逊详情图上传 (A+ Content)...")
  762. template_path = self.detail_template_path.get()
  763. asin = self.asin_input.get().strip()
  764. detail_cols_str = self.detail_image_columns.get()
  765. detail_col_names = [col.strip() for col in detail_cols_str.split(',') if col.strip()]
  766. # 获取图片尺寸
  767. try:
  768. target_width = int(self.image_width.get())
  769. target_height = int(self.image_height.get())
  770. except ValueError:
  771. self.detail_log("❌ 图片尺寸必须为数字")
  772. self.detail_progress_var.set("上传失败")
  773. return
  774. module_headline = self.module_headline.get().strip()
  775. module_body = self.module_body.get().strip()
  776. self.detail_log(f"读取模板文件: {template_path}")
  777. self.detail_log(f"使用的ASIN: {asin}")
  778. self.detail_log(f"图片尺寸: {target_width}x{target_height}")
  779. # 读取Excel
  780. df = pd.read_excel(template_path)
  781. self.detail_log(f"找到 {len(df)} 行数据")
  782. # 检查详情图列
  783. existing_detail_cols = [col for col in detail_col_names if col in df.columns]
  784. if not existing_detail_cols:
  785. self.detail_log("❌ 未找到任何详情图列")
  786. self.detail_progress_var.set("上传失败")
  787. return
  788. self.detail_log(f"详情图列: {existing_detail_cols}")
  789. # 尝试导入PIL
  790. try:
  791. from PIL import Image
  792. from io import BytesIO
  793. HAS_PIL = True
  794. except ImportError:
  795. HAS_PIL = False
  796. self.detail_log("⚠️ 未安装Pillow,将使用原始图片尺寸")
  797. # 获取Access Token
  798. self.detail_log("🔑 获取Access Token...")
  799. try:
  800. token = self.amazon_uploader.get_access_token()
  801. self.detail_log("✅ Token获取成功")
  802. except Exception as e:
  803. self.detail_log(f"❌ 获取Token失败: {str(e)}")
  804. self.detail_progress_var.set("上传失败")
  805. return
  806. # 处理每一行
  807. success_count = 0
  808. fail_count = 0
  809. total_rows = min(len(df), 1) # 每次只处理一行(A+ Content每次只能绑定一个ASIN)
  810. # 只处理第一行
  811. row = df.iloc[0]
  812. # 收集所有图片URL
  813. image_urls = []
  814. for col_name in existing_detail_cols:
  815. img_url = str(row[col_name]).strip()
  816. if img_url and img_url != 'nan' and img_url != 'None' and img_url.startswith('http'):
  817. image_urls.append(img_url)
  818. if not image_urls:
  819. self.detail_log("❌ 未找到有效的图片URL")
  820. self.detail_progress_var.set("上传失败")
  821. return
  822. self.detail_log(f"找到 {len(image_urls)} 张详情图")
  823. # 处理每张图片
  824. upload_ids = []
  825. for i, img_url in enumerate(image_urls, start=1):
  826. self.detail_log(f"\n处理第 {i} 张图片: {img_url[:60]}...")
  827. try:
  828. # 下载图片
  829. img_resp = requests.get(img_url, timeout=30)
  830. img_resp.raise_for_status()
  831. image_binary = img_resp.content
  832. # 调整尺寸
  833. if HAS_PIL:
  834. try:
  835. img = Image.open(BytesIO(image_binary))
  836. original_size = img.size
  837. # 调整尺寸
  838. img_resized = img.resize((target_width, target_height), Image.LANCZOS)
  839. buffer = BytesIO()
  840. # 判断原图格式
  841. if img_url.lower().endswith('.png'):
  842. img_resized.save(buffer, format="PNG")
  843. else:
  844. img_resized.save(buffer, format="JPEG")
  845. image_binary = buffer.getvalue()
  846. self.detail_log(f" 图片尺寸调整: {original_size} -> {target_width}x{target_height}")
  847. except Exception as e:
  848. self.detail_log(f" 图片调整失败: {str(e)},使用原图")
  849. # 计算MD5
  850. md5_hash = hashlib.md5(image_binary).digest()
  851. content_md5 = base64.b64encode(md5_hash).decode('utf-8')
  852. # 确定Content-Type
  853. content_type = "image/jpeg"
  854. if img_url.lower().endswith('.png'):
  855. content_type = "image/png"
  856. self.detail_log(f" 图片大小: {len(image_binary)} bytes")
  857. # 创建上传目标
  858. upload_data = self.amazon_uploader.create_upload_destination(token, content_md5, content_type)
  859. upload_id = upload_data.get("uploadDestinationId")
  860. upload_url = upload_data.get("url")
  861. if not upload_id or not upload_url:
  862. self.detail_log(f" ❌ 创建上传目标失败")
  863. fail_count += 1
  864. continue
  865. self.detail_log(f" ✅ 上传目标创建成功")
  866. # 上传到S3
  867. self.amazon_uploader.upload_to_s3(upload_url, image_binary, content_type)
  868. self.detail_log(f" ✅ S3上传成功")
  869. upload_ids.append({
  870. "upload_id": upload_id,
  871. "width": target_width,
  872. "height": target_height,
  873. "alt_text": f"Product detail image {i}"
  874. })
  875. success_count += 1
  876. time.sleep(0.5)
  877. except Exception as e:
  878. self.detail_log(f" ❌ 处理失败: {str(e)}")
  879. fail_count += 1
  880. if not upload_ids:
  881. self.detail_log("❌ 没有成功上传的图片")
  882. self.detail_progress_var.set("上传失败")
  883. return
  884. # 构建A+ Content内容文档
  885. self.detail_log("\n📝 构建A+ Content文档...")
  886. # 构建模块列表 - 每张图片一个模块
  887. content_module_list = []
  888. for i, upload_info in enumerate(upload_ids):
  889. module = {
  890. "contentModuleType": "STANDARD_HEADER_IMAGE_TEXT",
  891. "standardHeaderImageText": {
  892. "headline": {
  893. "value": module_headline if i == 0 else f"{module_headline} - {i+1}",
  894. "decoratorSet": []
  895. },
  896. "block": {
  897. "image": {
  898. "uploadDestinationId": upload_info["upload_id"],
  899. "imageCropSpecification": {
  900. "size": {
  901. "width": {"value": upload_info["width"], "units": "pixels"},
  902. "height": {"value": upload_info["height"], "units": "pixels"}
  903. },
  904. "offset": {
  905. "x": {"value": 0, "units": "pixels"},
  906. "y": {"value": 0, "units": "pixels"}
  907. }
  908. },
  909. "altText": upload_info["alt_text"]
  910. },
  911. "headline": {
  912. "value": f"Image {i+1}",
  913. "decoratorSet": []
  914. },
  915. "body": {
  916. "textList": [
  917. {
  918. "value": module_body if i == 0 else f"Product detail view {i+1}",
  919. "decoratorSet": []
  920. }
  921. ]
  922. }
  923. }
  924. }
  925. }
  926. content_module_list.append(module)
  927. # 构建content文档
  928. content_doc = {
  929. "name": f"Product Images for {asin}",
  930. "contentType": "EBC",
  931. "locale": "en-US",
  932. "contentModuleList": content_module_list
  933. }
  934. # 创建内容文档
  935. self.detail_log("📤 创建A+ Content文档...")
  936. try:
  937. content_key = self.amazon_uploader.create_content_document(token, content_doc)
  938. self.detail_log(f"✅ Content文档创建成功,Content Key: {content_key}")
  939. except Exception as e:
  940. self.detail_log(f"❌ 创建Content文档失败: {str(e)}")
  941. self.detail_progress_var.set("上传失败")
  942. return
  943. # 绑定ASIN
  944. self.detail_log(f"🔗 绑定ASIN: {asin}...")
  945. try:
  946. self.amazon_uploader.bind_asin_to_content(token, content_key, asin)
  947. self.detail_log(f"✅ ASIN绑定成功")
  948. except Exception as e:
  949. self.detail_log(f"❌ 绑定ASIN失败: {str(e)}")
  950. self.detail_progress_var.set("上传失败")
  951. return
  952. # 提交审核
  953. self.detail_log("📋 提交审核...")
  954. try:
  955. self.amazon_uploader.submit_for_approval(token, content_key)
  956. self.detail_log(f"✅ 审核提交成功")
  957. self.detail_log(f"⚠️ 审核通常需要 1-7 个工作日")
  958. except Exception as e:
  959. self.detail_log(f"❌ 提交审核失败: {str(e)}")
  960. self.detail_progress_var.set("上传失败")
  961. return
  962. self.detail_log(f"\n✅ 处理完成!成功: {success_count}, 失败: {fail_count}")
  963. self.detail_log(f" Content Key: {content_key}")
  964. self.detail_log(f" ASIN: {asin}")
  965. self.detail_progress_var.set(f"上传完成!成功: {success_count}, 失败: {fail_count}")
  966. messagebox.showinfo("完成", f"亚马逊详情图上传完成!\n成功: {success_count}\n失败: {fail_count}\n\nContent Key: {content_key}\nASIN: {asin}\n\n审核通常需要 1-7 个工作日")
  967. except Exception as e:
  968. error_msg = f"处理出错: {str(e)}"
  969. self.detail_log(f"❌ {error_msg}")
  970. self.detail_progress_var.set("处理出错")
  971. messagebox.showerror("错误", error_msg)
  972. import traceback
  973. self.detail_log(traceback.format_exc())
  974. def main():
  975. root = tk.Tk()
  976. app = ImageMatcherApp(root)
  977. root.mainloop()
  978. if __name__ == "__main__":
  979. main()