Skip to content

Python 完整示例

此文件根据实际使用的 test.py 整理,仅把 API Key 和 Secret 替换为占位符。

支持:

  • 自动获取所有分页中的 sku_code
  • 关联商品标题、描述、价格和库存
  • 使用 sku_code + quantity 下单
  • 自动轮询订单直到交付完成
  • \n 拆分 fulfillment.payload
  • order_<order_no>.txt 保存,每行一条购买数据

命令

powershell
pip install -r requirements.txt
python test.py
python test.py order SKU-1 2

完整源码

python
import argparse
import time
import hmac
import hashlib
import json
from pathlib import Path
import requests

# ---------- 配置区域(请修改为你的实际信息) ----------
BASE_URL = "https://cheapemail.cc"          # 你的 dujiao-next 站点域名
API_KEY = "your_api_key"                 # 后台生成的 API Key
API_SECRET = "your_api_secret"           # 后台生成的 API Secret
ORDER_DATA_DIR = Path(__file__).resolve().parent / "orders"
REQUEST_TIMEOUT_SECONDS = 30
ORDER_POLL_INTERVAL_SECONDS = 5
ORDER_COMPLETION_TIMEOUT_SECONDS = 600
# ----------------------------------------------------

def generate_signature(method, path, timestamp, body_bytes):
    body_md5 = hashlib.md5(body_bytes).hexdigest()
    sign_string = f"{method}\n{path}\n{timestamp}\n{body_md5}"
    secret_bytes = API_SECRET.encode("utf-8")
    sign_bytes = sign_string.encode("utf-8")
    signature = hmac.new(secret_bytes, sign_bytes, hashlib.sha256).hexdigest()
    return signature

def build_headers(method, path, body_bytes):
    timestamp = str(int(time.time()))
    signature = generate_signature(method, path, timestamp, body_bytes)
    return {
        "Dujiao-Next-Api-Key": API_KEY,
        "Dujiao-Next-Timestamp": timestamp,
        "Dujiao-Next-Signature": signature,
        "Content-Type": "application/json",
    }

def list_products(page=1, page_size=100):
    path = "/api/v1/upstream/products"
    url = BASE_URL + path
    method = "GET"
    body_bytes = b''
    headers = build_headers(method, path, body_bytes)
    response = requests.get(
        url,
        headers=headers,
        params={"page": page, "page_size": page_size},
        timeout=REQUEST_TIMEOUT_SECONDS,
    )
    return response.json()

def get_all_products():
    products = []
    page = 1

    while True:
        result = list_products(page=page)
        if not result.get("ok"):
            raise RuntimeError(f"获取商品列表失败: {result.get('error_message', result)}")

        items = result.get("items", [])
        products.extend(items)
        total = result.get("total", len(products))
        if not items or len(products) >= total:
            return products
        page += 1

def localized_text(value, language="zh-CN"):
    if isinstance(value, str):
        return value.strip()
    if not isinstance(value, dict):
        return ""
    for key in (language, "zh-CN", "zh-TW", "en-US", "en"):
        text = value.get(key)
        if isinstance(text, str) and text.strip():
            return text.strip()
    return next(
        (text.strip() for text in value.values() if isinstance(text, str) and text.strip()),
        "",
    )

def get_sku_list():
    sku_list = []
    for product in get_all_products():
        for sku in product.get("skus", []):
            if sku.get("is_active") is False or not sku.get("sku_code"):
                continue
            sku_list.append({
                "sku_code": sku["sku_code"],
                "title": localized_text(product.get("title")),
                "description": localized_text(product.get("description")),
                "price": sku.get("price_amount", product.get("price_amount", "")),
                "stock_status": sku.get("stock_status", "unknown"),
                "stock_quantity": sku.get("stock_quantity", 0),
                "sku_id": sku.get("id"),
            })
    return sorted(sku_list, key=lambda item: item["sku_code"])

def print_sku_list():
    sku_list = get_sku_list()
    print(f"共获取到 {len(sku_list)} 个 SKU:")
    for item in sku_list:
        print(
            f"{item['sku_code']} | {item['title']} | {item['description']} | "
            f"价格: {item['price']} | 库存: {item['stock_status']} "
            f"({item['stock_quantity']})"
        )

def create_order(sku_id, quantity):
    path = "/api/v1/upstream/orders"
    url = BASE_URL + path
    method = "POST"
    payload = {"sku_id": sku_id, "quantity": quantity}

    body_bytes = json.dumps(payload, separators=(',', ':')).encode('utf-8')
    headers = build_headers(method, path, body_bytes)
    response = requests.post(url, headers=headers, data=body_bytes, timeout=REQUEST_TIMEOUT_SECONDS)
    return response.json()

