在情人节这天写技术文档,太没有情调了。不过有情人的话,哪天不是情人节,哈哈。
0x00 背景
在流程不太明确,每个人的责任边界比较模糊的时候,软件需求与产品都是个深坑。
0x001 未必是坑
对于在这个工作的经办人而言,这个时候的需求与产品会是:
- 一个关键的流程梳理工具
- 划分管理责任的重要依据
- 项目工作流程实时可见的重要手段
- 问题追溯定位的重要来源
0x002 要求不低
理想很丰满,毕竟在一片混沌之中,有个人杀出一条路之后,此后也就有路了。所以,为了完成这个目标,系统应该要具备:
- 有一个可以快速开发的框架,至少包括数据持久化,后台处理与前台交互
- 需要一个随需应变(On Demand)的设计架构,因为业务会随时的改,包括原来起初为一个会议参会人员设计的功能要变成多个会议同时进行的功能
- 开发起来时间一定要短,效果一定不能太差,而且必须通过网络访问
严格来说,上面那些需求都是非功能需求,那功能需求是:
- 能够通过处理相片的规则化命名,新建参会人员的姓名、单位、证卡类型、参加会议的种类
- 保留那些命名错误的照片,等待手工处理,总有一部分人提交的数据每次都有问题
- 整理完一个批次的入库资料后,会打包并新建一个excel表格,经校验完之后生成一个zip压缩包,同时将证件的状态变成“送打印”
- 每次打包的资料都是当前证件类型中未打印的资料
- 卡证制作完毕后对每张证件进行校验,核校数据,通过点击“核校”按钮,将证件状态变成“已核校”
- 对成产完成的证卡信息自动生成移交文档,通过选择批次数来选定数据,并合并同类证卡的人员名单
- 会议结束后,整理出每种证件制作的数量,并打印名单
在功能需求中提到的工作,就是我实际工作的内容,数据提交很零散,要汇总成一个大表,耗费的时间比较长,没有一个确切的统计数据和列表,核对制作证卡的时候就容易出错,上面的活不是很难,就是很反锁,很容易错,而且时候要做统计的时候又要花去不少的时间,此外还可能有多次提交制卡信息的单位,一个人实在是hold不住啊。
事情做久了,自己对业务熟悉了,别人就不清楚了,所以到头来重担还得自己挑。所以拿起武器,开干,核心程序写了三个晚上,白天在真实数据与环境下调试了几天,主要是那些不按照规定乱命名的文件名,让我写了很多异常处理的语句。
0x01 准备
我能拿起来的开发语言无非就是asp.net、java、php、python,至于javascript的什么Node.js,什么angularJs,什么TypeScript,今年应该就没问题了。备选的框架有:
- asp.net MVC5
- java Spring
- php thinkPhp
- python django/flask
为了保持技术的连贯性,我选择python,框架的选择是一个难题,选熟悉的django还是选更流行的flask,看了一下午的文档之后最终选择,挑战自己,选择从未碰过的flask。
0x011 配置环境
0x0111 选择Anaconda
选择Anaconda python2.7 的发行版,下次选择python3.5的,有windows,mac与linux版本。
0x0112 选择pycharm
工欲善其事,必先利其器
Python的IDE有很多,当然你也可以选择不用,但是我选的是Pycharm
0x0113
需要安装的一些插件
pip install flask_bootstrap
pip install flask_moment
pip install docx
pip install pymysql
缺了的套件都可以用 pip install来安装
0x02 架构
大体来说,代码架构是这样的
flack/
├── app.py
├── database.py
├── models.py
├── home.py
├── templates/
| └── some template html files
├── static/
| └── client-side js and css files
└── requirements.txt
0x021 大致框架
flask的框架大概是这样的:
- app.py的主要内容是包括页面显示,后台处理,Ajax服务以及自定义的页面面模版管道
- database.py与models.py主要是做数据持久化,以及数据实体的处理
- home.py是处理flask部署的时候设置
0x022 简单的后台处理流程
而页面处理主要是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| """ 初始化flask框架 """ app = Flask(__name__) """ 初始化bootstrap前端样式 """ bootstrap = Bootstrap(app) @app.route('/index') def index(): SQLQuery = '''select `user`.type_id,count(*) from attend,`user` where attend.user_id=`user`.id and attend.`status`=TRUE and attend.meeting_id = %s GROUP BY `attend`.cardtype_id''' result = db_session.execute(SQLQuery % getMeeting_id())
return render_template('index.html', result=result)
|
- @app.route(‘/index’) 表示uri,资源的访问路径
- def index(): 定义一个函数,可以与路径名不一样,但是必须唯一,不支持函数重载
- return render_template(‘index.html’, result=result) 啪啦啪啦处理完了之后,通过render_template来与模版一起渲染,通过ninja引擎,来实现展现页面
0x023 简单的页面前端实现
必须实现框架的基本模版,扩展的是bootstrap框架的模版
base.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| {% extends "bootstrap/base.html" %}
{% block title %}资料管理系统{% endblock %}
{% block navbar %} <div class="navbar navbar-inverse" role="navigation"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="/index">参会人员管理系统</a> </div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav"> <li><a href="/zhengjian/jizhe">记者证</a></li> <li><a href="/zhengjian/lan">蓝色工作证</a></li> <li><a href="/zhengjian/lv">绿色工作证</a></li> <li><a href="/zhengjian/anbao">安保证</a></li> </ul> </div> </div> </div> {% endblock %}
{% block content %} <div class="container"> {% block page_content %}{% endblock %} </div> {% endblock %}
{% block scripts %} {{ super() }} {{ moment.include_moment() }} {{ moment.lang('zh-CN') }} <script type="application/javascript"> $SCRIPT_ROOT = {{ request.script_root|tojson|safe }} </script>
{% endblock %}
|
index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| {% extends "base.html" %}
{% block title %}资料管理系统{% endblock %}
{% block page_content %} <h1>本次会议共制卡<b id="sum"></b>张,具体情况如下:</h1>
<div> {% if result %} {% for r in result %} <li id="result">{{ r[0]|getTypeRealName }}:{{ r[1] }}</li> {% endfor %} {% endif %} </div> <div> 如果显示效果不好,请点击下载<a href="/static/Soft/Chrome.exe">谷歌浏览器</a> </div> {% endblock %}
{% block scripts %} {{ super() }} <script type="text/javascript"> $(function () { $sum=0; $('li#result').each(function () { li = $(this); text = li.text(); array = text.split(':'); $sum+=parseInt(array[1]); }); $('#sum').text($sum); }); </script>
{% endblock %}
|
index.html是通过获得后台的数据通过ninja引擎渲染获得的显示页面
0x024 数据对象持久化处理
最早开发的时候是用sqlite,后来改用mysql
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| from sqlalchemy import create_engine from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.ext.declarative import declarative_base #'sqlite:///./card.db' engine = create_engine('mysql+pymysql://root:root@localhost/card?charset=utf8', convert_unicode=False) # 创建数据库引擎( 当前目录下保存数据库文件) db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine)) Base = declarative_base() Base.query = db_session.query_property()
def init_db(): # 在这里导入所有的可能与定义模型有关的模块,这样他们才会合适地 # 在 metadata 中注册。否则,您将不得不在第一次执行 init_db() 时 # 先导入他们。 import models Base.metadata.create_all(bind=engine)
|
0x025 数据实体定义
一个数据实体类的定义,脱离了实际的数据库,很简单的完成CRUD的操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class User(Base): __tablename__ = 'User'
id = Column(Integer, primary_key=True) name = Column(String(50), unique=True) type_id = Column(Integer, ForeignKey('CardType.id')) department = Column(String(250)) status = Column(BOOLEAN, default=True) path = Column(String(500)) updatetime = Column(String(500), default=datetime.datetime.now()) batchlog = relationship('BatchLog', backref='users', lazy='dynamic')
@hybrid_property def getUpdateTime(self): time = datetime.datetime.strptime(self.updatetime, '%Y-%m-%d %H:%M:%S.%f')
return time - datetime.timedelta(hours=8)
|
0x03 开发
光靠上面的只言片语,想完成一个flask的demo,或许还是有点困难的,有问题可以问我,或者看看flask的入门教程,也可以看看鄙人的代码。记录一下掉过的坑。
0x031 中文编码
python2.7处理中文永远绕不过的就是中文编码的问题,那个该死的‘utf8’,下一次我一定用python3.5了
0x0311 mysql编码
我怎么设置mysql的字符集
1
| show variables like 'character%';
|
mysql死活出来的乱码,于是使用了下面这一招
1 2 3 4 5
| engine = create_engine("mysql+pymysql://root:root@localhost/card?charset=utf8", convert_unicode=False) # 创建数据库引擎( 当前目录下保存数据库文件) db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine))
|
0x0312 路径中文编码
普通变量,使用decode(‘utf8’)函数
路径拼接,是用强制unicode的字符串
1
| rawpath = os.path.join(rawpath, u'记者证')
|
0x032 word文档的仿宋字体设置
在docx的源码,-->Lib-->site-packages-->docx-->text-->font.py
1 2 3 4 5 6 7
| @name.setter def name(self,value): rPr = self._element.get_or_add_rPr() rPr.rFonts_ascii = value rPr.rFonts_hAnsi = value #加上下面一句,亚洲字体 rPr.rFonts_eastAsia = value
|
一般的段落是
1 2
| acceptorparagraph.runs[0].font.name = u'仿宋_GB2312' acceptorparagraph._element.r_lst[0].rPr.rFonts.set(qn('w:eastAsia'), u'仿宋_GB2312')
|
表格的话是
1 2
| row_cells[0].paragraphs[0].runs[0].font.name = u'仿宋_GB2312' row_cells[0].paragraphs[0].runs[0].font.element.rPr.rFonts.eastAsia = u'仿宋_GB2312'
|
0x04 心得
其实整个程序开发还是费了不少劲,但是成效还是很明显了:
- 给我数据的人可以通过访问网页来查看相关参会人员的是否已经被成功接受
- 所有人都能通过网页得知当前的证卡制作情况
- 我理直气壮的提交做好的证件,不会被人说做漏了,也不会发生移交了卡发生丢失还赖我
- 我能在任何时间统计出已经做了多少卡的数量,毕竟是工作量啊,不然总是个模糊数,岂不是“葫芦僧盘葫芦案”
- 移交证卡的数量和名单列表自动生成,就不用每次都重新数,还容易出错,不然还说不清楚交出去多少
- 连报账材料都自动生成了,以后再也不用费时做这些体力活了,只要排版打印就好
- 只要我能说清楚了我做了啥,那就意味着标准在我手里,获得了标准的制定权就是拿到了游戏规则