利用Python的Flask框架来构建一个简单的数字商品支付解决方案


Posted in Python onMarch 31, 2015

作为一个程序员,我有时候忘了自己所具有的能力。当事情没有按照你想要的方式发展时,却很容易忘记你有能力去改变它。昨天,我意识到,我已经对我所出售的书的付款处理方式感到忍无可忍了。我的书完成后,我使用了三个不同的数字商品支付处理器,在对它们三个都感到不满后,我用Python和Flask,两个小时的时间写出了我自己的解决方案。没错!两个小时!现在,这个系统支撑着我的书籍付费流程,整个过程难以置信的简单,你可以在20秒内购买书籍并开始阅读。

往下看,看我是如何在一夜之间完成我自己的数字商品支付解决方案的。
支付处理器的付款问题

在我开始卖书的时候,我综合用了两种支付服务(一种是信用卡,另一种是PayPal)。最终,我发现了一个可以支持两者的处理方式。可是我对它们都不满意。最常用的那个支付处理器,要求用户在销售商的系统中创建一个账户,并且输入他们的邮箱地址(尽管邮箱并没有用)。

另外,我尝试使用Google Analytics 在全部的访问中追踪访客,包括他们的结账过程,这一过程也很艰辛。我经常感觉到,如果我能够让它工作起来,并且能够在我的书籍页面进行A/B测试,我能极大地提高销量。但是因为不能很好的追踪,我就没那么走运了。

最后,使用三个不同的支付处理器,发送书籍更新非常耗时。没有一个能很好的支持更新,而我希望有个“一键”解决方案来发送我的书籍更新。我就没找到一个类似的服务。
欧耶,我是个程序员

昨天,当我收到一个顾客的邮件,抱怨支付过程是如何的困难并且告诉我可能因此损失了很多销量后,我忍无可忍了。我决定整一个自己的数字商品管理解决方案。我需要做到下面这样了流程:

当客户点击“Buy Now”按钮后,他们应该仅仅被要求输入他们的email地址和信用卡信息。点击“Confirm”后被带到一个独一无二的URL去下载书籍(专门为了这次交易而生成的)。一封包含这个URL的邮件应该被发送给客户(防止用户需要重新下载这本书)。对他们重复下载的次数应该有个限制(5次)。交易信息和客户信息应该被存放在数据库中,发送书籍更新应该只是一个命令的事。

显然,这并不是那么的复杂。最复杂的部分是动态生成导向特定书籍版本的独一无二的URL。其他的事情都挺简单的。
“Flask前来救援”或是“一个100行代码的数字商品支付解决方案”

剧透:程序的最终结果正好是100行代码。对于这种规模的web应用,Flask是个很好的选择。并不需要大量的模板(cough就像 Django cough),但是有很多很好的插件作为支持。Bottle会是另外一个不错的选择,但是我最近都在用Flask,所以我就选它了。

开始的时候,我需要决定如何来存放用户和交易信息。我决定使用SQLAlchemy,因为sandman的原因,我比较熟悉它SQLAlchemy。Flask有一个插件叫Flask-SQLAlchemy,这使得结合使用两者非常容易。因为我不需要任何花哨的数据库操作,我选择SQLite作为我的后台数据库。

决定这样做之后,我创建了一个app.py 文件并且创建了如下的模型:

class Product(db.Model):
  __tablename__ = 'product'
  id = db.Column(db.Integer, primary_key=True)
  name = db.Column(db.String)
  file_name = db.Column(db.String)
  version = db.Column(db.String)
  is_active = db.Column(db.Boolean, default=True)
  price = db.Column(db.Float)
 
class Purchase(db.Model):
  __tablename__ = 'purchase'
  uuid = db.Column(db.String, primary_key=True)
  email = db.Column(db.String)
  product_id = db.Column(db.Integer, db.ForeignKey('product.id'))
  product = db.relationship(Product)
  downloads_left = db.Column(db.Integer, default=5)

在向数据库添加了5种不同版本的书籍后(我创建了一个populate_db.py 文件,并把这些版本作为SQLAlchemy模型来添加进数据库),我需要决定我究竟要如何处理支付。幸运的是,Stripe让接受一个信用卡变得非常简单,并且我也已经有了一个他们的账户。他们的”checkout.js”方案会在你的页面上创建一个表单和按钮。当点击这个按钮时,一个简洁又引人注目的浮动层会弹出来。

利用Python的Flask框架来构建一个简单的数字商品支付解决方案

