从零开始实现放置游戏(八)——实现后台管理系统(6)代码重构
前几张,我们主要实现了升级经验、人物等级属性、地图、地图怪物,这四种配置的增删查改以及Excel导入功能。我们主要以地图怪物为例,因此在文章末尾提供的源代码中只实现了地图怪物这部分的逻辑功能。
如果你照猫画虎,把4种配置功能的逻辑全部实现的话,就会发现,增删查改的代码基本相同,除了SQL语句和模型对象不同,其他地方变化不大。
本章我们利用泛型模板,对整个系统就行重构。在重构结束后,你就会发现写代码简直就是特喵的艺术!
后端重构
idlewow-core
我们从最底层开始,首先重构位于core模块中的数据访问层。目前看来,基本上所有的模型对象,都应包含增删查改、批量添加、列表查询这些基本方法。那我们把这些方法抽象到一个单独的Mapper和Manager里。
新建com.idlewow.common包,再该包下新建接口类BaseMapper:
package com.idlewow.common; import com.idlewow.common.model.QueryParam; import java.util.List; public interface BaseMapper<T> { /** * 添加记录 * @param t */ int insert(T t); /** * 批量添加记录 * @param list * @return */ int batchInsert(List<T> list); /** * 更新记录 * @param t */ int update(T t); /** * 删除记录 * @param id */ int delete(String id); /** * 根据id查询 * @param id * @return */ T find(String id); /** * 根据条件查询总数 * @param queryParam * @return */ int count(QueryParam queryParam); /** * 根据条件查询列表 * @param queryParam * @return */ List<T> list(QueryParam queryParam); }
BaseMapper.java
再在该包下,新建一个抽象类BaseManager,代码如下:
package com.idlewow.common; import com.idlewow.common.model.PageList; import com.idlewow.common.model.QueryParam; import org.springframework.beans.factory.annotation.Autowired; import java.util.List; public abstract class BaseManager<T> { @Autowired public BaseMapper<T> baseMapper; public void insert(T t) { int effected = baseMapper.insert(t); if (effected == 0) { throw new RuntimeException("sql effected 0 rows"); } } public void batchInsert(List<T> list) { int splitSize = 100; int index = 0; int total = list.size(); while (index <= total) { int end = index + splitSize; if (end > total) { end = total; } List<T> sublist = list.subList(index, end); int effected = baseMapper.batchInsert(sublist); if (effected == 0) { throw new RuntimeException("sql effected 0 rows"); } index += splitSize; } } public void update(T t) { int effected = baseMapper.update(t); if (effected == 0) { throw new RuntimeException("sql effected 0 rows"); } } public void delete(String id) { int effected = baseMapper.delete(id); if (effected == 0) { throw new RuntimeException("sql effected 0 rows"); } } public T find(String id) { T t = (T)baseMapper.find(id); return t; } public PageList<T> list(QueryParam queryParam) { PageList<T> pageList = new PageList<>(); int count = baseMapper.count(queryParam); List<T> list = baseMapper.list(queryParam); pageList.setTotalCount(count); pageList.setData(list); pageList.setPageParam(queryParam.getPageParam()); return pageList; } }
BaseManager.java
还是以地图怪物为例,我们重构MapMobMapper和MapMobManager,只要让他们继承BaseMapper和BaseManager即可,代码如下:
package com.idlewow.mob.mapper; import com.idlewow.common.BaseMapper; import com.idlewow.mob.model.MapMob; public interface MapMobMapper extends BaseMapper<MapMob> { }
MapMobMapper.java
package com.idlewow.mob.manager; import com.idlewow.common.BaseManager; import com.idlewow.mob.model.MapMob; import org.springframework.stereotype.Component; @Component public class MapMobManager extends BaseManager<MapMob> { }
MapMobManager.java
重构后的Mapper和Manager直接继承基类的增删查改方法,无需在各个业务中一遍又一遍的书写重复代码。
idlewow-rms
在rms模块中,主要对controller中的重复代码进行重构。在前几章,我们抽象出过一个BaseController,在里面实现了一些最基础的方法。这里,我们再抽象出一个CrudController来实现数据的增删查该;在CrudController的基础上,再抽象出一个ExcelController来实现Excel的批量导入。
在com.idlewow.rms.controlelr包下新建抽象类CrudContoller,代码如下:
package com.idlewow.rms.controller; import com.idlewow.common.BaseManager; import com.idlewow.common.model.BaseModel; import com.idlewow.common.model.CommonResult; import com.idlewow.common.model.PageList; import com.idlewow.common.model.QueryParam; import com.idlewow.util.validation.ValidateGroup; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; public abstract class CrudController<T extends BaseModel, Q extends QueryParam> extends BaseController { private final String path = this.getClass().getAnnotation(RequestMapping.class).value()[0]; @Autowired BaseManager<T> baseManager; @RequestMapping("/list") public Object list() { return this.path + "/list"; } @ResponseBody @RequestMapping(value = "/list", method = RequestMethod.POST) public Object list(@RequestParam(value = "page", defaultValue = "1") int pageIndex, @RequestParam(value = "limit", defaultValue = "10") int pageSize, Q q) { q.setPage(pageIndex, pageSize); PageList<T> pageList = baseManager.list(q); return this.parseTable(pageList); } @RequestMapping("/add") public Object add() { return this.path + "/add"; } @ResponseBody @RequestMapping(value = "/add", method = RequestMethod.POST) public Object add(@RequestBody T t) { try { CommonResult commonResult = this.validate(t, ValidateGroup.Create.class); if (!commonResult.isSuccess()) return commonResult; t.setCreateUser(this.currentUserName()); baseManager.insert(t); return CommonResult.success(); } catch (Exception ex) { logger.error(ex.getMessage(), ex); return CommonResult.fail(); } } @RequestMapping(value = "/edit/{id}", method = RequestMethod.GET) public Object edit(@PathVariable String id, Model model) { T t = baseManager.find(id); model.addAttribute(t); return this.path + "/edit"; } @ResponseBody @RequestMapping(value = "/edit/{id}", method = RequestMethod.POST) public Object edit(@PathVariable String id, @RequestBody T t) { try { if (!id.equals(t.getId())) { return CommonResult.fail("id不一致"); } CommonResult commonResult = this.validate(t, ValidateGroup.Update.class); if (!commonResult.isSuccess()) return commonResult; t.setUpdateUser(this.currentUserName()); baseManager.update(t); return CommonResult.success(); } catch (Exception ex) { logger.error(ex.getMessage(), ex); return CommonResult.fail(); } } @ResponseBody @RequestMapping(value = "/delete/{id}", method = RequestMethod.POST) public Object delete(@PathVariable String id) { try { baseManager.delete(id); return CommonResult.success(); } catch (Exception ex) { logger.error(ex.getMessage(), ex); return CommonResult.fail(); } } }
CrudController.java
这个CrudController起到了一个模板的作用,可以说非常的精髓。首先,利用泛型约束,解决了不同业务数据模型、查询参数不同的问题。然后,通过反射获取不同业务controller的Url映射,解决了不同业务跳转页面路径不同的问题。具体业务的controller直接继承此类,无需再写任何代码,即可实现增删查改。
下面我们再实现Excel导入功能的模板类,在该包内新建一个抽象类ExcelController,代码如下:
package com.idlewow.rms.controller; import com.idlewow.common.model.BaseModel; import com.idlewow.common.model.CommonResult; import com.idlewow.common.model.QueryParam; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.multipart.commons.CommonsMultipartResolver; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import java.io.File; import java.util.Iterator; import java.util.List; public abstract class ExcelController<T extends BaseModel, Q extends QueryParam> extends CrudController<T, Q> { @ResponseBody @RequestMapping(value = "/importExcel", method = RequestMethod.POST) public Object importExcel(HttpServletRequest request) { try { ServletContext servletContext = request.getServletContext(); String uploadPath = servletContext.getRealPath("/upload"); File dir = new File(uploadPath); if (!dir.exists()) { dir.mkdir(); } CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver(servletContext); if (multipartResolver.isMultipart(request)) { MultipartHttpServletRequest multiRequest = (MultipartHttpServletRequest) request; Iterator<String> iter = multiRequest.getFileNames(); while (iter.hasNext()) { MultipartFile file = multiRequest.getFile(iter.next()); if (file.getSize() > 0) { String fileName = file.getOriginalFilename(); String extension = fileName.substring(fileName.lastIndexOf(".")); if (!extension.toLowerCase().equals(".xls") && !extension.toLowerCase().equals(".xlsx")) { throw new Exception("不支持的文档格式!请上传.xls或.xlsx格式的文档!"); } String destFileName = fileName + "_" + System.currentTimeMillis() + extension; File destFile = new File(uploadPath, destFileName); file.transferTo(destFile); List<T> dataList = this.loadExcelData(destFile.getPath()); this.saveExcelData(dataList); if (destFile.exists() && !destFile.delete()) { logger.error("删除临时文件失败!" + destFile.getAbsolutePath()); } } } } return CommonResult.success(); } catch (Exception ex) { logger.error(ex.getMessage(), ex); return CommonResult.fail(); } } protected abstract List<T> loadExcelData(String excelPath) throws Exception; protected void saveExcelData(List<T> dataList) { this.baseManager.batchInsert(dataList); } }
ExcelController.java
在这个类中,我们将Excel导入功能分解成3个方法。importExcel,对应前端点击事件,保存上传的临时文件;saveExcelData,保存解析出的数据,即调用mapper的批量添加方法;这两个方法都是通用的,直接在ExcelController中实现即可。只有loadExcelData,解析Excel数据这个方法,不同业务的实现不同,我们把它定义成抽象方法,等待各个业务自己实现。
好了,基类已经定义好了,我们让MapMobController继承ExcelController即可,代码如下:
package com.idlewow.rms.controller; import com.idlewow.common.constant.DataDict; import com.idlewow.map.model.WowMap; import com.idlewow.mob.model.MapMob; import com.idlewow.query.model.MapMobQueryParam; import com.idlewow.util.poi.PoiUtil; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.xssf.usermodel.XSSFRow; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.ehcache.Cache; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import java.io.FileInputStream; import java.util.ArrayList; import java.util.List; @Controller @RequestMapping("/manage/map_mob") public class MapMobController extends ExcelController<MapMob, MapMobQueryParam> { @Autowired protected Cache mapCache; protected List<MapMob> loadExcelData(String excelPath) throws Exception { FileInputStream fileInputStream = new FileInputStream(excelPath); XSSFWorkbook workbook = new XSSFWorkbook(fileInputStream); Sheet sheet = workbook.getSheet("怪物"); List<MapMob> mapMobList = new ArrayList<>(); // 处理当前页,循环读取每一行 String createUser = this.currentUserName(); for (int rowNum = 2; rowNum <= sheet.getLastRowNum(); rowNum++) { XSSFRow row = (XSSFRow) sheet.getRow(rowNum); String mapName = PoiUtil.getCellValue(row.getCell(1)); String mobName = PoiUtil.getCellValue(row.getCell(2)); String faction = PoiUtil.getCellValue(row.getCell(3)); String mobClass = PoiUtil.getCellValue(row.getCell(4)); String mobType = PoiUtil.getCellValue(row.getCell(5)); Integer level = Integer.valueOf(PoiUtil.getCellValue(row.getCell(6))); Integer hp = Integer.valueOf(PoiUtil.getCellValue(row.getCell(7))); Integer damage = Integer.valueOf(PoiUtil.getCellValue(row.getCell(8))); Integer amour = Integer.valueOf(PoiUtil.getCellValue(row.getCell(9))); WowMap wowMap = (WowMap)mapCache.get(mapName); MapMob mapMob = new MapMob(); mapMob.setMapId(wowMap.getId()); mapMob.setMapName(wowMap.getName()); mapMob.setName(mobName); mapMob.setFaction(DataDict.Faction.getByDesc(faction).getCode()); mapMob.setMobClass(DataDict.MobClass.getByDesc(mobClass).getCode()); mapMob.setMobType(DataDict.MobType.getByDesc(mobType).getCode()); mapMob.setLevel(level); mapMob.setHp(hp); mapMob.setDamage(damage); mapMob.setAmour(amour); mapMob.setCreateUser(createUser); mapMobList.add(mapMob); } fileInputStream.close(); return mapMobList; } }
MapMobController.java
现在业务Controller里,只有一个独立实现的loadExcelData方法,再也不用重复书写增删查改了。(注意:这个类里多了个缓存对象mapCache,是我用来缓存地图数据的。pom中添加了对应包,具体可在源码中查看。用法比较简单,可搜EhCache。)
前端重构
除了后端代码冗余外,其实很容易发现,前端的代码重复部分也很多,尤其是写ajax请求的部分,其实每次变化的只有url地址,或者请求参数等。我们直接在js里定义一个类来实现增删查改等这些ajax请求的通用部分。
在/webapp/js/helper.js中,定义一个类CRUD,代码如下:
………… ………… var CRUD = function () { }; CRUD.prototype = { list: function (cols, url) { var table = layui.table; table.render({ elem: \'#datatable\' , url: url , method: \'post\' , cellMinWidth: 80 , cols: cols , page: { layout: [\'limit\', \'count\', \'prev\', \'page\', \'next\', \'skip\'] //自定义分页布局 , limits: [10, 20, 30, 40, 50] , groups: 3 //只显示 1 个连续页码 , first: \'首页\' , last: \'尾页\' } }); }, upload: function (url, extension) { layui.upload.render({ elem: \'#btnSelectFile\', url: url, accept: \'file\', exts: extension, auto: false, bindAction: \'#btnImport\', done: function (result) { if (result.code === 1) { layer.alert(result.message, {icon: 6}, function () { layui.layer.closeAll(); layui.table.reload(\'datatable\'); }); } else { layer.alert(result.message, {icon: 5}); } } }); }, search: function (data) { var table = layui.table; table.reload(\'datatable\', { where: data, page: { curr: 1 } }); }, add: function (url) { var form = layui.form; form.on(\'submit(add)\', function (data) { $.ajax({ url: url, type: \'post\', contentType: "application/json; charset=utf-8", data: JSON.stringify(data.field), success: function (result) { if (result.code === 1) { layer.alert(result.message, {icon: 6}, function () { xadmin.close(); xadmin.father_reload(); }); } else { layer.alert(result.message, {icon: 5}); } }, error: function () { layer.alert("请求失败", {icon: 5}); } }); }); }, edit: function (url) { var form = layui.form; form.on(\'submit(edit)\', function (data) { $.ajax({ url: url + \'/\' + data.field.id, type: \'post\', contentType: "application/json; charset=utf-8", data: JSON.stringify(data.field), success: function (result) { if (result.code === 1) { layer.alert(result.message, {icon: 6}, function () { xadmin.close(); xadmin.father_reload(); }); } else { layer.alert(result.message, {icon: 5}); } }, error: function () { layer.alert("请求失败", {icon: 5}); } }); }); }, remove: function (obj, url) { layer.confirm(\'确认要删除吗?\', function () { $.ajax({ url: url, type: \'post\', success: function (result) { if (result.code === 1) { $(obj).parents("tr").remove(); layer.msg(\'删除成功\', {icon: 1, time: 1000}); } else { layer.alert("删除失败", {icon: 5}); } }, error: function () { layer.alert("请求失败", {icon: 5}); } }); }); } }; window.crud = new CRUD(); ………… …………
Helper.js
然后,在/webapp/js/wow/map_mob中,修改add.js, edit.js 和 list.js 如下:
layui.use([\'form\', \'layer\'], function () { var form = layui.form; form.verify({}); crud.add(\'/manage/map_mob/add\'); });
add.js
layui.use([\'form\', \'layer\'], function () { var form = layui.form; form.render(); form.verify({}); crud.edit(\'/manage/map_mob/edit/\'); });
edit.js
layui.use([\'upload\', \'table\', \'form\'], function () { var cols = [[ {field: \'id\', width: 50, title: \'id\'} , {field: \'name\', title: \'怪物名称\'} , {field: \'mapName\', title: \'地图名称\'} , { field: \'faction\', title: \'阵营\', templet: function (d) { return enumUtil.faction(d.faction); } } , { field: \'mobClass\', title: \'怪物种类\', templet: function (d) { return enumUtil.mobClass(d.mobClass); } } , { field: \'mobType\', title: \'怪物类型\', templet: function (d) { return enumUtil.mobType(d.mobType); } } , {field: \'level\', title: \'等级\'} , {field: \'hp\', title: \'生命值\'} , {field: \'damage\', title: \'伤害\'} , {field: \'amour\', title: \'护甲\'} , { title: \'操作\', width: 150, templet: function (d) { return \'<button class="layui-btn layui-btn-xs" onclick="xadmin.open(\\'编辑怪物\\',\\'edit/\' + d.id + \'\\', 500, 500)" type="button"><i class="layui-icon"></i>编辑</button>\' + \'<button class="layui-btn-danger layui-btn layui-btn-xs" onclick="remove(this, \\'\' + d.id + \'\\')" type="button"><i class="layui-icon"></i>删除</button>\'; } } ]]; crud.list(cols, \'/manage/map_mob/list\'); crud.upload(\'/manage/map_mob/importExcel\', \'xls|xlsx\'); }); function search() { var data = { name: $(\'input[name="name"]\').val(), levelStart: $(\'input[name="levelStart"]\').val(), levelEnd: $(\'input[name="levelEnd"]\').val(), faction: $(\'select[name="faction"]\').val(), mobClass: $(\'select[name="mobClass"]\').val(), mobType: $(\'select[name="mobType"]\').val() }; crud.search(data); } function reset(){ $(\'#queryForm\').reset(); } function remove(obj, id) { crud.remove(obj, \'/manage/map_mob/delete/\' + id); }
list.js
可以看到,重构后的js,和后端一样,简洁多了。
运行效果
这里,由于改动了core模块,需要先对项目编译打包。再运行rms模块,即可正常启动项目。启动后,效果和之前一样,只是代码变得简洁多了,这里就不再截图了。
小结
本章终于把冗余的代码进行了重构,整个代码瞬间提升了几层逼格,变得干净多了。
源码下载地址:https://idlestudio.ctfile.com/fs/14960372-386521083
本文原文地址:https://www.cnblogs.com/lyosaki88/p/idlewow_8.html
说明:我的代码风格就是不套用设计模式,在不确定最终效果时,不做过多提前设计,先实现了再说,实现了再慢慢重构,还有就是几乎不注释。
最近因为有其他的事情,所以停更了一周。写到这里其实发现这一大章题目似乎叫RMS系统的初步实现更好。
写代码和写文章其实差别还有点大。自己写代码的时候,想到哪写到哪,各个模块并行随缘开发。但写文章就必须按一定顺序来,否则容易让人困惑。所以文章里的代码都是单独拉一个分支重新整理的。
项目交流群:329989095