- 原文地址:Accepting Payments with Stripe, Vue.js, and Flask
- 原文做者:Michael Herman
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:Mcskiller
- 校對者:kasheemlew
在本教程中,咱們將會開發一個使用 Stripe(處理付款訂單),Vue.js(客戶端應用)以及 Flask(服務端 API)的 web 應用來售賣書籍。javascript
這是一個進階教程。咱們默認您已經基本掌握了 Vue.js 和 Flask。若是你尚未了解過它們,請查看下面的連接以瞭解更多:html
最終效果:前端
主要依賴:vue
在本教程結束的時候,你可以...java
Clone flask-vue-crud 倉庫,而後在 master 分支找到 v1 標籤:python
$ git clone https://github.com/testdrivenio/flask-vue-crud --branch v1 --single-branch
$ cd flask-vue-crud
$ git checkout tags/v1 -b master
複製代碼
搭建並激活一個虛擬環境,而後運行 Flask 應用:android
$ cd server
$ python3.6 -m venv env
$ source env/bin/activate
(env)$ pip install -r requirements.txt
(env)$ python app.py
複製代碼
上述搭建環境的命令可能因操做系統和運行環境而異。webpack
用瀏覽器訪問 http://localhost:5000/ping。你會看到:ios
"pong!"
複製代碼
而後,安裝依賴並在另外一個終端中運行 Vue 應用:git
$ 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 函數 GET,POST 和 PUT。
首先在 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>
複製代碼
你如今應該會看到:
添加一個新 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)
複製代碼
趕忙測試一下吧!
不要忘了處理客戶端和服務端的錯誤!
一樣的操做,不過此次是編輯書籍,該你本身動手了:
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 帳號的話須要先註冊一個,而後再去獲取你的 測試模式 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 倉庫找到源碼。
想挑戰更多?
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。