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

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