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

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