"""ABCPAY Python 3.8+ 示例:发送六类请求,也可接收两类回调。""" import hashlib import hmac import json import os import re import sys import time import uuid from http.server import BaseHTTPRequestHandler, HTTPServer from pathlib import Path from urllib.error import HTTPError from urllib.request import Request, urlopen PATHS = { "/abcpay/collection/create", "/abcpay/payout/create", "/abcpay/wallet/balance", "/abcpay/upi/query", "/abcpay/utr/query", "/abcpay/order/query", } def required(name): value = os.environ.get(name) if not value: raise ValueError("请设置环境变量 " + name) return value def signature(key, method, path, timestamp, nonce, body): digest = hashlib.sha256(body).hexdigest() source = "\n".join(("ABCPAY-OPEN-1", method.upper(), path, timestamp, nonce, digest)).encode("utf-8") return hmac.new(key.encode("utf-8"), source, hashlib.sha256).hexdigest() def send(args): path = args[0] if args else "/abcpay/wallet/balance" if path not in PATHS: raise ValueError("接口路径不在示例范围内") body = Path(args[1]).read_bytes() if len(args) > 1 else b"{}" base = os.environ.get("ABCPAY_BASE_URL", "https://abc.yamipay.cc").rstrip("/") timestamp = str(int(time.time())) nonce = uuid.uuid4().hex headers = { "Content-Type": "application/json; charset=UTF-8", "Abcpay-Merchant": required("ABCPAY_MERCHANT"), "Abcpay-Timestamp": timestamp, "Abcpay-Nonce": nonce, "Abcpay-Signature": signature(required("ABCPAY_API_KEY"), "POST", path, timestamp, nonce, body), } request = Request(base + path, data=body, headers=headers, method="POST") try: with urlopen(request, timeout=20) as response: print("HTTP", response.status) print(response.read().decode("utf-8")) except HTTPError as error: print("HTTP", error.code) print(error.read().decode("utf-8")) class CallbackHandler(BaseHTTPRequestHandler): def reply(self, status, text): body = text.encode("utf-8") self.send_response(status) self.send_header("Content-Type", "text/plain; charset=UTF-8") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def do_POST(self): path = self.path.partition("?")[0] if path not in ("/notify/collection", "/notify/payout"): self.reply(404, "回调路径不正确") return try: body = self.rfile.read(int(self.headers.get("Content-Length", "0"))) merchant = self.headers.get("Abcpay-Merchant", "") timestamp = self.headers.get("Abcpay-Timestamp", "") nonce = self.headers.get("Abcpay-Nonce", "") received = self.headers.get("Abcpay-Signature", "") if (merchant != required("ABCPAY_MERCHANT") or not re.fullmatch(r"[0-9]{1,15}", timestamp) or abs(int(time.time()) - int(timestamp)) > 300 or not re.fullmatch(r"[A-Za-z0-9_-]{16,64}", nonce) or not re.fullmatch(r"[0-9a-fA-F]{64}", received)): self.reply(401, "回调鉴权失败") return expected = signature(required("ABCPAY_API_KEY"), "POST", path, timestamp, nonce, body) if not hmac.compare_digest(expected, received.lower()): self.reply(401, "回调签名错误") return event = json.loads(body.decode("utf-8")) if not isinstance(event, dict) or not event.get("noticeId"): self.reply(400, "回调报文不正确") return # 演示接收:正式业务应先持久化 noticeId 并完成幂等处理,再返回 ACK。 print("ABCPAY 回调", event["noticeId"]) self.reply(200, "ACK") except (ValueError, UnicodeDecodeError, json.JSONDecodeError): self.reply(400, "回调报文不正确") def serve(): port = int(os.environ.get("ABCPAY_CALLBACK_PORT", "8080")) server = HTTPServer(("0.0.0.0", port), CallbackHandler) print("回调监听端口:", port) server.serve_forever() if __name__ == "__main__": if len(sys.argv) > 1 and sys.argv[1] == "serve": serve() else: send(sys.argv[1:])