内容简介:在本教程中,我们将会开发一个使用Stripe(处理付款订单),Vue.js(客户端应用)以及Flask(服务端 API)的 web 应用来售卖书籍。在本教程结束的时候,你能够...Clone
在本教程中,我们将会开发一个使用Stripe(处理付款订单),Vue.js(客户端应用)以及Flask(服务端 API)的 web 应用来售卖书籍。
这是一个进阶教程。我们默认您已经基本掌握了 Vue.js 和 Flask。如果你还没有了解过它们,请查看下面的链接以了解更多:
最终效果:
主要依赖:
- Vue v2.5.2
- Vue CLI v2.9.3
- Node v10.3.0
- NPM v6.1.0
- Flask v1.0.2
- Python v3.6.5
目录
目的
在本教程结束的时候,你能够...
- 获得一个现有的 CRUD 应用,由 Vue 和 Flask 驱动
- 创建一个订单结算组件
- 使用原生 JavaScript 验证一个表单
- 使用 Stripe 验证信用卡信息
- 通过 Stripe API 处理付款
项目安装
Clone flask-vue-crud 仓库,然后在 master 分支找到 v1 标签:
$ git clone https://github.com/testdrivenio/flask-vue-crud --branch v1 --single-branch $ cd flask-vue-crud $ git checkout tags/v1 -b master 复制代码
搭建并激活一个虚拟环境,然后运行 Flask 应用:
$ cd server $ python3.6 -m venv env $ source env/bin/activate (env)$ pip install -r requirements.txt (env)$ python app.py 复制代码
上述搭建环境的命令可能因操作系统和运行环境而异。
用浏览器访问 http://localhost:5000/ping 。你会看到:
"pong!" 复制代码
然后,安装依赖并在另一个终端中运行 Vue 应用:
$ cd client $ npm install $ npm run dev 复制代码
转到 http://localhost:8080 。确保 CRUD 基本功能正常工作:
想学习如何构建这个项目?查看 用 Flask 和 Vue.js 开发一个单页面应用 文章。
我们要做什么?
我们的目标是构建一个允许终端用户购买书籍的 web 应用。
客户端 Vue 应用将会显示出可供购买的书籍并记录付款信息,然后从 Stripe 获得 token,最后发送 token 和付款信息到服务端。
然后 Flask 应用获取到这些信息,并把它们都打包发送到 Stripe 去处理。
最后,我们会用到一个客户端 Stripe 库Stripe.js,它会生成一个专有 token 来创建账单,然后使用服务端 Python Stripe 库 和 Stripe API 交互。
和之前的教程 一样,我们会简化步骤,你应该自己处理产生的其他问题,这样也会加强你的理解。
CRUD 书籍
首先,让我们将购买价格添加到服务器端的现有书籍列表中,然后在客户端上更新相应的 CRUD 函数 GET,POST 和 PUT。
GET
首先在 server/app.py 中添加 price
到 BOOKS
列表的每一个字典元素中:
BOOKS = [ { 'id': uuid.uuid4().hex, 'title': 'On the Road', 'author': 'Jack Kerouac', 'read': True, 'price': '19.99' }, { 'id': uuid.uuid4().hex, 'title': 'Harry Potter and the Philosopher\'s Stone', 'author': 'J. K. Rowling', 'read': False, 'price': '9.99' }, { 'id': uuid.uuid4().hex, 'title': 'Green Eggs and Ham', 'author': 'Dr. Seuss', 'read': True, 'price': '3.99' } ] 复制代码
然后,在 Books
组件 client/src/components/Books.vue 中更新表格以显示购买价格。
<table class="table table-hover"> <thead> <tr> <th scope="col">Title</th> <th scope="col">Author</th> <th scope="col">Read?</th> <th scope="col">Purchase Price</th> <th></th> </tr> </thead> <tbody> <tr v-for="(book, index) in books" :key="index"> <td>{{ book.title }}</td> <td>{{ book.author }}</td> <td> <span v-if="book.read">Yes</span> <span v-else>No</span> </td> <td>${{ book.price }}</td> <td> <button type="button" class="btn btn-warning btn-sm" v-b-modal.book-update-modal @click="editBook(book)"> Update </button> <button type="button" class="btn btn-danger btn-sm" @click="onDeleteBook(book)"> Delete </button> </td> </tr> </tbody> </table> 复制代码
你现在应该会看到:
POST
添加一个新 b-form-group
到 addBookModal
中,在 Author 和 read 的 b-form-group
类之间:
<b-form-group id="form-price-group" label="Purchase price:" label-for="form-price-input"> <b-form-input id="form-price-input" type="number" v-model="addBookForm.price" required placeholder="Enter price"> </b-form-input> </b-form-group> 复制代码
这个模态现在看起来应该是这样:
<!-- add book modal --> <b-modal ref="addBookModal" id="book-modal" title="Add a new book" hide-footer> <b-form @submit="onSubmit" @reset="onReset" class="w-100"> <b-form-group id="form-title-group" label="Title:" label-for="form-title-input"> <b-form-input id="form-title-input" type="text" v-model="addBookForm.title" required placeholder="Enter title"> </b-form-input> </b-form-group> <b-form-group id="form-author-group" label="Author:" label-for="form-author-input"> <b-form-input id="form-author-input" type="text" v-model="addBookForm.author" required placeholder="Enter author"> </b-form-input> </b-form-group> <b-form-group id="form-price-group" label="Purchase price:" label-for="form-price-input"> <b-form-input id="form-price-input" type="number" v-model="addBookForm.price" required placeholder="Enter price"> </b-form-input> </b-form-group> <b-form-group id="form-read-group"> <b-form-checkbox-group v-model="addBookForm.read" id="form-checks"> <b-form-checkbox value="true">Read?</b-form-checkbox> </b-form-checkbox-group> </b-form-group> <b-button type="submit" variant="primary">Submit</b-button> <b-button type="reset" variant="danger">Reset</b-button> </b-form> </b-modal> 复制代码
然后,添加 price
到 addBookForm
属性中:
addBookForm: { title: '', author: '', read: [], price: '', }, 复制代码
addBookForm
现在和表单的输入值进行了绑定。想想这意味着什么。当 addBookForm
被更新时,表单的输入值也会被更新,反之亦然。以下是 vue-devtools 浏览器扩展的示例。
将 price
添加到 onSubmit
方法的 payload
中,像这样:
onSubmit(evt) { evt.preventDefault(); this.$refs.addBookModal.hide(); let read = false; if (this.addBookForm.read[0]) read = true; const payload = { title: this.addBookForm.title, author: this.addBookForm.author, read, // property shorthand price: this.addBookForm.price, }; this.addBook(payload); this.initForm(); }, 复制代码
更新 initForm
函数,在用户提交表单点击 "重置" 按钮后清除已有的值:
initForm() { this.addBookForm.title = ''; this.addBookForm.author = ''; this.addBookForm.read = []; this.addBookForm.price = ''; this.editForm.id = ''; this.editForm.title = ''; this.editForm.author = ''; this.editForm.read = []; }, 复制代码
最后,更新 server/app.py 中的路由:
@app.route('/books', methods=['GET', 'POST']) def all_books(): response_object = {'status': 'success'} if request.method == 'POST': post_data = request.get_json() BOOKS.append({ 'id': uuid.uuid4().hex, 'title': post_data.get('title'), 'author': post_data.get('author'), 'read': post_data.get('read'), 'price': post_data.get('price') }) response_object['message'] = 'Book added!' else: response_object['books'] = BOOKS return jsonify(response_object) 复制代码
赶紧测试一下吧!
不要忘了处理客户端和服务端的错误!
PUT
同样的操作,不过这次是编辑书籍,该你自己动手了:
- 添加一个新输入表单到模态中
- 更新属性中的
editForm
部分 - 添加
price
到onSubmitUpdate
方法的payload
中 - 更新
initForm
- 更新服务端路由
需要帮助吗?重新看看前面的章节。或者你可以从 flask-vue-crud 仓库获得源码。
订单页面
接下来,让我们添加一个订单页面,用户可以在其中输入信用卡信息来购买图书。
TODO:添加图片
添加一个购买按钮
首先给 Books
组件添加一个“购买”按钮,就在“删除”按钮的下方:
<td> <button type="button" class="btn btn-warning btn-sm" v-b-modal.book-update-modal @click="editBook(book)"> Update </button> <button type="button" class="btn btn-danger btn-sm" @click="onDeleteBook(book)"> Delete </button> <router-link :to="`/order/${book.id}`" class="btn btn-primary btn-sm"> Purchase </router-link> </td> 复制代码
这里,我们使用了router-link 组件来生成一个连接到 client/src/router/index.js 中的路由的锚点,我们马上就会用到它。
创建模板
添加一个叫做 Order.vue 的新组件文件到 client/src/components :
<template> <div class="container"> <div class="row"> <div class="col-sm-10"> <h1>Ready to buy?</h1> <hr> <router-link to="/" class="btn btn-primary"> Back Home </router-link> <br><br><br> <div class="row"> <div class="col-sm-6"> <div> <h4>You are buying:</h4> <ul> <li>Book Title: <em>Book Title</em></li> <li>Amount: <em>$Book Price</em></li> </ul> </div> <div> <h4>Use this info for testing:</h4> <ul> <li>Card Number: 4242424242424242</li> <li>CVC Code: any three digits</li> <li>Expiration: any date in the future</li> </ul> </div> </div> <div class="col-sm-6"> <h3>One time payment</h3> <br> <form> <div class="form-group"> <label>Credit Card Info</label> <input type="text" class="form-control" placeholder="XXXXXXXXXXXXXXXX" required> </div> <div class="form-group"> <input type="text" class="form-control" placeholder="CVC" required> </div> <div class="form-group"> <label>Card Expiration Date</label> <input type="text" class="form-control" placeholder="MM/YY" required> </div> <button class="btn btn-primary btn-block">Submit</button> </form> </div> </div> </div> </div> </div> </template> 复制代码
你可能会想收集买家的联系信息,比如姓名,邮件地址,送货地址等等。这就得靠你自己了。
添加路由
client/src/router/index.js:
import Vue from 'vue'; import Router from 'vue-router'; import Ping from '@/components/Ping'; import Books from '@/components/Books'; import Order from '@/components/Order'; Vue.use(Router); export default new Router({ routes: [ { path: '/', name: 'Books', component: Books, }, { path: '/order/:id', name: 'Order', component: Order, }, { path: '/ping', name: 'Ping', component: Ping, }, ], mode: 'hash', }); 复制代码
测试一下。
获取产品信息
接下来,让我们在订单页面 上更新书名和金额的占位符:
回到服务端并更新以下路由接口:
@app.route('/books/<book_id>', methods=['GET', 'PUT', 'DELETE']) def single_book(book_id): response_object = {'status': 'success'} if request.method == 'GET': # TODO: refactor to a lambda and filter return_book = '' for book in BOOKS: if book['id'] == book_id: return_book = book response_object['book'] = return_book if request.method == 'PUT': post_data = request.get_json() remove_book(book_id) BOOKS.append({ 'id': uuid.uuid4().hex, 'title': post_data.get('title'), 'author': post_data.get('author'), 'read': post_data.get('read'), 'price': post_data.get('price') }) response_object['message'] = 'Book updated!' if request.method == 'DELETE': remove_book(book_id) response_object['message'] = 'Book removed!' return jsonify(response_object) 复制代码
我们可以在 script
中使用这个路由向订单页面添加书籍信息:
<script> import axios from 'axios'; export default { data() { return { book: { title: '', author: '', read: [], price: '', }, }; }, methods: { getBook() { const path = `http://localhost:5000/books/${this.$route.params.id}`; axios.get(path) .then((res) => { this.book = res.data.book; }) .catch((error) => { // eslint-disable-next-line console.error(error); }); }, }, created() { this.getBook(); }, }; </script> 复制代码
转到生产环境?你将需要使用环境变量来动态设置基本服务器端 URL(现在 URL 为 http://localhost:5000
)。查看文档 获取更多信息。
然后,更新 template 中的第一个 ul
:
<ul> <li>Book Title: <em>{{ book.title }}</em></li> <li>Amount: <em>${{ book.price }}</em></li> </ul> 复制代码
你现在会看到:
表单验证
让我们设置一些基本的表单验证。
使用 v-model
指令去绑定 表单输入值到属性中:
<form> <div class="form-group"> <label>Credit Card Info</label> <input type="text" class="form-control" placeholder="XXXXXXXXXXXXXXXX" v-model="card.number" required> </div> <div class="form-group"> <input type="text" class="form-control" placeholder="CVC" v-model="card.cvc" required> </div> <div class="form-group"> <label>Card Expiration Date</label> <input type="text" class="form-control" placeholder="MM/YY" v-model="card.exp" required> </div> <button class="btn btn-primary btn-block">Submit</button> </form> 复制代码
添加 card 属性,就像这样:
card: { number: '', cvc: '', exp: '', }, 复制代码
接下来,更新“提交”按钮,以便在单击按钮时忽略正常的浏览器行为,并调用 validate
方法:
<button class="btn btn-primary btn-block" @click.prevent="validate">Submit</button> 复制代码
将数组添加到属性中以保存验证错误信息:
data() { return { book: { title: '', author: '', read: [], price: '', }, card: { number: '', cvc: '', exp: '', }, errors: [], }; }, 复制代码
就添加在表单的下方,我们能够依次显示所有错误:
<div v-show="errors"> <br> <ol class="text-danger"> <li v-for="(error, index) in errors" :key="index"> {{ error }} </li> </ol> </div> 复制代码
添加 validate
方法:
validate() { this.errors = []; let valid = true; if (!this.card.number) { valid = false; this.errors.push('Card Number is required'); } if (!this.card.cvc) { valid = false; this.errors.push('CVC is required'); } if (!this.card.exp) { valid = false; this.errors.push('Expiration date is required'); } if (valid) { this.createToken(); } }, 复制代码
由于所有字段都是必须填入的,而我们只是验证了每一个字段是否都有一个值。Stripe 将会验证下一节你看到的信用卡信息,所以你不必过度验证表单信息。也就是说,只需要保证你自己添加的其他字段通过验证。
最后,添加 createToken
方法:
createToken() { // eslint-disable-next-line console.log('The form is valid!'); }, 复制代码
测试一下。
Stripe
如果你没有Stripe 账号的话需要先注册一个,然后再去获取你的 测试模式API Publishable key。
客户端
添加 stripePublishableKey 和 stripeCheck
(用来禁用提交按钮)到 data 中:
data() { return { book: { title: '', author: '', read: [], price: '', }, card: { number: '', cvc: '', exp: '', }, errors: [], stripePublishableKey: 'pk_test_aIh85FLcNlk7A6B26VZiNj1h', stripeCheck: false, }; }, 复制代码
确保添加你自己的 Stripe key 到上述代码中。
同样,如果表单有效,触发 createToken
方法(通过Stripe.js)验证信用卡信息然后返回一个错误信息(如果无效)或者返回一个 token(如果有效):
createToken() { this.stripeCheck = true; window.Stripe.setPublishableKey(this.stripePublishableKey); window.Stripe.createToken(this.card, (status, response) => { if (response.error) { this.stripeCheck = false; this.errors.push(response.error.message); // eslint-disable-next-line console.error(response); } else { // pass } }); }, 复制代码
如果没有错误的话,我们就发送 token 到服务器,在那里我们会完成扣费并把用户转回主页:
createToken() { this.stripeCheck = true; window.Stripe.setPublishableKey(this.stripePublishableKey); window.Stripe.createToken(this.card, (status, response) => { if (response.error) { this.stripeCheck = false; this.errors.push(response.error.message); // eslint-disable-next-line console.error(response); } else { const payload = { book: this.book, token: response.id, }; const path = 'http://localhost:5000/charge'; axios.post(path, payload) .then(() => { this.$router.push({ path: '/' }); }) .catch((error) => { // eslint-disable-next-line console.error(error); }); } }); }, 复制代码
按照上述代码更新 createToken()
,然后添加Stripe.js 到 client/index.html 中:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <title>Books!</title> </head> <body> <div id="app"></div> <!-- built files will be auto injected --> <script type="text/javascript" src="https://js.stripe.com/v2/"></script> </body> </html> 复制代码
Stripe 支持 v2 和 v3(Stripe Elements)版本的 Stripe.js。如果你对 Stripe Elements 和如何把它集成到 Vue 中感兴趣,参阅以下资源:1. Stripe Elements 迁移指南 2. 集成 Stripe Elements 和 Vue.js 来创建一个自定义付款表单
现在,当 createToken
被触发是, stripeCheck
值被更改为 true
,为了防止重复收费,我们在 stripeCheck
值为 true
时禁用“提交”按钮:
<button class="btn btn-primary btn-block" @click.prevent="validate" :disabled="stripeCheck"> Submit </button> 复制代码
测试一下 Stripe 验证的无效反馈:
- 信用卡卡号
- 安全码
- 有效日期
现在,让我们开始设置服务端路由。
服务端
安装Stripe 库:
$ pip install stripe==1.82.1 复制代码
添加路由接口:
@app.route('/charge', methods=['POST']) def create_charge(): post_data = request.get_json() amount = round(float(post_data.get('book')['price']) * 100) stripe.api_key = os.environ.get('STRIPE_SECRET_KEY') charge = stripe.Charge.create( amount=amount, currency='usd', card=post_data.get('token'), description=post_data.get('book')['title'] ) response_object = { 'status': 'success', 'charge': charge } return jsonify(response_object), 200 复制代码
在这里设定书籍价格(转换为美分),专有 token(来自客户端的 createToken
方法),以及书名,然后我们利用API Secret key 生成一个新的 Stripe 账单。
了解更多创建账单的信息,参考官方 API文档。
Update the imports:
import os import uuid import stripe from flask import Flask, jsonify, request from flask_cors import CORS 复制代码
获取 测试模式 API Secret key :
把它设置成一个环境变量:
$ export STRIPE_SECRET_KEY=sk_test_io02FXL17hrn2TNvffanlMSy 复制代码
确保使用的是你自己的 Stripe key!
测试一下吧!
在Stripe Dashboard 中你应该会看到购买记录:
你可能还想创建顾客,而不仅仅是创建账单。这样一来有诸多优点。你能同时购买多个物品,以便跟踪客户购买记录。你可以向经常购买的用户提供优惠,或者向许久未购买的用户联系,还有许多用处这里就不做介绍了。它还可以用来防止欺诈。参考以下 Flask项目 来看看如何添加客户。
订单完成页面
比起把买家直接转回主页,我们更应该把他们重定向到一个订单完成页面,以感谢他们的购买。
添加一个叫 OrderComplete.vue 的新组件文件到 “client/src/components” 中:
<template> <div class="container"> <div class="row"> <div class="col-sm-10"> <h1>Thanks for purchasing!</h1> <hr><br> <router-link to="/" class="btn btn-primary btn-sm">Back Home</router-link> </div> </div> </div> </template> 复制代码
更新路由:
import Vue from 'vue'; import Router from 'vue-router'; import Ping from '@/components/Ping'; import Books from '@/components/Books'; import Order from '@/components/Order'; import OrderComplete from '@/components/OrderComplete'; Vue.use(Router); export default new Router({ routes: [ { path: '/', name: 'Books', component: Books, }, { path: '/order/:id', name: 'Order', component: Order, }, { path: '/complete', name: 'OrderComplete', component: OrderComplete, }, { path: '/ping', name: 'Ping', component: Ping, }, ], mode: 'hash', }); 复制代码
在 createToken
方法中更新重定向:
createToken() { this.stripeCheck = true; window.Stripe.setPublishableKey(this.stripePublishableKey); window.Stripe.createToken(this.card, (status, response) => { if (response.error) { this.stripeCheck = false; this.errors.push(response.error.message); // eslint-disable-next-line console.error(response); } else { const payload = { book: this.book, token: response.id, }; const path = 'http://localhost:5000/charge'; axios.post(path, payload) .then(() => { this.$router.push({ path: '/complete' }); }) .catch((error) => { // eslint-disable-next-line console.error(error); }); } }); }, 复制代码
最后,你还可以在订单完成页面显示客户刚刚购买的书籍的(标题,金额,等等)。
获取唯一的账单 ID 然后传递给 path
:
createToken() { this.stripeCheck = true; window.Stripe.setPublishableKey(this.stripePublishableKey); window.Stripe.createToken(this.card, (status, response) => { if (response.error) { this.stripeCheck = false; this.errors.push(response.error.message); // eslint-disable-next-line console.error(response); } else { const payload = { book: this.book, token: response.id, }; const path = 'http://localhost:5000/charge'; axios.post(path, payload) .then((res) => { // updates this.$router.push({ path: `/complete/${res.data.charge.id}` }); }) .catch((error) => { // eslint-disable-next-line console.error(error); }); } }); }, 复制代码
更新客户端路由:
{ path: '/complete/:id', name: 'OrderComplete', component: OrderComplete, }, 复制代码
然后,在 OrderComplete.vue 中,从 URL 中获取账单 ID 并发送到服务端:
<script> import axios from 'axios'; export default { data() { return { book: '', }; }, methods: { getChargeInfo() { const path = `http://localhost:5000/charge/${this.$route.params.id}`; axios.get(path) .then((res) => { this.book = res.data.charge.description; }) .catch((error) => { // eslint-disable-next-line console.error(error); }); }, }, created() { this.getChargeInfo(); }, }; </script> 复制代码
在服务器上配置新路由来检索 账单:
@app.route('/charge/<charge_id>') def get_charge(charge_id): stripe.api_key = os.environ.get('STRIPE_SECRET_KEY') response_object = { 'status': 'success', 'charge': stripe.Charge.retrieve(charge_id) } return jsonify(response_object), 200 复制代码
最后,在 template 中更新 <h1></h1>
:
<h1>Thanks for purchasing - {{ this.book }}!</h1> 复制代码
最后一次测试。
总结
完成了!一定要从最开始进行阅读。你可以在 GitHub 中的 flask-vue-crud 仓库找到源码。
想挑战更多?
- 添加客户端和服务端的单元和集成测试。
- 创建一个购物车以方便顾客能够一次购买多本书。
- 使用 Postgres 来储存书籍和订单。
- 使用 Docker 整合 Vue 和 Flask(以及 Postgres,如果你加入了的话)来简化开发工作流程。
- 给书籍添加图片来创建一个更好的产品页面。
- 获取 email 然后发送 email 确认邮件(查阅 使用 Flask、Redis Queue 和 Amazon SES 发送确认电子邮件 )。
- 部署客户端静态文件到 AWS S3 然后部署服务端应用到一台 EC2 实例。
- 投入生产环境?思考一个最好的更新 Stripe key 的方法,让它们基于环境动态更新。
- 创建一个分离组件来退订。
如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为掘金 上的英文分享文章。内容覆盖 Android 、 iOS 、 前端 、 后端 、 区块链 、 产品 、 设计 、 人工智能 等领域,想要查看更多优质译文请持续关注 掘金翻译计划 、官方微博、 知乎专栏 。
以上所述就是小编给大家介绍的《[译] 使用 Stripe, Vue.js 和 Flask 接受付款》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- [学习笔记] “付款到公钥” 和 “付款到公钥哈希”
- python实现12306抢票及自动邮件发送提醒付款功能
- 想用快速开关一键收付款?微信没适配但你可以自己做
- YunGouOS 个人支付接口 2.0.3 版本发布,新增付款到零钱、转账到支付宝接口
- PHP 支付类库 PaySDK v1.0.2,新增支付宝微信企业付款等
- 接受祝福 * 5
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Introduction to Tornado
Michael Dory、Adam Parrish、Brendan Berg / O'Reilly Media / 2012-3-28 / USD 23.99
Tornado is a scalable, non-blocking web server and web application framework written in Python. It is also light-weight to deploy, fun to write for, and incredibly powerful. Tornado was written with p......一起来看看 《Introduction to Tornado》 这本书的介绍吧!