test2.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  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. MARKETPLACE_ID = "ATVPDKIKX0DER"
  18. ASIN = "B0FR1WRK4M"
  19. IMAGE_URL = "https://cunchu.site/temp/8c7a90e1-a5a1-41af-a119-41b1ca74b40d.png"
  20. TARGET_WIDTH = 970
  21. TARGET_HEIGHT = 600
  22. # ==================== 签名工具 ====================
  23. def sha256_hash(data):
  24. return hashlib.sha256(data.encode("utf-8")).hexdigest()
  25. def hmac_sha256(key, msg):
  26. return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
  27. def get_signature_key(secret_key, date_stamp, region, service):
  28. k_date = hmac_sha256(("AWS4" + secret_key).encode("utf-8"), date_stamp)
  29. k_region = hmac_sha256(k_date, region)
  30. k_service = hmac_sha256(k_region, service)
  31. return hmac_sha256(k_service, "aws4_request")
  32. def get_access_token():
  33. resp = requests.post("https://api.amazon.com/auth/o2/token", data={
  34. "grant_type": "refresh_token",
  35. "refresh_token": REFRESH_TOKEN,
  36. "client_id": CLIENT_ID,
  37. "client_secret": CLIENT_SECRET,
  38. })
  39. resp.raise_for_status()
  40. return resp.json()["access_token"]
  41. def spapi_request(method, path, query, body=None, access_token=None):
  42. service = "execute-api"
  43. host = "sellingpartnerapi-na.amazon.com"
  44. payload = json.dumps(body, separators=(",", ":")) if body else ""
  45. t = datetime.datetime.now(datetime.timezone.utc)
  46. amz_date = t.strftime("%Y%m%dT%H%M%SZ")
  47. date_stamp = t.strftime("%Y%m%d")
  48. canonical_headers = (
  49. f"host:{host}\n"
  50. f"x-amz-access-token:{access_token}\n"
  51. f"x-amz-date:{amz_date}\n"
  52. )
  53. signed_headers = "host;x-amz-access-token;x-amz-date"
  54. payload_hash = sha256_hash(payload)
  55. canonical_request = (
  56. f"{method}\n{path}\n{query}\n"
  57. f"{canonical_headers}\n{signed_headers}\n{payload_hash}"
  58. )
  59. algorithm = "AWS4-HMAC-SHA256"
  60. credential_scope = f"{date_stamp}/{AWS_REGION}/{service}/aws4_request"
  61. string_to_sign = (
  62. f"{algorithm}\n{amz_date}\n{credential_scope}\n"
  63. f"{sha256_hash(canonical_request)}"
  64. )
  65. signing_key = get_signature_key(AWS_SECRET_KEY, date_stamp, AWS_REGION, service)
  66. signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
  67. auth_header = (
  68. f"{algorithm} Credential={AWS_ACCESS_KEY}/{credential_scope}, "
  69. f"SignedHeaders={signed_headers}, Signature={signature}"
  70. )
  71. headers = {
  72. "Host": host,
  73. "x-amz-access-token": access_token,
  74. "x-amz-date": amz_date,
  75. "Authorization": auth_header,
  76. }
  77. if body:
  78. headers["Content-Type"] = "application/json"
  79. url = f"https://{host}{path}?{query}"
  80. return requests.request(method, url, headers=headers, data=payload or None)
  81. # ==================== 主程序 ====================
  82. if __name__ == "__main__":
  83. print("=" * 60)
  84. print("A+ Content 完整流程 - marketplaceId 修正版")
  85. print("=" * 60)
  86. # Step 0
  87. print("\n🔑 Step 0: 获取 Access Token...")
  88. token = get_access_token()
  89. print(" ✅ Token 获取成功")
  90. # Step 1
  91. print("\n📥 Step 1: 下载图片并调整尺寸...")
  92. img_resp = requests.get(IMAGE_URL, timeout=30)
  93. img_resp.raise_for_status()
  94. try:
  95. from PIL import Image
  96. img = Image.open(BytesIO(img_resp.content))
  97. print(f" 原图尺寸: {img.size}")
  98. img_resized = img.resize((TARGET_WIDTH, TARGET_HEIGHT), Image.LANCZOS)
  99. buffer = BytesIO()
  100. img_resized.save(buffer, format="PNG")
  101. image_binary = buffer.getvalue()
  102. print(f" ✅ 调整后尺寸: {TARGET_WIDTH}x{TARGET_HEIGHT}")
  103. except ImportError:
  104. print(" ❌ 需要安装 Pillow: pip install Pillow")
  105. exit(1)
  106. md5_hash = hashlib.md5(image_binary).digest()
  107. content_md5 = base64.b64encode(md5_hash).decode('utf-8')
  108. print(f" 图片大小: {len(image_binary)} bytes")
  109. # Step 2: Uploads API 用 marketplaceIds(复数)✅
  110. print("\n📝 Step 2: 创建上传目标...")
  111. resource = "aplus/2020-11-01/contentDocuments"
  112. path = f"/uploads/2020-11-01/uploadDestinations/{quote(resource, safe='/')}"
  113. query = f"marketplaceIds={MARKETPLACE_ID}&contentMD5={quote(content_md5, safe='')}&contentType=image%2Fpng"
  114. resp = spapi_request("POST", path, query, access_token=token)
  115. print(f" 状态码: {resp.status_code}")
  116. if resp.status_code not in [200, 201]:
  117. print(f" ❌ 失败: {resp.text}")
  118. exit(1)
  119. payload = resp.json().get("payload", {})
  120. upload_id = payload.get("uploadDestinationId")
  121. upload_url = payload.get("url")
  122. print(f" ✅ uploadDestinationId: {upload_id}")
  123. # Step 3
  124. print("\n⬆️ Step 3: 上传图片到 S3...")
  125. parsed = urlparse(upload_url)
  126. base_url = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
  127. query_params = dict(parse_qsl(parsed.query))
  128. files = {"File": ("image.png", image_binary, "image/png")}
  129. s3_resp = requests.post(base_url, data=query_params, files=files, timeout=60)
  130. print(f" S3 状态码: {s3_resp.status_code}")
  131. if s3_resp.status_code not in [200, 201, 204]:
  132. print(" ❌ S3 上传失败")
  133. exit(1)
  134. print(" ✅ S3 上传成功")
  135. # 构建 contentDocument
  136. content_doc = {
  137. "name": "Product Image",
  138. "contentType": "EMC",
  139. "locale": "en-US",
  140. "contentModuleList": [
  141. {
  142. "contentModuleType": "STANDARD_HEADER_IMAGE_TEXT",
  143. "standardHeaderImageText": {
  144. "headline": {
  145. "value": "Premium Quality Bar Table",
  146. "decoratorSet": []
  147. },
  148. "block": {
  149. "image": {
  150. "uploadDestinationId": upload_id,
  151. "imageCropSpecification": {
  152. "size": {
  153. "width": {"value": TARGET_WIDTH, "units": "pixels"},
  154. "height": {"value": TARGET_HEIGHT, "units": "pixels"}
  155. },
  156. "offset": {
  157. "x": {"value": 0, "units": "pixels"},
  158. "y": {"value": 0, "units": "pixels"}
  159. }
  160. },
  161. "altText": "Product detail image"
  162. },
  163. "headline": {
  164. "value": "Elegant Design for Modern Living",
  165. "decoratorSet": []
  166. },
  167. "body": {
  168. "textList": [
  169. {
  170. "value": "Space-saving bar-height table comfortably seats 2-6 people. Adjustable height, durable engineered wood construction, and smart storage shelves make it perfect for any home.",
  171. "decoratorSet": []
  172. }
  173. ]
  174. }
  175. }
  176. }
  177. }
  178. ]
  179. }
  180. # Step 4: 验证 - A+ API 用 marketplaceId(单数)❌ 之前错了
  181. print("\n📝 Step 4: 验证内容文档...")
  182. validate_body = {"contentDocument": content_doc}
  183. # 关键修正:A+ Content API 用 marketplaceId(单数)
  184. resp = spapi_request("POST", "/aplus/2020-11-01/contentAsinValidations", f"marketplaceId={MARKETPLACE_ID}", body=validate_body, access_token=token)
  185. print(f" 验证状态码: {resp.status_code}")
  186. print(f" 验证响应: {resp.text[:800]}")
  187. # Step 5: 创建文档 - A+ API 用 marketplaceId(单数)❌ 之前错了
  188. print("\n📝 Step 5: 创建 Content Document...")
  189. # 关键修正:A+ Content API 用 marketplaceId(单数)
  190. resp = spapi_request("POST", "/aplus/2020-11-01/contentDocuments", f"marketplaceId={MARKETPLACE_ID}", body={"contentDocument": content_doc}, access_token=token)
  191. print(f" 状态码: {resp.status_code}")
  192. print(f" 响应: {resp.text}")
  193. if resp.status_code not in [200, 201]:
  194. print("\n ❌ 创建失败")
  195. exit(1)
  196. doc_data = resp.json()
  197. content_ref = doc_data.get("contentReference", {})
  198. content_key = content_ref.get("key")
  199. if not content_key:
  200. print(" ❌ 响应中缺少 contentReference.key")
  201. exit(1)
  202. print(f" ✅ 创建成功,Content Key: {content_key}")
  203. # Step 6: 绑定 ASIN - A+ API 用 marketplaceId(单数)❌ 之前错了
  204. print("\n🔗 Step 6: 绑定 ASIN...")
  205. body = {"asinSet": [ASIN]}
  206. # 关键修正:A+ Content API 用 marketplaceId(单数)
  207. resp = spapi_request("POST", f"/aplus/2020-11-01/contentDocuments/{content_key}/asins", f"marketplaceId={MARKETPLACE_ID}", body=body, access_token=token)
  208. print(f" 状态码: {resp.status_code}")
  209. print(f" 响应: {resp.text[:500]}")
  210. if resp.status_code not in [200, 201]:
  211. print(" ❌ 绑定失败")
  212. exit(1)
  213. print(" ✅ ASIN 绑定成功")
  214. # Step 7: 提交审核 - A+ API 用 marketplaceId(单数)❌ 之前错了
  215. print("\n📋 Step 7: 提交审核...")
  216. # 关键修正:A+ Content API 用 marketplaceId(单数)
  217. resp = spapi_request("POST", f"/aplus/2020-11-01/contentDocuments/{content_key}/approvalSubmissions", f"marketplaceId={MARKETPLACE_ID}", access_token=token)
  218. print(f" 状态码: {resp.status_code}")
  219. print(f" 响应: {resp.text[:500]}")
  220. if resp.status_code not in [200, 201]:
  221. print(" ❌ 提交失败")
  222. exit(1)
  223. print(" ✅ 审核提交成功")
  224. print("\n" + "=" * 60)
  225. print("🎉 全部步骤执行成功!")
  226. print(f" Content Key: {content_key}")
  227. print(f" ASIN: {ASIN}")
  228. print("=" * 60)
  229. print("\n⚠️ 请保存 Content Key,用于后续查询状态")
  230. print(" 审核通常需要 1-7 个工作日")