基于Node.js+React的Stripe钱包API开发需求求助
基于Stripe的多用户门户支付解决方案(Node.js + React)
后端(Node.js)实现
核心依赖
stripe:Stripe官方SDKexpress:API框架mongoose(或你使用的ORM):用户/钱包/交易数据存储cors:跨域支持
1. 基础数据模型设计
先定义核心数据模型,确保业务数据和Stripe数据关联:
// models/Wallet.js const mongoose = require('mongoose'); const WalletSchema = new mongoose.Schema({ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', unique: true }, balance: { type: Number, default: 0 }, // 单位:分(与Stripe统一) stripeCustomerId: { type: String, required: true } }); // models/Transaction.js const TransactionSchema = new mongoose.Schema({ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, type: { type: String, enum: ['deposit', 'withdrawal', 'transfer_in', 'transfer_out'] }, amount: { type: Number, required: true }, stripeTransferId: { type: String }, // 提现时关联的Stripe转账ID targetUserId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, // 转账目标用户ID createdAt: { type: Date, default: Date.now } });
2. Stripe初始化配置
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
3. API功能实现
获取钱包余额
app.get('/api/wallet/balance', async (req, res) => { const { userId } = req.user; // 假设已通过JWT完成用户身份验证 try { const wallet = await Wallet.findOne({ userId }).select('balance'); if (!wallet) return res.status(404).json({ error: '钱包不存在' }); res.json({ balance: wallet.balance / 100 }); // 转换为元返回给前端 } catch (err) { res.status(500).json({ error: err.message }); } });
提现到用户绑定银行卡
app.post('/api/wallet/withdraw', async (req, res) => { const { userId } = req.user; const { amount } = req.body; // 前端传入单位:元 const amountInCents = Math.round(amount * 100); try { const wallet = await Wallet.findOne({ userId }); if (!wallet) return res.status(404).json({ error: '钱包不存在' }); if (wallet.balance < amountInCents) return res.status(400).json({ error: '余额不足' }); // 调用Stripe API发起转账(需用户已绑定支付方式到对应Stripe Customer) const transfer = await stripe.transfers.create({ amount: amountInCents, currency: 'usd', // 根据业务调整币种 destination: wallet.stripeCustomerId, }); // 更新钱包余额并生成交易记录 wallet.balance -= amountInCents; await wallet.save(); await Transaction.create({ userId, type: 'withdrawal', amount: amountInCents, stripeTransferId: transfer.id }); res.json({ success: true, transferId: transfer.id }); } catch (err) { res.status(500).json({ error: err.message }); } });
获取交易记录
app.get('/api/wallet/transactions', async (req, res) => { const { userId } = req.user; const { page = 1, limit = 10 } = req.query; try { const transactions = await Transaction.find({ userId }) .sort({ createdAt: -1 }) .limit(limit * 1) .skip((page - 1) * limit) .populate('targetUserId', 'name email'); // 关联展示转账目标用户信息 // 转换金额为元,便于前端展示 const formattedTransactions = transactions.map(tx => ({ ...tx._doc, amount: tx.amount / 100 })); res.json(formattedTransactions); } catch (err) { res.status(500).json({ error: err.message }); } });
内部转账给其他用户
app.post('/api/wallet/transfer', async (req, res) => { const { userId } = req.user; const { targetUserId, amount } = req.body; // amount单位:元 const amountInCents = Math.round(amount * 100); if (userId === targetUserId) return res.status(400).json({ error: '不能转账给自己' }); try { // 开启数据库事务,保证转账操作原子性 const session = await mongoose.startSession(); session.startTransaction(); const senderWallet = await Wallet.findOne({ userId }).session(session); const receiverWallet = await Wallet.findOne({ userId: targetUserId }).session(session); if (!senderWallet) { await session.abortTransaction(); return res.status(404).json({ error: '发送方钱包不存在' }); } if (!receiverWallet) { await session.abortTransaction(); return res.status(404).json({ error: '接收方钱包不存在' }); } if (senderWallet.balance < amountInCents) { await session.abortTransaction(); return res.status(400).json({ error: '余额不足' }); } // 更新双方钱包余额 senderWallet.balance -= amountInCents; receiverWallet.balance += amountInCents; await senderWallet.save({ session }); await receiverWallet.save({ session }); // 生成双方交易记录 await Transaction.create([ { userId, type: 'transfer_out', amount: amountInCents, targetUserId }, { userId: targetUserId, type: 'transfer_in', amount: amountInCents, targetUserId: userId } ], { session }); await session.commitTransaction(); res.json({ success: true }); } catch (err) { await session.abortTransaction(); res.status(500).json({ error: err.message }); } });
前端(React.js)实现
核心依赖
axios:API请求工具stripe-js:Stripe前端SDK(用于用户绑定支付方式)
1. 钱包余额组件
import { useEffect, useState } from 'react'; import axios from 'axios'; const WalletBalance = () => { const [balance, setBalance] = useState(0); useEffect(() => { const fetchBalance = async () => { const res = await axios.get('/api/wallet/balance', { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }); setBalance(res.data.balance); }; fetchBalance(); }, []); return ( <div className="wallet-balance"> <h3>当前余额</h3> <p className="balance-amount">${balance.toFixed(2)}</p> </div> ); }; export default WalletBalance;
2. 提现组件
import { useState } from 'react'; import axios from 'axios'; const WithdrawForm = () => { const [amount, setAmount] = useState(''); const [loading, setLoading] = useState(false); const [message, setMessage] = useState(''); const handleSubmit = async (e) => { e.preventDefault(); setLoading(true); setMessage(''); try { await axios.post('/api/wallet/withdraw', { amount }, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }); setMessage('提现申请已提交,处理完成后将到账你的银行卡'); setAmount(''); } catch (err) { setMessage(err.response.data.error || '提现失败,请稍后重试'); } finally { setLoading(false); } }; return ( <form onSubmit={handleSubmit} className="withdraw-form"> <h3>提现到银行卡</h3> <input type="number" step="0.01" value={amount} onChange={(e) => setAmount(e.target.value)} placeholder="输入提现金额" required disabled={loading} /> <button type="submit" disabled={loading}> {loading ? '处理中...' : '提交提现'} </button> {message && <p className={message.includes('失败') ? 'error' : 'success'}>{message}</p>} </form> ); }; export default WithdrawForm;
3. 交易记录组件
import { useEffect, useState } from 'react'; import axios from 'axios'; const TransactionHistory = () => { const [transactions, setTransactions] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { const fetchTransactions = async () => { const res = await axios.get('/api/wallet/transactions', { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }); setTransactions(res.data); setLoading(false); }; fetchTransactions(); }, []); if (loading) return <div>加载中...</div>; return ( <div className="transaction-history"> <h3>交易记录</h3> <table> <thead> <tr> <th>时间</th> <th>类型</th> <th>金额</th> <th>对方</th> </tr> </thead> <tbody> {transactions.map(tx => ( <tr key={tx._id}> <td>{new Date(tx.createdAt).toLocaleString()}</td> <td> {tx.type === 'deposit' && '充值'} {tx.type === 'withdrawal' && '提现'} {tx.type === 'transfer_out' && '转出'} {tx.type === 'transfer_in' && '转入'} </td> <td>{tx.type.includes('out') || tx.type === 'withdrawal' ? '-' : '+'}${tx.amount.toFixed(2)}</td> <td>{tx.targetUserId ? tx.targetUserId.name || tx.targetUserId.email : '-'}</td> </tr> ))} </tbody> </table> </div> ); }; export default TransactionHistory;
4. 内部转账组件
import { useState } from 'react'; import axios from 'axios'; const TransferForm = () => { const [targetEmail, setTargetEmail] = useState(''); const [amount, setAmount] = useState(''); const [loading, setLoading] = useState(false); const [message, setMessage] = useState(''); const handleSubmit = async (e) => { e.preventDefault(); setLoading(true); setMessage(''); try { // 通过邮箱查询目标用户ID(需后端提供对应接口) const userRes = await axios.get(`/api/users/by-email?email=${targetEmail}`, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }); const targetUserId = userRes.data._id; await axios.post('/api/wallet/transfer', { targetUserId, amount }, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }); setMessage('转账成功'); setTargetEmail(''); setAmount(''); } catch (err) { setMessage(err.response.data.error || '转账失败,请稍后重试'); } finally { setLoading(false); } }; return ( <form onSubmit={handleSubmit} className="transfer-form"> <h3>内部转账</h3> <input type="email" value={targetEmail} onChange={(e) => setTargetEmail(e.target.value)} placeholder="输入对方邮箱" required disabled={loading} /> <input type="number" step="0.01" value={amount} onChange={(e) => setAmount(e.target.value)} placeholder="输入转账金额" required disabled={loading} /> <button type="submit" disabled={loading}> {loading ? '处理中...' : '提交转账'} </button> {message && <p className={message.includes('失败') ? 'error' : 'success'}>{message}</p>} </form> ); }; export default TransferForm;
关键注意事项
- Stripe用户绑定:用户首次使用钱包时,需通过Stripe Elements创建Stripe Customer并绑定支付方式,否则无法发起提现。
- 金额精度:始终以分作为存储单位,避免浮点运算误差,前端展示时转换为元。
- 事务保障:内部转账必须使用数据库事务,确保余额更新和记录生成的原子性。
- 身份验证:所有API接口需添加JWT等身份验证机制,防止非法操作。
- 错误处理:前后端需完善异常捕获和用户提示,覆盖余额不足、用户不存在等场景。
内容的提问来源于stack exchange,提问作者sharans singh
相关产品推荐
相关产品推荐

