System Overview
An aggregated payment platform consolidates multiple payment channels (Alipay, WeChat Pay, etc.) into a unified payment gateway that merchants can integrate with. The core functionality includes:
- Merchant registration and authentication
- Application management for each merchant
- Payment channel configuration and binding
- Unified QR code generation for in-store payments
- Payment order management and status tracking
Core Architecture
Client Request → API Gateway → Application Layer → Microservices → Database
↓
Message Queue
The system uses a microservices architecture where each microservice handles specific business domains. The payment channel agent service isolates third-party payment provider integration logic from the core transaction service, reducing coupling.
Database Design
Merchant Service Database (shanjupay_merchant_service)
Core tables: merchant, app, staff, store, store_staff
Transaction Database (shanjupay_transaction)
Core tables for payment processing and order management.
Key Design Pattern: Dual ID Strategy
Each entity uses two identifiers:
- Database auto-increment ID: Used for internal table operations, avoiding random I/O overhead
- Business ID (UUID/Snowflake): Used for external references and cross-table relationships
This separation improves query performance while maintaining unique business references across distributed systems.
Payment Channel Parameter Caching
Payment operations are high-frequency events. To improve performance, payment channel parameters are cached in Redis:
public class PaymentChannelCache {
private RedisTemplate<String, Object> cache;
public void updateChannelCache(String merchantId, String channelCode) {
String cacheKey = buildCacheKey(merchantId, channelCode);
// Invalidate existing cache entry
if (Boolean.TRUE.equals(cache.hasKey(cacheKey))) {
cache.delete(cacheKey);
}
// Fetch from database and re-cache
List<ChannelParameter> params = fetchParametersFromDB(merchantId, channelCode);
if (params != null && !params.isEmpty()) {
cache.opsForValue().set(cacheKey, JsonUtil.toJson(params));
}
}
}
Cache invalidation occurs when parameters are updated, ensuring consistency between database and cache layers.
C2B Payment Flow (Consumer Scans Business QR Code)
Process Overview
Merchant Side:
Store Management → Select Application → Generate QR Code
Consumer Side:
Scan QR Code → Enter Amount → Confirm Payment → Enter Password → Complete
Platform Workflow
- Generate unified payment QR code containing merchant and store identifiers
- Platform communicates with Alipay/WeChat APIs as an intermediary
- Payment execution (bank transaction) occurs through the third-party provider
Alipay Integration
Gateway Configuration
public class AlipayConfig {
private String gatewayUrl = "https://openapi.alipay.com/gateway.do";
private String appId; // Assigned by Alipay
private String privateKey; // RSA private key
private String publicKey; // Alipay public key
private String format = "JSON";
private String charset = "UTF-8";
private String signType = "RSA2";
}
Order Creation Request
public class AlipayOrderRequest {
private String outTradeNo; // Merchant's unique order number
private String totalAmount; // Order total (decimal, e.g., "88.88")
private String subject; // Product title
private String body; // Product description
private String productCode = "QUICK_WAP_PAY";
private String timeoutExpress = "30m";
}
Implementation
public PaymentResponse createAlipayOrder(AlipayConfig config, AlipayOrderRequest request) {
AlipayClient client = new DefaultAlipayClient(
config.getGatewayUrl(),
config.getAppId(),
config.getPrivateKey(),
config.getFormat(),
config.getCharset(),
config.getPublicKey(),
config.getSignType()
);
AlipayTradeWapPayRequest payRequest = new AlipayTradeWapPayRequest();
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
model.setOutTradeNo(request.getOutTradeNo());
model.setTotalAmount(request.getTotalAmount());
model.setSubject(request.getSubject());
model.setProductCode(request.getProductCode());
payRequest.setBizModel(model);
payRequest.setReturnUrl(config.getReturnUrl());
payRequest.setNotifyUrl(config.getNotifyUrl());
AlipayTradeWapPayResponse response = client.pageExecute(payRequest);
return new PaymentResponse(response.getBody());
}
WeChat Pay Integration
JSAPI Payment Flow
WeChat JSAPI requires OAuth 2.0 authentication before下单:
- Client requests authorization code
- Platform redirects to WeChat authorization endpoint
- User grants permission
- Platform receives callback with authorization code
- Platform exchanges code for openid
- Platform creates order with openid
Authorization Flow
@GetMapping("/wx-oauth-redirect")
public String handleWeChatOAuth(@RequestParam String code, @RequestParam String state) {
// Exchange authorization code for openid
String openid = fetchWeChatOpenId(code);
// Redirect to payment confirmation page
return "redirect:/payment-confirm?openid=" + openid + "&state=" + state;
}
private String fetchWeChatOpenId(String code) {
String tokenUrl = String.format(
"https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code",
appId, appSecret, code
);
ResponseEntity<String> response = restTemplate.exchange(tokenUrl, HttpMethod.GET, null, String.class);
JSONObject body = JSON.parseObject(response.getBody());
return body.getString("openid");
}
Unified Order Creation
public Map<String, String> createWeChatOrder(WXConfig config, UnifiedOrderRequest request) {
WXPay wxPay = new WXPay(new WXPayConfigImpl(config));
Map<String, String> params = new HashMap<>();
params.put("out_trade_no", request.getTradeNo());
params.put("body", request.getDescription());
params.put("total_fee", String.valueOf(request.getAmountInFen())); // WeChat uses fen
params.put("spbill_create_ip", request.getClientIp());
params.put("trade_type", "JSAPI");
params.put("openid", request.getOpenid());
Map<String, String> response = wxPay.unifiedOrder(params);
// Generate JSAPI parameters for H5 page
Map<String, String> jsapiParams = new HashMap<>();
jsapiParams.put("appId", config.getAppId());
jsapiParams.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000));
jsapiParams.put("nonceStr", UUID.randomUUID().toString());
jsapiParams.put("package", "prepay_id=" + response.get("prepay_id"));
jsapiParams.put("signType", "HMAC-SHA256");
jsapiParams.put("paySign", WXPayUtil.generateSignature(jsapiParams, config.getMchKey(), WXPayConstants.SignType.HMACSHA256));
return jsapiParams;
}
Payment Result Handling
Challenge
Third-party payment providers use two mechanisms:
- Async notification: Provider calls merchant's callback URL
- Active polling: Merchant queries provider for status
Neither mechanism is fully reliable alone, so both must be implemented.
Delayed Query Strategy
After initiating payment, delay before querying status:
// In payment channel agent service
public void sendDelayedQueryMessage(PaymentContext context) {
PaymentResponseDTO notice = new PaymentResponseDTO();
notice.setOutTradeNo(context.getOrderId());
notice.setContent(context.getChannelConfig());
notice.setMsg(context.getChannelType()); // e.g., "ALIPAY_WAP"
// Send delayed message (level 3 = 10 seconds)
rocketMQTemplate.syncSend("TP_PAYMENT_QUERY",
MessageBuilder.withPayload(notice).build(),
1000, 3);
}
Message Consumer
@Component
@RocketMQMessageListener(topic = "TP_PAYMENT_QUERY", consumerGroup = "CG_PAYMENT_AGENT")
public class PaymentQueryConsumer implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt message) {
PaymentResponseDTO queryRequest = JSON.parseObject(
new String(message.getBody()), PaymentResponseDTO.class);
PaymentResponseDTO result = paymentAgentService.queryOrderStatus(
queryRequest.getContent(),
queryRequest.getOutTradeNo());
if (result.getTradeState() == TradeStatus.SUCCESS) {
// Notify transaction service
paymentResultProducer.sendSuccessNotification(result);
} else if (result.getTradeState() == TradeStatus.UNKNOWN) {
throw new RuntimeException("Payment status unclear, will retry");
}
}
}
Status Mapping
| Alipay Status | Platform Status |
|---|---|
| WAIT_BUYER_PAY | PENDING |
| TRADE_SUCCESS | SUCCESS |
| TRADE_CLOSED | CANCELLED |
| TRADE_FINISHED | COMPLETED |
Order Number Generation
The platform generates unique order numbers using the Snowflake algorithm:
public class OrderNumberGenerator {
private final long workerId;
private final long dataCenterId;
private long sequence = 0L;
public String generateOrderNumber() {
long timestamp = System.currentTimeMillis() - EPOCH;
long sequenceId = nextSequence();
return String.format("%d%02d%012d%010d",
timestamp,
workerId,
dataCenterId,
sequenceId
);
}
}
The Snowflake algorithm ensures uniqueness across distributed systems without coordination.
Unified Payment Entry
The payment entry point handles all payment requests uniformly:
@RequestMapping("/pay/{ticket}")
public String paymentEntry(@PathVariable String ticket, HttpServletRequest request) {
// Decode base64 ticket containing order parameters
String jsonParams = Base64Util.decode(ticket);
PayOrderDTO orderParams = JSON.parseObject(jsonParams, PayOrderDTO.class);
// Determine payment channel from user agent
BrowserType clientType = BrowserTypeDetector.detect(request.getHeader("user-agent"));
switch (clientType) {
case ALIPAY:
return "forward:/confirm-payment-alipay" + buildQueryString(orderParams);
case WECHAT:
return "forward:/confirm-payment-wechat" + buildQueryString(orderParams);
default:
return "forward:/payment-error";
}
}
The platform identifies the payment method by examining the user agent string, as Alipay and WeChat clients send different headers when opening URLs.
Store QR Code Generation
Store QR codes encode paymant entry parameters:
public String generateStoreQRCode(StoreQRRequest request) {
// Verify store belongs to merchant
verifyStoreOwnership(request.getMerchantId(), request.getStoreId());
// Build payment entry parameters
PaymentEntryDTO entry = new PaymentEntryDTO();
entry.setMerchantId(request.getMerchantId());
entry.setStoreId(request.getStoreId());
entry.setAppId(request.getAppId());
entry.setServiceType("shanju_c2b");
entry.setSubject(request.getSubject());
// Encode parameters as base64 ticket
String ticket = Base64Util.encode(JSON.toJsonString(entry));
// Generate QR code URL
return PAYMENT_ENTRY_URL + ticket;
}
The QR code essentially contains a URL with encoded merchant and store identifiers, allowing any client to initiate payment.
Payment Channel Binding
Service Type Binding
Each merchant application can bind multiple service types:
- shanju_b2c: Business scans consumer QR code
- shanju_c2b: Consumer scans business QR code
Channel Configuration
public class PayChannelParam {
private Long merchantId;
private String appId;
private String platformChannel; // shanju_c2b, shanju_b2c
private String payChannel; // ALIPAY_WAP, WX_JSAPI
private String channelName;
private String configParams; // JSON: appId, privateKey, publicKey, etc.
}
The relationship between platform service types and actual payment channels:
| Platform Service | Payment Channel | Description |
|---|---|---|
| shanju_b2c | WX_MICROPAY | WeChat payment code |
| shanju_b2c | ALIPAY_BAR_CODE | Alipay barcode |
| shanju_c2b | WX_JSAPI | WeChat JSAPI |
| shanju_c2b | ALIPAY_WAP | Alipay mobile web |
Data Transfer Objects
The system uses distinct object types for different layers:
| Object Type | Layer | Purpose |
|---|---|---|
| VO | View | UI display data |
| DTO | Service | Inter-layer transfer |
| DO | Domain | Business entities |
| Entity | Persistence | Database mapping |
This separation allows each layer to maintain its own data structure, improving maintainability and reducing coupling.
Key Spring Annotations
Controller Layer
@RestController
@Api(tags = "Payment Controller")
public class PaymentController {
@GetMapping("/orders/{id}")
@ApiOperation("Query order by ID")
public OrderDTO getOrder(@PathVariable Long id) {
return paymentService.findById(id);
}
@PostMapping("/orders")
@ApiOperation("Create new order")
public OrderDTO createOrder(@RequestBody OrderCreateDTO dto) {
return paymentService.create(dto);
}
}
Service Layer
@Service
@Slf4j
@Transactional
public class PaymentServiceImpl implements PaymentService {
@Autowired
private PaymentChannelMapper channelMapper;
@Value("${payment.timeout:30m}")
private String defaultTimeout;
}
Exception Handling
Java exceptions are categorized as:
- Unchecked (Runtime): NullPointerException, IndexOutOfBoundsException - not required to be caught
- Checked: IOException, SQLException - must be handled or declared
public class PaymentException extends RuntimeException {
private final String errorCode;
public PaymentException(String code, String message) {
super(message);
this.errorCode = code;
}
}
SaaS Integration
The platform integrates with a multi-tenant SaaS system for unified account management:
- Create tenant when merchant registers
- Create user linked to tenant
- Assign default permissions
- Set admin role for initial user
This separation allows the payment platform to focus on business logic while delegating authentication and authorization to the SaaS layer.
Payment Result Status
| Status Code | Description |
|---|---|
| 0 | Order created |
| 1 | Payment in progress |
| 2 | Payment successful |
| 3 | Payment completed |
| 4 | Order closed |
| 5 | Payment failed |
Status transitions are managed through message queue consumers that listen for payment results from channel agents.