You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Node.js+React的Stripe钱包API开发需求求助

基于Stripe的多用户门户支付解决方案(Node.js + React)

后端(Node.js)实现

核心依赖

  • stripe:Stripe官方SDK
  • express: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 21:25:12