这个项目从我们这个学期开了系统分析与设计这门课之后就开始了,首次创建这个项目是在3月26号,实际上真正的开发时间集中在6月上旬。差不多用两个星期的闲暇时间时间完成了这个简单的进销存系统。这个系统基于Flask框架,使用SQLite3数据库,前端则是使用Bootstrap框架,实现了包括客户,库存,销售,进货等管理子系统。说是管理,其实也就是简单的增加和删除而已。 前人经常说,不要重复造轮子。既然有类似于Odoo这样强大的ERP系统珠玉在前,我为什么要再自己造一个功能简单,甚至根本不会有人用的轮子呢?从前我确实是这么想的,但是稍微有一点工作经验之后发现再造轮子的原因有两条:

  • 作为一个处在知识积累阶段的学生而言,再造轮子很有必要。不造一次轮子,你就永远不会明白轮子内部的真实构造。很多技术虽然看起来简单,但是想要有一个完善且健壮的实现却有很多坑需要踩。踩这些坑的过程,也正是一个技术人员成长最快的过程。
  • 别人的轮子终究是别人的。你不知道这个轮子最大能承受多少压力,你也不知道这个轮子在什么样的情况下会爆胎。当一个轮子装上企业呼呼向前的列车,再要想把它替换下来是一件很难且成本很高的事情。

接下来,讲讲造这个轮子的一些经历吧~

项目启动

正如前言中说的,这个项目是系统分析与设计这门课的课程设计,也是贯穿着整个学期的一个主旋律。早在这学期开学之初,我们就知道了自己需要做这样的一个系统。经历了上个学期各种技术炫了半天,最后却只是抄袭各种开源项目草草实现的失败,这个学期我务实了很多。从最开始的讨论就一直跟组员强调技术的可行性,基本上否决了所有试图增大项目复杂度的建议,把主题定在了一个纯粹的进销存管理系统。之所以如此,是因为我的观念发生了很大的变化:原来觉得技术的先进是最重要的,开发项目一定要用最现代的技术,最炫目的特效;现在觉得哪怕是一个用VB写出来的能稳定运行的丑陋系统都要胜过采用了种种最新潮技术却漏洞百出根本没法运行的“先进”系统。 在讨论到最极端的时候,我甚至想只做一个仓库的管理系统,只包括进库和出库这样的简单功能。这样的态度曾经闹得有一次讨论直接不欢而散,好在组员都是非常Nice的人,在我主动表达歉意之后大度地表示没什么。经过多次讨论之后,我们终于达成了共识,要做一个进销存系统,功能尽可能简单,在实现基础功能的前提下,再考虑加入新的功能。 接下来的事情就没有什么大的分歧了,整体系统采用B/S架构,开发语言选用我个人比较喜欢的Python,使用Flask框架,数据库选用无需配置的SQLite。然后服务器选择阿里云的学生机,系统选择成熟稳定的CentOS 7.2,Web Server选用Nginx,也就是我个人目前比较喜欢的开发平台——LESP(Linux, Nginx, SQLite, Python)。

项目细节

下面再介绍一下项目的细节。

设计模式

项目大体上采用了MVC的思想,不过在具体的实现上并没有太过纠结于概念上的东西。所有的网页模板都在templates文件夹下,静态资源都扔到了static目录,使用一个models.py模块来单独存放所有的数据库定义,然后所有的路由以及操作都在views中实现。对于我来说,快速地实现需求才是头等大事,是不是符合正统的MVC理念并不是十分重要。实际上现在这样的结构开发起来感觉也蛮顺手的,需要调整前端的样式和表单就在templates文件夹下操作,需要修改程序的逻辑就去views,他们基本上是一一对应的关系。这里有更加清晰的讲解,值得一看。

项目中的问题&解决方案

在Flask框架中同时使用蓝图与Flask_SQLAlchemy

https://segmentfault.com/q/1010000005640527

[email protected]@,他们强有力地向我证明了有师傅带路的好处。

这个问题纯粹是因为一直以来都是一个脚本Boy,没有系统性的学习过软件开发导致的。很显然,按照我原来的代码去产生实例,就会导致循环导入的错误。想要解决这样的问题,就需要使用工厂函数去生成实例,而不是自己去生成它。

__init__.py中添加一个create_app()函数,在函数中进行参数配置,初始化和导入蓝图的操作:

bps = ['jade_ims.views.dashboard:dashboard',
       'jade_ims.views.install:install',
       'jade_ims.views.login:login',
       'jade_ims.views.sale:sale',
       'jade_ims.views.customer:customer',
       'jade_ims.views.purchase.inputbill:inputbill',
       'jade_ims.views.purchase.supplier:supplier',
       'jade_ims.views.stock.enterstockbill:enterstockbill',
       'jade_ims.views.stock.leavestockbill:leavestockbill',
       'jade_ims.views.stock.stock:stock'
       ]

