test2.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. import requests
  2. import json
  3. import datetime
  4. import hashlib
  5. import hmac
  6. import uuid
  7. import base64
  8. from urllib.parse import quote, urlparse, parse_qsl
  9. from io import BytesIO
  10. # ==================== 配置(全部填好,直接运行) ====================
  11. CLIENT_ID = "amzn1.application-oa2-client.9f775d35077a4c7598116e5a89b60222"
  12. CLIENT_SECRET = "amzn1.oa2-cs.v1.6909777baa82b796555a431cd23709342e8e01434abcad1f8b59cadf048b51c3"
  13. REFRESH_TOKEN = "Atzr|IwEBICh_fojh7qAfrePzSjOZhQxTnHRZCYOuWNvSAHGaBs1WfkjR4qjC1Dmv0uK3oyq_L-R9SRKK4cYmPwF1MQXQdqKlpzOAGItWeXnNzh-SyhHHISv0_vCt3D6hluVmIEmF2kA7ro_XF-PslD41g3Eq6bv0Qq4y4pQbkDj-9dudDuz7nku8UL-nc4EWgJCrHCJTAs8TK48Yvrfi3E35uv97tYhs6qb8D9d4dR9C7daaC49eoDqP88qyUHThQV1Kqon8xukx4AT2_wLqWNcabpADzuK-5w7D-vvFFtvzfkbmtfzoPeEdHPulG6scc59BPFTyBlGU7a3d1JSFg2ROHIlrljRm"
  14. AWS_ACCESS_KEY = "AKIA24CLQSVTW2K4S6RP"
  15. AWS_SECRET_KEY = "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"
  16. AWS_REGION = "us-east-1"
  17. SELLER_ID = "A1H50Z0WYYLEGQ"
  18. SKU = "bar table pink"
  19. MARKETPLACE_ID = "ATVPDKIKX0DER"
  20. ASIN = "B0FR1WRK4M"
  21. CONTENT_KEY = f"amzn1.aplus.content.v1.{uuid.uuid4()}"
  22. IMAGE_URL = "https://cunchu.site/temp/8c7a90e1-a5a1-41af-a119-41b1ca74b40d.png"
  23. # ==================== 签名工具(复用你之前的) ====================
  24. def sha256_hash(data):
  25. return hashlib.sha256(data.encode("utf-8")).hexdigest()
  26. def hmac_sha256(key, msg):
  27. return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
  28. def get_signature_key(secret_key, date_stamp, region, service):
  29. k_date = hmac_sha256(("AWS4" + secret_key).encode("utf-8"), date_stamp)
  30. k_region = hmac_sha256(k_date, region)
  31. k_service = hmac_sha256(k_region, service)
  32. return hmac_sha256(k_service, "aws4_request")
  33. def get_access_token():
  34. url = "https://api.amazon.com/auth/o2/token"
  35. resp = requests.post(url, data={
  36. "grant_type": "refresh_token",
  37. "refresh_token": REFRESH_TOKEN,
  38. "client_id": CLIENT_ID,
  39. "client_secret": CLIENT_SECRET,
  40. })
  41. resp.raise_for_status()
  42. return resp.json()["access_token"]
  43. def spapi_request(method, path, query, body=None, access_token=None):
  44. service = "execute-api"
  45. host = "sellingpartnerapi-na.amazon.com"
  46. payload = json.dumps(body, separators=(",", ":")) if body else ""
  47. t = datetime.datetime.now(datetime.timezone.utc)
  48. amz_date = t.strftime("%Y%m%dT%H%M%SZ")
  49. date_stamp = t.strftime("%Y%m%d")
  50. canonical_headers = (
  51. f"host:{host}\n"
  52. f"x-amz-access-token:{access_token}\n"
  53. f"x-amz-date:{amz_date}\n"
  54. )
  55. signed_headers = "host;x-amz-access-token;x-amz-date"
  56. payload_hash = sha256_hash(payload)
  57. canonical_request = (
  58. f"{method}\n{path}\n{query}\n"
  59. f"{canonical_headers}\n{signed_headers}\n{payload_hash}"
  60. )
  61. algorithm = "AWS4-HMAC-SHA256"
  62. credential_scope = f"{date_stamp}/{AWS_REGION}/{service}/aws4_request"
  63. string_to_sign = (
  64. f"{algorithm}\n{amz_date}\n{credential_scope}\n"
  65. f"{sha256_hash(canonical_request)}"
  66. )
  67. signing_key = get_signature_key(AWS_SECRET_KEY, date_stamp, AWS_REGION, service)
  68. signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
  69. auth_header = (
  70. f"{algorithm} Credential={AWS_ACCESS_KEY}/{credential_scope}, "
  71. f"SignedHeaders={signed_headers}, Signature={signature}"
  72. )
  73. headers = {
  74. "Host": host,
  75. "x-amz-access-token": access_token,
  76. "x-amz-date": amz_date,
  77. "Authorization": auth_header,
  78. }
  79. if body:
  80. headers["Content-Type"] = "application/json"
  81. url = f"https://{host}{path}?{query}"
  82. return requests.request(method, url, headers=headers, data=payload or None)
  83. # ==================== Step 1: 下载图片并计算 MD5 ====================
  84. def download_and_hash_image(image_url):
  85. print(f"⬇️ 下载图片: {image_url}")
  86. resp = requests.get(image_url, timeout=30)
  87. resp.raise_for_status()
  88. image_binary = resp.content
  89. print(f" 图片大小: {len(image_binary)} bytes")
  90. md5_hash = hashlib.md5(image_binary).digest()
  91. content_md5 = base64.b64encode(md5_hash).decode('utf-8')
  92. print(f" Content-MD5: {content_md5}")
  93. width, height = 1940, 1200
  94. try:
  95. from PIL import Image
  96. img = Image.open(BytesIO(image_binary))
  97. width, height = img.size
  98. print(f" 图片尺寸: {width}x{height}")
  99. except ImportError:
  100. print(f" 未安装 Pillow,使用默认尺寸: {width}x{height}")
  101. return image_binary, content_md5, width, height
  102. # ==================== Step 2: 创建上传目标(官方修正版) ====================
  103. def create_upload_destination(access_token, content_md5, content_type):
  104. """
  105. 官方修正:resource 固定为 aplus/2020-11-01/contentDocuments
  106. 不需要 contentDocumentId,不需要图片文件名!
  107. """
  108. resource = "aplus/2020-11-01/contentDocuments" # ← 关键修正:固定字符串
  109. encoded_resource = quote(resource, safe="/")
  110. path = f"/uploads/2020-11-01/uploadDestinations/{encoded_resource}"
  111. query = f"marketplaceIds={MARKETPLACE_ID}&contentMD5={quote(content_md5, safe='')}&contentType={quote(content_type, safe='')}"
  112. print(f"\n📝 创建上传目标...")
  113. print(f" Resource: {resource}")
  114. print(f" Content-MD5: {content_md5}")
  115. print(f" Content-Type: {content_type}")
  116. resp = spapi_request("POST", path, query, access_token=access_token)
  117. print(f"📤 状态码: {resp.status_code}")
  118. print(f"📄 响应: {resp.text}")
  119. if resp.status_code != 200:
  120. return None, None
  121. data = resp.json()
  122. upload_destination_id = data.get("uploadDestinationId")
  123. url = data.get("url")
  124. print(f"✅ uploadDestinationId: {upload_destination_id}")
  125. print(f"✅ upload URL: {url[:120]}...")
  126. return upload_destination_id, url
  127. # ==================== Step 3: Multipart Form POST 上传图片到 S3 ====================
  128. def upload_image_to_s3(upload_url, image_binary, content_type):
  129. print(f"\n⬆️ 解析预签名 URL 并上传图片...")
  130. parsed = urlparse(upload_url)
  131. base_url = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
  132. query_params = dict(parse_qsl(parsed.query))
  133. print(f" Base URL: {base_url}")
  134. print(f" Form fields: {list(query_params.keys())}")
  135. files = {
  136. "File": ("image.png", image_binary, content_type)
  137. }
  138. resp = requests.post(base_url, data=query_params, files=files, timeout=60)
  139. print(f"📤 S3 上传状态码: {resp.status_code}")
  140. if resp.text:
  141. print(f"📄 S3 响应: {resp.text[:500]}")
  142. return resp.status_code in [200, 201, 204]
  143. # ==================== Step 4: 创建 A+ Content 文档 ====================
  144. def create_content_document(access_token, content_key, upload_id, img_width, img_height):
  145. path = "/aplus/2020-11-01/contentDocuments"
  146. query = f"marketplaceIds={MARKETPLACE_ID}"
  147. body = {
  148. "contentDocument": {
  149. "name": "Product Detail Image",
  150. "contentReference": {
  151. "key": content_key,
  152. "version": "0"
  153. },
  154. "contentModuleList": [
  155. {
  156. "moduleType": "STANDARD_SINGLE_IMAGE",
  157. "standardSingleImage": {
  158. "image": {
  159. "uploadDestinationId": upload_id,
  160. "imageCropSpecification": {
  161. "size": {
  162. "width": {"value": img_width, "units": "pixels"},
  163. "height": {"value": img_height, "units": "pixels"}
  164. },
  165. "offset": {
  166. "x": {"value": 0, "units": "pixels"},
  167. "y": {"value": 0, "units": "pixels"}
  168. }
  169. },
  170. "altText": "Product detail image"
  171. }
  172. }
  173. }
  174. ]
  175. }
  176. }
  177. print(f"\n📝 创建 A+ Content 文档...")
  178. print(f" Content Key: {content_key}")
  179. resp = spapi_request("POST", path, query, body=body, access_token=access_token)
  180. print(f"📤 状态码: {resp.status_code}")
  181. print(resp.text)
  182. return resp
  183. # ==================== Step 5: 绑定 ASIN ====================
  184. def associate_asin(access_token, content_key, asin):
  185. path = f"/aplus/2020-11-01/contentDocuments/{content_key}/asins"
  186. query = f"marketplaceIds={MARKETPLACE_ID}"
  187. body = {"asinSet": [asin]}
  188. print(f"\n🔗 绑定 ASIN: {asin}")
  189. resp = spapi_request("POST", path, query, body=body, access_token=access_token)
  190. print(f"📤 状态码: {resp.status_code}")
  191. print(resp.text)
  192. return resp
  193. # ==================== Step 6: 提交审核 ====================
  194. def submit_approval(access_token, content_key):
  195. path = f"/aplus/2020-11-01/contentDocuments/{content_key}/approvalSubmissions"
  196. query = f"marketplaceIds={MARKETPLACE_ID}"
  197. print(f"\n📋 提交审核...")
  198. resp = spapi_request("POST", path, query, access_token=access_token)
  199. print(f"📤 状态码: {resp.status_code}")
  200. print(resp.text)
  201. return resp
  202. # ==================== 主程序 ====================
  203. if __name__ == "__main__":
  204. print("🔑 获取 Access Token...")
  205. token = get_access_token()
  206. print("✅ 令牌获取成功")
  207. print(f"\n🆔 Content Key: {CONTENT_KEY}")
  208. print(f"🖼️ 图片URL: {IMAGE_URL}")
  209. print(f"🔗 ASIN: {ASIN}")
  210. # Step 1
  211. print("\n=== Step 1: 下载图片 ===")
  212. image_binary, content_md5, img_width, img_height = download_and_hash_image(IMAGE_URL)
  213. # Step 2(官方修正版)
  214. print("\n=== Step 2: 创建上传目标 ===")
  215. upload_id, upload_url = create_upload_destination(token, content_md5, "image/png")
  216. if not upload_id or not upload_url:
  217. print("\n⛔ 创建上传目标失败,终止")
  218. exit(1)
  219. # Step 3
  220. print("\n=== Step 3: 上传图片到 S3 ===")
  221. success = upload_image_to_s3(upload_url, image_binary, "image/png")
  222. if not success:
  223. print("\n⛔ 图片上传失败,终止")
  224. exit(1)
  225. print("✅ 图片上传成功")
  226. # Step 4
  227. print("\n=== Step 4: 创建内容文档 ===")
  228. create_content_document(token, CONTENT_KEY, upload_id, img_width, img_height)
  229. # Step 5
  230. print("\n=== Step 5: 绑定ASIN ===")
  231. associate_asin(token, CONTENT_KEY, ASIN)
  232. # Step 6
  233. print("\n=== Step 6: 提交审核 ===")
  234. submit_approval(token, CONTENT_KEY)
  235. print("\n🎉 全部步骤执行完毕!")
  236. print(" A+ 内容已提交审核,通常需要 1-7 个工作日")
  237. print(f" Content Key: {CONTENT_KEY}")
  238. print(" 请保存此 Key 以便后续查询状态")