利用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中使用PIL模块对图片进行高斯模糊处理的教程
May 05 Python
python基础知识小结之集合
Nov 25 Python
在python中,使用scatter绘制散点图的实例
Jul 03 Python
python实现两张图片拼接为一张图片并保存
Jul 16 Python
python如何删除文件中重复的字段
Jul 16 Python
在Django下创建项目以及设置settings.py教程
Dec 03 Python
python 回溯法模板详解
Feb 26 Python
QML实现钟表效果
Jun 02 Python
python time.strptime格式化实例详解
Feb 03 Python
利用python实现汉诺塔游戏
Mar 01 Python
python百行代码实现汉服圈图片爬取
Nov 23 Python
python turtle绘图
May 04 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
PHP的FTP学习(四)
2006/10/09 PHP
PHP 图片文件上传实现代码
2010/12/29 PHP
php入门学习知识点一 PHP与MYSql连接与查询
2011/07/14 PHP
PHP下使用CURL方式POST数据至API接口的代码
2013/02/14 PHP
php实现数组中出现次数超过一半的数字的统计方法
2018/10/14 PHP
通过PHP实现获取访问用户IP
2020/05/09 PHP
HTML-CSS群中单选引发的“事件”
2007/03/05 Javascript
Dojo 学习要点
2010/09/03 Javascript
js隐藏与显示回到顶部按钮及window.onscroll事件应用
2013/01/25 Javascript
js图片延迟加载的实现方法及思路
2013/07/22 Javascript
根据表格中的某一列进行排序的javascript代码
2013/11/29 Javascript
关于img的href和src取变量及赋值的方法
2014/04/28 Javascript
JavaScript中的replace()方法使用详解
2015/06/06 Javascript
使用JavaScript制作一个简单的计数器的方法
2015/07/07 Javascript
基于Jquery+div+css实现弹出登录窗口(代码超简单)
2015/10/27 Javascript
微信小程序 wxapp导航 navigator详解
2016/10/31 Javascript
JS实现页面跳转参数不丢失的方法
2016/11/28 Javascript
jQuery中 bind的用法简单介绍
2017/02/13 Javascript
[04:16]DOTA2英雄梦之声_第09期_斧王
2014/06/21 DOTA
[51:26]VP vs VG 2018国际邀请赛小组赛BO2 第二场 8.19
2018/08/21 DOTA
django 链接多个数据库 并使用原生sql实现
2020/03/28 Python
Python + selenium + crontab实现每日定时自动打卡功能
2020/03/31 Python
通过Python扫描代码关键字并进行预警的实现方法
2020/05/24 Python
Pandas之缺失数据的实现
2021/01/06 Python
html5 Canvas画图教程(11)—使用lineTo/arc/bezierCurveTo画椭圆形
2013/01/09 HTML / CSS
澳大利亚旅游网站:Lastminute
2017/08/07 全球购物
什么是Deployment descriptors;都有什么类型的部署描述符
2015/07/28 面试题
金鑫耀Java笔试题
2014/09/06 面试题
工程管理英文求职信
2014/03/18 职场文书
党支部党的群众路线对照检查材料
2014/09/24 职场文书
2015年专项整治工作总结
2015/04/03 职场文书
遗嘱范文
2015/08/07 职场文书
为什么代码规范要求SQL语句不要过多的join
2021/06/23 MySQL
python Tkinter模块使用方法详解
2022/04/07 Python
Java后端 Dubbo retries 超时重试机制的解决方案
2022/04/14 Java/Android
python使用shell脚本创建kafka连接器
2022/04/29 Python