def create_app():
    app = Flask(__name__)
    app.config.from_object('config')
    app.config.from_pyfile('config.py')

    db.init_app(app)

    for path in bps:
        bp = import_string(path)
        app.register_blueprint(bp)

    return app

Flask如何优雅地重定向所有未登录用户到登陆界面

https://segmentfault.com/q/1010000005645821

[email protected]@igaozp

有一个Feature是想要把所有未登录的用户都重定向到登录页面。当时手头上的技术方案主要有两种:第一种是自己包装一个装饰器,并添加在每一个视图函数中;第二种是使用第三方库Flask-Login,在视图函数中添加@login_required。但是感觉姿势都不怎么优雅,因为我需要不断地在视图函数中添加这个装饰器,不太符合DRY(Don’t Repeat Yourself)准则。 SF的老司机给了我一个相当优雅的方案——Hook到app的before_request方法。也就是这样来实现:

@app.before_request
def check_need_login():
    # 检查登录的逻辑
    pass

我第一次是这样实现的:

@app.before_request
def check_need_login():
    if 'logged_in' not in session:
        return redirect(url_for('login.user_login'))

这样就带来了一个问题,对于未登录的用户来说,所有页面都会不断地重定向从而导致整个应用崩溃。因此还需要指定在某些情况下停止重定向,所以我们还需要修改一下check_need_login的逻辑:

@app.before_request
def check_need_login():
    if 'logged_in' not in session and request.endpoint not in ('login.user_login', 'static'):
        return redirect(url_for('login.user_login'))

session中没有logged_in字段且endpoint不是user_loginstatic的时候就重定向到登录页面,这样就比较优雅地解决了重定向未登录用户到登陆界面的需求。

flask_sqlalchemy 插入数据时发生错误后如何处理

https://segmentfault.com/q/1010000005647431

在实际开发的过程中发现,如果db.session中添加的记录有问题,在commit的时候会出现一个报错。所以我们需要在出错的时候进行一些处理,以保证程序出错之后还能够正常地运行。网友 @学不会编程的永仲 给出的回答是我应该在form中验证数据的正确性。虽然我相信他的理解可能是正确的,但是不太符合我的哲学,我更加倾向于让它崩溃,然后处理异常而不是事先处理好所有的错误。 通过查阅文档,了解到db.session.rollback()这一函数可以在出错是进行回滚,于是最后的实现变成了这样:

@supplier.route('/purchase/supplier/add', methods=['POST'])
def add_supplier():
    form = request.form
    if request.method == 'POST':
        print(form)
        supplier = Supplier(form['supplier_name'],
                            form['supplier_constract'],
                            form['supplier_phone'],
                            form['supplier_address'],
                            form['supplier_remark'])
        try:
            db.session.add(supplier)
            db.session.commit()
            flash('供应商添加成功!', 'success')
        except:
            db.session.rollback()
            flash('输入不合法,请重新输入!', 'danger')
    return redirect(url_for('supplier.list_supplier'))

项目总结

随着答辩的完成,这个项目进入了尾声。因为很多东西需要去学习,有新的东西需要去探索,所以这个项目也很有可能不会再继续维护了。那么这个项目开源出来的价值在哪里呢?我想,这个项目虽然简单,但它毕竟还算是一个完整的Flask项目,除了flask_sqlalchemy之外没有多余的依赖,非常适合一个跟我一样的Python初学者进行入门学习。如果说有人能够因为这个项目少走一些弯路,少在一些死胡同浪费时间,我想这个项目的价值就已经实现了。如果再往大一点来说,开源的价值也就实现了。 如果要我自评的话,我觉得这样的项目应该有60分。之所以给一个及格分,是因为这个项目毕竟实现了需求的大多数功能,也有好好地跑在阿里云的服务器上。丢分的话,大体上是因为这个项目毫无注释,没有安装文档,也没有配置手册,很多东西都是硬编码到了代码中。不仅如此,编程的风格也是相当的不佳,在模板部分缺乏良好的顶层设计,出现了大量的重复代码。这些问题希望自己能在下一个项目中得到一些改善。 Anyway,随着这个项目的结束,我的大三生涯也要结束了。接下来是波澜壮阔的实习阶段,希望自己能够在实习的时候多踩一些坑,多走一些弯路,多 得到一些来自“长者”的指点。

参考资源

更新日志

  • 2016年06月17日 首次发布