Stripe支付01-demo项目介绍

Stripe支付 在国内的相关资料非常少, 包括了B站,CSDN中的示例大多数都是比较老旧,也都不系统。这个demo项目是一个前后端都带的示例。基本看明白过程, 拿过来就能用。

首先,来个项目介绍:

本项目基于前端vue+vite构建,后端使用springboot构建的web接口项目

完成的功能如下:

1. 支持简单的用户登录和注册

2. 绑定信用卡

3. 使用信用卡支付

4. 使用微信或支付宝支付

1. 官方文档地址

Stripe 文档icon-default.png?t=N7T8https://stripe.com/docs

2. Stripe 的官方示例github地址 

Stripe Samples · GitHubicon-default.png?t=N7T8https://github.com/stripe-samples

3. 实现首页、用户注册、登录

本项目的前端我是基于官方示例

GitHub - stripe-samples/accept-a-payment: Learn how to accept a payment from customers around the world with a variety of payment methods.icon-default.png?t=N7T8https://github.com/stripe-samples/accept-a-payment的路径 (accept-a-payment/payment-element/client/vue-cva )的一个vue项目来构建的。自己创建一个也可以, 都无所谓了

首页

首先是要写一个Index.vue

HTML部分, 很简单就是要展示几个功能  退出登录,绑定银行卡,使用支付宝或微信支付,去支付(是指的用以绑定的信用卡支付)

<template>
  <main>
    <h1>Well Come Stripe Demo</h1> 
    <ul>
      <li @click="handleExit">
        退出登录
      </li>
      <li>
        <a href="/bindCard">绑定银行卡</a>
      </li>
      <li>
        <a href="/useAlipayOrWechat">使用支付宝/微信支付</a>
      </li>
      <li>
        <a href="/pay">去支付</a>
      </li>
    </ul>     
  </main>
</template>

js部分

  <script setup>
  import { onMounted } from "vue";

  
  onMounted(async () => {
    const name = sessionStorage.getItem('name')
    // 判断 token 是否存在
    if (!name) {
    // 如果 token 不存在,跳转到登录页面
    window.location.href = '/login';  // 你需要替换成你的登录页面的实际路径
    }

  });
  const handleExit = async () => {
    console.log('exit')
    sessionStorage.clear()
    window.location.href = '/login'; 
  }
  
  </script>

首先onMounted部分当页面加载完成后, 检查本地存储是否有name属性, 如果没有就跳转到登录

基于退出登录@click="handleExit"  实现一个函数用于清空登录信息,并跳转到登录页面。

登录

创建一个login.vue,仍然是两部分 html和js部分

<template>
  <main>
    <h1>登录页面</h1>

    <form id="payment-form" @submit.prevent="handleSubmit">
      <div>
        <input id="name">
      </div>
      
      <button 
        id="submit"
        :disabled="isLoading"
      >
        登录
      </button>
      <sr-messages :messages="messages" />
    </form>
    <div>
      <a href="/registry">没有账号去注册</a>
    </div>
  </main>
</template>

这里就是定义了一个表单,并且有一个登录按钮, 表单监听了提交,并使用一个函数来处理

<script setup>
import { ref } from "vue";

import SrMessages from "./SrMessages.vue";

const isLoading = ref(false);
const messages = ref([]);

const handleSubmit = async () => {
  if (isLoading.value) {
    return;
  }

  isLoading.value = true;
  const nameInput = document.querySelector('#name');
  console.log(nameInput.value)
  await fetch(
      '/api/user/login',
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          name: nameInput.value,
        }),
      }
    ).then((r) => {
      console.log('第一个then', r)
      return r.json()
    }).then((r) => {
      console.log('第二个then', r)
      if (r.code == 200) {
        sessionStorage.setItem('id', r.data.id)
        sessionStorage.setItem('name', r.data.name)
        sessionStorage.setItem('email', r.data.email)
        sessionStorage.setItem('stripeCustomerId', r.data.stripeCustomerId)
        window.location.href = '/'; 
      } else {
        messages.value.push('登录信息错误, 请重新登录');
      }
    });

  isLoading.value = false;
}
</script>

js部分:主要逻辑是登录完成后将用户信息存放到本地存储中。然后跳回到首页。

其中调用接口 /api/user/login

接口的实现也很简单核心代码如下,别告诉我你看不懂

