Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

EPay Support

EPay (易支付) is the payment gateway integration that enables third-party payment processing through aggregator services. The system supports multiple payment providers with different channels, handles async payment callbacks, and ensures payment security through signature verification.

Core Concept

EPay acts as an abstraction layer over payment aggregators that support:

  • AliPay (alipay)
  • WeChat Pay (wxpay)
  • USDT cryptocurrency (usdt)

The system is designed to support multiple providers simultaneously, each with their own credentials and enabled payment channels. This allows failover capability and regional/method-specific provider selection.

Data Model

pub struct EpayProviderCredential {
    pub id: i32,
    pub display_name: String,           // User-facing provider name
    pub enabled_channels: Vec<EpaySupportedChannels>,
    pub enabled: bool,                  // Admin on/off switch
    pub key: String,                    // Merchant secret key
    pub pid: i32,                       // Merchant ID
    pub merchant_url: String,           // Gateway endpoint
}

Provider Library

The libs/epay crate provides:

  • Signature generation: MD5-based signing for payment requests
  • Signature verification: Validate callbacks to prevent fraud
  • Request/Response types: Type-safe payment gateway communication
  • Channel enumeration: Standardized payment method identifiers

Payment Flow Architecture

The EPay payment flow involves multiple stages with async processing:

1. Payment URL Generation

User Journey: User creates order → selects provider and channel → receives payment URL

Process:

  1. User calls GetPaymentUrl RPC with order ID, provider ID, and channel
  2. System loads provider credentials from database
  3. System generates signed payment request using provider’s key
  4. Returns redirect URL: {merchant_url}?{signed_parameters}

The signed parameters include order details, callback URLs, and an MD5 signature. The signature ensures the gateway can verify the request came from an authorized merchant.

2. External Payment

User is redirected to the EPay gateway (external site) where they complete payment through their chosen method (AliPay, WeChat, USDT). This happens entirely outside the system.

3. Async Callback Processing

Critical Architecture: The callback must return immediately to the gateway (within 2-3 seconds), so processing happens asynchronously through RabbitMQ.

Flow:

EPay Gateway → POST /api/shop/epay/callback → Publish to RabbitMQ → Return 200 OK
                                                       ↓
                                            EpayHook Consumer
                                                       ↓
                                            Verify Signature
                                                       ↓
                                            Update Order Status
                                                       ↓
                                            Publish OrderPaidEvent

Components:

  • api/epay.rs: HTTP endpoint that receives gateway callback
  • events/epay.rs: EpayCallback event definition
  • hooks/epay.rs: EpayHook consumer that processes the callback

Why Async?: Payment gateways expect immediate HTTP responses. If the server takes too long, the gateway may retry the callback multiple times, potentially causing duplicate processing.

4. Callback Verification

Security Model: All callbacks MUST verify the signature before processing.

Verification Process:

  1. Extract callback parameters (order ID, amount, status, etc.)
  2. Load provider credentials from database using pid from callback
  3. Reconstruct signature using provider’s secret key
  4. Compare computed signature with received signature
  5. Reject if signatures don’t match

Protection Against:

  • Forged callbacks from malicious actors
  • Man-in-the-middle attacks
  • Replay attacks with modified amounts

5. Idempotency Handling

Callbacks can be received multiple times due to network retries. The system handles this by:

  • Checking order status before processing
  • Only updating unpaid orders
  • Returning success for already-paid orders

Multi-Provider System

Provider Discovery

Frontend clients discover available providers via ListEpayProviders RPC:

pub struct EpayProviderSummary {
    pub id: i32,
    pub display_name: String,
    pub enabled_channels: Vec<EpaySupportedChannels>,
}

Query Filters:

  • Only returns providers where enabled = TRUE
  • Excludes providers with empty enabled_channels
  • Filters out channels not in the enabled list

This allows dynamic provider selection in the UI based on current availability.

Provider Selection

When requesting a payment URL, the user specifies:

  • Provider ID: Which payment aggregator to use
  • Channel: Which payment method (alipay, wxpay, usdt)

The system validates:

  • Provider exists and is enabled
  • Requested channel is in provider’s enabled_channels
  • Order is unpaid and belongs to the requesting user

Provider Management

The enabled flag (added via migration 20250929232831) allows administrators to:

  • Temporarily disable problematic providers without deletion
  • Switch between providers during incidents
  • A/B test different payment gateways
  • Phase in new providers gradually

