作为一个程序员,我有时候忘了自己所具有的能力。当事情没有按照你想要的方式发展时,却很容易忘记你有能力去改变它。昨天,我意识到,我已经对我所出售的书的付款处理方式感到忍无可忍了。我的书完成后,我使用了三个不同的数字商品支付处理器,在对它们三个都感到不满后,我用Python和Flask,两个小时的时间写出了我自己的解决方案。没错!两个小时!现在,这个系统支撑着我的书籍付费流程,整个过程难以置信的简单,你可以在20秒内购买书籍并开始阅读。
往下看,看我是如何在一夜之间完成我自己的数字商品支付解决方案的。
在我开始卖书的时候,我综合用了两种支付服务(一种是信用卡,另一种是PayPal)。最终,我发现了一个可以支持两者的处理方式。可是我对它们都不满意。最常用的那个支付处理器,要求用户在销售商的系统中创建一个账户,并且输入他们的邮箱地址(尽管邮箱并没有用)。
另外,我尝试使用Google Analytics 在全部的访问中追踪访客,包括他们的结账过程,这一过程也很艰辛。我经常感觉到,如果我能够让它工作起来,并且能够在我的书籍页面进行A/B测试,我能极大地提高销量。但是因为不能很好的追踪,我就没那么走运了。
最后,使用三个不同的支付处理器,发送书籍更新非常耗时。没有一个能很好的支持更新,而我希望有个“一键”解决方案来发送我的书籍更新。我就没找到一个类似的服务。
昨天,当我收到一个顾客的邮件,抱怨支付过程是如何的困难并且告诉我可能因此损失了很多销量后,我忍无可忍了。我决定整一个自己的数字商品管理解决方案。我需要做到下面这样了流程:
当客户点击“Buy Now”按钮后,他们应该仅仅被要求输入他们的email地址和信用卡信息。点击“Confirm”后被带到一个独一无二的URL去下载书籍(专门为了这次交易而生成的)。一封包含这个URL的邮件应该被发送给客户(防止用户需要重新下载这本书)。对他们重复下载的次数应该有个限制(5次)。交易信息和客户信息应该被存放在数据库中,发送书籍更新应该只是一个命令的事。
显然,这并不是那么的复杂。最复杂的部分是动态生成导向特定书籍版本的独一无二的URL。其他的事情都挺简单的。
剧透:程序的最终结果正好是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”方案会在你的页面上创建一个表单和按钮。当点击这个按钮时,一个简洁又引人注目的浮动层会弹出来。
这个表单的 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公共仓库“。你还需要让大家知道有这个东西。从邮件列表到论坛再到个人博客,从来都不缺少让大家知道你创造了一些东西的途径。我总是设法回馈社区,因为我从中得到了很多东西。
Jeff Knupp,发表于2014年1月18日