@PostMapping("login")
    public String login(@RequestBody UserLoginBO userLoginBO) {
        User user = userService.lambdaQuery().eq(User::getName, userLoginBO.getName()).one();
        if (Objects.isNull(user)) {
            ResponseR r = ResponseR.error();
            return JSONUtil.toJsonStr(r);
        }

        ResponseR<User> r = ResponseR.ok().data(user);
        return JSONUtil.toJsonStr(r);
    }

注册

新建一个Registry.vue的页面

<template>
  <main>
    <h1>注册页面</h1>

    <form id="payment-form" @submit.prevent="handleSubmit">
      <div>
        <label for="name">名字</label>
        <input id="name">
      </div>

      <div>
        <label for="email">邮箱</label>
        <input id="email">
      </div>
      
      <button
        id="submit"
        :disabled="isLoading"
      >
        注册
      </button>
      <sr-messages :messages="messages" />
    </form>
    <div>
        <a href="/login">有账号去登录</a>
    </div>
  </main>
</template>
<script setup>
import { ref } from "vue";
const isLoading = ref(false);
const handleSubmit = async () => {
  if (isLoading.value) {
    return;
  }

  isLoading.value = true;
  const nameInput = document.querySelector('#name');
  const emailInput = document.querySelector('#email');
  console.log(nameInput.value, '***', emailInput.value)
  await fetch(
      '/api/user/registry',
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          name: nameInput.value,
          email: emailInput.value,
        }),
      }
    ).then((r) => {
      console.log('第一个then', r)
      return r.json()
    }).then((r) => {
      //{"code":200,"data":{"name":"an","id":3,"stripeCustomerId":"cus_PTbTKK6AOYHRn7","email":"qwe@163.com"},"message":"SUCCESS"}
      console.log('第二个then', r)
      if (r.code == 200) {
        sessionStorage.setItem('id', r.data.id)
        sessionStorage.setItem('name', r.data.name)
        sessionStorage.setItem('email', r.data.email)
        sessionStorage.setItem('stripeCustomerId', r.data.stripeCustomerId)
        window.location.href = '/'; 
      }
    });

  isLoading.value = false;
}
</script>

java 注册接口

@PostMapping("registry")
    public String registry(@RequestBody UserRegistryBO userRegistryBO) throws StripeException {
        User user = userService.lambdaQuery().eq(User::getName, userRegistryBO.getName()).one();
        if (Objects.isNull(user)) {
            user = new User();
            user.setName(userRegistryBO.getName());
            user.setEmail(userRegistryBO.getEmail());
            Customer customer = stripeService.create(userRegistryBO.getName(), userRegistryBO.getEmail());
            user.setStripeCustomerId(customer.getId());
            userService.save(user);
        }
        ResponseR r = ResponseR.ok().data(user);
        return JSONUtil.toJsonStr(r);
    }
@Override
    public Customer create(String name, String email) throws StripeException {
        CustomerCreateParams params =
                CustomerCreateParams.builder()
                        .setName(name)
                        .setEmail(email)
                        .build();
        return Customer.create(params);
    }

这里注册接口分两部分, 一部分是入口函数 stripeService.create是用于将用户在stripe上去创建一个用户, 这也是本demo用到的第一个Stripe java api

这一步很重要, 因为他会返回一个用户在stripe管理平台的customer的id。

好了, 注册完返回就行了。

绑定信用卡

新建一个页面 BindCard.vue

<template>
    <main>
        <h1>绑定新的银行卡</h1>
        <form id="payment-form" @submit.prevent="handleBind">
            <div id="card-element"></div>
            <button id="submit-button">去绑定</button>
        </form>
        
        
        <h1>已经绑定的银行卡</h1>
        <ul v-for="(item, index) in objectArray" :key="index">
            <li>序号:{{ index + 1 }}</li>
            <li>方式ID:{{ item.paymentMethodId }}</li>
            <li>卡类型:{{ item.cardBrand }}</li>
            <li>后四位:{{ item.last4Digits }}</li>
        </ul>

        <sr-messages :messages="messages" />
    </main>
</template>
<script setup>
  import { ref, onMounted } from "vue";
  import { loadStripe } from "@stripe/stripe-js";
  
  import SrMessages from "./SrMessages.vue";
  
  const messages = ref([]);
  let stripe;
  let elements;
  let cardElement;
  
  let objectArray = ref([]);
