【tornado】系列项目(二)基于领域驱动模型的区域后台管理+前端easyui实现
本项目是一个系列项目,最终的目的是开发出一个类似京东商城的网站。本文主要介绍后台管理中的区域管理,以及前端基于easyui插件的使用。本次增删改查因数据量少,因此采用模态对话框方式进行,关于数据量大采用跳转方式修改,详见博主后续博文。
后台界面展示:
地区管理包含省市县的管理。详见下文。
一、数据库设计
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
|
class Province(Base): """ 省 """ __tablename__ = 'province' nid = Column(Integer, primary_key = True ) caption = Column(VARCHAR( 16 ), index = True ) class City(Base): """ 市 """ __tablename__ = 'city' nid = Column(Integer, primary_key = True ) caption = Column(VARCHAR( 16 ), index = True ) province_id = Column(Integer, ForeignKey( 'province.nid' )) class County(Base): """ 县(区) """ __tablename__ = 'county' nid = Column(Integer, primary_key = True ) caption = Column(VARCHAR( 16 ), index = True ) city_id = Column(Integer, ForeignKey( 'city.nid' )) |
本次采用的是sqlAlchemy模块创建数据库,关于sqlAlchemy的数据库链接以及数据库创建本文不做介绍,详细见Python操作 RabbitMQ、Redis、Memcache、SQLAlchemy(点击进入详细介绍)
表关系分析:上述表关系比较简单,市中有外键,代表这是市是属于哪个省;同理县中也有外键,代表这个县是属于哪个市。
二、目录结构
该目录结构在前面博文【tornado】系列项目(一)之基于领域驱动模型架构设计的京东用户管理后台 中有详细介绍,本博文不再赘述。
三、路由映射
1
2
3
4
5
6
|
(r "/ProvinceManager.html$" , Region.ProvinceManagerHandler), #省份模板展示 (r "/province.html$" , Region.ProvinceHandler), #省份的增删改查 (r "/CityManager.html$" , Region.CityManagerHandler), #市模板展示 (r "/City.html$" , Region.CityHandler), #市的增删改查 (r "/CountyManager.html$" , Region.CountyManagerHandler), #县的模板展示 (r "/County.html$" , Region.CountyHandler), #县的增删改查 |
四、后台模板展示Handler
#以省份为例进行介绍(市县类似):
数据获取Handler:
1
2
3
4
5
|
class ProvinceManagerHandler(AdminRequestHandler): def get( self , * args, * * kwargs): # 打开页面,显示所有的省 self .render( 'Region/ProvinceManager.html' ) |
本Handler主要用于从模板展示,默认如果只有这一个handler,用户看到的将是空页面。关于数据的增删改查,详见下文。
五、核心增删改查操作
再介绍增删改查之前,我们先介绍母板文件layout的前端html和继承该模板的ProvinceManager.html部分JavaScript代码:
母版layout 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
|
<! DOCTYPE html> < html > < head lang="en"> < meta charset="UTF-8"> < title ></ title > < link rel="stylesheet" type="text/css" href="/Statics/Admin/Plugins/jquery-easyui/themes/default/easyui.css"> #导入easyui的css < link rel="stylesheet" type="text/css" href="/Statics/Admin/Plugins/jquery-easyui/themes/icon.css"> #导入easyui的图标 < link rel="stylesheet" type="text/css" href="/Statics/Admin/Css/Common.css"> #自定义css < script type="text/javascript" src="/Statics/Admin/Plugins/jquery-easyui/jquery.min.js"></ script > #导入jQuery < script type="text/javascript" src="/Statics/Admin/Plugins/jquery-easyui/jquery.easyui.min.js"></ script > #导入easyui的js </ head > < body class="easyui-layout"> < div data-options="region:'north'" style="height:50px"> </ div > < div data-options="region:'south',split:true" style="height:30px;"></ div > < div data-options="region:'west',split:true" title="后台管理" style="200px;"> < div id="aa" class="easyui-accordion" data-options="fit:true,border:false"> < div title="地区管理"> #easyui订制的左侧菜单 < a id="jd_menu_province" class="jd-menu" href="/ProvinceManager.html">省</ a > < a id="jd_menu_city" class="jd-menu" href="/CityManager.html">市</ a > < a id="jd_menu_county" class="jd-menu" href="/CountyManager.html">县(区)</ a > </ div > < div title="用户管理"> < a id="user" class="jd-menu" href="#">用户管理</ a > < a id="jd_menu_merchant" class="jd-menu" href="/MerchantManager.html">商户管理</ a > </ div > < div title="JD自营"> < a id="jd_menu_product" class="jd-menu" href="/ProductManager.html">产品管理</ a > </ div > </ div > </ div > < div data-options="region:'center'" title="{% block crumbs %} {% end %}"> #内容显示区 {% block content %} {% end %} </ div > </ body > </ html > |
省份内容展示区html:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
< div > < table id="dg"></ table > #easyui订制table < div id="dlg" class="easyui-dialog" style="400px;height:200px;padding:10px 20px" closed="true" buttons="#dlg-buttons"> #easyui订制模态对话框,默认关闭状态 < form id="fm1"> < div class="input-group clearfix"> < div class="group-label" style=" 80px;"> < span >省份:</ span > </ div > < div class="group-input" style=" 300px;"> < input id="dlg_nid" style=" 200px;display: none" name="nid"/> < input id="dlg_province" style=" 200px" class="easyui-textbox" type="text" name="caption" data-options="required:true,missingMessage:'省份不能为空'" /> #easyui订制form验证+错误信息提示 </ div > </ div > </ form > </ div > < div id="dlg-buttons"> #easyui订制按钮 < span id="dlg_summary" style="color: red"></ span > < a href="#" class="easyui-linkbutton" iconCls="icon-ok" onclick="Save()">保存</ a > < a href="#" class="easyui-linkbutton" iconCls="icon-cancel" onclick="javascript:$('#dlg').dialog('close')">取消</ a > </ div > </ div > |
JavaScript代码:
1
2
3
4
5
6
7
|
$( function () { // 加载表格数据 InitTable(); #初始化表格内容(即查询) InitPagination(); #初始化分页 InitMenu(); #初始化左侧菜单 }); |
首先介绍两个简单的js:
1
2
3
4
5
6
7
|
/* 初始化左侧菜单 */ function InitMenu(){ $( '#aa' ).accordion( 'select' ,0); #easyui语法:选择左侧第0个标签 $( '#jd_menu_province' ).addClass( 'active' ); #让省份默认选中 } |
1
2
3
4
5
6
7
8
9
10
11
12
|
/* 初始化分页 */ function InitPagination(){ var pager = $( '#dg' ).datagrid( 'getPager' ); $(pager).pagination({ beforePageText: '第' , afterPageText: '页 共{pages}页' , displayMsg: '当前显示{from}-{to}条记录 共{total}条数据' }) } |
关键的表格数据初始化js(查询js):
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
44
45
46
47
48
49
50
51
52
53
54
|
function InitTable(){ $( '#dg' ).datagrid({ title: '省份列表' , iconCls: 'icon-save' , #省份图标 url: '/province.html' , #获取数据的url method: 'get' , #获取方式 //fitColumns: true, idField: 'nid' , singleSelect: true , #默认单选 rownumbers: true , #显示行号 striped: true , #奇数行与偶数行颜色有区别 columns:[[ #每一列标题(easyui默认根据field将后端传来的数据按表格进行显示) { field: 'ck' , checkbox: true #显示checkbox }, { field: 'nid' , #从数据库获取的nid title: 'ID' , #显示名称为ID 80, #宽度80px align: 'center' #居中显示 }, { field: 'caption' , title: '标题' , 180, align: 'center' } ]], toolbar: [ #显示的按钮 { text: '添加' , #按钮名称 iconCls: 'icon-add' , #按钮图标 handler: AddRow #点击按钮后执行的返回函数 },{ text: '删除' , iconCls: 'icon-remove' , handler: RemoveRow },{ text: '修改' , iconCls: 'icon-edit' , handler: EditRow } ], pagePosition: 'both' , #上下均显示分页 pagination: true , #显示分页 pageSize:10, #默认每页显示的数据总数 pageNumber: 1, #默认第一页 pageList: [10,20,50], #分页可选每页显示数量 loadFilter: function (data){ #过滤函数 return data; } }); } |
上述js代码即查询时的js代码,接下来我们先看查询的后端业务处理类:
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
|
def get( self , * args, * * kwargs): """ 获取 :param args: :param kwargs: :return: """ if self .get_argument( 'type' , None ) = = 'all' : #如果是获取所有数据 ret = { 'status' : True , 'rows' : "", 'summary' :''} #将来要返回给前端的字典,包含是否获取成功的状态、获取的数据、错误信息 try : region_service = RegionService(RegionRepository()) #将数据库处理类的对象传入数据库业务协调类 all_province_list = region_service.get_province() #获取所有省份 ret[ 'rows' ] = all_province_list #将省份数据添加进返回前端的字典 except Exception as e: ret[ 'status' ] = False ret[ 'summary' ] = str (e) self .write(json.dumps(ret)) #返回给前端 else : #如果是获取分页数据 ret = { 'status' : True , 'total' : 0 , 'rows' : [], 'summary' : ''} try : rows = int ( self .get_argument( 'rows' , 10 )) #每页显示10条 page = int ( self .get_argument( 'page' , 1 )) #显示第一页 start = (page - 1 ) * rows 开始条数 region_service = RegionService(RegionRepository()) row_list = region_service.get_province_by_page(start, rows) #根据分页获取省份数据 row_count = region_service.get_province_count() #获取省份总数 ret[ 'total' ] = row_count ret[ 'rows' ] = row_list except Exception as e: ret[ 'status' ] = False ret[ 'summary' ] = str (e) self .write(json.dumps(ret)) #返回给前端 |
数据库业务协调处理类的对应操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class RegionService: def __init__( self , region_repository): self .regionRepository = region_repository def get_province_count( self ): count = self .regionRepository.fetch_province_count() #获取省份总数 return count def get_province_by_page( self , start, offset): #根据分页获取省份 result = self .regionRepository.fetch_province_by_page(start, offset) return result def get_province( self ): #获取所有省份 return self .regionRepository.fetch_province() |
数据库操作类相关操作:
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
|
class RegionRepository(IRegionRepository): def __init__( self ): self .db_conn = DbConnection() #实例化数据库链接对象(只需创建一次对象,下面所有方法都不需要再创建) def fetch_province( self ): #获取所有省份 cursor = self .db_conn.connect() sql = """select nid,caption from province order by nid desc """ cursor.execute(sql) db_result = cursor.fetchall() self .db_conn.close() return db_result def fetch_province_by_page( self , start, offset): #根据分页获取省份 ret = None cursor = self .db_conn.connect() sql = """select nid,caption from province order by nid desc limit %s offset %s """ cursor.execute(sql, (offset, start)) db_result = cursor.fetchall() self .db_conn.close() return db_result def fetch_province_count( self ): #获取省份总数 cursor = self .db_conn.connect() sql = """select count(1) as count from province """ cursor.execute(sql) db_result = cursor.fetchone() self .db_conn.close() return db_result[ 'count' ] |
以上就是查询操作的所有过程。
增加:
js:
1
2
3
4
5
6
7
8
9
10
|
/* 添加 */ function AddRow(){ // 显示对话框,由于希望添加则将方法设置为POST $( '#fm1' ).form( 'clear' ); #清空上次form的内容 $( '#dlg' ).dialog( 'open' ).dialog( 'setTitle' , '创建省份' ); #设置模态对话框标签是创建省份 $( '#dlg_summary' ).empty(); #清空错误信息 METHOD = 'post' ; #设置提交方式为post } |
增加操作实际上就做了一个操作:打开模态对话框。
前端页面展示:
当用户输入需要添加的省份,接下来点击保存按钮,数据将被写入数据库并在前端显示:
保存的js代码:
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
|
/* 保存按钮 */ function Save(){ var isValid = $( '#fm1' ).form( 'validate' );前端form验证 if (isValid){ $.ajax({ url: '/province.html' , #提交的url type: METHOD, #根据之前定义的方法进行提交 data: {caption: $( '#dlg_province' ).val(),nid: $( '#dlg_nid' ).val()}, #提交的数据 dataType: 'json' , #数据格式 success: function (data){ #如果后端成功返回数据 if (data.status){ #后端操作成功 $( '#fm1' ).form( 'clear' ); #清空form内容 $( '#dlg' ).dialog( 'close' ); #关闭模态对话框 $( '#dg' ).datagrid( 'reload' ); #重新加载数据 } else { $( '#dlg_summary' ).text(data.summary); #否则显示错误信息 } } }) } else { // 前端验证通过 } // $('#fm').form('clear'); } |
增加对应的后端业务处理方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
def post( self , * args, * * kwargs): """ 添加 :param args: :param kwargs: :return: """ ret = { 'status' : False , 'summary' : ''} caption = self .get_argument( 'caption' , None ) if not caption: ret[ 'summary' ] = '省份不能为空' else : try : region_service = RegionService(RegionRepository()) result = region_service.create_province(caption) #创建省份,如果省份已存在,返回None if not result: ret[ 'summary' ] = '省份已经存在' else : ret[ 'status' ] = True #操作成功 except Exception as e: ret[ 'summary' ] = str (e) self .write(json.dumps(ret)) #返回给前端 |
数据库协调处理类对应的方法:
1
2
3
4
5
|
def create_province( self , caption): exist = self .regionRepository.exist_province(caption) #先判断省份是否存在,如果存在,该方法返回值为None if not exist: self .regionRepository.add_province(caption) #创建省份 return True |
数据库对应操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
def exist_province( self , caption): #省份是否存在 cursor = self .db_conn.connect() sql = """select count(1) as count from province where caption=%s """ cursor.execute(sql, (caption,)) db_result = cursor.fetchone() self .db_conn.close() return db_result[ 'count' ] def add_province( self , caption): #创建省份 cursor = self .db_conn.connect() sql = """insert into province (caption) values(%s)""" effect_rows = cursor.execute(sql, (caption,)) self .db_conn.close() return effect_rows |
以上就是省份添加的全部过程。
修改:
实际上修改和添加基本上是一样的,接下来只介绍与添加不同的地方:
js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
/* 修改 */ function EditRow(){ // 显示对话框,由于希望修改则将方法设置为PUT // 获取选中的值,将其赋值到页面上,然后ajax提交 var row = $( '#dg' ).datagrid( 'getSelected' ); $( '#dlg_summary' ).empty(); if (row){ METHOD = 'put' ; $( '#fm1' ).form( 'clear' ); $( '#fm1' ).form( 'load' ,row); $( '#dlg' ).dialog( 'open' ).dialog( 'setTitle' , '修改省份' ); } else { $.messager.alert( '警告' , '请选择要修改的行' , 'warning' ); } } |
这里弹出模态对话框,与添加不同的是,修改需要将用户原有数据存放在input标签中,方便用户进行修改。同时,将提交方法修改为put。
修改模态对话框示例截图:
接下来用户修改完成后的点击保存,关于保存的js代码详见上文添加部分。
保存的后台业务处理handler方法:
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
|
def put( self , * args, * * kwargs): """ 更新 :param args: :param kwargs: :return: """ ret = { 'status' : False , 'summary' : ''} nid = self .get_argument( 'nid' , None ) caption = self .get_argument( 'caption' , None ) if not caption or not nid: ret[ 'summary' ] = '省份不能为空' else : try : region_service = RegionService(RegionRepository()) result = region_service.modify_province(nid, caption) if not result: ret[ 'summary' ] = '省份已经存在' else : ret[ 'status' ] = True except Exception as e: ret[ 'summary' ] = str (e) self .write(json.dumps(ret)) |
该方法与添加时的方法基本一致,这里不做过多介绍。
数据库协调处理类对应的方法:
1
2
3
4
5
|
def modify_province( self , nid, caption): exist = self .regionRepository.exist_province(caption) if not exist: self .regionRepository.update_province(nid, caption) #更新省份 return True |
数据库操作对应类的方法:
1
2
3
4
5
6
|
def update_province( self , nid, caption): #更新省份 cursor = self .db_conn.connect() sql = """update province set caption=%s where nid=%s """ effect_rows = cursor.execute(sql, (caption, nid,)) self .db_conn.close() return effect_rows |
以上就是省份数据修改的全部过程。
删除:
js:
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
/* 删除 */ function RemoveRow(){ // 获取已经选中的行 var rows = $( '#dg' ).datagrid( 'getSelections' ); console.log(rows); if (rows.length<=0){ // 警告框 $.messager.alert( '警告' , '请选择要删除的行' , 'warning' ); } else if (rows.length>1){ $.messager.alert( '警告' , '不支持批量删除' ); } else { // 确认框 $.messager.confirm( '确定' , '您确定要删除吗?' , function (status) { #easyui订制的确认框 if (status){ // 点击确定 // 获取当前选中行的值,Ajax发送到后台 var row = rows[0]; $.ajax({ url: 'province.html' , type: 'delete' , data: {nid: row.nid}, dataType: 'json' , success: function (data) { if (data.status){ //删除成功 $.messager.show({ #easyui订制的messager框 msg: '删除成功' , showType: 'slide' , #淡出 showSpeed: 500, #速度 timeout: 5, #显示5秒 style:{ right: '' , top:document.body.scrollTop+document.documentElement.scrollTop, #在屏幕上方显示 bottom: '' } }); // 重新加载表格 var rowIndex = $( '#dg' ).datagrid( 'getRowIndex' , row); $( '#dg' ).datagrid( 'deleteRow' ,rowIndex); $( '#dg' ).datagrid( 'reload' ); // 删除指定行 //var rowIndex = dt.datagrid('getRowIndex', row); //dt.datagrid('deleteRow',rowIndex); } else { //删除失败 // $.messager.alert('错误信息', data.summary ,'error'); $.messager.show({ #显示错误信息 icon: 'error' , title: '错误信息' , msg:data.summary, showType: 'slide' , timeout: 0, style:{ right: '' , top:document.body.scrollTop+document.documentElement.scrollTop, bottom: '' } }); } } }); } }) } } |
后台handler类对应方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
def delete( self , * args, * * kwargs): """ 删除 :param args: :param kwargs: :return: """ ret = { 'status' : False , 'summary' : ''} nid = self .get_argument( 'nid' , None ) if not nid: ret[ 'summary' ] = '请选择要删除的省份' else : # 调用service去删除吧... # 如果删除失败,则显示错误信息 try : region_service = RegionService(RegionRepository()) region_service.delete_province(nid) #根据nid删除省份 ret[ 'status' ] = True except Exception as e: ret[ 'summary' ] = str (e) self .write(json.dumps(ret)) |
数据库业务协调处理类对应的方法:
1
2
3
|
def delete_province( self , nid): self .regionRepository.remove_province(nid) |
数据库操作类对应方法:
1
2
3
4
5
6
|
def remove_province( self , nid): cursor = self .db_conn.connect() sql = """delete from province where nid=%s """ effect_rows = cursor.execute(sql, (nid,)) self .db_conn.close() return effect_rows |
以上就是删除的全部过程。
总结:本文主要以省份的增删改查为例介绍了前端easyui的使用,后端handler、数据库业务协调处理类、数据库操作类的整个流程。