
08-实战项目/01-电商平台后端/05-支付服务实现
支付服务实现
学习目标
- 掌握支付服务架构设计
- 理解支付宝/微信支付集成
- 学会支付回调安全验证
- 实现退款处理
前置知识
- 微服务架构
- 消息队列
- 支付安全基础
知识点列表
1. 支付服务架构
1.1 服务模块划分
// payment-service/src/modules/index.js
const PaymentModules = {
// 支付模块
payment: {
create: '创建支付订单',
query: '查询支付状态',
callback: '支付回调处理',
refund: '申请退款'
},
// 渠道管理
channel: {
alipay: '支付宝支付',
wechat: '微信支付',
bankCard: '银行卡支付'
},
// 账户管理
account: {
balance: '余额支付',
recharge: '账户充值',
withdraw: '提现申请'
},
// 账单管理
bill: {
generate: '生成账单',
statement: '对账文件',
reconciliation: '对账处理'
}
};
2. 数据模型设计
— payment-service/src/models/schema.sql
— 支付订单表
CREATE TABLE payment_orders (
id BIGSERIAL PRIMARY KEY,
payment_no VARCHAR(32) UNIQUE NOT NULL,
order_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
currency VARCHAR(3) DEFAULT 'CNY',
channel VARCHAR(20) NOT NULL,
channel_order_no VARCHAR(64),
status VARCHAR(20) DEFAULT 'pending',
subject VARCHAR(200) NOT NULL,
body TEXT,
expire_time TIMESTAMP,
pay_time TIMESTAMP,
refund_amount DECIMAL(10,2) DEFAULT 0,
refund_time TIMESTAMP,
client_ip VARCHAR(45),
user_agent TEXT,
return_url VARCHAR(500),
notify_url VARCHAR(500),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
— 支付回调记录表
CREATE TABLE payment_callbacks (
id BIGSERIAL PRIMARY KEY,
payment_no VARCHAR(32) NOT NULL,
callback_data JSONB,
callback_status VARCHAR(20) DEFAULT 'pending',
retry_count INTEGER DEFAULT 0,
error_message TEXT,
processed_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
— 退款记录表
CREATE TABLE refund_records (
id BIGSERIAL PRIMARY KEY,
refund_no VARCHAR(32) UNIQUE NOT NULL,
payment_no VARCHAR(32) NOT NULL,
order_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
reason VARCHAR(500),
status VARCHAR(20) DEFAULT 'pending',
channel_refund_no VARCHAR(64),
refund_time TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
— 用户账户表
CREATE TABLE user_accounts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT UNIQUE NOT NULL,
balance DECIMAL(10,2) DEFAULT 0,
frozen_balance DECIMAL(10,2) DEFAULT 0,
total_recharge DECIMAL(10,2) DEFAULT 0,
total_withdraw DECIMAL(10,2) DEFAULT 0,
total_payment DECIMAL(10,2) DEFAULT 0,
version INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
— 交易流水表
CREATE TABLE transaction_records (
id BIGSERIAL PRIMARY KEY,
transaction_no VARCHAR(32) UNIQUE NOT NULL,
user_id BIGINT NOT NULL,
type VARCHAR(20) NOT NULL,
amount DECIMAL(10,2) NOT NULL,
balance_before DECIMAL(10,2) NOT NULL,
balance_after DECIMAL(10,2) NOT NULL,
related_no VARCHAR(32),
description VARCHAR(200),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
— 创建索引
CREATE INDEX idx_payment_orders_order_id ON payment_orders(order_id);
CREATE INDEX idx_payment_orders_user_id ON payment_orders(user_id);
CREATE INDEX idx_payment_orders_status ON payment_orders(status);
CREATE INDEX idx_payment_orders_payment_no ON payment_orders(payment_no);
CREATE INDEX idx_refund_records_payment_no ON refund_records(payment_no);
CREATE INDEX idx_refund_records_order_id ON refund_records(order_id);
CREATE INDEX idx_transaction_records_user_id ON transaction_records(user_id);
3. 支付服务实现
// payment-service/src/services/payment.service.js
const crypto = require('crypto');
const axios = require('axios');
const { v4: uuidv4 } = require('uuid');
class PaymentService {
constructor(models, redisClient, mqClient, config) {
this.models = models;
this.redis = redisClient;
this.mq = mqClient;
this.config = config;
this.channels = {
alipay: new AlipayChannel(config.alipay),
wechat: new WechatChannel(config.wechat)
};
}
async createPayment(orderData) {
const {
orderId,
userId,
amount,
channel,
subject,
body,
returnUrl,
notifyUrl,
clientIp
} = orderData;
// 生成支付单号
const paymentNo = this.generatePaymentNo();
// 创建支付订单
const payment = await this.models.PaymentOrder.create({
paymentNo,
orderId,
userId,
amount,
channel,
subject,
body,
returnUrl,
notifyUrl,
clientIp,
expireTime: new Date(Date.now() + 30 * 60 * 1000) // 30分钟过期
});
// 调用第三方支付接口
const channelInstance = this.channels[channel];
if (!channelInstance) {
throw new Error(`Unsupported payment channel: ${channel}`);
}
const payResult = await channelInstance.createOrder({
outTradeNo: paymentNo,
totalAmount: amount,
subject,
body,
returnUrl,
notifyUrl,
clientIp
});
// 更新支付订单
payment.channelOrderNo = payResult.channelOrderNo;
await payment.save();
// 缓存支付信息
await this.cachePayment(paymentNo, payment);
return {
paymentNo,
channelOrderNo: payResult.channelOrderNo,
payUrl: payResult.payUrl,
qrCode: payResult.qrCode
};
}
async queryPayment(paymentNo) {
// 先查缓存
let payment = await this.getCachedPayment(paymentNo);
if (!payment) {
payment = await this.models.PaymentOrder.findOne({
where: { paymentNo }
});
}
if (!payment) {
throw new Error('Payment order not found');
}
// 如果状态不是终态,查询第三方
if (!this.isFinalStatus(payment.status)) {
const channel = this.channels[payment.channel];
const channelResult = await channel.queryOrder(payment.channelOrderNo);
if (channelResult.status !== payment.status) {
await this.updatePaymentStatus(payment, channelResult.status, channelResult);
}
}
return payment;
}
async handleCallback(channel, callbackData) {
// 验证签名
const channelInstance = this.channels[channel];
const isValid = await channelInstance.verifySignature(callbackData);
if (!isValid) {
throw new Error('Invalid signature');
}
// 获取支付单号
const paymentNo = channelInstance.getOrderNo(callbackData);
const channelStatus = channelInstance.getStatus(callbackData);
// 查询支付订单
const payment = await this.models.PaymentOrder.findOne({
where: { paymentNo }
});
if (!payment) {
throw new Error('Payment order not found');
}
// 记录回调
await this.models.PaymentCallback.create({
paymentNo,
callbackData,
callbackStatus: 'received'
});
// 更新支付状态
let paymentStatus;
switch (channelStatus) {
case 'SUCCESS':
paymentStatus = 'success';
break;
case 'FAILED':
paymentStatus = 'failed';
break;
case 'CLOSED':
paymentStatus = 'closed';
break;
default:
paymentStatus = 'pending';
}
if (paymentStatus !== payment.status) {
await this.updatePaymentStatus(payment, paymentStatus, callbackData);
}
return { success: true };
}
async updatePaymentStatus(payment, status, channelData) {
const oldStatus = payment.status;
payment.status = status;
if (status === 'success') {
payment.payTime = new Date();
}
await payment.save();
// 发送支付结果通知
await this.mq.publish('payment.status.changed', {
paymentNo: payment.paymentNo,
orderId: payment.orderId,
userId: payment.userId,
oldStatus,
newStatus: status,
channelData
});
// 如果是支付成功,处理账户和订单
if (status === 'success') {
await this.handlePaymentSuccess(payment, channelData);
}
// 更新缓存
await this.cachePayment(payment.paymentNo, payment);
}
async handlePaymentSuccess(payment, channelData) {
// 更新用户账户
await this.updateUserBalance(payment.userId, payment.amount, 'pay');
// 记录交易流水
await this.createTransactionRecord({
userId: payment.userId,
type: 'payment',
amount: payment.amount,
relatedNo: payment.paymentNo,
description: `支付订单 ${payment.orderId}`
});
// 发送支付成功事件
await this.mq.publish('order.payment.success', {
orderId: payment.orderId,
paymentNo: payment.paymentNo,
amount: payment.amount,
channel: payment.channel,
payTime: payment.payTime
});
}
async refund(paymentNo, amount, reason) {
const payment = await this.models.PaymentOrder.findOne({
where: { paymentNo }
});
if (!payment) {
throw new Error('Payment order not found');
}
if (payment.status !== 'success') {
throw new Error('Payment not successful');
}
if (payment.refundAmount + amount > payment.amount) {
throw new Error('Refund amount exceeds payment amount');
}
// 创建退款记录
const refundNo = this.generateRefundNo();
const refund = await this.models.RefundRecord.create({
refundNo,
paymentNo,
orderId: payment.orderId,
userId: payment.userId,
amount,
reason,
status: 'pending'
});
// 调用第三方退款接口
const channel = this.channels[payment.channel];
const refundResult = await channel.refund({
outTradeNo: paymentNo,
outRefundNo: refundNo,
refundAmount: amount,
totalAmount: payment.amount
});
// 更新退款状态
refund.status = 'success';
refund.channelRefundNo = refundResult.channelRefundNo;
refund.refundTime = new Date();
await refund.save();
// 更新支付订单退款金额
payment.refundAmount += amount;
await payment.save();
// 更新用户账户
await this.updateUserBalance(payment.userId, amount, 'refund');
// 记录交易流水
await this.createTransactionRecord({
userId: payment.userId,
type: 'refund',
amount: amount,
relatedNo: refundNo,
description: `退款 ${payment.orderId},原因:${reason}`
});
// 发送退款事件
await this.mq.publish('order.payment.refund', {
orderId: payment.orderId,
paymentNo,
refundNo,
amount,
reason
});
return refund;
}
async updateUserBalance(userId, amount, type) {
const transaction = await this.models.sequelize.transaction();
try {
const account = await this.models.UserAccount.findOne({
where: { userId },
lock: transaction.LOCK.UPDATE,
transaction
});
if (!account) {
throw new Error('Account not found');
}
const oldBalance = account.balance;
if (type === 'pay') {
if (account.balance < amount) {
throw new Error('Insufficient balance');
}
account.balance -= amount;
} else if (type === 'refund') {
account.balance += amount;
}
account.version += 1;
await account.save({ transaction });
await transaction.commit();
// 清除账户缓存
await this.redis.del(`user:balance:${userId}`);
return {
oldBalance,
newBalance: account.balance
};
} catch (error) {
await transaction.rollback();
throw error;
}
}
async getBalance(userId) {
const cached = await this.redis.get(`user:balance:${userId}`);
if (cached) {
return JSON.parse(cached);
}
const account = await this.models.UserAccount.findOne({
where: { userId }
});
const balance = account ? account.balance : 0;
await this.redis.setex(`user:balance:${userId}`, 60, balance);
return balance;
}
async createTransactionRecord(data) {
const transactionNo = this.generateTransactionNo();
const account = await this.models.UserAccount.findOne({
where: { userId: data.userId }
});
const balanceBefore = account ? account.balance : 0;
const balanceAfter = data.type === 'pay'
? balanceBefore – data.amount
: balanceBefore + data.amount;
await this.models.TransactionRecord.create({
transactionNo,
userId: data.userId,
type: data.type,
amount: data.amount,
balanceBefore,
balanceAfter,
relatedNo: data.relatedNo,
description: data.description
});
}
async cachePayment(paymentNo, payment) {
await this.redis.setex(
`payment:${paymentNo}`,
3600,
JSON.stringify(payment)
);
}
async getCachedPayment(paymentNo) {
const cached = await this.redis.get(`payment:${paymentNo}`);
return cached ? JSON.parse(cached) : null;
}
isFinalStatus(status) {
return ['success', 'failed', 'closed'].includes(status);
}
generatePaymentNo() {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 8).toUpperCase();
return `PAY${timestamp}${random}`;
}
generateRefundNo() {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 8).toUpperCase();
return `REF${timestamp}${random}`;
}
generateTransactionNo() {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 8).toUpperCase();
return `TXN${timestamp}${random}`;
}
}
module.exports = PaymentService;
4. 支付宝支付集成
// payment-service/src/channels/alipay.channel.js
const AlipaySdk = require('alipay-sdk').default;
const AlipayFormData = require('alipay-sdk/lib/form').default;
class AlipayChannel {
constructor(config) {
this.alipay = new AlipaySdk({
appId: config.appId,
privateKey: config.privateKey,
alipayPublicKey: config.alipayPublicKey,
gateway: config.gateway || 'https://openapi.alipay.com/gateway.do',
signType: 'RSA2'
});
this.notifyUrl = config.notifyUrl;
this.returnUrl = config.returnUrl;
}
async createOrder(params) {
const {
outTradeNo,
totalAmount,
subject,
body,
returnUrl,
notifyUrl,
clientIp
} = params;
const formData = new AlipayFormData();
formData.setMethod('get');
formData.addField('bizContent', {
outTradeNo,
productCode: 'FAST_INSTANT_TRADE_PAY',
totalAmount: totalAmount.toString(),
subject,
body,
timeoutExpress: '30m'
});
const result = await this.alipay.exec(
'alipay.trade.page.pay',
{},
{ formData }
);
return {
channelOrderNo: outTradeNo,
payUrl: result,
qrCode: null
};
}
async queryOrder(outTradeNo) {
const result = await this.alipay.exec('alipay.trade.query', {
bizContent: { outTradeNo }
});
let status = 'pending';
if (result.trade_status === 'TRADE_SUCCESS' ||
result.trade_status === 'TRADE_FINISHED') {
status = 'success';
} else if (result.trade_status === 'TRADE_CLOSED') {
status = 'closed';
}
return {
status,
channelOrderNo: result.out_trade_no,
tradeNo: result.trade_no,
totalAmount: result.total_amount,
payTime: result.send_pay_date
};
}
async refund(params) {
const { outTradeNo, outRefundNo, refundAmount, totalAmount } = params;
const result = await this.alipay.exec('alipay.trade.refund', {
bizContent: {
outTradeNo,
outRequestNo: outRefundNo,
refundAmount: refundAmount.toString(),
refundReason: params.reason || '用户申请退款'
}
});
return {
success: result.code === '10000',
channelRefundNo: result.trade_no,
refundAmount: result.refund_fee
};
}
async verifySignature(params) {
return this.alipay.checkNotifySign(params);
}
getOrderNo(params) {
return params.out_trade_no;
}
getStatus(params) {
const tradeStatus = params.trade_status;
if (tradeStatus === 'TRADE_SUCCESS' || tradeStatus === 'TRADE_FINISHED') {
return 'SUCCESS';
}
if (tradeStatus === 'TRADE_CLOSED') {
return 'CLOSED';
}
return 'PENDING';
}
}
module.exports = AlipayChannel;
5. 微信支付集成
// payment-service/src/channels/wechat.channel.js
const axios = require('axios');
const crypto = require('crypto');
const wxpay = require('wechatpay-node-v3');
class WechatChannel {
constructor(config) {
this.config = config;
this.pay = new wxpay({
appid: config.appId,
mchid: config.mchId,
key: config.apiKey,
publicKey: config.publicKey,
privateKey: config.privateKey
});
}
async createOrder(params) {
const {
outTradeNo,
totalAmount,
subject,
body,
returnUrl,
notifyUrl,
clientIp
} = params;
const result = await this.pay.transactions_native({
description: subject,
out_trade_no: outTradeNo,
notify_url: notifyUrl,
amount: {
total: Math.round(totalAmount * 100),
currency: 'CNY'
},
payer: {
client_ip: clientIp
}
});
return {
channelOrderNo: result.prepay_id,
payUrl: result.code_url,
qrCode: result.code_url
};
}
async queryOrder(outTradeNo) {
const result = await this.pay.query(outTradeNo, {
query_by: 'out_trade_no'
});
let status = 'pending';
if (result.trade_state === 'SUCCESS') {
status = 'success';
} else if (result.trade_state === 'REFUND') {
status = 'refund';
} else if (result.trade_state === 'CLOSED') {
status = 'closed';
}
return {
status,
channelOrderNo: result.out_trade_no,
tradeNo: result.transaction_id,
totalAmount: result.amount.total / 100,
payTime: result.success_time
};
}
async refund(params) {
const { outTradeNo, outRefundNo, refundAmount, totalAmount } = params;
const result = await this.pay.refund({
out_trade_no: outTradeNo,
out_refund_no: outRefundNo,
amount: {
refund: Math.round(refundAmount * 100),
total: Math.round(totalAmount * 100),
currency: 'CNY'
},
reason: params.reason || '用户申请退款'
});
return {
success: result.status === 'SUCCESS',
channelRefundNo: result.refund_id,
refundAmount: result.amount.refund / 100
};
}
async verifySignature(params, body, signature) {
const signStr = `${params.timestamp}\\n${params.nonce}\\n${body}\\n`;
const expected = crypto
.createHmac('sha256', this.config.apiKey)
.update(signStr)
.digest('hex');
return expected === signature;
}
getOrderNo(params) {
return params.out_trade_no;
}
getStatus(params) {
if (params.trade_state === 'SUCCESS') {
return 'SUCCESS';
}
if (params.trade_state === 'CLOSED') {
return 'CLOSED';
}
if (params.trade_state === 'REFUND') {
return 'REFUND';
}
return 'PENDING';
}
}
module.exports = WechatChannel;
6. 控制器实现
// payment-service/src/controllers/payment.controller.js
class PaymentController {
constructor(paymentService) {
this.paymentService = paymentService;
}
createPayment = async (req, res) => {
try {
const result = await this.paymentService.createPayment({
…req.body,
userId: req.user.id,
clientIp: req.ip
});
res.json({
success: true,
data: result
});
} catch (error) {
res.status(400).json({
success: false,
message: error.message
});
}
};
queryPayment = async (req, res) => {
try {
const { paymentNo } = req.params;
const payment = await this.paymentService.queryPayment(paymentNo);
res.json({
success: true,
data: payment
});
} catch (error) {
res.status(404).json({
success: false,
message: error.message
});
}
};
alipayCallback = async (req, res) => {
try {
await this.paymentService.handleCallback('alipay', req.body);
res.send('success');
} catch (error) {
console.error('Alipay callback error:', error);
res.send('failure');
}
};
wechatCallback = async (req, res) => {
try {
const xml = req.body;
const result = await this.paymentService.handleCallback('wechat', xml);
res.send(`
<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg>
</xml>
`);
} catch (error) {
res.send(`
<xml>
<return_code><![CDATA[FAIL]]></return_code>
<return_msg><![CDATA[${error.message}]]></return_msg>
</xml>
`);
}
};
refund = async (req, res) => {
try {
const { paymentNo, amount, reason } = req.body;
const result = await this.paymentService.refund(paymentNo, amount, reason);
res.json({
success: true,
data: result
});
} catch (error) {
res.status(400).json({
success: false,
message: error.message
});
}
};
getBalance = async (req, res) => {
try {
const balance = await this.paymentService.getBalance(req.user.id);
res.json({
success: true,
data: { balance }
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
};
recharge = async (req, res) => {
try {
const { amount } = req.body;
const result = await this.paymentService.createPayment({
userId: req.user.id,
amount,
channel: 'balance',
subject: '账户充值',
body: `用户${req.user.id}充值${amount}元`,
clientIp: req.ip
});
res.json({
success: true,
data: result
});
} catch (error) {
res.status(400).json({
success: false,
message: error.message
});
}
};
}
module.exports = PaymentController;
练习题
基础题
进阶题
挑战题
练习题参考答案
基础题1
CREATE TABLE payment_orders (
id BIGSERIAL PRIMARY KEY,
payment_no VARCHAR(32) UNIQUE NOT NULL,
order_id BIGINT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status VARCHAR(20) DEFAULT 'pending'
);
基础题2
async createPayment(data) {
const payment = await PaymentOrder.create(data);
const payUrl = await channel.createOrder(payment);
return { payment, payUrl };
}
基础题3
async handleCallback(data) {
if (verifySign(data)) {
await updatePaymentStatus(data.out_trade_no, 'success');
}
}


