wechatpayv3 Python SDK

repository·master·Indexed 23 days ago

https://github.com/minibear2021/wechatpayv3

A Python SDK for WeChat Pay API v3 that supports Direct Connection and Partner modes. It automates platform certificate management, encrypts sensitive data, and handles callback notification verification and decryption. The library provides both synchronous and asynchronous clients (via httpx and asyncio), with specific integration examples for FastAPI and Tornado. Key supported operations include initiating payments via .pay(), querying order status with .query(), applying for refunds, and closing orders.

Tokens
14.6K
Snippets
26
Records
38
Agent score
80%

What's inside wechatpayv3

  1. Handle duplicate callback messages and timeouts

    master

    When implementing callback handlers, address two operational concerns:

    1. Idempotency: You may receive the same notification multiple times. Implement logic in your business layer to check if a transaction has already been processed (e.g., by checking the transaction_id or out_trade_no).
    2. Timeouts: If your business logic takes too long, WeChat Pay may consider the callback failed. It is recommended to process messages asynchronously: receive the message, cache it, and return a success response immediately to avoid the server marking your endpoint as unavailable.
  2. Identify the correct API function for your use case

    master

    When choosing an API function from the SDK, you must consider two factors:

    1. Functionality Category: APIs are grouped into categories like 基础支付 (Basic Payment), 营销工具 (Marketing Tools), 资金应用 (Fund Application), etc.
    2. Merchant Mode:
      • Direct Merchant (直连商户): Suitable for standard merchant integrations. Many 经营能力 (Business Capability) and 商户进件 (Merchant Onboarding) APIs are not available in this mode.
      • Service Provider (服务商): Suitable for platforms managing multiple sub-merchants. Many APIs (like applyment_submit or submch_fundflow_bill) are only available in this mode.
  3. Determine WeChat Pay account mode (Platform Public Key vs. Platform Certificate)

    master

    Since September 2024, new WeChat Pay accounts may use the "Platform Public Key" mode. Older accounts can continue using the "WeChat Pay Platform Certificate" mode. The SDK supports both and handles the transition.

    To determine your mode:

    1. Log in to the WeChat Pay Merchant Administration backend.
    2. Navigate to Account Center -> API Security.
    3. If you can apply for a "WeChat Pay Public Key", use the Platform Public Key mode for initialization.
    4. If not, use the Platform Certificate mode.
  4. Process callback notifications using event_type

    master

    The SDK uses a single unified callback method for all notification types. To handle specific business logic, inspect the event_type field in the returned result.

    Example logic for a successful transaction:

    1. Call wxpay.callback(headers, body).
    2. Check if result.get('event_type') == 'TRANSACTION.SUCCESS'.
    3. Extract details from the resource object (e.g., appid, mchid, out_trade_no, transaction_id, amount).
    4. Important: After processing, your endpoint must return a 200 or 204 status code to WeChat Pay to acknowledge receipt.
    @app.route('/notify', methods=['POST'])
    def notify():
        result = wxpay.callback(request.headers, request.data)
        if result and result.get('event_type') == 'TRANSACTION.SUCCESS':
            resource = result.get('resource')
            # ... extract fields like appid, mchid, amount ...
            # TODO: Perform business logic
            return jsonify({'code': 'SUCCESS', 'message': '成功'}) # Returns 200
        else:
            return jsonify({'code': 'FAILED', 'message': '失败'}), 500
  5. Use AsyncWeChatPay with an asynchronous context manager

    master

    The asynchronous client must be managed using the async with syntax to ensure proper connection lifecycle management and resource cleanup. All API method calls must be prefixed with await.

    import asyncio
    from wechatpayv3.async_ import AsyncWeChatPay, WeChatPayType
    
    async def main():
        with open('/path/to/apiclient_key.pem') as f:
            private_key = f.read()
        
        async with AsyncWeChatPay(
            wechatpay_type=WeChatPayType.NATIVE,
            mchid='1230000109',
            private_key=private_key,
            cert_serial_no='444F4864EA9B34415...',
            apiv3_key='MIIEvwIBADANBgkqhkiG9w0BAQE...',
            appid='wxd678efh567hg6787',
            notify_url='https://www.xxxx.com/notify',
            cert_dir='./cert'
        ) as wxpay:
            # Example: Native payment
            code, message = await wxpay.pay(
                description='Async test',
                out_trade_no='async_demo_001',
                amount={'total': 100},
                pay_type=WeChatPayType.NATIVE
            )
            print(f"Code: {code}, Message: {message}")
    
    asyncio.run(main())
  6. Configure Service Provider (Partner) mode

    master

    The SDK defaults to Direct Merchant (直连商户) mode. To switch to Service Provider (服务商) mode, set partner_mode=True during initialization.

    Note: While many interfaces are shared, some are exclusive to Direct Merchants or Service Providers. For shared interfaces, be aware that parameter requirements may vary slightly depending on the mode.

  7. Integrate AsyncWeChatPay with FastAPI

    master

    When using FastAPI, it is recommended to initialize the AsyncWeChatPay client within a lifespan context manager. This allows the client to start and clean up resources (like connection pools) when the server starts and stops.

    from contextlib import asynccontextmanager
    from fastapi import FastAPI, HTTPException
    from pydantic import BaseModel
    from wechatpayv3.async_ import AsyncWeChatPay, WeChatPayType
    
    wxpay = None
    
    @asynccontextmanager
    async def lifespan(app: FastAPI):
        global wxpay
        with open('/path/to/apiclient_key.pem') as f:
            private_key = f.read()
        
        wxpay = AsyncWeChatPay(
            wechatpay_type=WeChatPayType.NATIVE,
            mchid='1230000109',
            private_key=private_key,
            cert_serial_no='444F4864EA9B34415...',
            apiv3_key='MIIEvwIBADANBgkqhkiG9w0BAQE...',
            appid='wxd678efh567hg6787',
            notify_url='https://www.xxxx.com/notify',
            cert_dir='./cert'
        )
        await wxpay.__aenter__()
        yield
        if wxpay:
            await wxpay.__aexit__(None, None, None)
    
    app = FastAPI(lifespan=lifespan)
    
    class PaymentRequest(BaseModel):
        description: str
        out_trade_no: str
        total: int
    
    @app.post("/pay")
    async def create_payment(payment: PaymentRequest):
        code, message = await wxpay.pay(
            description=payment.description,
            out_trade_no=payment.out_trade_no,
            amount={'total': payment.total},
            pay_type=WeChatPayType.NATIVE
        )
        if code == 200:
            return {"code": code, "message": message}
        else:
            raise HTTPException(status_code=code, detail=message)
  8. Integrate AsyncWeChatPay with Tornado

    master

    In Tornado, you can either create a new AsyncWeChatPay instance per request using async with or manage a global instance. The following example demonstrates the per-request pattern.

    import tornado.web
    import tornado.ioloop
    import json
    from wechatpayv3.async_ import AsyncWeChatPay, WeChatPayType
    
    # Configuration (simplified for example)
    WECHATPAY_CONFIG = {
        'wechatpay_type': WeChatPayType.NATIVE,
        'mchid': '1230000109',
        'private_key': '...', 
        'cert_serial_no': '...',
        'apiv3_key': '...',
        'appid': '...',
        'notify_url': '...',
        'cert_dir': './cert'
    }
    
    class PaymentHandler(tornado.web.RequestHandler):
        async def post(self):
            async with AsyncWeChatPay(**WECHATPAY_CONFIG) as wxpay:
                data = json.loads(self.request.body)
                code, message = await wxpay.pay(
                    description=data['description'],
                    out_trade_no=data['out_trade_no'],
                    amount={'total': data['total']},
                    pay_type=WeChatPayType.NATIVE
                )
                self.write({"code": code, "message": message})
    
    class QueryHandler(tornado.web.RequestHandler):
        async def get(self, out_trade_no):
            async with AsyncWeChatPay(**WECHATPAY_CONFIG) as wxpay:
                code, message = await wxpay.query(out_trade_no=out_trade_no)
                self.write({"code": code, "message": message})
    
    def make_app():
        return tornado.web.Application([
            (r"/pay", PaymentHandler),
            (r"/query/([^/]+)", QueryHandler),
        ])
    
    if __name__ == "__main__":
        app = make_app()
        app.listen(8888)
        tornado.ioloop.IOLoop.current().start()
  9. Prepare credentials for WeChat Pay API v3

    master

    Before initializing the SDK, you must gather the following credentials from your WeChat Pay merchant backend:

    Required for all modes:

    • PRIVATE_KEY: Your Merchant API certificate private key (usually apiclient_key.pem). Do not expose this file publicly.
    • CERT_SERIAL_NO: The unique serial number of your Merchant API certificate.
    • APIV3_KEY: The APIv3 key used for symmetric encryption (AES-256-GCM) during callback decryption.

    Required for WeChat Pay Platform Public Key mode:

    If you are using the Platform Public Key mode instead of the Platform Certificate mode, you also need:

    • PUBLIC_KEY: The WeChat Pay platform public key downloaded from the 'API Security' menu.
    • PUBLIC_KEY_ID: The ID associated with the platform public key (e.g., PUB_KEY_ID_1234567890...).
  10. Handle WeChat Pay callback verification in different web frameworks

    master

    A common issue is callback verification failure due to incorrect handling of headers and body types by different web frameworks. The wxpay.callback method requires headers (typically a dict) and body (typically bytes).

    Crucial Rule: Do not convert the body to a dict or a string; it must be passed as the raw value received from the request to ensure signature verification succeeds.

    ### Flask
    ```python
    result = wxpay.callback(headers=request.headers, body=request.data)

    Django

    result = wxpay.callback(headers=request.META, body=request.body)

    FastAPI

    result = wxpay.callback(headers=request.headers, body=await request.body())

    Tornado

    result = wxpay.callback(headers=request.headers, body=request.body)