def create_order_by_sku_code(sku_code, quantity):
    """根据 SKU 代码下单(自动查找 sku_id)"""
    sku = next((item for item in get_sku_list() if item["sku_code"] == sku_code), None)
    if sku is None:
        raise RuntimeError(f"未找到 SKU 代码: {sku_code}")
    if sku["stock_status"] == "out_of_stock":
        raise RuntimeError(f"SKU '{sku_code}' 库存不足")
    return create_order(sku_id=sku["sku_id"], quantity=quantity)

def get_order(order_id):
    path = f"/api/v1/upstream/orders/{order_id}"
    url = BASE_URL + path
    method = "GET"
    body_bytes = b''
    headers = build_headers(method, path, body_bytes)
    response = requests.get(url, headers=headers, timeout=REQUEST_TIMEOUT_SECONDS)
    return response.json()

def wait_for_order_completion(order_id, poll_interval=ORDER_POLL_INTERVAL_SECONDS,
                              timeout=ORDER_COMPLETION_TIMEOUT_SECONDS):
    """轮询订单,直到交付完成或等待超时。"""
    deadline = time.monotonic() + timeout

    while True:
        detail = get_order(order_id)
        if not detail.get("ok"):
            raise RuntimeError(f"查询订单失败: {detail.get('error_message', detail)}")

        status = detail.get("status")
        if status in {"delivered", "completed"} and detail.get("fulfillment"):
            return detail
        if status == "canceled":
            raise RuntimeError(f"订单 {order_id} 已取消")

        remaining = deadline - time.monotonic()
        if remaining <= 0:
            raise TimeoutError(
                f"等待订单 {order_id} 完成超时,最后状态: {status or 'unknown'}"
            )
        time.sleep(min(poll_interval, remaining))

def save_order_data(order_detail, output_dir=ORDER_DATA_DIR):
    """将 fulfillment.payload 按每条数据一行保存为 UTF-8 TXT 文件。"""
    order_no = order_detail.get("order_no")
    if not order_no:
        raise ValueError("订单详情缺少 order_no,无法保存")

    payload = order_detail.get("fulfillment", {}).get("payload")
    if not isinstance(payload, str) or not payload.strip():
        raise ValueError("订单详情缺少 fulfillment.payload,无法保存")

    # 同时兼容真实换行和 payload 中以文本形式出现的 \n / \r\n。
    normalized_payload = (
        payload.replace("\\r\\n", "\n")
        .replace("\\n", "\n")
        .replace("\\r", "\n")
    )
    items = [line.strip() for line in normalized_payload.splitlines() if line.strip()]
    if not items:
        raise ValueError("fulfillment.payload 中没有可保存的数据")

    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)
    safe_order_no = "".join(
        char if char.isalnum() or char in "-_" else "_" for char in str(order_no)
    ).strip("_")
    if not safe_order_no:
        raise ValueError("order_no 无法转换为有效文件名")

    output_path = output_dir / f"order_{safe_order_no}.txt"
    temp_path = output_path.with_suffix(".txt.tmp")

    with temp_path.open("w", encoding="utf-8") as file:
        file.write("\n".join(items) + "\n")
    temp_path.replace(output_path)
    return output_path

def run_order(sku_code, quantity):
    print(f"正在使用 SKU '{sku_code}' 购买 {quantity} 件...")
    try:
        order_result = create_order_by_sku_code(sku_code, quantity)
        print("下单响应:", json.dumps(order_result, indent=2, ensure_ascii=False))

        if order_result.get("ok"):
            order_id = order_result.get("order_id")
            print(f"下单成功,订单ID: {order_id}")
            print("正在等待订单交付完成...")
            detail = wait_for_order_completion(order_id)
            output_path = save_order_data(detail)
            print(f"购买数据已保存到: {output_path}")
        else:
            print("下单失败:", order_result.get("error_message"))
    except Exception as e:
        print("发生错误:", str(e))

def main():
    parser = argparse.ArgumentParser(description="CheapEmail API 示例")
    subparsers = parser.add_subparsers(dest="command")
    subparsers.add_parser("skus", help="获取全部 sku_code 和商品描述")
    order_parser = subparsers.add_parser("order", help="使用 sku_code 下单")
    order_parser.add_argument("sku_code", help="要购买的 SKU 代码")
    order_parser.add_argument("quantity", type=int, help="购买数量")
    args = parser.parse_args()

    if args.command in {None, "skus"}:
        print_sku_list()
    elif args.command == "order":
        if args.quantity < 1:
            parser.error("quantity 必须大于等于 1")
        run_order(args.sku_code, args.quantity)

if __name__ == "__main__":
    main()

CheapEmail Open API Documentation