Database Operations:

  • Providers are managed via admin interface or direct database access
  • No gRPC APIs exist for provider CRUD (admin-only operation)
  • Credentials are redacted in logs for security

Configuration

EPay requires configuration in the shop module config:

{
  "shop": {
    "epay_notify_url": "https://your-domain.com/api/shop/epay/callback",
    "epay_return_url": "https://your-domain.com/payment/success",
    ...
  }
}

Configuration Fields

  • epay_notify_url: Server-to-server callback endpoint (async notification)
  • epay_return_url: User redirect URL after payment (browser redirect)

Important Distinctions:

  • notify_url: Backend webhook for payment processing (reliable)
  • return_url: Frontend redirect for user experience (unreliable)

Never rely on return_url for order processing. Users may close the browser before redirecting. Always use the notify_url callback for payment confirmation.

Provider Credentials

Providers are stored in the shop.epay_provider_credential table:

INSERT INTO shop.epay_provider_credential (
  display_name,
  enabled_channels,
  enabled,
  key,
  pid,
  merchant_url
) VALUES (
  'My Payment Provider',
  ARRAY['alipay', 'wxpay']::text[],
  true,
  'your-merchant-secret-key',
  1234,
  'https://pay.provider.com/submit.php'
);

Obtaining Credentials: Register with an EPay-compatible payment aggregator to receive merchant credentials (PID, Key, Gateway URL).

Integration with Order System

Order Fields

Orders track EPay payment through:

  • paid_with_epay_provider: Stores provider ID when payment URL is generated
  • payment_method: Set to channel (AliPay, WeChat, USDT) after payment
  • order_status: Updated from Unpaid to Paid on successful callback

Payment Method Mapping

The system maps EPay channels to internal payment methods:

EpaySupportedChannels::AliPay => PaymentMethod::AliPay
EpaySupportedChannels::WeChatPay => PaymentMethod::WeChat
EpaySupportedChannels::Usdt => PaymentMethod::Usdt

Event Publishing

When a callback successfully processes:

  1. Order status updated to Paid
  2. OrderPaidEvent published to RabbitMQ (shop.order_paid)
  3. Downstream consumers (e.g., market module) react to the event

Error Handling

Callback Validation Failures

If signature verification fails:

  • Log warning (may indicate exposed webhook or malicious request)
  • Return error to gateway (gateway may retry with correct signature)
  • Do NOT update order status

Order State Errors

If order is not found or already paid:

  • Return success to gateway (prevent infinite retries)
  • Log the incident for monitoring

Provider Not Found

If the callback references an unknown provider:

  • Cannot verify signature (no key available)
  • Log error and return failure

Frontend Integration Points

When implementing EPay payment UI:

  1. List Providers: Call ListEpayProviders to get available providers and channels
  2. Display Options: Show provider names and channel icons (AliPay, WeChat, USDT)
  3. Request Payment: Call GetPaymentUrl with selected provider ID and channel
  4. Redirect User: Open payment URL in browser or webview
  5. Handle Return: When user returns via return_url, poll order status to confirm payment
  6. Status Polling: Use GetOrderById to check if payment completed

Key Points:

  • Payment confirmation happens via backend callback, not frontend redirect
  • Frontend should poll order status after user returns
  • Don’t assume payment succeeded just because user returned to app
  • Handle timeout scenarios (user abandons payment gateway)

Design Decisions

Why Multi-Provider Support?

Supporting multiple providers enables:

  • Failover: Switch to backup provider if primary has issues
  • Regional Optimization: Use different providers for different regions
  • Rate Shopping: Select providers with better fees for specific channels
  • Risk Distribution: Avoid single point of failure

Why Async Callback Processing?

Payment gateways expect fast responses (< 3 seconds). Database queries, signature verification, and event publishing can exceed this threshold. Async processing via RabbitMQ ensures:

  • Immediate HTTP response to gateway
  • Reliable processing with automatic retries
  • Decoupled webhook handling from business logic

Why Store Provider ID on Order?

When generating a payment URL, the system stores the provider ID in paid_with_epay_provider. This enables:

  • Signature verification (need provider’s key)
  • Callback validation (ensure callback matches expected provider)
  • Analytics and reporting (which provider processed the payment)

Why MD5 Signatures?

MD5 is cryptographically weak but widely used by Chinese payment aggregators. The EPay library uses MD5 for compatibility with existing gateway implementations. The signature prevents tampering but should not be considered cryptographically secure.