onMounted(async () => {
  //需要从服务器获取到公钥的key。其实这个公钥的key直接写给前端好像也行。
  //因为官方的示例是这么用的, 所以我也就这么用了
  //是.env中的 STRIPE_PUBLISHABLE_KEY
  const { publishableKey } = await fetch("/api/stripe/config").then((res) => res.json());
  // 使用公钥来初始化 Stripe js对象
  stripe = await loadStripe(publishableKey);
  //获取元素
  elements = stripe.elements();
  //创建card类型的表单, 这里card类型就是指的信用卡类型
  cardElement = elements.create('card');
  // 将这个表单挂在到页面的id上 就是html部分<div id="card-element"></div>
  cardElement.mount('#card-element');

  //这里是调用一个接口, 获取用户已绑定的信用卡信息
  objectArray.value  = await fetch(
      '/api/stripe/bindCardList',
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            customerId: sessionStorage.getItem('stripeCustomerId'),
        }),
      }
    ).then((r) => {
      console.log('第一个then', r)
      return r.json()
    }).then((r) => {
      console.log('第二个then', r)
      return r
    });
    return objectArray;
});

//绑定新的卡片
const handleBind = async () => {
// 调用前端方法跟stripe进行交互(stripe 绑定卡的表单是一个iframe。
// billing_details 字段有很多, 详细的可以看官方文档
    stripe.createPaymentMethod({
        type: 'card',
        card: cardElement,
        billing_details: {
            name: sessionStorage.getItem('name'),
        },
    })
    .then(function(result) {
        console.log(result)
        // Handle result.error or result.paymentMethod
        if (result.error) {
            // 处理错误
            console.error(result.error.message);
        } else {
            // PaymentMethod 创建成功,将其传递给服务器进行进一步处理
            //paymentMethodId = result.paymentMethod.id;
            // 将 paymentMethodId 发送到服务器端
            // 这里你可以使用 AJAX 或其他方法将 paymentMethodId 发送到服务器
            fetch(
                '/api/user/addPaymentMethod',
                {
                    method: 'POST',
                    headers: {
                    'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({
                        userId: sessionStorage.getItem('id'),
                        paymentMethodId: result.paymentMethod.id,
                    }),
                }).then((r) => {
                    console.log('第一个then', r)
                    return r.json()
                }).then((r) => {
                console.log('第二个then', r)
                    if (r.code == 200) {
                        //刷新已绑定银行卡列表
                    } else {
                        messages.value.push('服务错误,本地添加银行卡错误');
                    }
                });
        }
    });
}
  
</script>

js部分相信 能看到这里的同学一定也能看得懂了。无非就是一个查询已绑定卡的接口。获取并渲染到页面上。另一个是去新绑定卡片提交的地址。

