欢迎光临
我们一直在努力

八、实战项目——01-电商平台后端/05-支付服务实现

在这里插入图片描述

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');
    }
    }


    赞(0)
    未经允许不得转载:171主机测评 » 八、实战项目——01-电商平台后端/05-支付服务实现
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址