这个表单的action 属性指向你的站点的一个页面,当用户完成支付后就会被带到那里。我在我的书籍销售页面添加了5个这样的按钮,还有一个隐藏的表单栏,包含了被交易产品的id(product_id)(1-5之间的一个整数)
处理支付

显然,我的应用需要一个后端来处理一次成功付款。我添加了以下这些函数来完成这一目的:

@app.route('/buy', methods=['POST'])
def buy():
  stripe_token = request.form['stripeToken']
  email = request.form['stripeEmail']
  product_id = request.form['product_id']
  product = Product.query.get(product_id)
  try:
    charge = stripe.Charge.create(
        amount=int(product.price * 100),
        currency='usd',
        card=stripe_token,
        description=email)
  except stripe.CardError, e:
    return """<html><body><h1>Card Declined</h1><p>Your chard could not
    be charged. Please check the number and/or contact your credit card
    company.</p></body></html>"""
  print charge
  purchase = Purchase(uuid=str(uuid.uuid4()),
      email=email,
      product=product)
  db.session.add(purchase)
  db.session.commit()
  message = Message(
      subject='Thanks for your purchase!',
    sender="jeff@jeffknupp.com",
    html="""<html><body><h1>Thanks for buying Writing Idiomatic Python!</h1>
<p>If you didn't already download your copy, you can visit
<a href="http://buy.jeffknupp.com/{}">your private link</a>. You'll be able to
download the file up to five times, at which point the link will
expire.""".format(purchase.uuid),
    recipients=[email])
  with mail.connect() as conn:
    conn.send(message)
  return redirect('/{}'.format(purchase.uuid))

正如你所看到的,我写代码的时候有点偷懒了(因为我正在愤怒的编程……)首先,我有一个内联HTML,在付费不成功时返回,还有已购买成功时返回邮箱。这些东西应该被放在一个全局变量,或者,更好的方法是放在一个单独的文件中。另外,在创建Purchase 时我并没有进行任何的错误检查。但是实际上,唯一可能出错的地方是尝试插入重复的 uuid,但是我并不担心,因为发生的几率太低了(潜台词:微乎其微)。

你可以看的我使用了一个mail 对象,这个对象来自于Flask-Mail包,这个插件让发送邮件变得轻松。我就设置它使用GMail作为服务器,然后所有东西都可以正常工作了。
好吧,现在该给我书了

现在,支付的部分已经搞定,我需要添加一个后端功能,在完成支付后初始化下载。因为我使用UUID作为主键,所以我同样可以使用它作为URL。当有人访问包含UUID的URL时,我需要检查该UUID是不是包含在数据库中。如果是的话,提供书籍文件并且把剩余下载(downloads_left)次数属性减少1次。如果不是的话,就返回404错误。下面是我写的代码:
 

@app.route('/<uuid>')
def download_file(uuid):
  purchase = Purchase.query.get(uuid)
  if purchase:
    if purchase.downloads_left <= 0:
      return """<html><body><h1>No downloads left!</h1><p>You have
      exceeded the allowed number of downloads for this file. Please email
      jeff@jeffknupp.com with any questions.</p></body></html>"""
    purchase.downloads_left -= 1
    db.session.commit()
    return send_from_directory(directory='files',
        filename=purchase.product.file_name, as_attachment=True)
  else:
    abort(404)

非常直观。使用UUID作为一个URL变量,寻找交易信息。如果存在,就检查是否还有可用下载次数,然后提供所购买的文件。否则,等着你的是404错误。

最后,我需要我需要添加一个测试来让我可以模拟交易过程。下面是测试代码和让这个app运行的代码:

@app.route('/test')
def test():
  return """<http><body><form action="buy" method="POST">
<script
  src="https://checkout.stripe.com/checkout.js" class="stripe-button"
  data-key="pk_test_w3qNBkDR8A4jkKejBmsMdH34"
  data-amount="999"
  data-name="jeffknupp.com"
  data-description="Writing Idiomatic Python 3 PDF ($9.99)">
</script>
<input type="hidden" name="product_id" value="2" />
</form>
</body>
</html>
"""
if __name__ == '__main__':
  sys.exit(app.run(debug=True))

能力越大…责任越大!

实际上我对于自己能如此快速简单让这一切工作起来感到吃惊。整个应用程序包含在一个100行代码的文件中。而且它替代了我每天使用的那些重要服务,我对那些服务一直都不满意。最后,我可以追踪交易,没有任何问题,这让我确信自己可以提高销量。

作为一个开发者,能意识到你有能力塑造我们和数字世界的交互是很好的一件事。比方说,我会忘记,如果有一些科技不能按照我预想的方式去工作,我有能力改变它。从自动化机械式的任务比如输入数据,到自动排序和整理电子邮件,开发者们有能力去简化他们每天的工作。