handleBind 方法中,第一步stripe.createPaymentMethod 是完全由前端和stripe进行交互。成功后会返回一个支付方式的id。将支付方式id(paymentMethodId发送到后端, 后端根据当前登录用户去获取当前登录用户在stripe的customerId。 将两个id进行关联, 关联后, 支付卡片和用户就进行绑定了。

user/addPaymentMethod接口java代码

@PostMapping("addPaymentMethod")
    public String addPaymentMethod(@RequestBody UserAddPaymentMethodBO userAddPaymentMethodBO) throws StripeException {
        UserPaymentMethod userPaymentMethod = userPaymentMethodService.lambdaQuery()
                .eq(UserPaymentMethod::getPaymentMethodId, userAddPaymentMethodBO.getPaymentMethodId())
                .one();
        if (Objects.nonNull(userPaymentMethod)) {
            ResponseR r = ResponseR.error().message(StrUtil.format("已经存在当前支付方式id{}", userAddPaymentMethodBO.getPaymentMethodId()));
            return JSONUtil.toJsonStr(r);
        }
        userPaymentMethod = new UserPaymentMethod();
        userPaymentMethod.setUserId(userAddPaymentMethodBO.getUserId());
        userPaymentMethod.setPaymentMethodId(userAddPaymentMethodBO.getPaymentMethodId());
        userPaymentMethodService.save(userPaymentMethod);

        User user = userService.getById(userAddPaymentMethodBO.getUserId());

        //创建一个 PaymentMethodAttachParams 对象
        PaymentMethodAttachParams params = PaymentMethodAttachParams.builder()
                .setCustomer(user.getStripeCustomerId())
                .build();

        // 使用 paymentMethodId 和 params 进行关联
        PaymentMethod paymentMethodRes = PaymentMethod.retrieve(userAddPaymentMethodBO.getPaymentMethodId());
        paymentMethodRes.attach(params);

        System.out.println("PaymentMethod successfully attached to Customer.");
        return JSONUtil.toJsonStr(ResponseR.ok().data(paymentMethodRes));
    }

支付

支付的大概流程是 由前端发起一个支付的请求,将要付款的元素(例如付款金额)当然真正的生产环境中是不会从前端来获取真实的交易金额的。要使用的信用卡,这里是当前登录用户在stripe绑定的信用卡(对于应用系统来说是stripe的支付方式id), 因为应用是不允许持有用户信用卡信息的, 如果应用要持有这些信息那么需要应用的开发者(公司)提供合规证明。

前端指定一个支付方式和价格信息给到后端。

后端根据信用卡(也就是paymentMethodId)和加个信息到stripe创建一个支付意向信息(相当于是一个等级 某个paymentMethodId有意向 支付xx钱。然后stripe的远程服务会返回一个paymentIntent,其中的paymentIntentId 返回给前端

前端根据paymentIntentId 调用stripe.js 来确认支付,stripe给前端返回res里面包含了成功或失败的信息,如果成功paymentIntent.status的值为successed。如果失败则res.error中去获取详细的失败信息。

这时, 如果失败, 则给后端一个paymentIntentId告知后端支付失败, 后端去处理失败的逻辑

如果成功, 也是给paymentIntentId到后端, 处理成功的逻辑

前端页面

<template>
  <main>
    <a href="/">home</a>
    <form id="payment-form" @submit.prevent="handleSubmit">
      <div>
        <label for="price"><input name="price" id="price">美元</label>
      </div>
            
      <h1>请选择付款卡</h1>
      <ul v-for="(item, index) in objectArray" :key="index">
          <input type="radio" name="paymentMethodId" :id="`option${index}`" v-model="selectedOption" :value="item.paymentMethodId" @change="handleChange">
          <li>序号:{{ index + 1 }}</li>
          <li>方式ID:{{ item.paymentMethodId }}</li>
          <li>卡类型:{{ item.cardBrand }}</li>
          <li>后四位:{{ item.last4Digits }}</li>
      </ul>
      <button id="submit-button">支付</button>
    </form>
      <sr-messages :messages="messages" />
     
  </main>
</template>

前端js 分几个部分, 都在注释里面了。

<script setup>
import { ref, onMounted } from "vue";
import { loadStripe } from "@stripe/stripe-js";

import SrMessages from "./SrMessages.vue";

const messages = ref([]);
let stripe;


let objectArray = ref([]);
let selectedOption = ""
onMounted(async () => {
const { publishableKey } = await fetch("/api/stripe/config").then((res) => res.json());
stripe = await loadStripe(publishableKey);


// part1  获取已绑定卡
objectArray.value  = await fetch(
    '/api/stripe/bindCardList',
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
          customerId: sessionStorage.getItem('stripeCustomerId'),
      }),
    }
  ).then((r) => {
    console.log('第一个then', r)
    return r.json()
  }).then((r) => {
    console.log('第二个then', r)
    return r
  });
  return {
    objectArray
  }
});


//打印一个选择卡片事件, 这个其实没什么用。只是在demo中打个日志
const handleChange = async () => {
  messages.value.push(selectedOption)
  console.log(selectedOption)
}

// part2  提交支付, stripe/paymentIntent 接口创建支付意向信息, 并获取支付意向id
const handleSubmit = async () => {
  const priceInput = document.querySelector('#price');
  messages.value.push(priceInput.value)
  await fetch('/api/stripe/paymentIntent',
      {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            userId: sessionStorage.getItem('id'),
            price: priceInput.value
          }),
        })
        .then(r => r.json())
        .then(data => {
          messages.value.push('支付前')
          // part3 确认支付。这里可以增加确认按钮再去调用也行。 根据产品的流程进行即可
          confirmPayment(data.clientSecret, selectedOption)
        });
        
        
}
// part3 
const confirmPayment = async(clientSecret, selectedOption) => {
          messages.value.push('支付中')
            stripe.confirmCardPayment(clientSecret, {
                payment_method: selectedOption, // 使用已绑定的支付方式
            })
            .then(result => {
                // 获取支付确认返回
                console.log(result)
                if (result.error) {
                    // 处理错误, 我这里错误就没给后端了。真实的业务逻辑中其实也应该给后端
                    // 其实不给也是可以的。 stripe还有一个webhook。 你的系统通过这个钩子来异步的处理这个信息也是没有问题的
                    console.error(result);
                } else {
                    // 支付成功, 同步成功最好能直接给后端一个请求去验证支付是否成功了。 如果成功了, 直接让前端调转到下一个流程
                    console.log(result.paymentIntent.id)
                    messages.value.push("去确认结果")
                    fetch('/api/stripe/checkPayRes',{
                      method: 'POST',
                      headers: {
                        'Content-Type': 'application/json',
                      },
                      body: JSON.stringify({
                        userId: sessionStorage.getItem('id'),
                        paymentIntentId: result.paymentIntent.id
                      }),
                    })
                    .then(r => {
                      console.log('第一个then', r)
                      return r.json()
                    })
                    .then(res => {
                      console.log(res)
                      console.log(res.message)
                      messages.value.push(res.message)
                    })
                }
            });
        }  

