web.py 71 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502
  1. from flask import Flask, render_template, request, jsonify, send_file, send_from_directory
  2. import pandas as pd
  3. from openpyxl import load_workbook
  4. import os
  5. import shutil
  6. import requests
  7. import json
  8. import re
  9. import time
  10. from datetime import datetime
  11. from collections import Counter
  12. app = Flask(__name__)
  13. app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024
  14. BASE_DIR = os.path.dirname(os.path.abspath(__file__))
  15. UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')
  16. TEMPLATES_FOLDER = os.path.join(BASE_DIR, 'templates_excel')
  17. OUTPUT_FOLDER = os.path.join(BASE_DIR, 'output')
  18. EXAMPLE_FOLDER = os.path.join(BASE_DIR, 'example_templates')
  19. for folder in [UPLOAD_FOLDER, TEMPLATES_FOLDER, OUTPUT_FOLDER, EXAMPLE_FOLDER]:
  20. if not os.path.exists(folder):
  21. os.makedirs(folder)
  22. AI_CONFIG = {
  23. 'api_url': 'https://ark.cn-beijing.volces.com/api/v3/responses',
  24. 'authorization': 'Bearer ark-f7930e50-4837-4f8f-96b4-bd2f56313300-b9dd7',
  25. 'vision_model': 'doubao-seed-2-1-pro-260628',
  26. 'extract_model': 'deepseek-v4-pro-260425',
  27. }
  28. # AI 排除字段(这些字段永远不交给 AI 填写)
  29. # 全局排除字段(Amazon & Wayfair 都不填)
  30. AI_EXCLUDED_FIELDS = {
  31. # === 原排除字段 ===
  32. 'Amazon Seller SKU', 'Universal Product Code', 'Collection Name',
  33. 'Manufacturer Product URL', 'Group Reference ID',
  34. 'Variant Grouping 3', 'Variant Attribute Name On Site 3',
  35. 'Minimum Advertised Price', 'Everyday B2B Discount Rate', 'Marketing Copy',
  36. 'Freight Class', 'National Motor Freight Class', 'Ship Palletized',
  37. 'Document Type 1', 'Legal Document Type 1', 'Document Region 1', 'Document Language 1',
  38. 'Document File Name or URL 2', 'Document Type 2', 'Legal Document Type 2',
  39. 'Document Region 2', 'Document Language 2',
  40. 'Document File Name or URL 3', 'Document Type 3', 'Legal Document Type 3',
  41. 'Document Region 3', 'Document Language 3',
  42. 'Chemical 1', 'Country of Origin - Additional Details',
  43. 'Sustainability & Social Responsibility Certifications (North America Only)', 'Toxicity 1',
  44. # === Amazon 不需要填写的字段(你提供的列表) ===
  45. 'Product Id', 'Package Contains SKU Identifier', 'Construction Type', 'Number of Pieces',
  46. 'Weave Type', 'Surface Recommendation', 'Material Features', 'Pile Height', 'Orientation',
  47. 'Pattern', 'Finish Type', 'Product Site Launch Date', 'League Name', 'Team Name',
  48. 'Back Material Type', 'Embellishment Feature', 'Item Thickness', 'Item Thickness Unit',
  49. 'Animal Theme', 'Rug Form Type', 'Letter Character', 'Government Contract Name',
  50. 'Government Contract Number', 'Collection Item', 'Lifting Mechanism', 'Product Grade',
  51. 'Drawer Size', 'Drawer Weight Capacity', 'Drawer Weight Capacity Unit',
  52. 'Keyboard Tray Length', 'Keyboard Tray Length Unit', 'Keyboard Tray Width',
  53. 'Keyboard Tray Width Unit', 'Has Keyboard Tray', 'Sport Type', 'Hand Orientation',
  54. 'Model Variant', 'Seat Height', 'Seat Height Unit', 'Seat Material', 'Seat Width',
  55. 'Seat Width Unit', 'Specification Met', 'Plastic Type', 'Release Date', 'Metal Finish',
  56. 'Handle Finish Type', 'Athlete', 'Top Style', 'Minimum Required Door Width',
  57. 'Minimum Required Door Width Unit', 'Umbrella Hole Diameter', 'Umbrella Hole Diameter Unit',
  58. 'Skip Offer', 'Offer Condition Note', 'Product Tax Code', 'Merchant Release Date',
  59. 'Main Image Location', 'Other Image Location', 'Battery Life Percentage', 'Cosmetic',
  60. 'Features', 'Functional Condition', 'Source Type', 'Import Designation', 'Restock Date (US)',
  61. 'Your Price USD (Sell on Amazon, US)', 'Pricing Rule (Sell on Amazon, US)',
  62. 'Minimum Seller Allowed Price (Sell on Amazon, US)',
  63. 'Maximum Seller Allowed Price (Sell on Amazon, US)',
  64. 'Sale Price USD (Sell on Amazon, US)', 'Sale Start Date (Sell on Amazon, US)',
  65. 'Sale End Date (Sell on Amazon, US)', 'Offering Release Date (Sell on Amazon, US)',
  66. 'Stop Selling Date (Sell on Amazon, US)', 'Master Pack Layers per Pallet Quantity',
  67. 'Master Packs Per Layer Quantity', 'Minimum Height', 'Minimum Height Unit', 'Safety Warning',
  68. 'Radio Frequency Emission & Authorization Status', 'FCC ID', 'SDoC Contact Name',
  69. 'SDoC Contact US Mailing Address', 'SDoC Contact Email Address',
  70. 'SDOC Contact US Phone Number', 'Compliance Regulation Type', 'Regulatory Identification',
  71. 'Responsible Person\'s Email or Electronic Address',
  72. 'Compliance Media Source Location (en_US, Application Guide)',
  73. 'Compliance Media Source Location (en_US, Certificate of Analysis)',
  74. 'Compliance Media Source Location (en_US, Certificate of Compliance)',
  75. 'Compliance Media Source Location (en_US, Compatibility Guide)',
  76. 'Compliance Media Source Location (en_US, Data Act Transparency Declaration)',
  77. 'Compliance Media Source Location (en_US, Emergency Use Authorization)',
  78. 'Compliance Media Source Location (en_US, Emergency Use Authorization Amendment)',
  79. 'Compliance Media Source Location (en_US, Installation Manual)',
  80. 'Compliance Media Source Location (en_US, Instructions for Use)',
  81. 'Compliance Media Source Location (en_US, Patient Fact Sheet)',
  82. 'Compliance Media Source Location (en_US, Product Certificate of Conformity)',
  83. 'Compliance Media Source Location (en_US, Provider Fact Sheet)',
  84. 'Compliance Media Source Location (en_US, Safety Data Sheet)',
  85. 'Compliance Media Source Location (en_US, Safety Information)',
  86. 'Compliance Media Source Location (en_US, Specification Sheet)',
  87. 'Compliance Media Source Location (en_US, Troubleshooting Guide)',
  88. 'Compliance Media Source Location (en_US, User Guide)',
  89. 'Compliance Media Source Location (en_US, User Manual)',
  90. 'Compliance Media Source Location (en_US, Warranty)', 'Contains PFAS',
  91. 'GHS Chemical H Code', 'BAA/TAA Regulation Compliance', 'BAA TAA Compliance Acknowledgement',
  92. 'TAA Compliant Country', 'Battery Cell Composition', 'Battery Cell Composition Other Than Listed',
  93. 'Battery IEC Code', 'Battery Weight', 'Battery Weight Unit', 'Number of Batteries',
  94. 'Battery Type', 'Number of Lithium Metal Cells', 'Number of Lithium-ion Cells',
  95. 'Lithium Battery Energy Content', 'Lithium Battery Energy Content Unit',
  96. 'Lithium Battery Packaging', 'Lithium Battery Weight', 'Lithium Battery Weight Unit',
  97. 'GHS Class', 'Hazmat Aspect', 'Hazmat', 'Safety Data Sheet (SDS or MSDS) URL',
  98. 'Mfg Warranty Type (i.e. Parts, Labor)', 'California Proposition 65 Warning Type',
  99. 'California Proposition 65 Chemical Name(s)', 'Pesticide Marking',
  100. 'Pesticide Registration Status', 'Pesticide Certification Number',
  101. 'Has Multiple Battery Powered Components', 'Contains Battery or Cell',
  102. 'Battery Contains free Unabsorbed Liquid', 'Is Battery Non Spillable',
  103. 'Non-Lithium Battery Packaging', 'Has Replaceable Battery',
  104. 'Non-Lithium Battery Energy Content', 'Non-Lithium Battery Energy Content Unit',
  105. 'Has Less than 30 Percent State of Charge', 'Battery Installation Device Type', 'Value',
  106. 'Item Weight Unit', 'Country as Labeled', 'Product Compliance Certificate',
  107. 'Regulatory Organization Name', 'Compliance Certification Status',
  108. 'Compliance Certification Value', 'Recycled Content Percentage', 'Swatch Image URL',
  109. }
  110. AI_FIELD_MAPPING = {
  111. 'Table Top Shape': 'Table Top Shape',
  112. 'Coffee Table Lift Top': 'Coffee Table Lift Top',
  113. 'Upholstered': 'Upholstered',
  114. 'Plug-In': 'Plug-In',
  115. 'Storage Included': 'Storage Included',
  116. 'Shelves Included': 'Shelves Included',
  117. 'Drawers Included': 'Drawers Included',
  118. 'Cabinets Included': 'Cabinets Included',
  119. 'Decal/Laminate Design': 'Decal/Laminate Design',
  120. 'Set Type': 'Set Type',
  121. 'Durability': 'Durability',
  122. 'Number of Tables Included': 'Number of Tables Included',
  123. 'CAL TB 117-2013 Compliant': 'CAL TB 117-2013 Compliant',
  124. 'SOFFA Compliant': 'SOFFA Compliant',
  125. }
  126. ai_logs = []
  127. def add_ai_log(message, level='info'):
  128. timestamp = datetime.now().strftime('%H:%M:%S')
  129. log_entry = {'time': timestamp, 'message': message, 'level': level}
  130. ai_logs.append(log_entry)
  131. print(f"[AI] {timestamp} - {message}")
  132. if len(ai_logs) > 500:
  133. ai_logs.pop(0)
  134. def get_ai_logs():
  135. return ai_logs
  136. def clear_ai_logs():
  137. global ai_logs
  138. ai_logs = []
  139. TEMPLATE_CONFIG = {
  140. 'wayfair': {
  141. 'name': 'Wayfair',
  142. 'source_header': 2,
  143. 'template_header': 4,
  144. 'label': 'Wayfair'
  145. },
  146. 'amazon': {
  147. 'name': '亚马逊',
  148. 'source_header': 2,
  149. 'template_header': 4,
  150. 'label': '亚马逊'
  151. }
  152. }
  153. def load_valid_values(wb):
  154. valid_data = {}
  155. if "Valid Values" not in wb.sheetnames:
  156. return valid_data
  157. ws_valid = wb["Valid Values"]
  158. for row in range(1, ws_valid.max_row + 1):
  159. # 智能检测字段名所在列:优先第1列,为空则取第2列
  160. field_name = ws_valid.cell(row=row, column=1).value
  161. start_col = 2 # 值从第2列开始读
  162. if not field_name:
  163. field_name = ws_valid.cell(row=row, column=2).value
  164. start_col = 3 # 字段名在第2列,值从第3列开始
  165. if not field_name:
  166. continue
  167. field_name = str(field_name).strip()
  168. # 去掉 " - [ XXX ]" 后缀,如 "Product Type - [ UTILITY_CART_WAGON ]" → "Product Type"
  169. field_name = re.sub(r'\s*-\s*\[.*?\]\s*$', '', field_name)
  170. if not field_name:
  171. continue
  172. if field_name not in valid_data:
  173. valid_data[field_name] = []
  174. for col in range(start_col, ws_valid.max_column + 1):
  175. val = ws_valid.cell(row=row, column=col).value
  176. if val:
  177. val_str = str(val).strip()
  178. if val_str and val_str not in valid_data[field_name]:
  179. valid_data[field_name].append(val_str)
  180. return valid_data
  181. def get_valid_value(valid_values, field_name, default=None):
  182. if field_name in valid_values and valid_values[field_name]:
  183. return valid_values[field_name][0]
  184. return default
  185. def call_vision_api(image_url, row_index):
  186. add_ai_log(f"🖼️ [第{row_index}行] 开始识别图片: {image_url}", 'info')
  187. headers = {
  188. 'Accept': '*/*',
  189. 'Accept-Encoding': 'gzip, deflate, br',
  190. 'Authorization': AI_CONFIG['authorization'],
  191. 'Connection': 'keep-alive',
  192. 'Content-Type': 'application/json',
  193. 'User-Agent': 'PostmanRuntime-ApipostRuntime/1.1.0'
  194. }
  195. data = {
  196. "model": AI_CONFIG['vision_model'],
  197. "input": [
  198. {
  199. "role": "user",
  200. "content": [
  201. {"type": "input_image", "image_url": image_url},
  202. {"type": "input_text", "text": "请详细描述这张图片中的商品,包括但不限于:形状、颜色、材质、结构、功能特点、适用场景。描述要详细具体。"}
  203. ]
  204. }
  205. ]
  206. }
  207. add_ai_log(f"📤 [第{row_index}行] 调用视觉模型 ({AI_CONFIG['vision_model']})...", 'info')
  208. start_time = time.time()
  209. try:
  210. response = requests.post(AI_CONFIG['api_url'], headers=headers, json=data, timeout=360)
  211. elapsed = time.time() - start_time
  212. add_ai_log(f"⏱️ [第{row_index}行] 视觉识别耗时: {elapsed:.1f}秒", 'info')
  213. result = response.json()
  214. if 'output' in result:
  215. for item in result['output']:
  216. if item.get('type') == 'message' and 'content' in item:
  217. for content in item['content']:
  218. if content.get('type') == 'output_text':
  219. text = content.get('text', '')
  220. add_ai_log(f"✅ [第{row_index}行] 视觉识别成功,描述长度: {len(text)} 字符", 'success')
  221. return text
  222. add_ai_log(f"⚠️ [第{row_index}行] 视觉识别返回格式异常", 'warn')
  223. return None
  224. except requests.exceptions.Timeout:
  225. add_ai_log(f"❌ [第{row_index}行] 视觉识别超时 (360秒)", 'error')
  226. return None
  227. except Exception as e:
  228. add_ai_log(f"❌ [第{row_index}行] 视觉识别失败: {str(e)}", 'error')
  229. return None
  230. def call_extract_api(description, row_index, fields):
  231. add_ai_log(f"🧠 [第{row_index}行] 开始属性提取,候选字段 {len(fields)} 个...", 'info')
  232. headers = {
  233. 'Accept': '*/*',
  234. 'Accept-Encoding': 'gzip, deflate, br',
  235. 'Authorization': AI_CONFIG['authorization'],
  236. 'Connection': 'keep-alive',
  237. 'Content-Type': 'application/json',
  238. 'User-Agent': 'PostmanRuntime-ApipostRuntime/1.1.0'
  239. }
  240. fields_str = ', '.join(fields)
  241. data = {
  242. "model": AI_CONFIG['extract_model'],
  243. "stream": False,
  244. "tools": [{"type": "web_search", "max_keyword": 3}],
  245. "input": [
  246. {
  247. "role": "user",
  248. "content": [
  249. {
  250. "type": "input_text",
  251. "text": f"""请根据下面的商品图片识别结果,尽可能填写以下字段。对于每个字段,请根据商品描述推断最合适的值。
  252. 需要填写的字段列表:
  253. {fields_str}
  254. 返回格式要求:每个属性用方括号括起来,格式为 [属性名:值]
  255. 例如:[Table Top Shape:Rectangle with rounded corners] [Coffee Table Lift Top:No]
  256. 如果描述中没有相关信息,请填写 "Unknown"。回答的[属性名:值]都用英文。
  257. 商品描述:
  258. {description}"""
  259. }
  260. ]
  261. }
  262. ]
  263. }
  264. add_ai_log(f"📤 [第{row_index}行] 调用提取模型 ({AI_CONFIG['extract_model']})...", 'info')
  265. start_time = time.time()
  266. try:
  267. response = requests.post(AI_CONFIG['api_url'], headers=headers, json=data, timeout=360)
  268. elapsed = time.time() - start_time
  269. add_ai_log(f"⏱️ [第{row_index}行] 属性提取耗时: {elapsed:.1f}秒", 'info')
  270. result = response.json()
  271. if 'output' in result:
  272. for item in result['output']:
  273. if item.get('type') == 'message' and 'content' in item:
  274. for content in item['content']:
  275. if content.get('type') == 'output_text':
  276. text = content.get('text', '')
  277. add_ai_log(f"✅ [第{row_index}行] 属性提取成功,结果长度: {len(text)} 字符", 'success')
  278. return text
  279. add_ai_log(f"⚠️ [第{row_index}行] 属性提取返回格式异常", 'warn')
  280. return None
  281. except requests.exceptions.Timeout:
  282. add_ai_log(f"❌ [第{row_index}行] 属性提取超时 (360秒)", 'error')
  283. return None
  284. except Exception as e:
  285. add_ai_log(f"❌ [第{row_index}行] 属性提取失败: {str(e)}", 'error')
  286. return None
  287. def parse_ai_result(text):
  288. result = {}
  289. if not text:
  290. return result
  291. pattern = r'\[([^:\]]+):([^\]]+)\]'
  292. matches = re.findall(pattern, text)
  293. for field, value in matches:
  294. field = field.strip()
  295. value = value.strip()
  296. if value and value not in ['Unknown', 'Not Applicable', 'N/A', '']:
  297. result[field] = value
  298. return result
  299. def get_ai_filled_data(image_url, row_index, fields):
  300. add_ai_log(f"🚀 [第{row_index}行] 开始AI处理,候选字段数: {len(fields)}", 'info')
  301. add_ai_log(f"📷 [第{row_index}行] 图片URL: {image_url}", 'info')
  302. description = call_vision_api(image_url, row_index)
  303. if not description:
  304. add_ai_log(f"⚠️ [第{row_index}行] 图片识别失败,跳过该行", 'warn')
  305. return {}
  306. add_ai_log(f"📝 [第{row_index}行] 图片描述预览: {description}", 'info')
  307. extract_result = call_extract_api(description, row_index, fields)
  308. if not extract_result:
  309. add_ai_log(f"⚠️ [第{row_index}行] 属性提取失败,跳过该行", 'warn')
  310. return {}
  311. parsed = parse_ai_result(extract_result)
  312. if parsed:
  313. add_ai_log(f"📋 [第{row_index}行] 解析成功,提取到 {len(parsed)} 个字段:", 'success')
  314. for field, value in parsed.items():
  315. add_ai_log(f" └─ {field}: {value}", 'info')
  316. else:
  317. add_ai_log(f"⚠️ [第{row_index}行] 解析结果为空", 'warn')
  318. return parsed
  319. def fill_wayfair_row(src_row, source_map, col_indices, valid_values, row_num, ws, suffix="", mfr_suffix="", ai_data=None, src_row_index=0):
  320. fill_count = 0
  321. item_code = ""
  322. product_name = ""
  323. product_name_en = ""
  324. filled_summary = []
  325. try:
  326. item_code = src_row.get("Item Code", "")
  327. product_name = src_row.get("产品名称", "")
  328. product_name_en = src_row.get("产品英文名称", "")
  329. if pd.isna(item_code): item_code = ""
  330. if pd.isna(product_name): product_name = ""
  331. if pd.isna(product_name_en): product_name_en = ""
  332. # ==================== 1. 从 Valid Values 下拉取值的字段 ====================
  333. # Brand: 下拉一个即可(取第一个有效值)
  334. if "Brand" in col_indices:
  335. brand_val = get_valid_value(valid_values, "Brand", "DGpoco")
  336. ws.cell(row=row_num, column=col_indices["Brand"], value=brand_val)
  337. fill_count += 1
  338. filled_summary.append(f"Brand={brand_val}")
  339. # Variant Type: 下拉选择第3个(索引2),目标为 "Not Variant"
  340. variant_type_val = "Not Variant"
  341. if "Variant Type" in col_indices:
  342. vt_list = valid_values.get("Variant Type", [])
  343. if len(vt_list) >= 3:
  344. variant_type_val = vt_list[2]
  345. elif vt_list:
  346. # 若不足3个,尝试匹配 "Not Variant",否则取第一个
  347. variant_type_val = next((v for v in vt_list if "not variant" in v.lower()), vt_list[0])
  348. ws.cell(row=row_num, column=col_indices["Variant Type"], value=variant_type_val)
  349. fill_count += 1
  350. filled_summary.append(f"VariantType={variant_type_val}")
  351. is_not_variant = ("not variant" in variant_type_val.lower())
  352. # Variant Grouping 1: 默认选 color(仅当 Variant Type 不是 Not Variant 时填写)
  353. if "Variant Grouping 1" in col_indices and not is_not_variant:
  354. vg_list = valid_values.get("Variant Details - Variant Grouping",
  355. valid_values.get("Variant Grouping", []))
  356. # 优先查找 color(不区分大小写),找不到则取第一个
  357. vg1_val = next((v for v in vg_list if v.lower() == "color"), None)
  358. if vg1_val is None:
  359. vg1_val = next((v for v in vg_list if "color" in v.lower()),
  360. vg_list[0] if vg_list else "Color")
  361. ws.cell(row=row_num, column=col_indices["Variant Grouping 1"], value=vg1_val)
  362. fill_count += 1
  363. filled_summary.append(f"VarGroup1={vg1_val}")
  364. # Variant Attribute Name On Site 1: 默认 Finish(仅当不是 Not Variant)
  365. if "Variant Attribute Name On Site 1" in col_indices and not is_not_variant:
  366. ws.cell(row=row_num, column=col_indices["Variant Attribute Name On Site 1"], value="Finish")
  367. fill_count += 1
  368. # Variant Grouping 2: 默认选 size(仅当不是 Not Variant)
  369. if "Variant Grouping 2" in col_indices and not is_not_variant:
  370. vg_list = valid_values.get("Variant Details - Variant Grouping",
  371. valid_values.get("Variant Grouping", []))
  372. vg2_val = next((v for v in vg_list if v.lower() == "size"), None)
  373. if vg2_val is None:
  374. vg2_val = next((v for v in vg_list if "size" in v.lower()),
  375. vg_list[0] if vg_list else "Size")
  376. ws.cell(row=row_num, column=col_indices["Variant Grouping 2"], value=vg2_val)
  377. fill_count += 1
  378. filled_summary.append(f"VarGroup2={vg2_val}")
  379. # Variant Attribute Name On Site 2: 默认 Dimensions(仅当不是 Not Variant)
  380. if "Variant Attribute Name On Site 2" in col_indices and not is_not_variant:
  381. ws.cell(row=row_num, column=col_indices["Variant Attribute Name On Site 2"], value="Dimensions")
  382. fill_count += 1
  383. # ==================== 2. 固定值字段 ====================
  384. fixed_values = {
  385. "Minimum Order Quantity": 1,
  386. "Force Quantity Multiplier": 1,
  387. "Display Set Quantity": 1,
  388. "Lead Time": 48,
  389. "Replacement Lead Time": 120,
  390. "Flat Pack": "yes",
  391. "Assembly Required": "yes",
  392. "Canada Product Restriction": "yes",
  393. "CARB Phase II Compliant (formaldehyde emissions)": "yes",
  394. "Commercial Warranty": "yes",
  395. "CANFER Compliant": "yes",
  396. "Commercial Warranty Length": "1years",
  397. "Composite Wood Product (CWP)": "yes",
  398. "Country Of Manufacturer": "China",
  399. "General Certificate of Conformity (GCC)": "yes",
  400. "Hazard Class(es)": "Does Not Apply",
  401. "Hazardous Material / Dangerous Goods": "No",
  402. "Hazardous Material Weight": "Does Not Apply",
  403. "Level of Assembly": "Full Assembly Needed",
  404. "Packing Group": "Does Not Apply",
  405. "Reason for Restriction": "Does Not Apply",
  406. "Supplier Intended and Approved Use": "Residential Use",
  407. "TSCA Title VI Compliant (formaldehyde emissions)": "yes",
  408. "UN or ID number": "Does Not Apply",
  409. "Uniform Packaging and Labeling Regulations (UPLR) Compliant": "yes",
  410. "Warning Required": "NO",
  411. "Warranty Length": "1Years",
  412. "Battery or Batteries Included": "No",
  413. "Wayfair Compliance Verified Program (including Baby Safety Alliance fka JPMA) for this product category": "Yes",
  414. "CPSC - 16 CFR 1303 Compliant": "Yes",
  415. "ADA Compliant": "Yes",
  416. "Safety Listing(s)": "Does Not Apply",
  417. "SOR/2016-193 - Surface Coating Materials Regulations": "Yes"
  418. }
  419. fixed_count = 0
  420. for field_name, fixed_val in fixed_values.items():
  421. if field_name in col_indices:
  422. ws.cell(row=row_num, column=col_indices[field_name], value=fixed_val)
  423. fill_count += 1
  424. fixed_count += 1
  425. if fixed_count > 0:
  426. filled_summary.append(f"固定值x{fixed_count}")
  427. # ==================== 3. 从 Valid Values 匹配特定文本的字段 ====================
  428. # Ship Type: 默认 Small Parcel
  429. if "Ship Type" in col_indices:
  430. ship_list = valid_values.get("Ship Type", [])
  431. ship_val = next((v for v in ship_list if "small parcel" in v.lower()),
  432. ship_list[0] if ship_list else "Small Parcel")
  433. ws.cell(row=row_num, column=col_indices["Ship Type"], value=ship_val)
  434. fill_count += 1
  435. filled_summary.append(f"ShipType={ship_val}")
  436. # ISTA Certified: 包装认证(后端无法弹窗,优先取 Valid Values 第一个值,否则留空待人工确认)
  437. if "ISTA Certified" in col_indices:
  438. ista_list = valid_values.get("ISTA Certified", [])
  439. ista_val = ista_list[0] if ista_list else ""
  440. ws.cell(row=row_num, column=col_indices["ISTA Certified"], value=ista_val)
  441. fill_count += 1
  442. if ista_val:
  443. filled_summary.append(f"ISTA={ista_val}")
  444. else:
  445. filled_summary.append("ISTA=待人工确认")
  446. # ==================== 4. 从源数据映射的字段 ====================
  447. # Product Type: 取产品英文名称,没有则取产品名称
  448. if "Product Type" in col_indices:
  449. pt_val = product_name_en if product_name_en else product_name
  450. ws.cell(row=row_num, column=col_indices["Product Type"], value=pt_val)
  451. fill_count += 1
  452. filled_summary.append(f"ProductType={str(pt_val)[:30]}")
  453. # ==================== 5. Wayfair 基础字段映射(颜色/材质/尺寸/重量等)====================
  454. wayfair_field_map = {
  455. # === 颜色/材质(已有)===
  456. "Color": "颜色",
  457. "Base Color": "颜色",
  458. "Top Color": "颜色",
  459. "Frame Material": "材质",
  460. "Base Material": "材质",
  461. "Top Material": "材质",
  462. "Furniture Leg Material": "材质",
  463. "Upholstery Fabric Type": "材质",
  464. "Material": "材质", # ← 新增
  465. "Product Name": "产品名称", # ← 添加这一行
  466. # === 价格(新增)===
  467. "Base Cost": "优惠单价",
  468. "Manufacturer Suggested Retail Price": "单价",
  469. "List Price": "优惠单价",
  470. "Your Price": "优惠单价",
  471. # === 产品尺寸/重量(已有+新增)===
  472. "Item Length": "组装长度",
  473. "Item Width": "组装宽度",
  474. "Item Height": "组装高度",
  475. "Item Weight": "产品重量",
  476. "Product Weight": "产品重量", # ← 新增
  477. "Overall Product Weight": "产品重量", # ← 新增
  478. "Overall Width - Side to Side": "组装长度", # ← 新增(长边/横向)
  479. "Overall Depth - Front to Back": "组装宽度", # ← 新增(深度/前后)
  480. "Overall Height - Top to Bottom": "组装高度", # ← 新增(高度)
  481. "Item Length Longer Edge": "组装长度",
  482. "Item Width Shorter Edge": "组装宽度",
  483. "Length longer edge when assembled": "组装长度",
  484. "Width shorter edge when assembled": "组装宽度",
  485. "Height base to top when assembled": "组装高度",
  486. "Item Depth Front To Back": "组装宽度",
  487. "Item Height Floor To Top": "组装高度",
  488. "Item Width Side To Side": "组装长度",
  489. "Working Surface Length": "组装长度",
  490. "Working Surface Width": "组装宽度",
  491. # === 包装尺寸/重量(已有+新增)===
  492. "Item Package Length": "包装尺寸-长度",
  493. "Item Package Width": "包装尺寸-宽度",
  494. "Item Package Height": "包装尺寸-高度",
  495. "Package Weight": "包装尺寸-重量",
  496. "Carton Weight 1": "包装尺寸-重量", # ← 新增
  497. "Carton Height 1": "包装尺寸-高度", # ← 新增
  498. "Carton Width 1": "包装尺寸-宽度", # ← 新增
  499. "Carton Depth 1": "包装尺寸-长度", # ← 新增(Depth对应长度)
  500. "Unit Count": "包装尺寸-重量",
  501. # === 原产地(已有)===
  502. "Country of Origin": "原产地",
  503. "Image File Name or URL 1": "产品主图",
  504. "Image File Name or URL 2": "上架图片1",
  505. "Image File Name or URL 3": "上架图片2",
  506. "Image File Name or URL 4": "上架图片3",
  507. "Image File Name or URL 5": "上架图片4",
  508. }
  509. mapped_count = 0
  510. for wayfair_field, source_key in wayfair_field_map.items():
  511. if wayfair_field not in col_indices:
  512. continue
  513. col_idx = col_indices[wayfair_field]
  514. source_col_name = source_map.get(source_key) if source_map else None
  515. if source_col_name and source_col_name in src_row:
  516. val = src_row[source_col_name]
  517. if pd.notna(val):
  518. if wayfair_field == "Country of Origin":
  519. val = str(val).strip()
  520. if val in ["CHN", "CN"]:
  521. val = "China"
  522. ws.cell(row=row_num, column=col_idx, value=val)
  523. fill_count += 1
  524. mapped_count += 1
  525. if mapped_count > 0:
  526. filled_summary.append(f"基础映射x{mapped_count}")
  527. combined_value = f"{item_code}{product_name_en if product_name_en else product_name}"
  528. if suffix:
  529. combined_value = f"{combined_value}{suffix}"
  530. if "Supplier Part Number" in col_indices:
  531. ws.cell(row=row_num, column=col_indices["Supplier Part Number"], value=combined_value)
  532. fill_count += 1
  533. mapped_count += 1
  534. if "Manufacturer Part Number" in col_indices:
  535. ws.cell(row=row_num, column=col_indices["Manufacturer Part Number"], value=combined_value)
  536. fill_count += 1
  537. mapped_count += 1
  538. # ==================== 6. 图片字段 ====================
  539. image_fields = [
  540. ("Main Image URL", "产品主图"),
  541. ("Other Image URL", "上架图片1"), ("Other Image URL", "上架图片2"),
  542. ("Other Image URL", "上架图片3"), ("Other Image URL", "上架图片4"),
  543. ("Other Image URL", "上架图片5"), ("Other Image URL", "上架图片6"),
  544. ("Other Image URL", "上架图片7"), ("Other Image URL", "上架图片8"),
  545. ]
  546. img_count = 0
  547. for field_name, source_key in image_fields:
  548. if field_name not in col_indices:
  549. continue
  550. col_idx = col_indices[field_name]
  551. source_col = source_map.get(source_key, source_key) if source_map else source_key
  552. if source_col in src_row:
  553. val = src_row[source_col]
  554. if pd.notna(val) and str(val).strip():
  555. ws.cell(row=row_num, column=col_idx, value=val)
  556. fill_count += 1
  557. img_count += 1
  558. if img_count > 0:
  559. filled_summary.append(f"图片URLx{img_count}")
  560. # ==================== AI 字段填充(只填系统未填过的空单元格)====================
  561. ai_filled = []
  562. if ai_data:
  563. for ai_field, ai_value in ai_data.items():
  564. if ai_field in col_indices:
  565. current = ws.cell(row=row_num, column=col_indices[ai_field]).value
  566. if current is None or str(current).strip() == '':
  567. ws.cell(row=row_num, column=col_indices[ai_field], value=ai_value)
  568. fill_count += 1
  569. ai_filled.append(f"{ai_field}={str(ai_value)[:25]}")
  570. if ai_filled:
  571. filled_summary.append(f"[AI]{', '.join(ai_filled[:3])}")
  572. if len(ai_filled) > 3:
  573. filled_summary.append(f"...等{len(ai_filled)}个AI字段")
  574. # ==================== 结束 ====================
  575. # 日志汇总
  576. if filled_summary:
  577. summary_str = " | ".join(filled_summary)
  578. add_ai_log(f"📋 [Wayfair模板第{row_num}行] 填写详情({fill_count}个字段): {summary_str}", 'info')
  579. else:
  580. add_ai_log(f"⚠️ [Wayfair模板第{row_num}行] 未填写任何字段", 'warn')
  581. except Exception as e:
  582. add_ai_log(f"❌ [Wayfair模板第{row_num}行] 填写异常: {str(e)}", 'error')
  583. print(f"fill_wayfair_row error: {e}")
  584. return fill_count
  585. def fill_amazon_row(src_row, source_map, col_indices, valid_values, row_num, ws, suffix, mfr_suffix, ai_data=None, src_row_index=0):
  586. fill_count = 0
  587. item_code = ""
  588. product_name = ""
  589. product_name_en = ""
  590. filled_summary = []
  591. try:
  592. item_code = src_row.get("Item Code", "")
  593. product_name = src_row.get("产品名称", "")
  594. product_name_en = src_row.get("产品英文名称", "")
  595. if pd.isna(item_code): item_code = ""
  596. if pd.isna(product_name): product_name = ""
  597. if pd.isna(product_name_en): product_name_en = ""
  598. sku_name = product_name_en if product_name_en else product_name
  599. sku_value = f"{item_code}{sku_name}{suffix}"
  600. if "SKU" in col_indices:
  601. ws.cell(row=row_num, column=col_indices["SKU"], value=sku_value)
  602. fill_count += 1
  603. filled_summary.append(f"SKU={sku_value[:40]}")
  604. if "Model Number" in col_indices:
  605. ws.cell(row=row_num, column=col_indices["Model Number"], value=sku_value)
  606. fill_count += 1
  607. filled_summary.append(f"ModelNumber={sku_value[:40]}")
  608. if "Model Name" in col_indices:
  609. ws.cell(row=row_num, column=col_indices["Model Name"], value=sku_value)
  610. fill_count += 1
  611. if "Part Number" in col_indices:
  612. ws.cell(row=row_num, column=col_indices["Part Number"], value=sku_value)
  613. fill_count += 1
  614. if "Set Name" in col_indices:
  615. set_name = product_name_en if product_name_en else product_name
  616. ws.cell(row=row_num, column=col_indices["Set Name"], value=set_name)
  617. fill_count += 1
  618. filled_summary.append(f"SetName={str(set_name)[:40]}")
  619. field_map = {
  620. "Color": "颜色",
  621. "Base Color": "颜色",
  622. "Top Color": "颜色",
  623. "Frame Material": "材质",
  624. "Base Material": "材质",
  625. "Top Material": "材质",
  626. "Furniture Leg Material":"材质",
  627. "Upholstery Fabric Type": "材质",
  628. "Item Length": "组装长度",
  629. "Item Width": "组装宽度",
  630. "Item Height": "组装高度",
  631. "Item Weight": "产品重量",
  632. "Item Package Length": "包装尺寸-长度",
  633. "Item Package Width": "包装尺寸-宽度",
  634. "Item Package Height": "包装尺寸-高度",
  635. "Package Weight": "包装尺寸-重量",
  636. "List Price": "优惠单价",
  637. "Your Price USD": "优惠单价",
  638. "Unit Count": "包装尺寸-重量",
  639. "Item Length Longer Edge": "组装长度",
  640. "Item Width Shorter Edge": "组装宽度",
  641. "Length longer edge when assembled": "组装长度",
  642. "Width shorter edge when assembled": "组装宽度",
  643. "Height base to top when assembled": "组装高度",
  644. "Item Height Floor To Top": "组装高度",
  645. "Item Width Side To Side": "组装长度",
  646. "Working Surface Length": "组装长度",
  647. "Working Surface Width": "组装宽度",
  648. "Country of Origin": "原产地"
  649. }
  650. source_col_map = {
  651. "颜色": "颜色",
  652. "材质": "材质",
  653. "组装长度": "组装长度(英寸)",
  654. "组装宽度": "组装宽度(英寸)",
  655. "组装高度": "组装高度(英寸)",
  656. "产品重量": "产品重量(磅)",
  657. "包装尺寸-长度": "包装尺寸-长度(英寸)",
  658. "包装尺寸-宽度": "包装尺寸-宽度(英寸)",
  659. "包装尺寸-高度": "包装尺寸-高度(英寸)",
  660. "包装尺寸-重量": "包装尺寸-重量(磅)",
  661. "优惠单价": "优惠单价",
  662. "产品名称": "产品名称",
  663. "产品英文名称": "产品英文名称",
  664. "Item Code": "Item Code",
  665. "店铺名称": "店铺名称",
  666. "原产地": "原产地",
  667. "产品主图": "产品主图",
  668. "上架图片1": "上架图片1",
  669. "上架图片2": "上架图片2",
  670. "上架图片3": "上架图片3",
  671. "上架图片4": "上架图片4",
  672. "上架图片5": "上架图片5",
  673. "上架图片6": "上架图片6",
  674. "上架图片7": "上架图片7",
  675. "上架图片8": "上架图片8",
  676. "产品描述": "产品描述",
  677. "产品特点1": "产品特点1",
  678. "产品特点2": "产品特点2",
  679. "产品特点3": "产品特点3",
  680. "产品特点4": "产品特点4",
  681. "产品特点5": "产品特点5",
  682. }
  683. mapped_fields = []
  684. for amazon_field, source_key in field_map.items():
  685. if amazon_field not in col_indices:
  686. continue
  687. col_idx = col_indices[amazon_field]
  688. source_col_name = source_col_map.get(source_key, source_key)
  689. source_col = source_map.get(source_key, source_col_name)
  690. if source_col and source_col in src_row:
  691. val = src_row[source_col]
  692. if pd.notna(val):
  693. if amazon_field == "Country of Origin":
  694. val = str(val).strip()
  695. if val in ["CHN", "CN"]:
  696. val = "China"
  697. ws.cell(row=row_num, column=col_idx, value=val)
  698. fill_count += 1
  699. mapped_fields.append(f"{amazon_field}={str(val)[:25]}")
  700. if mapped_fields:
  701. filled_summary.extend(mapped_fields[:5])
  702. if len(mapped_fields) > 5:
  703. filled_summary.append(f"...等{len(mapped_fields)}个映射字段")
  704. # AI 字段填充(只填系统未填过的空单元格,避免覆盖)
  705. ai_filled = []
  706. if ai_data:
  707. for ai_field, ai_value in ai_data.items():
  708. if ai_field in col_indices and ai_field not in AI_EXCLUDED_FIELDS:
  709. current = ws.cell(row=row_num, column=col_indices[ai_field]).value
  710. if current is None or str(current).strip() == '':
  711. ws.cell(row=row_num, column=col_indices[ai_field], value=ai_value)
  712. fill_count += 1
  713. ai_filled.append(f"{ai_field}={str(ai_value)[:25]}")
  714. if ai_filled:
  715. filled_summary.append(f"[AI]{', '.join(ai_filled[:3])}")
  716. if len(ai_filled) > 3:
  717. filled_summary.append(f"...等{len(ai_filled)}个AI字段")
  718. image_fields = [
  719. ("Main Image URL", "产品主图"),
  720. ("Other Image URL", "上架图片1"), ("Other Image URL", "上架图片2"),
  721. ("Other Image URL", "上架图片3"), ("Other Image URL", "上架图片4"),
  722. ("Other Image URL", "上架图片5"), ("Other Image URL", "上架图片6"),
  723. ("Other Image URL", "上架图片7"), ("Other Image URL", "上架图片8"),
  724. ]
  725. img_count = 0
  726. for field_name, source_key in image_fields:
  727. if field_name not in col_indices:
  728. continue
  729. col_idx = col_indices[field_name]
  730. source_col = source_col_map.get(source_key, source_key)
  731. if source_col in src_row:
  732. val = src_row[source_col]
  733. if pd.notna(val) and str(val).strip():
  734. ws.cell(row=row_num, column=col_idx, value=val)
  735. fill_count += 1
  736. img_count += 1
  737. if img_count > 0:
  738. filled_summary.append(f"图片URLx{img_count}")
  739. fixed_count = 0
  740. fixed_values = {
  741. "Listing Action": "Create or Replace (Full Update)",
  742. "Package Level": "Unit",
  743. "Package Contains SKU Quantity": 1,
  744. "Number of Items": 1,
  745. "Item Package Quantity": 1,
  746. "Unit Count Type": "pound",
  747. "Item Thickness Decimal Value": 0.6,
  748. "Item Length Unit": "Inches",
  749. "Item Width Unit": "Inches",
  750. "Item Height Unit": "Inches",
  751. "Item Height Unit of Measure": "inch",
  752. "Item Depth Front To Back": "inch",
  753. "Item Package Length Unit": "Inches",
  754. "Item Package Width Unit": "Inches",
  755. "Item Package Height Unit": "Inches",
  756. "Package Weight Unit": "Pounds",
  757. "Item Weight Unit": "Pounds",
  758. "Recommended Number of People for Assembly": 2,
  759. "Has Finished Back": "TRUE",
  760. "Includes All Assembly Tools": "Yes",
  761. "Wood Type": "Character",
  762. "Number of Players": 2,
  763. "Is Fragile?": "No",
  764. "Load Capacity Unit": "Pound",
  765. "Surface Texture": "wood grain",
  766. "Maximum Order Quantity": 1,
  767. "Handling Time (US)": 3,
  768. "Number of Boxes": 1,
  769. "Is This Product Subject To Buyer Age Restrictions": "No",
  770. "Safety Attestation": "Yes",
  771. "Ships Globally": "Yes",
  772. "Are batteries required?": "No",
  773. "Are batteries included?": "No",
  774. "Is OEM Sourced Product": "Yes",
  775. "Is Customizable?": "No",
  776. "Is Foldable": "No",
  777. "Is Stain Resistant": "Yes",
  778. "Tilting": "No",
  779. "Offering Can Be Gift Messaged": "Yes",
  780. "Is Gift Wrap Available": "Yes",
  781. "Accessories": "Generic",
  782. "Packaging": "OEM Original",
  783. "Fulfillment Channel Code (US)": "DEFAULT",
  784. "Inventory Always Available (US)": "Disabled",
  785. "Dangerous Goods Regulations": "Not Applicable",
  786. "Product Id Type": "GTIN Exempt"
  787. }
  788. for field_name, fixed_val in fixed_values.items():
  789. if field_name in col_indices:
  790. ws.cell(row=row_num, column=col_indices[field_name], value=fixed_val)
  791. fill_count += 1
  792. fixed_count += 1
  793. if fixed_count > 0:
  794. filled_summary.append(f"固定值x{fixed_count}")
  795. valid_count = 0
  796. valid_fields = {
  797. "Product Type": "Product Type",
  798. "Brand Name": "Brand Name",
  799. "Item Type Keyword": "Item Type Keyword",
  800. "Warranty Type": "Warranty Type",
  801. "Theme": "Theme",
  802. "Frame Joint Type": "Frame Joint Type",
  803. "Assembly Instructions": "Assembly Instructions",
  804. "Specific Uses for Product": "Specific Uses for Product",
  805. "Room Type": "Room Type",
  806. "Maximum Weight Recommendation Unit": "Maximum Weight Recommendation Unit",
  807. "Leg Style": "Leg Style",
  808. "Indoor Outdoor Usage": "Indoor Outdoor Usage",
  809. "Table Design": "Table Design",
  810. "Furniture Base Movement": "Furniture Base Movement",
  811. "Item Condition": "Item Condition", "Natural Variation Type": "Natural Variation Type",
  812. "Warranty Description": "Warranty Description",
  813. "Special Features": "Special Features",
  814. "Style": "Style"
  815. }
  816. for field_name, valid_key in valid_fields.items():
  817. if field_name in col_indices:
  818. val = get_valid_value(valid_values, valid_key)
  819. if val:
  820. ws.cell(row=row_num, column=col_indices[field_name], value=val)
  821. fill_count += 1
  822. valid_count += 1
  823. if valid_count > 0:
  824. filled_summary.append(f"ValidValuesx{valid_count}")
  825. # ==================== 图片字段处理 ====================
  826. # 定义图片字段和对应的数据源列
  827. image_fields = [
  828. ("Main Image URL", "产品主图"),
  829. ("Other Image URL 1", "上架图片1"), # 注意这里加序号区分
  830. ("Other Image URL 2", "上架图片2"),
  831. ("Other Image URL 3", "上架图片3"),
  832. ("Other Image URL 4", "上架图片4"),
  833. ("Other Image URL 5", "上架图片5"),
  834. ("Other Image URL 6", "上架图片6"),
  835. ("Other Image URL 7", "上架图片7"),
  836. ("Other Image URL 8", "上架图片8"),
  837. ]
  838. img_count = 0
  839. for field_name, source_key in image_fields:
  840. # 检查模板中是否有这个字段
  841. if field_name not in col_indices:
  842. continue
  843. col_idx = col_indices[field_name]
  844. source_col = source_col_map.get(source_key, source_key)
  845. if source_col in src_row:
  846. val = src_row[source_col]
  847. if pd.notna(val) and str(val).strip():
  848. ws.cell(row=row_num, column=col_idx, value=val)
  849. fill_count += 1
  850. img_count += 1
  851. if img_count > 0:
  852. filled_summary.append(f"图片URLx{img_count}")
  853. care_vals = valid_values.get("Care Instructions", ["Wipe with Dry Cloth", "Wipe with Damp Cloth"])
  854. care_count = 0
  855. for i in range(5):
  856. field = f"Care Instructions {i+1}" if i > 0 else "Care Instructions"
  857. if field in col_indices:
  858. if i % 2 == 0:
  859. val = care_vals[0] if care_vals else "Wipe with Dry Cloth"
  860. else:
  861. val = care_vals[1] if len(care_vals) > 1 else "Wipe with Damp Cloth"
  862. ws.cell(row=row_num, column=col_indices[field], value=val)
  863. fill_count += 1
  864. care_count += 1
  865. theme_val = get_valid_value(valid_values, "Theme", "Space")
  866. theme_count = 0
  867. for i in range(5):
  868. field = f"Theme {i+1}" if i > 0 else "Theme"
  869. if field in col_indices:
  870. ws.cell(row=row_num, column=col_indices[field], value=theme_val)
  871. fill_count += 1
  872. theme_count += 1
  873. warranty_val = get_valid_value(valid_values, "Warranty Description", "1 Year Manufacturer")
  874. warranty_count = 0
  875. for i in range(5):
  876. field = f"Warranty Description {i+1}" if i > 0 else "Warranty Description"
  877. if field in col_indices:
  878. ws.cell(row=row_num, column=col_indices[field], value=warranty_val)
  879. fill_count += 1
  880. warranty_count += 1
  881. dgr_val = "Not Applicable"
  882. dgr_count = 0
  883. for i in range(5):
  884. field = f"Dangerous Goods Regulations {i+1}" if i > 0 else "Dangerous Goods Regulations"
  885. if field in col_indices:
  886. ws.cell(row=row_num, column=col_indices[field], value=dgr_val)
  887. fill_count += 1
  888. dgr_count += 1
  889. tools = ["Hammer", "Screw Driver"]
  890. tool_count = 0
  891. if "Tools Recommended For Assembly" in col_indices:
  892. for i, tool in enumerate(tools):
  893. if i == 0:
  894. field = "Tools Recommended For Assembly"
  895. else:
  896. field = f"Tools Recommended For Assembly {i+1}"
  897. if field in col_indices:
  898. ws.cell(row=row_num, column=col_indices[field], value=tool)
  899. fill_count += 1
  900. tool_count += 1
  901. components = ["Installation Tool", "Assembly Guide"]
  902. comp_count = 0
  903. if "Included Components" in col_indices:
  904. for i, comp in enumerate(components):
  905. if i == 0:
  906. field = "Included Components"
  907. else:
  908. field = f"Included Components {i+1}"
  909. if field in col_indices:
  910. ws.cell(row=row_num, column=col_indices[field], value=comp)
  911. fill_count += 1
  912. comp_count += 1
  913. if "Manufacturer" in col_indices:
  914. brand_val = None
  915. if "Brand Name" in col_indices:
  916. brand_val = ws.cell(row=row_num, column=col_indices["Brand Name"]).value
  917. if brand_val:
  918. ws.cell(row=row_num, column=col_indices["Manufacturer"], value=brand_val)
  919. fill_count += 1
  920. filled_summary.append(f"Manufacturer={brand_val}")
  921. if filled_summary:
  922. summary_str = " | ".join(filled_summary)
  923. add_ai_log(f"📋 [模板第{row_num}行] 填写详情({fill_count}个字段): {summary_str}", 'info')
  924. else:
  925. add_ai_log(f"⚠️ [模板第{row_num}行] 未填写任何字段", 'warn')
  926. except Exception as e:
  927. add_ai_log(f"❌ [模板第{row_num}行] 填写异常: {str(e)}", 'error')
  928. print(f"fill_amazon_row error: {e}")
  929. return fill_count
  930. @app.route('/')
  931. def index():
  932. return render_template('index.html')
  933. @app.route('/get_config')
  934. def get_config():
  935. return jsonify({
  936. 'templates': [
  937. {'key': 'wayfair', 'name': 'Wayfair'},
  938. {'key': 'amazon', 'name': '亚马逊'}
  939. ],
  940. 'config': TEMPLATE_CONFIG
  941. })
  942. @app.route('/download_example/<template_key>')
  943. def download_example(template_key):
  944. if template_key == 'wayfair':
  945. filename = 'wayfair模板.xlsx'
  946. elif template_key == 'amazon':
  947. filename = '亚马逊模板.xlsx'
  948. else:
  949. return '无效的模板类型', 400
  950. file_path = os.path.join(EXAMPLE_FOLDER, filename)
  951. if not os.path.exists(file_path):
  952. return f'示例模板文件不存在: {filename},请将模板文件放入 example_templates/ 目录', 404
  953. return send_file(file_path, as_attachment=True, download_name=filename)
  954. @app.route('/get_ai_logs')
  955. def get_ai_logs_api():
  956. return jsonify({'logs': ai_logs[-100:]})
  957. @app.route('/clear_ai_logs')
  958. def clear_ai_logs_api():
  959. clear_ai_logs()
  960. return jsonify({'success': True})
  961. @app.route('/preview', methods=['POST'])
  962. def preview():
  963. try:
  964. data_file = request.files.get('data_file')
  965. template_key = request.form.get('template_key')
  966. if not data_file:
  967. return jsonify({'error': '请选择数据文件'}), 400
  968. if not template_key or template_key not in TEMPLATE_CONFIG:
  969. return jsonify({'error': '请选择有效的模板类型'}), 400
  970. config = TEMPLATE_CONFIG[template_key]
  971. source_header = config['source_header']
  972. timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
  973. data_path = os.path.join(UPLOAD_FOLDER, f'data_{timestamp}_{data_file.filename}')
  974. data_file.save(data_path)
  975. from openpyxl import load_workbook as load_wb_preview
  976. wb_preview = load_wb_preview(data_path, read_only=True, data_only=True)
  977. sheet_names = wb_preview.sheetnames
  978. df = pd.read_excel(data_path, header=source_header - 1)
  979. preview_data = df.head(10).fillna('').to_dict('records')
  980. columns = df.columns.tolist()
  981. add_ai_log(f"📖 预览数据文件: {data_file.filename} | Sheet: '{sheet_names[0]}' | 共 {len(df)} 行 x {len(columns)} 列", 'info')
  982. add_ai_log(f" └─ 列名: {', '.join(columns)}", 'info')
  983. return jsonify({
  984. 'success': True,
  985. 'columns': columns,
  986. 'data': preview_data,
  987. 'total_rows': len(df),
  988. 'data_path': data_path,
  989. 'config': config
  990. })
  991. except Exception as e:
  992. return jsonify({'error': str(e)}), 500
  993. @app.route('/generate', methods=['POST'])
  994. def generate():
  995. try:
  996. data_path = request.form.get('data_path')
  997. template_key = request.form.get('template_key')
  998. use_ai = request.form.get('use_ai', 'false').lower() == 'true'
  999. if not data_path or not os.path.exists(data_path):
  1000. return jsonify({'error': '数据文件不存在'}), 400
  1001. if not template_key or template_key not in TEMPLATE_CONFIG:
  1002. return jsonify({'error': '请选择有效的模板类型'}), 400
  1003. template_file = request.files.get('template_file')
  1004. if not template_file:
  1005. return jsonify({'error': '请上传模板文件'}), 400
  1006. clear_ai_logs()
  1007. add_ai_log(f"🚀 开始生成任务,模板: {template_key},AI: {'启用' if use_ai else '关闭'}", 'info')
  1008. config = TEMPLATE_CONFIG[template_key]
  1009. source_header = config['source_header']
  1010. template_header_row = config['template_header']
  1011. timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
  1012. template_path = os.path.join(UPLOAD_FOLDER, f'template_{timestamp}_{template_file.filename}')
  1013. template_file.save(template_path)
  1014. # 读取数据文件并记录sheet信息
  1015. add_ai_log(f"📖 读取数据文件: {os.path.basename(data_path)}", 'info')
  1016. from openpyxl import load_workbook as load_wb_data
  1017. wb_data = load_wb_data(data_path, read_only=True, data_only=True)
  1018. data_sheet_names = wb_data.sheetnames
  1019. add_ai_log(f"📋 数据文件包含 Sheets: {', '.join(data_sheet_names)}", 'info')
  1020. add_ai_log(f" └─ 使用 Sheet: '{data_sheet_names[0]}' (表头第{source_header}行)", 'info')
  1021. df = pd.read_excel(data_path, header=source_header - 1)
  1022. print("【实际列名】", df.columns.tolist())
  1023. total_rows = len(df)
  1024. data_columns = df.columns.tolist()
  1025. add_ai_log(f"📊 数据文件读取完成: 共 {total_rows} 行 x {len(data_columns)} 列", 'info')
  1026. add_ai_log(f" └─ 列名: {', '.join(data_columns)}", 'info')
  1027. # 加载模板
  1028. add_ai_log(f"📖 加载模板文件: {os.path.basename(template_path)}", 'info')
  1029. wb = load_workbook(template_path)
  1030. ws = wb.active
  1031. add_ai_log(f"📋 模板活动Sheet名称: '{ws.title}'", 'info')
  1032. add_ai_log(f" └─ 模板尺寸: {ws.max_row} 行 x {ws.max_column} 列", 'info')
  1033. valid_values = load_valid_values(wb)
  1034. add_ai_log(f"📋 Valid Values Sheet 分析完成,共 {len(valid_values)} 个字段", 'info')
  1035. if valid_values:
  1036. sample_keys = list(valid_values.keys())
  1037. add_ai_log(f" └─ 字段: " + ', '.join(sample_keys), 'info')
  1038. # 读取模板表头
  1039. col_indices = {}
  1040. for col in range(1, ws.max_column + 1):
  1041. cell_value = ws.cell(row=template_header_row, column=col).value
  1042. if cell_value:
  1043. col_name = str(cell_value).strip()
  1044. if col_name and not col_name.startswith("Unnamed"):
  1045. col_indices[col_name] = col
  1046. # ==================== 计算系统已处理字段 & AI候选字段 ====================
  1047. system_fields = set()
  1048. if template_key == 'amazon':
  1049. # Amazon 固定值
  1050. system_fields.update([
  1051. "Listing Action", "Package Level", "Package Contains SKU Quantity",
  1052. "Number of Items", "Item Package Quantity", "Unit Count Type", "Item Thickness Decimal Value",
  1053. "Item Length Unit", "Item Width Unit", "Item Height Unit",
  1054. "Item Package Length Unit", "Item Package Width Unit", "Item Package Height Unit",
  1055. "Package Weight Unit", "Item Weight Unit", "Recommended Number of People for Assembly",
  1056. "Has Finished Back", "Includes All Assembly Tools", "Wood Type", "Number of Players",
  1057. "Is Fragile?", "Load Capacity Unit", "Surface Texture", "Maximum Order Quantity",
  1058. "Handling Time (US)", "Number of Boxes", "Is This Product Subject To Buyer Age Restrictions",
  1059. "Safety Attestation", "Ships Globally", "Are batteries required?", "Are batteries included?",
  1060. "Is OEM Sourced Product", "Is Customizable?", "Is Foldable", "Is Stain Resistant",
  1061. "Tilting", "Offering Can Be Gift Messaged", "Is Gift Wrap Available", "Accessories",
  1062. "Packaging", "Fulfillment Channel Code (US)", "Inventory Always Available (US)",
  1063. ])
  1064. # Amazon 映射字段
  1065. system_fields.update([
  1066. "Color", "Base Color", "Top Color", "Frame Material", "Base Material", "Top Material",
  1067. "Furniture Leg Material", "Upholstery Fabric Type", "Item Length", "Item Width", "Item Height",
  1068. "Item Weight", "Item Package Length", "Item Package Width", "Item Package Height",
  1069. "Package Weight", "List Price", "Your Price USD", "Unit Count", "Item Length Longer Edge",
  1070. "Item Width Shorter Edge", "Length longer edge when assembled", "Width shorter edge when assembled",
  1071. "Height base to top when assembled", "Item Depth Front To Back", "Item Height Floor To Top",
  1072. "Item Width Side To Side", "Working Surface Length", "Working Surface Width", "Country of Origin", "Manufacturer",
  1073. ])
  1074. # Amazon Valid Values / 特殊字段
  1075. system_fields.update([
  1076. "Product Type", "Listing Action", "Brand Name", "Product Id Type", "Item Type Keyword",
  1077. "Package Level", "Warranty Type", "Theme", "Frame Joint Type", "Assembly Instructions",
  1078. "Specific Uses for Product", "Room Type", "Maximum Weight Recommendation Unit", "Leg Style",
  1079. "Indoor Outdoor Usage", "Table Design", "Furniture Base Movement", "Item Condition",
  1080. "Natural Variation Type", "Includes All Assembly Tools", "Dangerous Goods Regulations",
  1081. "Warranty Description", "Care Instructions", "Care Instructions 2", "Care Instructions 3",
  1082. "Care Instructions 4", "Care Instructions 5", "Theme 2", "Theme 3", "Theme 4", "Theme 5",
  1083. "Warranty Description 2", "Warranty Description 3", "Warranty Description 4", "Warranty Description 5",
  1084. "Dangerous Goods Regulations 2", "Dangerous Goods Regulations 3", "Dangerous Goods Regulations 4",
  1085. "Dangerous Goods Regulations 5", "Tools Recommended For Assembly", "Tools Recommended For Assembly 2",
  1086. "Included Components", "Included Components 2",
  1087. ])
  1088. # Amazon 图片 + Parentage + SKU相关
  1089. system_fields.update([
  1090. "Main Image URL", "Other Image URL", "Other Image URL 2", "Other Image URL 3",
  1091. "Other Image URL 4", "Other Image URL 5", "Other Image URL 6", "Other Image URL 7", "Other Image URL 8",
  1092. "Parentage Level", "Parent SKU", "Variation Theme Name",
  1093. "SKU", "Model Number", "Model Name", "Part Number", "Set Name"
  1094. ])
  1095. elif template_key == 'wayfair':
  1096. # Wayfair 固定值
  1097. system_fields.update([
  1098. "Minimum Order Quantity", "Force Quantity Multiplier", "Display Set Quantity",
  1099. "Lead Time", "Replacement Lead Time", "Flat Pack", "Assembly Required",
  1100. "Canada Product Restriction", "CARB Phase II Compliant (formaldehyde emissions)",
  1101. "Commercial Warranty", "CANFER Compliant", "Commercial Warranty Length",
  1102. "Composite Wood Product (CWP)", "Country Of Manufacturer", "General Certificate of Conformity (GCC)",
  1103. "Hazard Class(es)", "Hazardous Material / Dangerous Goods", "Hazardous Material Weight",
  1104. "Level of Assembly", "Packing Group", "Reason for Restriction",
  1105. "Supplier Intended and Approved Use", "TSCA Title VI Compliant (formaldehyde emissions)",
  1106. "UN or ID number", "Uniform Packaging and Labeling Regulations (UPLR) Compliant",
  1107. "Warning Required", "Warranty Length", "Battery or Batteries Included",
  1108. ])
  1109. # Wayfair 映射字段
  1110. system_fields.update([
  1111. "Color", "Base Color", "Top Color", "Frame Material", "Base Material", "Top Material",
  1112. "Furniture Leg Material", "Upholstery Fabric Type", "Material", "Supplier Part Number",
  1113. "Manufacturer Part Number", "Base Cost", "Manufacturer Suggested Retail Price",
  1114. "Item Length", "Item Width", "Item Height", "Item Weight", "Product Weight",
  1115. "Overall Product Weight", "Overall Width - Side to Side", "Overall Depth - Front to Back",
  1116. "Overall Height - Top to Bottom", "Item Length Longer Edge", "Item Width Shorter Edge",
  1117. "Length longer edge when assembled", "Width shorter edge when assembled",
  1118. "Height base to top when assembled", "Item Depth Front To Back", "Item Height Floor To Top",
  1119. "Item Width Side To Side", "Working Surface Length", "Working Surface Width",
  1120. "Item Package Length", "Item Package Width", "Item Package Height", "Package Weight",
  1121. "Carton Weight 1", "Carton Height 1", "Carton Width 1", "Carton Depth 1",
  1122. "Unit Count", "List Price", "Your Price", "Country of Origin",
  1123. ])
  1124. # Wayfair Valid Values / 特殊字段
  1125. system_fields.update([
  1126. "Brand", "Variant Type", "Variant Grouping 1", "Variant Grouping 2",
  1127. "Variant Attribute Name On Site 1", "Variant Attribute Name On Site 2",
  1128. "Ship Type", "ISTA Certified", "Product Type",
  1129. ])
  1130. # Wayfair 图片
  1131. system_fields.update([
  1132. "Main Image URL", "Other Image URL", "Other Image URL 2", "Other Image URL 3",
  1133. "Other Image URL 4", "Other Image URL 5", "Other Image URL 6", "Other Image URL 7", "Other Image URL 8",
  1134. ])
  1135. all_template_fields = set(col_indices.keys())
  1136. ai_candidate_fields = sorted(list(all_template_fields - system_fields - AI_EXCLUDED_FIELDS))
  1137. add_ai_log(f"🤖 AI候选字段分析: 模板共 {len(all_template_fields)} 个字段", 'info')
  1138. add_ai_log(f" ├─ 系统已处理: {len(system_fields)} 个", 'info')
  1139. add_ai_log(f" ├─ 排除字段: {len(AI_EXCLUDED_FIELDS)} 个", 'info')
  1140. add_ai_log(f" └─ 交给AI填写: {len(ai_candidate_fields)} 个", 'info')
  1141. if ai_candidate_fields:
  1142. sample = ai_candidate_fields[:15]
  1143. add_ai_log(f" └─ AI字段示例: {', '.join(sample)}{'...' if len(ai_candidate_fields)>15 else ''}", 'info')
  1144. # ==================== 结束 ====================
  1145. # AI处理
  1146. ai_results = {}
  1147. if use_ai and '产品主图' in df.columns and ai_candidate_fields:
  1148. add_ai_log(f"🤖 开始AI识别,共 {total_rows} 行需要处理", 'info')
  1149. has_image_count = 0
  1150. for idx, row in df.iterrows():
  1151. img_url = row.get('产品主图', '')
  1152. if pd.notna(img_url) and str(img_url).strip():
  1153. has_image_count += 1
  1154. add_ai_log(f"📷 其中 {has_image_count} 行包含产品主图", 'info')
  1155. processed = 0
  1156. success_count = 0
  1157. for idx, row in df.iterrows():
  1158. img_url = row.get('产品主图', '')
  1159. if pd.notna(img_url) and str(img_url).strip():
  1160. processed += 1
  1161. add_ai_log(f"🔄 [第{idx+1}/{total_rows}行] 处理中 ({processed}/{has_image_count})...", 'info')
  1162. ai_data = get_ai_filled_data(str(img_url).strip(), idx + 1, ai_candidate_fields)
  1163. if ai_data:
  1164. ai_results[idx] = ai_data
  1165. success_count += 1
  1166. add_ai_log(f"✅ [第{idx+1}行] AI识别完成,提取 {len(ai_data)} 个字段", 'success')
  1167. if processed % 5 == 0:
  1168. add_ai_log(f"📊 进度: {processed}/{has_image_count} 行已处理,成功 {success_count} 行", 'info')
  1169. add_ai_log(f"🎉 AI处理完成!共处理 {processed} 行,成功 {success_count} 行", 'success')
  1170. elif use_ai and '产品主图' not in df.columns:
  1171. add_ai_log(f"⚠️ 数据中没有 '产品主图' 列,AI功能将跳过", 'warn')
  1172. elif use_ai and not ai_candidate_fields:
  1173. add_ai_log(f"⏭️ 没有需要AI填写的候选字段,AI功能将跳过", 'info')
  1174. else:
  1175. add_ai_log(f"⏭️ AI功能未启用", 'info')
  1176. add_ai_log(f"📋 模板表头(第{template_header_row}行)共解析出 {len(col_indices)} 个有效列", 'info')
  1177. sample_cols = list(col_indices.keys())
  1178. add_ai_log(f" └─ 列名: " + ', '.join(sample_cols), 'info')
  1179. # 找到已有数据的最后一行
  1180. start_row = 2
  1181. last_row = start_row
  1182. for row in range(start_row, ws.max_row + 2):
  1183. is_empty = True
  1184. for col in range(1, min(10, ws.max_column + 1)):
  1185. if ws.cell(row=row, column=col).value is not None:
  1186. is_empty = False
  1187. break
  1188. if is_empty:
  1189. last_row = row
  1190. break
  1191. else:
  1192. last_row = ws.max_row + 1
  1193. if last_row > start_row:
  1194. add_ai_log(f"📌 扫描模板已有数据: 第 {start_row} 行到第 {last_row-1} 行存在数据", 'info')
  1195. else:
  1196. add_ai_log(f"📌 模板暂无数据,从第 {start_row} 行开始填充", 'info')
  1197. add_ai_log(f"📌 将从第 {last_row} 行开始追加新数据", 'info')
  1198. # 匹配来源列
  1199. source_col_map = {
  1200. "颜色": "颜色",
  1201. "材质": "材质",
  1202. "组装长度": "组装长度(英寸)",
  1203. "组装宽度": "组装宽度(英寸)",
  1204. "组装高度": "组装高度(英寸)",
  1205. "产品重量": "产品重量(磅)",
  1206. "包装尺寸-长度": "包装尺寸-长度(英寸)",
  1207. "包装尺寸-宽度": "包装尺寸-宽度(英寸)",
  1208. "包装尺寸-高度": "包装尺寸-高度(英寸)",
  1209. "包装尺寸-重量": "包装尺寸-重量(磅)",
  1210. "优惠单价": "优惠单价",
  1211. "产品名称": "产品名称",
  1212. "产品英文名称": "产品英文名称",
  1213. "Item Code": "Item Code",
  1214. "店铺名称": "店铺名称",
  1215. "原产地": "原产地",
  1216. "产品主图": "产品主图",
  1217. "上架图片1": "上架图片1",
  1218. "上架图片2": "上架图片2",
  1219. "上架图片3": "上架图片3",
  1220. "上架图片4": "上架图片4",
  1221. "上架图片5": "上架图片5",
  1222. "上架图片6": "上架图片6",
  1223. "上架图片7": "上架图片7",
  1224. "上架图片8": "上架图片8",
  1225. "产品描述": "产品描述",
  1226. "产品特点1": "产品特点1",
  1227. "产品特点2": "产品特点2",
  1228. "产品特点3": "产品特点3",
  1229. "产品特点4": "产品特点4",
  1230. "产品特点5": "产品特点5",
  1231. }
  1232. print("【期望列名】", list(source_col_map.values()))
  1233. source_map = {}
  1234. matched_count = 0
  1235. for key, expected in source_col_map.items():
  1236. found = False
  1237. for col in df.columns:
  1238. if col == expected:
  1239. source_map[key] = col
  1240. found = True
  1241. matched_count += 1
  1242. break
  1243. if not found:
  1244. source_map[key] = None
  1245. add_ai_log(f"🔗 来源列匹配结果: {matched_count}/{len(source_col_map)} 列匹配成功", 'info')
  1246. matched_cols = [k for k, v in source_map.items() if v is not None]
  1247. if matched_cols:
  1248. add_ai_log(f" ✅ 匹配成功:" + ', '.join(matched_cols), 'info')
  1249. unmatched_cols = [k for k, v in source_map.items() if v is None]
  1250. if unmatched_cols:
  1251. add_ai_log(f" ⚠️ 未匹配: " + ', '.join(unmatched_cols), 'warn')
  1252. # 填充数据
  1253. def get_key(row):
  1254. item = row.get("Item Code", "")
  1255. name = row.get("产品英文名称", "")
  1256. if pd.isna(name) or not name:
  1257. name = row.get("产品名称", "")
  1258. return f"{item}_{name}"
  1259. keys = [get_key(row) for _, row in df.iterrows()]
  1260. key_count = Counter(keys)
  1261. key_index = Counter()
  1262. row_num = last_row
  1263. filled_count = 0
  1264. add_ai_log(f"✍️ 开始填充数据,共 {total_rows} 行,目标行号从 {last_row} 开始", 'info')
  1265. for idx, (_, src_row) in enumerate(df.iterrows()):
  1266. key = get_key(src_row)
  1267. if key_count[key] > 1:
  1268. key_index[key] += 1
  1269. suffix = f".{key_index[key] - 1}"
  1270. mfr_suffix = f".{key_index[key]}"
  1271. else:
  1272. suffix = ""
  1273. mfr_suffix = ""
  1274. is_parent = (idx == 0) or (key_index[key] == 1)
  1275. item_code = src_row.get("Item Code", "")
  1276. product_name = src_row.get("产品英文名称", "")
  1277. if pd.isna(product_name) or not product_name:
  1278. product_name = src_row.get("产品名称", "")
  1279. if pd.isna(item_code): item_code = ""
  1280. if pd.isna(product_name): product_name = ""
  1281. add_ai_log(f"✍️ [源数据第{idx+1}行] ItemCode={item_code} | 产品={product_name} → 开始填入模板第{row_num}行", 'info')
  1282. if "Parentage Level" in col_indices:
  1283. if not is_parent:
  1284. ws.cell(row=row_num, column=col_indices["Parentage Level"], value="Child")
  1285. add_ai_log(f" └─ Parentage Level = Child", 'info')
  1286. if "Parent SKU" in col_indices:
  1287. if not is_parent:
  1288. first_item = df.iloc[0].get("Item Code", "")
  1289. first_name = df.iloc[0].get("产品英文名称", "")
  1290. if pd.isna(first_name) or not first_name:
  1291. first_name = df.iloc[0].get("产品名称", "")
  1292. first_sku = f"{first_item}{first_name}"
  1293. ws.cell(row=row_num, column=col_indices["Parent SKU"], value=first_sku)
  1294. add_ai_log(f" └─ Parent SKU = {first_sku}", 'info')
  1295. if "Variation Theme Name" in col_indices:
  1296. if key_count[key] > 1:
  1297. ws.cell(row=row_num, column=col_indices["Variation Theme Name"], value="COLOR")
  1298. add_ai_log(f" └─ Variation Theme Name = COLOR", 'info')
  1299. ai_data = ai_results.get(idx, {}) if use_ai else {}
  1300. if template_key == 'amazon':
  1301. fill_count = fill_amazon_row(
  1302. src_row, source_map, col_indices, valid_values,
  1303. row_num, ws, suffix, mfr_suffix, ai_data, idx
  1304. )
  1305. elif template_key == 'wayfair':
  1306. fill_count = fill_wayfair_row(
  1307. src_row, source_map, col_indices, valid_values,
  1308. row_num, ws, suffix, mfr_suffix, ai_data, idx
  1309. )
  1310. else:
  1311. fill_count = 0
  1312. add_ai_log(f"✅ [源数据第{idx+1}行] 完成 → 模板第{row_num}行,共填写 {fill_count} 个单元格", 'success')
  1313. row_num += 1
  1314. filled_count += 1
  1315. if (idx + 1) % 10 == 0:
  1316. add_ai_log(f"📊 填充进度: {idx+1}/{total_rows} 行 (当前模板行: {row_num-1})", 'info')
  1317. add_ai_log(f"✅ 数据填充完成,共填充 {filled_count} 行,模板行号范围: {last_row} - {row_num-1}", 'success')
  1318. # 保存
  1319. output_filename = f"output_{template_key}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
  1320. output_path = os.path.join(OUTPUT_FOLDER, output_filename)
  1321. wb.save(output_path)
  1322. add_ai_log(f"💾 文件保存成功: {output_filename}", 'success')
  1323. add_ai_log(f" └─ 保存路径: {output_path}", 'info')
  1324. # 清理临时文件
  1325. # try:
  1326. # os.remove(data_path)
  1327. # os.remove(template_path)
  1328. # add_ai_log(f"🗑️ 临时文件已清理", 'info')
  1329. # except Exception as e:
  1330. # add_ai_log(f"⚠️ 清理临时文件失败: {str(e)}", 'warn')
  1331. ai_info = f",AI识别 {len(ai_results)} 行" if use_ai and ai_results else ""
  1332. return jsonify({
  1333. 'success': True,
  1334. 'filled_count': filled_count,
  1335. 'download_url': f'/download/{output_filename}',
  1336. 'message': f'成功追加 {filled_count} 行数据到 {config["name"]} 模板{ai_info}',
  1337. 'ai_used': use_ai,
  1338. 'ai_rows': len(ai_results) if ai_results else 0,
  1339. 'logs': ai_logs[-100:]
  1340. })
  1341. except Exception as e:
  1342. import traceback
  1343. error_detail = traceback.format_exc()
  1344. add_ai_log(f"❌ 生成失败: {str(e)}", 'error')
  1345. print(error_detail)
  1346. return jsonify({'error': str(e), 'detail': error_detail}), 500
  1347. @app.route('/download/<filename>')
  1348. def download(filename):
  1349. file_path = os.path.join(OUTPUT_FOLDER, filename)
  1350. if not os.path.exists(file_path):
  1351. return '文件不存在', 404
  1352. return send_file(file_path, as_attachment=True, download_name=filename)
  1353. if __name__ == '__main__':
  1354. app.run(host='0.0.0.0', port=5002, debug=True)