作为一个程序员,我有时候忘了自己所具有的能力。当事情没有按照你想要的方式发展时,却很容易忘记你有能力去改变它。昨天,我意识到,我已经对我所出售的书的付款处理方式感到忍无可忍了。我的书完成后,我使用了三个不同的数字商品支付处理器,在对它们三个都感到不满后,我用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”方案会在你的页面上创建一个表单和按钮。当点击这个按钮时,一个简洁又引人注目的浮动层会弹出来。

2015331102015017.jpg (1412×887)

这个表单的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 \"\"\"

Card Declined

Your chard could not be charged. Please check the number and/or contact your credit card company.

\"\"\" 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=\"\"\"

Thanks for buying Writing Idiomatic Python!

If you didn\'t already download your copy, you can visit your private link. 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(\'/\')
def download_file(uuid):
  purchase = Purchase.query.get(uuid)
  if purchase:
    if purchase.downloads_left <= 0:
      return \"\"\"

No downloads left!

You have exceeded the allowed number of downloads for this file. Please email jeff@jeffknupp.com with any questions.

\"\"\" 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 \"\"\"
\"\"\" if __name__ == \'__main__\': sys.exit(app.run(debug=True))

能力越大…责任越大!

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

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

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

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