| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291 |
- import requests
- import json
- import datetime
- import hashlib
- import hmac
- import uuid
- import base64
- from urllib.parse import quote, urlparse, parse_qsl
- from io import BytesIO
- # ==================== 配置(全部填好,直接运行) ====================
- CLIENT_ID = "amzn1.application-oa2-client.9f775d35077a4c7598116e5a89b60222"
- CLIENT_SECRET = "amzn1.oa2-cs.v1.6909777baa82b796555a431cd23709342e8e01434abcad1f8b59cadf048b51c3"
- REFRESH_TOKEN = "Atzr|IwEBICh_fojh7qAfrePzSjOZhQxTnHRZCYOuWNvSAHGaBs1WfkjR4qjC1Dmv0uK3oyq_L-R9SRKK4cYmPwF1MQXQdqKlpzOAGItWeXnNzh-SyhHHISv0_vCt3D6hluVmIEmF2kA7ro_XF-PslD41g3Eq6bv0Qq4y4pQbkDj-9dudDuz7nku8UL-nc4EWgJCrHCJTAs8TK48Yvrfi3E35uv97tYhs6qb8D9d4dR9C7daaC49eoDqP88qyUHThQV1Kqon8xukx4AT2_wLqWNcabpADzuK-5w7D-vvFFtvzfkbmtfzoPeEdHPulG6scc59BPFTyBlGU7a3d1JSFg2ROHIlrljRm"
- AWS_ACCESS_KEY = "AKIA24CLQSVTW2K4S6RP"
- AWS_SECRET_KEY = "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"
- AWS_REGION = "us-east-1"
- SELLER_ID = "A1H50Z0WYYLEGQ"
- SKU = "bar table pink"
- MARKETPLACE_ID = "ATVPDKIKX0DER"
- ASIN = "B0FR1WRK4M"
- CONTENT_KEY = f"amzn1.aplus.content.v1.{uuid.uuid4()}"
- IMAGE_URL = "https://cunchu.site/temp/8c7a90e1-a5a1-41af-a119-41b1ca74b40d.png"
- # ==================== 签名工具(复用你之前的) ====================
- def sha256_hash(data):
- return hashlib.sha256(data.encode("utf-8")).hexdigest()
- def hmac_sha256(key, msg):
- return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
- def get_signature_key(secret_key, date_stamp, region, service):
- k_date = hmac_sha256(("AWS4" + secret_key).encode("utf-8"), date_stamp)
- k_region = hmac_sha256(k_date, region)
- k_service = hmac_sha256(k_region, service)
- return hmac_sha256(k_service, "aws4_request")
- def get_access_token():
- url = "https://api.amazon.com/auth/o2/token"
- resp = requests.post(url, data={
- "grant_type": "refresh_token",
- "refresh_token": REFRESH_TOKEN,
- "client_id": CLIENT_ID,
- "client_secret": CLIENT_SECRET,
- })
- resp.raise_for_status()
- return resp.json()["access_token"]
- def spapi_request(method, path, query, body=None, access_token=None):
- service = "execute-api"
- host = "sellingpartnerapi-na.amazon.com"
- payload = json.dumps(body, separators=(",", ":")) if body else ""
-
- t = datetime.datetime.now(datetime.timezone.utc)
- amz_date = t.strftime("%Y%m%dT%H%M%SZ")
- date_stamp = t.strftime("%Y%m%d")
-
- canonical_headers = (
- f"host:{host}\n"
- f"x-amz-access-token:{access_token}\n"
- f"x-amz-date:{amz_date}\n"
- )
- signed_headers = "host;x-amz-access-token;x-amz-date"
- payload_hash = sha256_hash(payload)
-
- canonical_request = (
- f"{method}\n{path}\n{query}\n"
- f"{canonical_headers}\n{signed_headers}\n{payload_hash}"
- )
-
- algorithm = "AWS4-HMAC-SHA256"
- credential_scope = f"{date_stamp}/{AWS_REGION}/{service}/aws4_request"
- string_to_sign = (
- f"{algorithm}\n{amz_date}\n{credential_scope}\n"
- f"{sha256_hash(canonical_request)}"
- )
-
- signing_key = get_signature_key(AWS_SECRET_KEY, date_stamp, AWS_REGION, service)
- signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
-
- auth_header = (
- f"{algorithm} Credential={AWS_ACCESS_KEY}/{credential_scope}, "
- f"SignedHeaders={signed_headers}, Signature={signature}"
- )
-
- headers = {
- "Host": host,
- "x-amz-access-token": access_token,
- "x-amz-date": amz_date,
- "Authorization": auth_header,
- }
- if body:
- headers["Content-Type"] = "application/json"
-
- url = f"https://{host}{path}?{query}"
- return requests.request(method, url, headers=headers, data=payload or None)
- # ==================== Step 1: 下载图片并计算 MD5 ====================
- def download_and_hash_image(image_url):
- print(f"⬇️ 下载图片: {image_url}")
- resp = requests.get(image_url, timeout=30)
- resp.raise_for_status()
- image_binary = resp.content
- print(f" 图片大小: {len(image_binary)} bytes")
-
- md5_hash = hashlib.md5(image_binary).digest()
- content_md5 = base64.b64encode(md5_hash).decode('utf-8')
- print(f" Content-MD5: {content_md5}")
-
- width, height = 1940, 1200
- try:
- from PIL import Image
- img = Image.open(BytesIO(image_binary))
- width, height = img.size
- print(f" 图片尺寸: {width}x{height}")
- except ImportError:
- print(f" 未安装 Pillow,使用默认尺寸: {width}x{height}")
-
- return image_binary, content_md5, width, height
- # ==================== Step 2: 创建上传目标(官方修正版) ====================
- def create_upload_destination(access_token, content_md5, content_type):
- """
- 官方修正:resource 固定为 aplus/2020-11-01/contentDocuments
- 不需要 contentDocumentId,不需要图片文件名!
- """
- resource = "aplus/2020-11-01/contentDocuments" # ← 关键修正:固定字符串
- encoded_resource = quote(resource, safe="/")
-
- path = f"/uploads/2020-11-01/uploadDestinations/{encoded_resource}"
- query = f"marketplaceIds={MARKETPLACE_ID}&contentMD5={quote(content_md5, safe='')}&contentType={quote(content_type, safe='')}"
-
- print(f"\n📝 创建上传目标...")
- print(f" Resource: {resource}")
- print(f" Content-MD5: {content_md5}")
- print(f" Content-Type: {content_type}")
-
- resp = spapi_request("POST", path, query, access_token=access_token)
- print(f"📤 状态码: {resp.status_code}")
- print(f"📄 响应: {resp.text}")
-
- if resp.status_code != 200:
- return None, None
-
- data = resp.json()
- upload_destination_id = data.get("uploadDestinationId")
- url = data.get("url")
-
- print(f"✅ uploadDestinationId: {upload_destination_id}")
- print(f"✅ upload URL: {url[:120]}...")
-
- return upload_destination_id, url
- # ==================== Step 3: Multipart Form POST 上传图片到 S3 ====================
- def upload_image_to_s3(upload_url, image_binary, content_type):
- print(f"\n⬆️ 解析预签名 URL 并上传图片...")
-
- parsed = urlparse(upload_url)
- base_url = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
- query_params = dict(parse_qsl(parsed.query))
-
- print(f" Base URL: {base_url}")
- print(f" Form fields: {list(query_params.keys())}")
-
- files = {
- "File": ("image.png", image_binary, content_type)
- }
-
- resp = requests.post(base_url, data=query_params, files=files, timeout=60)
- print(f"📤 S3 上传状态码: {resp.status_code}")
-
- if resp.text:
- print(f"📄 S3 响应: {resp.text[:500]}")
-
- return resp.status_code in [200, 201, 204]
- # ==================== Step 4: 创建 A+ Content 文档 ====================
- def create_content_document(access_token, content_key, upload_id, img_width, img_height):
- path = "/aplus/2020-11-01/contentDocuments"
- query = f"marketplaceIds={MARKETPLACE_ID}"
-
- body = {
- "contentDocument": {
- "name": "Product Detail Image",
- "contentReference": {
- "key": content_key,
- "version": "0"
- },
- "contentModuleList": [
- {
- "moduleType": "STANDARD_SINGLE_IMAGE",
- "standardSingleImage": {
- "image": {
- "uploadDestinationId": upload_id,
- "imageCropSpecification": {
- "size": {
- "width": {"value": img_width, "units": "pixels"},
- "height": {"value": img_height, "units": "pixels"}
- },
- "offset": {
- "x": {"value": 0, "units": "pixels"},
- "y": {"value": 0, "units": "pixels"}
- }
- },
- "altText": "Product detail image"
- }
- }
- }
- ]
- }
- }
-
- print(f"\n📝 创建 A+ Content 文档...")
- print(f" Content Key: {content_key}")
- resp = spapi_request("POST", path, query, body=body, access_token=access_token)
- print(f"📤 状态码: {resp.status_code}")
- print(resp.text)
- return resp
- # ==================== Step 5: 绑定 ASIN ====================
- def associate_asin(access_token, content_key, asin):
- path = f"/aplus/2020-11-01/contentDocuments/{content_key}/asins"
- query = f"marketplaceIds={MARKETPLACE_ID}"
- body = {"asinSet": [asin]}
-
- print(f"\n🔗 绑定 ASIN: {asin}")
- resp = spapi_request("POST", path, query, body=body, access_token=access_token)
- print(f"📤 状态码: {resp.status_code}")
- print(resp.text)
- return resp
- # ==================== Step 6: 提交审核 ====================
- def submit_approval(access_token, content_key):
- path = f"/aplus/2020-11-01/contentDocuments/{content_key}/approvalSubmissions"
- query = f"marketplaceIds={MARKETPLACE_ID}"
-
- print(f"\n📋 提交审核...")
- resp = spapi_request("POST", path, query, access_token=access_token)
- print(f"📤 状态码: {resp.status_code}")
- print(resp.text)
- return resp
- # ==================== 主程序 ====================
- if __name__ == "__main__":
- print("🔑 获取 Access Token...")
- token = get_access_token()
- print("✅ 令牌获取成功")
-
- print(f"\n🆔 Content Key: {CONTENT_KEY}")
- print(f"🖼️ 图片URL: {IMAGE_URL}")
- print(f"🔗 ASIN: {ASIN}")
-
- # Step 1
- print("\n=== Step 1: 下载图片 ===")
- image_binary, content_md5, img_width, img_height = download_and_hash_image(IMAGE_URL)
-
- # Step 2(官方修正版)
- print("\n=== Step 2: 创建上传目标 ===")
- upload_id, upload_url = create_upload_destination(token, content_md5, "image/png")
-
- if not upload_id or not upload_url:
- print("\n⛔ 创建上传目标失败,终止")
- exit(1)
-
- # Step 3
- print("\n=== Step 3: 上传图片到 S3 ===")
- success = upload_image_to_s3(upload_url, image_binary, "image/png")
- if not success:
- print("\n⛔ 图片上传失败,终止")
- exit(1)
- print("✅ 图片上传成功")
-
- # Step 4
- print("\n=== Step 4: 创建内容文档 ===")
- create_content_document(token, CONTENT_KEY, upload_id, img_width, img_height)
-
- # Step 5
- print("\n=== Step 5: 绑定ASIN ===")
- associate_asin(token, CONTENT_KEY, ASIN)
-
- # Step 6
- print("\n=== Step 6: 提交审核 ===")
- submit_approval(token, CONTENT_KEY)
-
- print("\n🎉 全部步骤执行完毕!")
- print(" A+ 内容已提交审核,通常需要 1-7 个工作日")
- print(f" Content Key: {CONTENT_KEY}")
- print(" 请保存此 Key 以便后续查询状态")
|