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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679
  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
  15. import time
  16. class AmazonImageUploader:
  17. """亚马逊图片上传类"""
  18. def __init__(self):
  19. self.client_id = "amzn1.application-oa2-client.9f775d35077a4c7598116e5a89b60222"
  20. self.client_secret = "amzn1.oa2-cs.v1.6909777baa82b796555a431cd23709342e8e01434abcad1f8b59cadf048b51c3"
  21. self.refresh_token = "Atzr|IwEBICh_fojh7qAfrePzSjOZhQxTnHRZCYOuWNvSAHGaBs1WfkjR4qjC1Dmv0uK3oyq_L-R9SRKK4cYmPwF1MQXQdqKlpzOAGItWeXnNzh-SyhHHISv0_vCt3D6hluVmIEmF2kA7ro_XF-PslD41g3Eq6bv0Qq4y4pQbkDj-9dudDuz7nku8UL-nc4EWgJCrHCJTAs8TK48Yvrfi3E35uv97tYhs6qb8D9d4dR9C7daaC49eoDqP88qyUHThQV1Kqon8xukx4AT2_wLqWNcabpADzuK-5w7D-vvFFtvzfkbmtfzoPeEdHPulG6scc59BPFTyBlGU7a3d1JSFg2ROHIlrljRm"
  22. self.aws_access_key = "AKIA24CLQSVTW2K4S6RP"
  23. self.aws_secret_key = "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"
  24. self.aws_region = "us-east-1"
  25. self.seller_id = "A1H50Z0WYYLEGQ"
  26. self.marketplace_id = "ATVPDKIKX0DER"
  27. self.product_type = "TABLE"
  28. def sha256_hash(self, data):
  29. return hashlib.sha256(data.encode("utf-8")).hexdigest()
  30. def hmac_sha256(self, key, msg):
  31. return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
  32. def get_signature_key(self, secret_key, date_stamp, region, service):
  33. k_date = self.hmac_sha256(("AWS4" + secret_key).encode("utf-8"), date_stamp)
  34. k_region = self.hmac_sha256(k_date, region)
  35. k_service = self.hmac_sha256(k_region, service)
  36. return self.hmac_sha256(k_service, "aws4_request")
  37. def get_access_token(self):
  38. url = "https://api.amazon.com/auth/o2/token"
  39. resp = requests.post(url, data={
  40. "grant_type": "refresh_token",
  41. "refresh_token": self.refresh_token,
  42. "client_id": self.client_id,
  43. "client_secret": self.client_secret,
  44. })
  45. resp.raise_for_status()
  46. return resp.json()["access_token"]
  47. def spapi_request(self, method, path, query, body=None, access_token=None):
  48. service = "execute-api"
  49. host = "sellingpartnerapi-na.amazon.com"
  50. payload = json.dumps(body, separators=(",", ":")) if body else ""
  51. t = datetime.datetime.now(datetime.timezone.utc)
  52. amz_date = t.strftime("%Y%m%dT%H%M%SZ")
  53. date_stamp = t.strftime("%Y%m%d")
  54. canonical_headers = (
  55. f"host:{host}\n"
  56. f"x-amz-access-token:{access_token}\n"
  57. f"x-amz-date:{amz_date}\n"
  58. )
  59. signed_headers = "host;x-amz-access-token;x-amz-date"
  60. payload_hash = self.sha256_hash(payload)
  61. canonical_request = (
  62. f"{method}\n{path}\n{query}\n"
  63. f"{canonical_headers}\n{signed_headers}\n{payload_hash}"
  64. )
  65. algorithm = "AWS4-HMAC-SHA256"
  66. credential_scope = f"{date_stamp}/{self.aws_region}/{service}/aws4_request"
  67. string_to_sign = (
  68. f"{algorithm}\n{amz_date}\n{credential_scope}\n"
  69. f"{self.sha256_hash(canonical_request)}"
  70. )
  71. signing_key = self.get_signature_key(self.aws_secret_key, date_stamp, self.aws_region, service)
  72. signature = self.hmac_sha256(signing_key, string_to_sign.encode("utf-8")).hexdigest()
  73. auth_header = (
  74. f"{algorithm} Credential={self.aws_access_key}/{credential_scope}, "
  75. f"SignedHeaders={signed_headers}, Signature={signature}"
  76. )
  77. headers = {
  78. "Host": host,
  79. "x-amz-access-token": access_token,
  80. "x-amz-date": amz_date,
  81. "Authorization": auth_header,
  82. }
  83. if body:
  84. headers["Content-Type"] = "application/json"
  85. url = f"https://{host}{path}?{query}"
  86. return requests.request(method, url, headers=headers, data=payload or None)
  87. def patch_image(self, access_token, image_type, image_url, sku):
  88. """
  89. image_type 可选值:
  90. "main" → 主图
  91. "other_1" ~ "other_8" → 副图1~8
  92. "swatch" → 变体图
  93. """
  94. encoded_sku = quote(sku, safe="")
  95. path = f"/listings/2021-08-01/items/{self.seller_id}/{encoded_sku}"
  96. query = f"marketplaceIds={self.marketplace_id}"
  97. if image_type == "main":
  98. attr_path = "/attributes/main_product_image_locator"
  99. elif image_type.startswith("other_") and 1 <= int(image_type.split("_")[1]) <= 8:
  100. n = image_type.split("_")[1]
  101. attr_path = f"/attributes/other_product_image_locator_{n}"
  102. elif image_type == "swatch":
  103. attr_path = "/attributes/swatch_product_image_locator"
  104. else:
  105. raise ValueError(f"不支持的 image_type: {image_type}")
  106. body = {
  107. "productType": self.product_type,
  108. "patches": [
  109. {
  110. "op": "replace",
  111. "path": attr_path,
  112. "value": [
  113. {
  114. "marketplace_id": self.marketplace_id,
  115. "media_location": image_url
  116. }
  117. ]
  118. }
  119. ]
  120. }
  121. return self.spapi_request("PATCH", path, query, body=body, access_token=access_token)
  122. class ImageMatcherApp:
  123. def __init__(self, root):
  124. self.root = root
  125. self.root.title("图片匹配与上传工具 - 亚马逊版")
  126. self.root.geometry("950x750")
  127. # 存储文件路径
  128. self.template_path = tk.StringVar()
  129. self.image_dir = tk.StringVar()
  130. # 固定上传配置
  131. self.upload_url = "https://cos.port.run/uploadFile"
  132. self.upload_group = "soundSynthesis"
  133. self.upload_user = "1"
  134. # 亚马逊上传器
  135. self.amazon_uploader = AmazonImageUploader()
  136. # 创建UI
  137. self.create_widgets()
  138. # 加载保存的配置
  139. self.load_config()
  140. def create_widgets(self):
  141. # 主框架
  142. main_frame = ttk.Frame(self.root, padding="10")
  143. main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  144. # 配置网格权重
  145. self.root.columnconfigure(0, weight=1)
  146. self.root.rowconfigure(0, weight=1)
  147. main_frame.columnconfigure(0, weight=1)
  148. main_frame.rowconfigure(1, weight=1)
  149. # 创建Notebook(选项卡)
  150. self.notebook = ttk.Notebook(main_frame)
  151. self.notebook.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10)
  152. # 选项卡1: 图片匹配与上传
  153. self.tab_matching = ttk.Frame(self.notebook)
  154. self.notebook.add(self.tab_matching, text="图片匹配上传")
  155. self.create_matching_tab()
  156. # 选项卡2: 亚马逊主图更换
  157. self.tab_amazon = ttk.Frame(self.notebook)
  158. self.notebook.add(self.tab_amazon, text="亚马逊主图更换")
  159. self.create_amazon_tab()
  160. # 状态栏
  161. self.status_var = tk.StringVar()
  162. self.status_var.set("就绪")
  163. status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
  164. status_bar.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=5)
  165. def create_matching_tab(self):
  166. """创建图片匹配上传选项卡"""
  167. tab = self.tab_matching
  168. tab.columnconfigure(1, weight=1)
  169. tab.rowconfigure(4, weight=1)
  170. # 标题
  171. title_label = ttk.Label(tab, text="图片匹配与上传", font=("Arial", 14, "bold"))
  172. title_label.grid(row=0, column=0, columnspan=3, pady=10)
  173. # 模板文件选择
  174. ttk.Label(tab, text="模板文件:").grid(row=1, column=0, sticky=tk.W, pady=5)
  175. ttk.Entry(tab, textvariable=self.template_path, width=60).grid(row=1, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
  176. ttk.Button(tab, text="选择模板", command=self.select_template).grid(row=1, column=2, pady=5, padx=5)
  177. # 图片目录选择
  178. ttk.Label(tab, text="图片目录:").grid(row=2, column=0, sticky=tk.W, pady=5)
  179. ttk.Entry(tab, textvariable=self.image_dir, width=60).grid(row=2, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
  180. ttk.Button(tab, text="选择目录", command=self.select_image_dir).grid(row=2, column=2, pady=5, padx=5)
  181. # 按钮框架
  182. button_frame = ttk.Frame(tab)
  183. button_frame.grid(row=3, column=0, columnspan=3, pady=10)
  184. ttk.Button(button_frame, text="开始处理", command=self.start_matching, width=15).pack(side=tk.LEFT, padx=5)
  185. ttk.Button(button_frame, text="清空日志", command=self.clear_log, width=15).pack(side=tk.LEFT, padx=5)
  186. # 日志框
  187. log_frame = ttk.LabelFrame(tab, text="日志", padding="5")
  188. log_frame.grid(row=4, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10)
  189. log_frame.columnconfigure(0, weight=1)
  190. log_frame.rowconfigure(0, weight=1)
  191. self.log_text = scrolledtext.ScrolledText(log_frame, height=15, width=80, wrap=tk.WORD)
  192. self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  193. # 存储生成的文件路径
  194. self.generated_file_path = None
  195. def create_amazon_tab(self):
  196. """创建亚马逊主图更换选项卡"""
  197. tab = self.tab_amazon
  198. tab.columnconfigure(1, weight=1)
  199. tab.rowconfigure(6, weight=1)
  200. # 标题
  201. title_label = ttk.Label(tab, text="亚马逊主图/附图批量更换", font=("Arial", 14, "bold"))
  202. title_label.grid(row=0, column=0, columnspan=3, pady=10)
  203. # 模板文件选择
  204. ttk.Label(tab, text="模板文件:").grid(row=1, column=0, sticky=tk.W, pady=5)
  205. self.amazon_template_path = tk.StringVar()
  206. ttk.Entry(tab, textvariable=self.amazon_template_path, width=60).grid(row=1, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
  207. ttk.Button(tab, text="选择模板", command=self.select_amazon_template).grid(row=1, column=2, pady=5, padx=5)
  208. # SKU输入(手动输入,不是从表格读取)
  209. ttk.Label(tab, text="SKU (手动输入):").grid(row=2, column=0, sticky=tk.W, pady=5)
  210. self.sku_input = tk.StringVar(value="bar grey 03")
  211. ttk.Entry(tab, textvariable=self.sku_input, width=40).grid(row=2, column=1, sticky=tk.W, pady=5, padx=5)
  212. # 主图列选择
  213. ttk.Label(tab, text="主图列名:").grid(row=3, column=0, sticky=tk.W, pady=5)
  214. self.main_image_column = tk.StringVar(value="产品主图")
  215. ttk.Entry(tab, textvariable=self.main_image_column, width=30).grid(row=3, column=1, sticky=tk.W, pady=5, padx=5)
  216. # 附图列选择
  217. ttk.Label(tab, text="附图列名(逗号分隔):").grid(row=4, column=0, sticky=tk.W, pady=5)
  218. self.other_image_columns = tk.StringVar(value="上架图片1,上架图片2,上架图片3,上架图片4,上架图片5,上架图片6,上架图片7,上架图片8")
  219. ttk.Entry(tab, textvariable=self.other_image_columns, width=60).grid(row=4, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
  220. # 按钮框架
  221. button_frame = ttk.Frame(tab)
  222. button_frame.grid(row=5, column=0, columnspan=3, pady=10)
  223. ttk.Button(button_frame, text="保存配置", command=self.save_amazon_config, width=12).pack(side=tk.LEFT, padx=5)
  224. ttk.Button(button_frame, text="加载配置", command=self.load_amazon_config, width=12).pack(side=tk.LEFT, padx=5)
  225. ttk.Button(button_frame, text="开始上传", command=self.start_amazon_upload, width=15).pack(side=tk.LEFT, padx=5)
  226. ttk.Button(button_frame, text="清空日志", command=self.clear_amazon_log, width=15).pack(side=tk.LEFT, padx=5)
  227. # 日志框
  228. log_frame = ttk.LabelFrame(tab, text="上传日志", padding="5")
  229. log_frame.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10)
  230. log_frame.columnconfigure(0, weight=1)
  231. log_frame.rowconfigure(0, weight=1)
  232. self.amazon_log_text = scrolledtext.ScrolledText(log_frame, height=15, width=80, wrap=tk.WORD)
  233. self.amazon_log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  234. # 进度标签
  235. self.amazon_progress_var = tk.StringVar(value="就绪")
  236. progress_label = ttk.Label(tab, textvariable=self.amazon_progress_var, relief=tk.SUNKEN, anchor=tk.W)
  237. progress_label.grid(row=7, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
  238. def select_template(self):
  239. filename = filedialog.askopenfilename(
  240. title="选择模板文件",
  241. filetypes=[("Excel文件", "*.xlsx *.xls"), ("所有文件", "*.*")]
  242. )
  243. if filename:
  244. self.template_path.set(filename)
  245. self.log(f"已选择模板文件: {filename}")
  246. def select_image_dir(self):
  247. directory = filedialog.askdirectory(title="选择图片目录")
  248. if directory:
  249. self.image_dir.set(directory)
  250. self.log(f"已选择图片目录: {directory}")
  251. def select_amazon_template(self):
  252. filename = filedialog.askopenfilename(
  253. title="选择模板文件",
  254. filetypes=[("Excel文件", "*.xlsx *.xls"), ("所有文件", "*.*")]
  255. )
  256. if filename:
  257. self.amazon_template_path.set(filename)
  258. self.amazon_log(f"已选择模板文件: {filename}")
  259. def log(self, message):
  260. """在匹配选项卡日志框中添加消息"""
  261. self.log_text.insert(tk.END, f"{message}\n")
  262. self.log_text.see(tk.END)
  263. self.root.update_idletasks()
  264. def clear_log(self):
  265. """清空匹配选项卡日志"""
  266. self.log_text.delete(1.0, tk.END)
  267. def amazon_log(self, message):
  268. """在亚马逊选项卡日志框中添加消息"""
  269. self.amazon_log_text.insert(tk.END, f"{message}\n")
  270. self.amazon_log_text.see(tk.END)
  271. self.root.update_idletasks()
  272. def clear_amazon_log(self):
  273. """清空亚马逊选项卡日志"""
  274. self.amazon_log_text.delete(1.0, tk.END)
  275. def get_config_path(self):
  276. """获取配置文件路径"""
  277. return os.path.join(os.path.dirname(os.path.abspath(__file__)), "amazon_config.json")
  278. def save_amazon_config(self):
  279. """保存亚马逊配置"""
  280. config = {
  281. "sku": self.sku_input.get(),
  282. "main_image_column": self.main_image_column.get(),
  283. "other_image_columns": self.other_image_columns.get(),
  284. "template_path": self.amazon_template_path.get()
  285. }
  286. try:
  287. with open(self.get_config_path(), 'w', encoding='utf-8') as f:
  288. json.dump(config, f, ensure_ascii=False, indent=2)
  289. self.amazon_log("✅ 配置已保存")
  290. except Exception as e:
  291. self.amazon_log(f"❌ 保存配置失败: {str(e)}")
  292. def load_amazon_config(self):
  293. """加载亚马逊配置"""
  294. try:
  295. config_path = self.get_config_path()
  296. if os.path.exists(config_path):
  297. with open(config_path, 'r', encoding='utf-8') as f:
  298. config = json.load(f)
  299. self.sku_input.set(config.get("sku", "bar grey 03"))
  300. self.main_image_column.set(config.get("main_image_column", "产品主图"))
  301. self.other_image_columns.set(config.get("other_image_columns", "上架图片1,上架图片2,上架图片3,上架图片4,上架图片5,上架图片6,上架图片7,上架图片8"))
  302. if config.get("template_path"):
  303. self.amazon_template_path.set(config.get("template_path"))
  304. self.amazon_log("✅ 配置已加载")
  305. else:
  306. self.amazon_log("⚠️ 未找到配置文件,使用默认配置")
  307. except Exception as e:
  308. self.amazon_log(f"❌ 加载配置失败: {str(e)}")
  309. def load_config(self):
  310. """启动时自动加载亚马逊配置"""
  311. try:
  312. config_path = self.get_config_path()
  313. if os.path.exists(config_path):
  314. with open(config_path, 'r', encoding='utf-8') as f:
  315. config = json.load(f)
  316. self.sku_input.set(config.get("sku", "bar grey 03"))
  317. self.main_image_column.set(config.get("main_image_column", "产品主图"))
  318. self.other_image_columns.set(config.get("other_image_columns", "上架图片1,上架图片2,上架图片3,上架图片4,上架图片5,上架图片6,上架图片7,上架图片8"))
  319. if config.get("template_path"):
  320. self.amazon_template_path.set(config.get("template_path"))
  321. except Exception:
  322. pass
  323. def start_matching(self):
  324. """开始匹配处理"""
  325. if not self.template_path.get():
  326. messagebox.showerror("错误", "请先选择模板文件!")
  327. return
  328. if not self.image_dir.get():
  329. messagebox.showerror("错误", "请先选择图片目录!")
  330. return
  331. thread = threading.Thread(target=self.process_matching, daemon=True)
  332. thread.start()
  333. def process_matching(self):
  334. """执行匹配处理"""
  335. try:
  336. self.status_var.set("处理中...")
  337. self.log("=" * 50)
  338. self.log("开始处理...")
  339. template_path = self.template_path.get()
  340. image_dir = self.image_dir.get()
  341. self.log(f"读取模板文件: {template_path}")
  342. # 使用openpyxl加载工作簿,保留所有样式
  343. wb = load_workbook(template_path)
  344. ws = wb.active
  345. # 获取表头(第二行)
  346. headers = []
  347. for col in range(1, ws.max_column + 1):
  348. cell_value = ws.cell(row=2, column=col).value
  349. headers.append(str(cell_value) if cell_value else '')
  350. self.log(f"找到 {len(headers)} 列")
  351. # 查找各列的列号
  352. col_a_index = 1 # A列是第1列
  353. product_main_col = None
  354. image_cols = {}
  355. for idx, header in enumerate(headers, start=1):
  356. if header == '产品主图':
  357. product_main_col = idx
  358. elif '上架图片' in header:
  359. match = re.search(r'(\d+)', header)
  360. if match:
  361. num = int(match.group(1))
  362. image_cols[num] = idx
  363. if product_main_col is None:
  364. self.log("警告: 未找到'产品主图'列")
  365. else:
  366. self.log(f"找到产品主图列: 第{product_main_col}列")
  367. self.log(f"找到上架图片列: {len(image_cols)} 列")
  368. # 处理每一行(从第三行开始)
  369. processed_count = 0
  370. for row_idx in range(3, ws.max_row + 1):
  371. a_cell = ws.cell(row=row_idx, column=col_a_index)
  372. a_value = str(a_cell.value).strip() if a_cell.value else ''
  373. if not a_value or a_value == 'nan' or a_value == 'None':
  374. continue
  375. self.log(f"\n处理第 {row_idx} 行, A列值: {a_value}")
  376. # 处理产品主图 (图片编号1)
  377. if product_main_col is not None:
  378. img_url = self.find_and_upload_image(a_value, 1, image_dir)
  379. if img_url:
  380. ws.cell(row=row_idx, column=product_main_col, value=img_url)
  381. self.log(f" 产品主图上传成功: {img_url[:50]}...")
  382. else:
  383. self.log(f" 产品主图: 未找到或上传失败")
  384. # 处理上架图片1-6 (图片编号2-7)
  385. for i in range(1, 7):
  386. if i in image_cols:
  387. img_url = self.find_and_upload_image(a_value, i+1, image_dir)
  388. if img_url:
  389. ws.cell(row=row_idx, column=image_cols[i], value=img_url)
  390. self.log(f" 上架图片{i}上传成功: {img_url[:50]}...")
  391. else:
  392. self.log(f" 上架图片{i}: 未找到或上传失败")
  393. processed_count += 1
  394. # 保存结果(保留所有样式)
  395. output_path = self.generate_output_path(template_path)
  396. wb.save(output_path)
  397. self.generated_file_path = output_path
  398. self.log(f"\n处理完成!共处理 {processed_count} 行数据")
  399. self.log(f"结果已保存到: {output_path}")
  400. self.status_var.set(f"处理完成!共处理 {processed_count} 行")
  401. messagebox.showinfo("完成", f"处理完成!\n共处理 {processed_count} 行数据\n结果保存在:\n{output_path}")
  402. except Exception as e:
  403. error_msg = f"处理出错: {str(e)}"
  404. self.log(f"错误: {error_msg}")
  405. self.status_var.set("处理出错")
  406. messagebox.showerror("错误", error_msg)
  407. import traceback
  408. self.log(traceback.format_exc())
  409. def generate_output_path(self, template_path):
  410. """生成输出文件路径"""
  411. dir_path = os.path.dirname(template_path)
  412. base_name = os.path.basename(template_path)
  413. name_without_ext = os.path.splitext(base_name)[0]
  414. new_filename = f"{name_without_ext}_结果.xlsx"
  415. return os.path.join(dir_path, new_filename)
  416. def find_and_upload_image(self, a_value, num, image_dir):
  417. """查找图片并上传,返回URL"""
  418. extensions = ['.jpg', '.jpeg', '.png']
  419. local_file = None
  420. for ext in extensions:
  421. filename = f"{a_value}_{num}{ext}"
  422. filepath = os.path.join(image_dir, filename)
  423. if os.path.exists(filepath):
  424. local_file = filepath
  425. break
  426. if not local_file:
  427. return None
  428. try:
  429. full_url = f"{self.upload_url}?group={self.upload_group}&user={self.upload_user}"
  430. content_type = 'image/jpeg'
  431. if local_file.lower().endswith('.png'):
  432. content_type = 'image/png'
  433. with open(local_file, 'rb') as f:
  434. files = {'file': (os.path.basename(local_file), f, content_type)}
  435. response = requests.post(full_url, files=files, timeout=30)
  436. if response.status_code == 200:
  437. result = response.json()
  438. if result.get('err') == 0:
  439. return result.get('url')
  440. else:
  441. self.log(f" 上传失败: {result.get('msg', '未知错误')}")
  442. return None
  443. else:
  444. self.log(f" 上传请求失败,状态码: {response.status_code}")
  445. return None
  446. except Exception as e:
  447. self.log(f" 上传出错: {str(e)}")
  448. return None
  449. def start_amazon_upload(self):
  450. """开始亚马逊图片上传"""
  451. if not self.amazon_template_path.get():
  452. messagebox.showerror("错误", "请先选择模板文件!")
  453. return
  454. if not self.sku_input.get().strip():
  455. messagebox.showerror("错误", "请先输入SKU!")
  456. return
  457. thread = threading.Thread(target=self.process_amazon_upload, daemon=True)
  458. thread.start()
  459. def process_amazon_upload(self):
  460. """执行亚马逊图片上传"""
  461. try:
  462. self.amazon_progress_var.set("处理中...")
  463. self.amazon_log("=" * 50)
  464. self.amazon_log("开始亚马逊图片上传...")
  465. template_path = self.amazon_template_path.get()
  466. sku = self.sku_input.get().strip()
  467. main_img_col_name = self.main_image_column.get()
  468. other_cols_str = self.other_image_columns.get()
  469. other_col_names = [col.strip() for col in other_cols_str.split(',') if col.strip()]
  470. self.amazon_log(f"读取模板文件: {template_path}")
  471. self.amazon_log(f"使用的SKU: {sku}")
  472. # 读取Excel
  473. df = pd.read_excel(template_path)
  474. self.amazon_log(f"找到 {len(df)} 行数据")
  475. # 检查必要的列
  476. if main_img_col_name not in df.columns:
  477. self.amazon_log(f"❌ 未找到主图列: {main_img_col_name}")
  478. self.amazon_progress_var.set("上传失败")
  479. return
  480. # 检查附图列
  481. existing_other_cols = [col for col in other_col_names if col in df.columns]
  482. if not existing_other_cols:
  483. self.amazon_log("⚠️ 未找到任何附图列")
  484. self.amazon_log(f"主图列: {main_img_col_name}")
  485. self.amazon_log(f"附图列: {existing_other_cols}")
  486. # 获取Access Token
  487. self.amazon_log("🔑 获取Access Token...")
  488. try:
  489. token = self.amazon_uploader.get_access_token()
  490. self.amazon_log("✅ Token获取成功")
  491. except Exception as e:
  492. self.amazon_log(f"❌ 获取Token失败: {str(e)}")
  493. self.amazon_progress_var.set("上传失败")
  494. return
  495. # 处理每一行
  496. success_count = 0
  497. fail_count = 0
  498. total_rows = len(df)
  499. for idx, row in df.iterrows():
  500. self.amazon_log(f"\n处理第 {idx+1}/{total_rows} 行")
  501. # 处理主图
  502. main_img_url = str(row[main_img_col_name]).strip()
  503. if main_img_url and main_img_url != 'nan' and main_img_url != 'None':
  504. if main_img_url.startswith('http'):
  505. try:
  506. resp = self.amazon_uploader.patch_image(token, "main", main_img_url, sku)
  507. if resp.status_code == 200:
  508. self.amazon_log(f" ✅ 主图上传成功")
  509. success_count += 1
  510. else:
  511. self.amazon_log(f" ❌ 主图上传失败: {resp.status_code}")
  512. fail_count += 1
  513. # 打印错误详情
  514. try:
  515. error_data = resp.json()
  516. self.amazon_log(f" 错误详情: {json.dumps(error_data, ensure_ascii=False)[:200]}")
  517. except:
  518. pass
  519. except Exception as e:
  520. self.amazon_log(f" ❌ 主图上传异常: {str(e)}")
  521. fail_count += 1
  522. else:
  523. self.amazon_log(f" ⚠️ 主图URL无效: {main_img_url[:50]}...")
  524. else:
  525. self.amazon_log(f" ⚠️ 主图为空,跳过")
  526. # 处理附图
  527. for i, col_name in enumerate(existing_other_cols, start=1):
  528. img_url = str(row[col_name]).strip()
  529. if img_url and img_url != 'nan' and img_url != 'None':
  530. if img_url.startswith('http'):
  531. try:
  532. image_type = f"other_{i}"
  533. resp = self.amazon_uploader.patch_image(token, image_type, img_url, sku)
  534. if resp.status_code == 200:
  535. self.amazon_log(f" ✅ 附图{i}上传成功")
  536. success_count += 1
  537. else:
  538. self.amazon_log(f" ❌ 附图{i}上传失败: {resp.status_code}")
  539. fail_count += 1
  540. except Exception as e:
  541. self.amazon_log(f" ❌ 附图{i}上传异常: {str(e)}")
  542. fail_count += 1
  543. else:
  544. self.amazon_log(f" ⚠️ 附图{i} URL无效: {img_url[:50]}...")
  545. else:
  546. self.amazon_log(f" ⚠️ 附图{i}为空,跳过")
  547. # 避免请求过快
  548. time.sleep(0.5)
  549. self.amazon_log(f"\n✅ 处理完成!成功: {success_count}, 失败: {fail_count}")
  550. self.amazon_progress_var.set(f"上传完成!成功: {success_count}, 失败: {fail_count}")
  551. messagebox.showinfo("完成", f"亚马逊图片上传完成!\n成功: {success_count}\n失败: {fail_count}")
  552. except Exception as e:
  553. error_msg = f"处理出错: {str(e)}"
  554. self.amazon_log(f"❌ {error_msg}")
  555. self.amazon_progress_var.set("处理出错")
  556. messagebox.showerror("错误", error_msg)
  557. import traceback
  558. self.amazon_log(traceback.format_exc())
  559. def main():
  560. root = tk.Tk()
  561. app = ImageMatcherApp(root)
  562. root.mainloop()
  563. if __name__ == "__main__":
  564. main()