</script>

java 接口核心

/**
     * 创建预付- 前端拿到客户端动态密钥后 去确认支付
     * @param stripePaymentIntentBO
     * @return
     * @throws StripeException
     */
    @PostMapping("paymentIntent")
    public String paymentIntent(@RequestBody StripePaymentIntentBO stripePaymentIntentBO) throws StripeException {
        User user = userService.getById(stripePaymentIntentBO.getUserId());
        Long price = stripePaymentIntentBO.getPrice().multiply(new BigDecimal(1000)).longValue();
        // 创建 PaymentIntent
        PaymentIntent paymentIntent = PaymentIntent.create(
                new PaymentIntentCreateParams.Builder()
                        .setCustomer(user.getStripeCustomerId())
                        .setCurrency("cad")
                        .setAmount(price)  // 金额以分为单位
                        .build()
        );
        return JSONUtil.toJsonStr(paymentIntent);
    }

    /**
     * 检查支付结果
     * @param stripeCheckPayResBO
     * @return
     * @throws StripeException
     */
    @PostMapping("checkPayRes")
    public String checkPayRes(@RequestBody StripeCheckPayResBO stripeCheckPayResBO) throws StripeException {
        ResponseR r = ResponseR.ok().message("经服务端验证,支付成功");
        // 使用 PaymentIntent.retrieve 方法检查支付状态
        PaymentIntent paymentIntent = PaymentIntent.retrieve(stripeCheckPayResBO.getPaymentIntentId());
        System.out.println(JSONUtil.toJsonStr(paymentIntent));
        if (!StringUtils.equals(paymentIntent.getStatus(), "succeeded")) {
            r = ResponseR.error().message("经服务端验证,支付失败");
        }

        return JSONUtil.toJsonStr(r.message("充值成功"));
    }

微信或支付宝支付

关于微信和支付宝支付, 具体不写了。看demo。能看到这里的应该不难了。

其中支付宝支付我是能测试成功的。 但是微信支付总是失败, 不知道是不是跟网络有关系。

附录中的demo有相关代码。请自行查阅

附录

表结构:

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `stripe_customer_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户在stripe注册得客户id',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;


DROP TABLE IF EXISTS `user_payment_method`;
CREATE TABLE `user_payment_method`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_id` int NULL DEFAULT NULL,
  `payment_method_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

开发时服务器配置

vite.config.js

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  server: {
    host: "0.0.0.0",
    proxy: {
      "/api": {
        target: "http://localhost:9900/",
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ""),
      },
    },
    
  },
})

使用到的java api

Customer.create(params);

相关配置类和配置文件

.env

# Stripe API keys - see https://stripe.com/docs/development/quickstart#api-keys
STRIPE_PUBLISHABLE_KEY=pk_test_xxxx
STRIPE_SECRET_KEY=sk_test_xxxx

DotenvConf.java

@Configuration
public class DotenvConf {
    @Bean("dotenv")
    public Dotenv dotenv() {
        return Dotenv.load();
    }
}

AppStartAndStopNotify.java

这里其实就是干了一个事情, 把密钥赋值给Stripe对象的一个静态变量apiKey中。不用我这个方式, 用其他方式都行。

@Component
@Slf4j
public class AppStartAndStopNotify implements CommandLineRunner {

    @Autowired
    private Dotenv dotenv;
    @Override
    public void run(String... args) throws Exception {

        Stripe.apiKey = dotenv.get("STRIPE_SECRET_KEY");
//这里是实验环境, 在控制台打印了key, 实际开发中没必要打印他
        log.info(StrUtil.format("设置Stripe.apiKey成功:{}", Stripe.apiKey));
        log.info("应用启动成功");
    }
}

示例代码

stripepay-bindcard: stripe支付的一个 从用户创建 绑卡 消费的一个全流程示例项目icon-default.png?t=N7T8https://gitee.com/azhw/stripepay-bindcard.git

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值