拥有Flask这样的工具对解决这些问题非常重要。正如像作为程序员那样进步,你应该建立你的一套解决“核心”问题的工具集。Flask就是很好的例子,因为匆匆忙忙的拼凑一个web应用是一件司空见惯的事情。

当然,分享你的作品同样非常的重要。如果我做一些东西,对我自己有用,但没有去分享给别人,我就会怠慢。“分享”不仅仅意味着”把项目放进一个GitHub公共仓库“。你还需要让大家知道有这个东西。从邮件列表到论坛再到个人博客,从来都不缺少让大家知道你创造了一些东西的途径。我总是设法回馈社区,因为我从中得到了很多东西。

Python 相关文章推荐
python进阶教程之异常处理
Aug 30 Python
在阿里云服务器上配置CentOS+Nginx+Python+Flask环境
Jun 18 Python
Python在线运行代码助手
Jul 15 Python
python搭建虚拟环境的步骤详解
Sep 27 Python
python的变量与赋值详细分析
Nov 08 Python
Python自然语言处理 NLTK 库用法入门教程【经典】
Jun 26 Python
Sanic框架配置操作分析
Jul 17 Python
详解python3 + Scrapy爬虫学习之创建项目
Apr 12 Python
python多进程并行代码实例
Sep 30 Python
Python使用Tkinter实现转盘抽奖器的步骤详解
Jan 06 Python
Python requests获取网页常用方法解析
Feb 20 Python
python代码实现扫码关注公众号登录的实战
Nov 01 Python
用Python进行基础的函数式编程的教程
Mar 31 #Python
python使用多线程不断刷新网页的方法
Mar 31 #Python
Python新手实现2048小游戏
Mar 31 #Python
举例介绍Python中的25个隐藏特性
Mar 30 #Python
在Python的循环体中使用else语句的方法
Mar 30 #Python
python实现2048小游戏
Mar 30 #Python
利用一个简单的例子窥探CPython内核的运行机制
Mar 30 #Python
You might like
PHPWind与Discuz截取字符函数substrs与cutstr性能比较
2011/12/05 PHP
PHP中文字符串截断无乱码解决方法
2016/10/10 PHP
PHP中类的自动加载的方法
2017/03/17 PHP
JQuery 选择器 xpath 语法应用
2010/05/13 Javascript
使用script的src实现跨域和类似ajax效果
2014/11/10 Javascript
详谈JavaScript内存泄漏
2014/11/14 Javascript
JavaScript 异常处理 详解
2015/02/06 Javascript
jquery插件jSignature实现手动签名
2015/05/04 Javascript
省市区三级联动jquery实现代码
2020/04/15 Javascript
微信小程序 122100版本更新问题解决方案
2016/12/22 Javascript
javascript中递归的两种写法
2017/01/17 Javascript
EasyUI实现下拉框多选功能
2017/11/07 Javascript
angularJS实现动态添加,删除div方法
2018/02/27 Javascript
Node.js安装详细步骤教程(Windows版)详解
2019/09/01 Javascript
vue组件中节流函数的失效的原因和解决方法
2020/12/02 Vue.js
python的id()函数介绍
2013/02/10 Python
深入浅析ImageMagick命令执行漏洞
2016/10/11 Python
Python的装饰器使用详解
2017/06/26 Python
pycharm运行程序时在Python console窗口中运行的方法
2018/12/03 Python
Python 函数返回值的示例代码
2019/03/11 Python
Python操作rabbitMQ的示例代码
2019/03/19 Python
Python面向对象之类和实例用法分析
2019/06/08 Python
详解Python对JSON中的特殊类型进行Encoder
2019/07/15 Python
Pytorch之保存读取模型实例
2019/12/30 Python
通过python检测字符串的字母
2020/02/18 Python
基于 Python 实践感知器分类算法
2021/01/07 Python
Html5游戏开发之乒乓Ping Pong游戏示例(三)
2013/01/21 HTML / CSS
来自Ocado的宠物商店:Fetch
2018/07/10 全球购物
what is the difference between ext2 and ext3
2015/08/25 面试题
保护环境建议书400字
2014/05/13 职场文书
新教师培训方案
2014/06/08 职场文书
ktv好的活动方案
2014/08/15 职场文书
解除劳动关系协议书2篇
2014/11/28 职场文书
2014年保险业务员工作总结
2014/12/23 职场文书
2015年社区流动人口工作总结
2015/05/12 职场文书
一文读懂go中semaphore(信号量)源码
2021/04/03 Golang