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:
- User calls
GetPaymentUrlRPC with order ID, provider ID, and channel - System loads provider credentials from database
- System generates signed payment request using provider’s key
- 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 callbackevents/epay.rs:EpayCallbackevent definitionhooks/epay.rs:EpayHookconsumer 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:
- Extract callback parameters (order ID, amount, status, etc.)
- Load provider credentials from database using
pidfrom callback - Reconstruct signature using provider’s secret key
- Compare computed signature with received signature
- 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 generatedpayment_method: Set to channel (AliPay, WeChat, USDT) after paymentorder_status: Updated fromUnpaidtoPaidon 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:
- Order status updated to
Paid OrderPaidEventpublished to RabbitMQ (shop.order_paid)- 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:
- List Providers: Call
ListEpayProvidersto get available providers and channels - Display Options: Show provider names and channel icons (AliPay, WeChat, USDT)
- Request Payment: Call
GetPaymentUrlwith selected provider ID and channel - Redirect User: Open payment URL in browser or webview
- Handle Return: When user returns via
return_url, poll order status to confirm payment - Status Polling: Use
GetOrderByIdto 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.