RabbitMQ 订阅 Demo
本页配套 RabbitMQ 指南/RabbitMQ 信息。PetaX 通过 RabbitMQ 单向推送三类结算通知,商户只需要做消费者,不需要发布消息。连接要求 mTLS(CA 证书 + 客户端证书 + 私钥,由 PetaX 提供,不要把证书内容硬编码进代码仓库)。消息字段形状是基于 HTTP 回调 DTO 的假设,未经真实 PetaX 消息实测确认——尤其notify_issue_result队列的局/期 id 字段可能是issueId而非roundId,接入前务必用真实消息逐字段核对,不要假设跟本 demo 一致。真实队列名由 PetaX 分配,本 demo 默认值只是官方文档已公开的示例格式,不代表你的真实队列名。
PHP
依赖:composer require php-amqplib/php-amqplib。运行:php consumer.php。
<?php
// consumer.php — PetaX RabbitMQ 订阅 Demo(PHP,php-amqplib)
// 运行:composer require php-amqplib/php-amqplib && php consumer.php
// 注意:字段形状是基于 HTTP 回调 DTO 的假设,未经真实 PetaX 消息验证,
// 尤其 notify_issue_result 的 id 字段可能是 issueId 而非 roundId,接入前务必用真实消息核对。
require __DIR__ . '/vendor/autoload.php';
use PhpAmqpLib\Connection\AMQPSSLConnection;
use PhpAmqpLib\Message\AMQPMessage;
$HOST = getenv('RABBITMQ_HOST') ?: 'localhost';
$PORT = (int) (getenv('RABBITMQ_PORT') ?: 5671);
$VHOST = getenv('RABBITMQ_VHOST') ?: '/';
$USERNAME = getenv('RABBITMQ_USERNAME') ?: 'guest';
$PASSWORD = getenv('RABBITMQ_PASSWORD') ?: 'guest';
$CA_CERT = getenv('RABBITMQ_CA_CERT') ?: 'ca_certificate.pem';
$CLIENT_CERT = getenv('RABBITMQ_CLIENT_CERT') ?: 'client_certificate.pem';
$CLIENT_KEY = getenv('RABBITMQ_CLIENT_KEY') ?: 'client_key.pem';
$SERVER_NAME = getenv('RABBITMQ_SERVER_NAME') ?: '';
// 三个队列名默认值是官方文档已公开的示例格式;真实队列名由 PetaX 分配,
// 务必以 PetaX 提供的为准,不要假设跟这里的示例值相同。
$QUEUE_SETTLED_WIN = getenv('RABBITMQ_QUEUE_SETTLED_WIN') ?: 'staging.10102.merchant102_server.settled_win.q';
$QUEUE_SETTLED_LOSS = getenv('RABBITMQ_QUEUE_SETTLED_LOSS') ?: 'staging.10102.merchant102_server.settled_loss.q';
$QUEUE_ISSUE_RESULT = getenv('RABBITMQ_QUEUE_ISSUE_RESULT') ?: 'staging.10102.merchant102_server.notify_issue_result.q';
$queueRoles = [
$QUEUE_SETTLED_WIN => 'settled_ticket',
$QUEUE_SETTLED_LOSS => 'settled_ticket',
$QUEUE_ISSUE_RESULT => 'settled_round',
];
// 仅用于 Demo:记录已经"模拟失败过一次"的 key(ticketId 或 roundId),
// 第二次投递(redelivered=true)就放行成功。真实商户不需要这个逻辑,
// 这里只是让 demo 能演示 nack+requeue 而不会无限热循环重投同一条坏消息。
$failedOnce = [];
function handleMessage(AMQPMessage $msg, string $role, array &$failedOnce): void {
$channel = $msg->getChannel();
$deliveryTag = $msg->getDeliveryTag();
$redelivered = $msg->delivery_info['redelivered'] ?? false;
$payload = json_decode($msg->getBody(), true);
if (json_last_error() !== JSON_ERROR_NONE) {
// 1. 解析失败 = 永久错误:ack 丢弃,避免毒消息卡队列。
echo "[bad-json] discarding, redelivered=" . var_export($redelivered, true) . "\n";
$channel->basic_ack($deliveryTag);
return;
}
if ($role === 'settled_ticket') {
// settled_win/settled_loss 假定按 SettledTicketRequest 字段解析。
$key = $payload['ticketId'] ?? null;
echo "[settled_ticket] ticketId=$key status=" . ($payload['status'] ?? '?') .
" ccy=" . ($payload['ccy'] ?? '?') . " redelivered=" . var_export($redelivered, true) . "\n";
} else {
// notify_issue_result 假定按 SettledRoundRequest 字段解析;roundId 字段名
// 是本仓库运行手册标注的最大不确定性来源,真实接入前务必用真实消息核对。
$key = $payload['roundId'] ?? null;
echo "[settled_round] roundId=$key result=" . ($payload['result'] ?? '?') .
" redelivered=" . var_export($redelivered, true) . "\n";
}
if ($key === null) {
echo "[warn] missing key field, discarding (ack)\n";
$channel->basic_ack($deliveryTag);
return;
}
// 2. 第一次见到这个 key:模拟一次瞬时处理失败(如真实场景里数据库暂时
// 不可达),nack + requeue,让 RabbitMQ 重新投递。只失败一次,
// 避免同一条消息被无限重投形成热循环。
if (!isset($failedOnce[$key])) {
$failedOnce[$key] = true;
echo "[simulate-failure] $key -> nack+requeue\n";
$channel->basic_nack($deliveryTag, false, true);
return;
}
// 3. 第二次投递(redelivered=true):放行成功,ack。
echo "[success] $key -> ack\n";
$channel->basic_ack($deliveryTag);
}
$sslOptions = [
'cafile' => $CA_CERT,
'local_cert' => $CLIENT_CERT,
'local_pk' => $CLIENT_KEY,
'verify_peer' => true,
'verify_peer_name' => $SERVER_NAME !== '',
];
if ($SERVER_NAME !== '') {
$sslOptions['peer_name'] = $SERVER_NAME;
}
$connection = new AMQPSSLConnection($HOST, $PORT, $USERNAME, $PASSWORD, $VHOST, $sslOptions);
$channel = $connection->channel();
$channel->basic_qos(null, (int) (getenv('RABBITMQ_PREFETCH') ?: 10), null);
foreach ($queueRoles as $queue => $role) {
// passive=true 只检查队列存在,不主动创建——真实队列由 PetaX 预先建好。
$channel->queue_declare($queue, true, false, false, false);
$channel->basic_consume($queue, '', false, false, false, false,
function (AMQPMessage $msg) use ($role, &$failedOnce) {
handleMessage($msg, $role, $failedOnce);
});
}
echo "listening on " . implode(', ', array_keys($queueRoles)) . "\n";
while ($channel->is_consuming()) {
$channel->wait();
}
Node.js
依赖:npm install amqplib。运行:node consumer.js。
// consumer.js — PetaX RabbitMQ 订阅 Demo (Node.js, amqplib)
// 运行:npm install amqplib && node consumer.js
// 注意:字段形状是基于 HTTP 回调 DTO 的假设,未经真实 PetaX 消息验证,
// 尤其 notify_issue_result 的 id 字段可能是 issueId 而非 roundId,接入前务必用真实消息核对。
const fs = require('fs');
const amqp = require('amqplib');
const HOST = process.env.RABBITMQ_HOST || 'localhost';
const PORT = process.env.RABBITMQ_PORT || '5671';
const VHOST = process.env.RABBITMQ_VHOST || '/';
const USERNAME = process.env.RABBITMQ_USERNAME || 'guest';
const PASSWORD = process.env.RABBITMQ_PASSWORD || 'guest';
const CA_CERT = process.env.RABBITMQ_CA_CERT || 'ca_certificate.pem';
const CLIENT_CERT = process.env.RABBITMQ_CLIENT_CERT || 'client_certificate.pem';
const CLIENT_KEY = process.env.RABBITMQ_CLIENT_KEY || 'client_key.pem';
const SERVER_NAME = process.env.RABBITMQ_SERVER_NAME || '';
// 三个队列名默认值是官方文档已公开的示例格式;真实队列名由 PetaX 分配,
// 务必以 PetaX 提供的为准,不要假设跟这里的示例值相同。
const QUEUE_ROLES = {
[process.env.RABBITMQ_QUEUE_SETTLED_WIN || 'staging.10102.merchant102_server.settled_win.q']: 'settled_ticket',
[process.env.RABBITMQ_QUEUE_SETTLED_LOSS || 'staging.10102.merchant102_server.settled_loss.q']: 'settled_ticket',
[process.env.RABBITMQ_QUEUE_ISSUE_RESULT || 'staging.10102.merchant102_server.notify_issue_result.q']: 'settled_round',
};
// 仅用于 Demo:记录已经"模拟失败过一次"的 key(ticketId 或 roundId),
// 第二次投递(redelivered=true)就放行成功。真实商户不需要这个逻辑,
// 这里只是让 demo 能演示 nack+requeue 而不会无限热循环重投同一条坏消息。
const failedOnce = new Set();
function handleMessage(channel, msg, role) {
const redelivered = msg.fields.redelivered;
let payload;
try {
payload = JSON.parse(msg.content.toString());
} catch (e) {
// 1. 解析失败 = 永久错误:ack 丢弃,避免毒消息卡队列。
console.log(`[bad-json] discarding, redelivered=${redelivered}`);
channel.ack(msg);
return;
}
let key;
if (role === 'settled_ticket') {
// settled_win/settled_loss 假定按 SettledTicketRequest 字段解析。
key = payload.ticketId;
console.log(`[settled_ticket] ticketId=${key} status=${payload.status} ccy=${payload.ccy} redelivered=${redelivered}`);
} else {
// notify_issue_result 假定按 SettledRoundRequest 字段解析;roundId 字段名
// 是本仓库运行手册标注的最大不确定性来源,真实接入前务必用真实消息核对。
key = payload.roundId;
console.log(`[settled_round] roundId=${key} result=${payload.result} redelivered=${redelivered}`);
}
if (!key) {
console.log('[warn] missing key field, discarding (ack)');
channel.ack(msg);
return;
}
// 2. 第一次见到这个 key:模拟一次瞬时处理失败,nack + requeue。
// 只失败一次,避免同一条消息被无限重投形成热循环。
if (!failedOnce.has(key)) {
failedOnce.add(key);
console.log(`[simulate-failure] ${key} -> nack+requeue`);
channel.nack(msg, false, true);
return;
}
// 3. 第二次投递(redelivered=true):放行成功,ack。
console.log(`[success] ${key} -> ack`);
channel.ack(msg);
}
async function main() {
// 注意:amqplib 的 connect(url, socketOptions) 只从第一个参数里挑
// host/port/servername 拷进 socket 层,ca/cert/key 等 TLS 材料必须放进
// 第二个参数(socketOptions),否则会静默走系统信任库、连不上 mTLS。
const connection = await amqp.connect(
{
protocol: 'amqps',
hostname: HOST,
port: parseInt(PORT, 10),
vhost: VHOST,
username: USERNAME,
password: PASSWORD,
},
{
ca: [fs.readFileSync(CA_CERT)],
cert: fs.readFileSync(CLIENT_CERT),
key: fs.readFileSync(CLIENT_KEY),
servername: SERVER_NAME || undefined,
}
);
const channel = await connection.createChannel();
await channel.prefetch(parseInt(process.env.RABBITMQ_PREFETCH || '10', 10));
for (const [queue, role] of Object.entries(QUEUE_ROLES)) {
// checkQueue 只检查队列存在,不主动创建——真实队列由 PetaX 预先建好。
await channel.checkQueue(queue);
channel.consume(queue, (msg) => handleMessage(channel, msg, role), { noAck: false });
}
console.log(`listening on ${Object.keys(QUEUE_ROLES).join(', ')}`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
Java
依赖:Maven(pom.xml 见下)+ 需要把 PetaX 提供的证书/私钥转换成 PKCS12(openssl pkcs12 -export ...,命令见代码注释)。运行:mvn compile exec:java。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.petax</groupId>
<artifactId>rabbitmq-subscribe-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.21.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<mainClass>com.example.petax.RabbitmqConsumer</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
Maven 标准目录结构,消费者源码放在 src/main/java/com/example/petax/RabbitmqConsumer.java:
package com.example.petax;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
// consumer.java — PetaX RabbitMQ 订阅 Demo (Java, com.rabbitmq:amqp-client)
// 运行:mvn compile exec:java
// 注意:字段形状是基于 HTTP 回调 DTO 的假设,未经真实 PetaX 消息验证,
// 尤其 notify_issue_result 的 id 字段可能是 issueId 而非 roundId,接入前务必用真实消息核对。
public class RabbitmqConsumer {
private static final ObjectMapper MAPPER = new ObjectMapper();
// 仅用于 Demo:记录已经"模拟失败过一次"的 key(ticketId 或 roundId),
// 第二次投递(redelivered=true)就放行成功。真实商户不需要这个逻辑,
// 这里只是让 demo 能演示 nack+requeue 而不会无限热循环重投同一条坏消息。
private static final Set<String> FAILED_ONCE = ConcurrentHashMap.newKeySet();
public static void main(String[] args) throws Exception {
String host = getenv("RABBITMQ_HOST", "localhost");
int port = Integer.parseInt(getenv("RABBITMQ_PORT", "5671"));
String vhost = getenv("RABBITMQ_VHOST", "/");
String username = getenv("RABBITMQ_USERNAME", "guest");
String password = getenv("RABBITMQ_PASSWORD", "guest");
String caCert = getenv("RABBITMQ_CA_CERT", "ca_certificate.pem");
String serverName = getenv("RABBITMQ_SERVER_NAME", "");
// Java 的 TLS API 需要 PKCS12 keystore,不能直接读 PEM 私钥;
// 用 openssl 把 PetaX 给的证书/私钥转换一次:
// openssl pkcs12 -export -in client_certificate.pem -inkey client_key.pem -out client.p12 -passout pass:
String clientKeystore = getenv("RABBITMQ_CLIENT_KEYSTORE", "client.p12");
// 三个队列名默认值是官方文档已公开的示例格式;真实队列名由 PetaX 分配,
// 务必以 PetaX 提供的为准,不要假设跟这里的示例值相同。
Map<String, String> queueRoles = Map.of(
getenv("RABBITMQ_QUEUE_SETTLED_WIN", "staging.10102.merchant102_server.settled_win.q"), "settled_ticket",
getenv("RABBITMQ_QUEUE_SETTLED_LOSS", "staging.10102.merchant102_server.settled_loss.q"), "settled_ticket",
getenv("RABBITMQ_QUEUE_ISSUE_RESULT", "staging.10102.merchant102_server.notify_issue_result.q"), "settled_round"
);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
Certificate ca;
try (var in = new FileInputStream(caCert)) {
ca = cf.generateCertificate(in);
}
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null);
trustStore.setCertificateEntry("ca", ca);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
KeyStore keyStore = KeyStore.getInstance("PKCS12");
try (var in = new FileInputStream(clientKeystore)) {
keyStore.load(in, "".toCharArray());
}
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, "".toCharArray());
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
ConnectionFactory factory = new ConnectionFactory();
factory.setHost(host);
factory.setPort(port);
factory.setVirtualHost(vhost);
factory.setUsername(username);
factory.setPassword(password);
// 如果用 IP 连接,但证书 CN/SAN 是 rabbitmq,可通过 RABBITMQ_SERVER_NAME
// 指定 TLS 校验和 SNI 使用的服务名;TCP 仍连接 RABBITMQ_HOST。
factory.setSocketFactory(new ServerNameSslSocketFactory((SSLSocketFactory) sslContext.getSocketFactory(), serverName));
factory.enableHostnameVerification();
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.basicQos(Integer.parseInt(getenv("RABBITMQ_PREFETCH", "10")));
for (Map.Entry<String, String> entry : queueRoles.entrySet()) {
String queue = entry.getKey();
String role = entry.getValue();
// queueDeclarePassive 只检查队列存在,不主动创建——真实队列由 PetaX 预先建好。
channel.queueDeclarePassive(queue);
DeliverCallback callback = (consumerTag, delivery) -> {
boolean redelivered = delivery.getEnvelope().isRedeliver();
long tag = delivery.getEnvelope().getDeliveryTag();
JsonNode payload;
try {
payload = MAPPER.readTree(delivery.getBody());
} catch (Exception e) {
// 1. 解析失败 = 永久错误:ack 丢弃,避免毒消息卡队列。
System.out.println("[bad-json] discarding, redelivered=" + redelivered);
channel.basicAck(tag, false);
return;
}
String key;
if ("settled_ticket".equals(role)) {
// settled_win/settled_loss 假定按 SettledTicketRequest 字段解析。
key = payload.path("ticketId").asText(null);
System.out.println("[settled_ticket] ticketId=" + key + " status=" + payload.path("status").asText()
+ " ccy=" + payload.path("ccy").asText() + " redelivered=" + redelivered);
} else {
// notify_issue_result 假定按 SettledRoundRequest 字段解析;roundId
// 字段名是本仓库运行手册标注的最大不确定性来源,真实接入前务必用真实消息核对。
key = payload.path("roundId").asText(null);
System.out.println("[settled_round] roundId=" + key + " result=" + payload.path("result").asText()
+ " redelivered=" + redelivered);
}
if (key == null) {
System.out.println("[warn] missing key field, discarding (ack)");
channel.basicAck(tag, false);
return;
}
// 2. 第一次见到这个 key:模拟一次瞬时处理失败,nack + requeue。
// 只失败一次,避免同一条消息被无限重投形成热循环。
if (FAILED_ONCE.add(key)) {
System.out.println("[simulate-failure] " + key + " -> nack+requeue");
channel.basicNack(tag, false, true);
return;
}
// 3. 第二次投递(redelivered=true):放行成功,ack。
System.out.println("[success] " + key + " -> ack");
channel.basicAck(tag, false);
};
channel.basicConsume(queue, false, callback, consumerTag -> {});
}
System.out.println("listening on " + queueRoles.keySet());
Thread.currentThread().join();
}
private static class ServerNameSslSocketFactory extends SSLSocketFactory {
private final SSLSocketFactory delegate;
private final String serverName;
ServerNameSslSocketFactory(SSLSocketFactory delegate, String serverName) {
this.delegate = delegate;
this.serverName = serverName == null ? "" : serverName;
}
private String peerHost(String host) {
return serverName.isBlank() ? host : serverName;
}
private Socket wrap(Socket raw, String host, int port) throws IOException {
SSLSocket socket = (SSLSocket) delegate.createSocket(raw, peerHost(host), port, true);
SSLParameters params = socket.getSSLParameters();
params.setEndpointIdentificationAlgorithm("HTTPS");
socket.setSSLParameters(params);
return socket;
}
@Override public String[] getDefaultCipherSuites() { return delegate.getDefaultCipherSuites(); }
@Override public String[] getSupportedCipherSuites() { return delegate.getSupportedCipherSuites(); }
@Override public Socket createSocket() throws IOException { return delegate.createSocket(); }
@Override public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
SSLSocket socket = (SSLSocket) delegate.createSocket(s, peerHost(host), port, autoClose);
SSLParameters params = socket.getSSLParameters();
params.setEndpointIdentificationAlgorithm("HTTPS");
socket.setSSLParameters(params);
return socket;
}
@Override public Socket createSocket(String host, int port) throws IOException {
return wrap(new Socket(host, port), host, port);
}
@Override public Socket createSocket(String host, int port, InetAddress localAddress, int localPort) throws IOException {
return wrap(new Socket(host, port, localAddress, localPort), host, port);
}
@Override public Socket createSocket(InetAddress host, int port) throws IOException {
return wrap(new Socket(host, port), host.getHostName(), port);
}
@Override public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
return wrap(new Socket(address, port, localAddress, localPort), address.getHostName(), port);
}
}
private static String getenv(String key, String def) {
String v = System.getenv(key);
return (v == null || v.isEmpty()) ? def : v;
}
}
Go
依赖:go get github.com/rabbitmq/amqp091-go。运行:go run consumer.go。
// consumer.go — PetaX RabbitMQ 订阅 Demo (Go, rabbitmq/amqp091-go)
// 运行:go mod init demo && go get github.com/rabbitmq/amqp091-go && go run consumer.go
// 注意:字段形状是基于 HTTP 回调 DTO 的假设,未经真实 PetaX 消息验证,
// 尤其 notify_issue_result 的 id 字段可能是 issueId 而非 roundId,接入前务必用真实消息核对。
package main
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"log"
"net"
"net/url"
"os"
"strconv"
"strings"
"sync"
amqp "github.com/rabbitmq/amqp091-go"
)
func getenv(k, def string) string {
if v := os.Getenv(k); v != "" {
return v
}
return def
}
// 仅用于 Demo:记录已经"模拟失败过一次"的 key(ticketId 或 roundId),
// 第二次投递(redelivered=true)就放行成功。真实商户不需要这个逻辑,
// 这里只是让 demo 能演示 nack+requeue 而不会无限热循环重投同一条坏消息。
// 3 个队列各自开一个 goroutine 消费,共享这个 map,必须加锁。
var (
mu sync.Mutex
failedOnce = map[string]bool{}
)
func handleMessage(d amqp.Delivery, role string) {
var payload map[string]interface{}
if err := json.Unmarshal(d.Body, &payload); err != nil {
// 1. 解析失败 = 永久错误:ack 丢弃,避免毒消息卡队列。
log.Printf("[bad-json] discarding, redelivered=%v", d.Redelivered)
d.Ack(false)
return
}
var key string
if role == "settled_ticket" {
// settled_win/settled_loss 假定按 SettledTicketRequest 字段解析。
key, _ = payload["ticketId"].(string)
log.Printf("[settled_ticket] ticketId=%s status=%v ccy=%v redelivered=%v", key, payload["status"], payload["ccy"], d.Redelivered)
} else {
// notify_issue_result 假定按 SettledRoundRequest 字段解析;roundId
// 字段名是本仓库运行手册标注的最大不确定性来源,真实接入前务必用真实消息核对。
key, _ = payload["roundId"].(string)
log.Printf("[settled_round] roundId=%s result=%v redelivered=%v", key, payload["result"], d.Redelivered)
}
if key == "" {
log.Printf("[warn] missing key field, discarding (ack)")
d.Ack(false)
return
}
// 2. 第一次见到这个 key:模拟一次瞬时处理失败,nack + requeue。
// 只失败一次,避免同一条消息被无限重投形成热循环。
mu.Lock()
seen := failedOnce[key]
if !seen {
failedOnce[key] = true
}
mu.Unlock()
if !seen {
log.Printf("[simulate-failure] %s -> nack+requeue", key)
d.Nack(false, true)
return
}
// 3. 第二次投递(redelivered=true):放行成功,ack。
log.Printf("[success] %s -> ack", key)
d.Ack(false)
}
func main() {
host := getenv("RABBITMQ_HOST", "localhost")
port := getenv("RABBITMQ_PORT", "5671")
vhost := getenv("RABBITMQ_VHOST", "/")
username := getenv("RABBITMQ_USERNAME", "guest")
password := getenv("RABBITMQ_PASSWORD", "guest")
caFile := getenv("RABBITMQ_CA_CERT", "ca_certificate.pem")
certFile := getenv("RABBITMQ_CLIENT_CERT", "client_certificate.pem")
keyFile := getenv("RABBITMQ_CLIENT_KEY", "client_key.pem")
serverName := getenv("RABBITMQ_SERVER_NAME", "")
prefetchRaw := getenv("RABBITMQ_PREFETCH", "10")
prefetch, err := strconv.Atoi(prefetchRaw)
if err != nil || prefetch <= 0 {
log.Fatalf("invalid RABBITMQ_PREFETCH=%q", prefetchRaw)
}
// 三个队列名默认值是官方文档已公开的示例格式;真实队列名由 PetaX 分配,
// 务必以 PetaX 提供的为准,不要假设跟这里的示例值相同。
queueRoles := map[string]string{
getenv("RABBITMQ_QUEUE_SETTLED_WIN", "staging.10102.merchant102_server.settled_win.q"): "settled_ticket",
getenv("RABBITMQ_QUEUE_SETTLED_LOSS", "staging.10102.merchant102_server.settled_loss.q"): "settled_ticket",
getenv("RABBITMQ_QUEUE_ISSUE_RESULT", "staging.10102.merchant102_server.notify_issue_result.q"): "settled_round",
}
caCert, err := os.ReadFile(caFile)
if err != nil {
log.Fatalf("read ca cert: %v", err)
}
caPool := x509.NewCertPool()
caPool.AppendCertsFromPEM(caCert)
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
log.Fatalf("load client cert: %v", err)
}
tlsConfig := &tls.Config{
RootCAs: caPool,
Certificates: []tls.Certificate{cert},
ServerName: serverName,
}
vhostPath := vhost
if !strings.HasPrefix(vhostPath, "/") {
vhostPath = "/" + vhostPath
}
amqpURL := url.URL{
Scheme: "amqps",
User: url.UserPassword(username, password),
Host: net.JoinHostPort(host, port),
Path: vhostPath,
}
conn, err := amqp.DialTLS(amqpURL.String(), tlsConfig)
if err != nil {
log.Fatalf("dial: %v", err)
}
defer conn.Close()
ch, err := conn.Channel()
if err != nil {
log.Fatalf("channel: %v", err)
}
if err := ch.Qos(prefetch, 0, false); err != nil {
log.Fatalf("qos: %v", err)
}
done := make(chan struct{})
for queue, role := range queueRoles {
// QueueDeclarePassive 只检查队列存在,不主动创建——真实队列由 PetaX 预先建好。
if _, err := ch.QueueDeclarePassive(queue, true, false, false, false, nil); err != nil {
log.Fatalf("passive declare %q: %v", queue, err)
}
deliveries, err := ch.Consume(queue, "", false, false, false, false, nil)
if err != nil {
log.Fatalf("consume %q: %v", queue, err)
}
go func(role string) {
for d := range deliveries {
handleMessage(d, role)
}
}(role)
}
names := make([]string, 0, len(queueRoles))
for q := range queueRoles {
names = append(names, q)
}
log.Printf("listening on %v", names)
<-done
}
Python
依赖:pip install pika。运行:python3 consumer.py。
# consumer.py — PetaX RabbitMQ 订阅 Demo (Python, pika)
# 运行:pip install pika && python3 consumer.py
# 注意:字段形状是基于 HTTP 回调 DTO 的假设,未经真实 PetaX 消息验证,
# 尤其 notify_issue_result 的 id 字段可能是 issueId 而非 roundId,接入前务必用真实消息核对。
import json
import os
import ssl
import pika
HOST = os.environ.get("RABBITMQ_HOST", "localhost")
PORT = int(os.environ.get("RABBITMQ_PORT", "5671"))
VHOST = os.environ.get("RABBITMQ_VHOST", "/")
USERNAME = os.environ.get("RABBITMQ_USERNAME", "guest")
PASSWORD = os.environ.get("RABBITMQ_PASSWORD", "guest")
CA_CERT = os.environ.get("RABBITMQ_CA_CERT", "ca_certificate.pem")
CLIENT_CERT = os.environ.get("RABBITMQ_CLIENT_CERT", "client_certificate.pem")
CLIENT_KEY = os.environ.get("RABBITMQ_CLIENT_KEY", "client_key.pem")
SERVER_NAME = os.environ.get("RABBITMQ_SERVER_NAME", "")
# 三个队列名默认值是官方文档已公开的示例格式;真实队列名由 PetaX 分配,
# 务必以 PetaX 提供的为准,不要假设跟这里的示例值相同。
QUEUE_ROLES = {
os.environ.get("RABBITMQ_QUEUE_SETTLED_WIN", "staging.10102.merchant102_server.settled_win.q"): "settled_ticket",
os.environ.get("RABBITMQ_QUEUE_SETTLED_LOSS", "staging.10102.merchant102_server.settled_loss.q"): "settled_ticket",
os.environ.get("RABBITMQ_QUEUE_ISSUE_RESULT", "staging.10102.merchant102_server.notify_issue_result.q"): "settled_round",
}
# 仅用于 Demo:记录已经"模拟失败过一次"的 key(ticketId 或 roundId),
# 第二次投递(redelivered=true)就放行成功。真实商户不需要这个逻辑,
# 这里只是让 demo 能演示 nack+requeue 而不会无限热循环重投同一条坏消息。
failed_once = set()
def handle_message(channel, method, properties, body, role):
redelivered = method.redelivered
try:
payload = json.loads(body)
except json.JSONDecodeError:
# 1. 解析失败 = 永久错误:ack 丢弃,避免毒消息卡队列。
print(f"[bad-json] discarding, redelivered={redelivered}", flush=True)
channel.basic_ack(method.delivery_tag)
return
if role == "settled_ticket":
# settled_win/settled_loss 假定按 SettledTicketRequest 字段解析。
key = payload.get("ticketId")
print(f"[settled_ticket] ticketId={key} status={payload.get('status')} ccy={payload.get('ccy')} redelivered={redelivered}", flush=True)
else:
# notify_issue_result 假定按 SettledRoundRequest 字段解析;roundId 字段名
# 是本仓库运行手册标注的最大不确定性来源,真实接入前务必用真实消息核对。
key = payload.get("roundId")
print(f"[settled_round] roundId={key} result={payload.get('result')} redelivered={redelivered}", flush=True)
if key is None:
print("[warn] missing key field, discarding (ack)", flush=True)
channel.basic_ack(method.delivery_tag)
return
# 2. 第一次见到这个 key:模拟一次瞬时处理失败,nack + requeue。
# 只失败一次,避免同一条消息被无限重投形成热循环。
if key not in failed_once:
failed_once.add(key)
print(f"[simulate-failure] {key} -> nack+requeue", flush=True)
channel.basic_nack(method.delivery_tag, requeue=True)
return
# 3. 第二次投递(redelivered=true):放行成功,ack。
print(f"[success] {key} -> ack", flush=True)
channel.basic_ack(method.delivery_tag)
def main():
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.load_verify_locations(CA_CERT)
context.load_cert_chain(CLIENT_CERT, CLIENT_KEY)
ssl_options = pika.SSLOptions(context, server_hostname=SERVER_NAME or HOST)
credentials = pika.PlainCredentials(USERNAME, PASSWORD)
parameters = pika.ConnectionParameters(
host=HOST, port=PORT, virtual_host=VHOST,
credentials=credentials, ssl_options=ssl_options,
)
connection = pika.BlockingConnection(parameters)
channel = connection.channel()
channel.basic_qos(prefetch_count=int(os.environ.get("RABBITMQ_PREFETCH", "10")))
for queue, role in QUEUE_ROLES.items():
# passive=True 只检查队列存在,不主动创建——真实队列由 PetaX 预先建好。
channel.queue_declare(queue=queue, passive=True)
channel.basic_consume(
queue=queue,
on_message_callback=lambda ch, method, properties, body, role=role: handle_message(ch, method, properties, body, role),
)
print(f"listening on {', '.join(QUEUE_ROLES.keys())}", flush=True)
channel.start_consuming()
if __name__ == "__main__":
main()