Github: https://github.com/iccb1013/Jade.Net
我们只消耗了8人/天的时间,完成了全部工作,基于我们 Jade.Net 的开源后台代码,任何小规模的后台管理系统,都可以在极短的时间内完成。
这是我们在 2017 年早些时候开发的一个项目,甲方是一家工艺美术品企业,需要开发一款 APP 展示产品,并引入会员(多级代理),在线下单,返点等功能。 在立项后由于一些原因,选择了使用 Java 来开发后台管理部分,面向 IOS 和 Android 版客户端提供服务。
项目的前期调研、分析、设计工作,及推进过程这里不作过多讨论,本文主要围绕改造工作中的技术问题进行记录和分析。
先简单看一下,了解这是怎么样的一个项目,终端 APP 如图:
后台管理端,有两个职责:
一)向 APP 端提供功能接口,如商品接口,会员接口等;
二)纯后台管理功能,如商品管理,会员管理等;
Java 版后台做出的效果如下:
其它管理类功能后台效果与之类似,总体而言并不复杂。除开业务逻辑之外,是很普通的管理后台。
下面我对这次改造工作的过程进行回顾与说明。
这次改造重构的难点是要保持 APP 接口的绝对兼容,不能影响生产环境的正常运行。
首先我们分析原 Java 版项目:
Java 版后台开发人员把其公司历史项目也掺杂在内,并且新项目内有多处引用(这个严重点都会涉及法律问题),代码结构引用关系比较混乱,后台的基本权限、菜单、字典散落分布在各处:
原后台使用的是 MySql 数据库,我们这次改造要将数据库换为 SQL Server ,并使用 Entity Framework 作为我们的数据库访问层。
由于 Java 版开发人员使用了原公司的系统,很多表可能仅仅只是改了下表名,里面有大量无关的字段,并且保留了原项目的所有表设计,包括一些公共组件需要用到的表,引用关系比较混乱,没有表结构说明书,我们需要重新核对整个数据库表结构的设计:
数据表字段名与类属性名的不合理,让我们的核对工作不是很乐观:
使用了 mybatis 做为数据访问层,我们在改造过程中,需要逐一核对数据库操作,以便在改造过程中保持百分之百的兼容:
下面开始我们的改造工作:
第一步:使用 SQL Server 重建数据库
迁移数据库分为两个步骤,一是建库,二是迁移数据。
理论上来说建库工作可以导出原 MySQL 的建库脚本调整后在 SQL Server 执行来建立数据库,但是为了后续工作更稳妥的开展,我们采购了人工核对,手工建立的方式,重新梳理表结构。
我们根据 Java 代码和原 MySQL 数据库,分析出了详细的的表结构设计:
在这一过程中,也对原表结构进行了修订与细节调整,主要修订以下存在的问题:
1)字段命名模糊,比如字段 modify_person ,或 person_id 。系统中存在 后台用户 和 客户 两个概念,一个用表 user 存储,一个用表 customer 存储,所以一些表中的 person_id 指向很模糊。
2)字段命名不统一,比如同样是备注,有些表使用 description ,有些表则使用 remark诸如此类,我们在重构中进行了完全的统一。
3)去除了原系统中不使用的表和字段,重新设计部分基础结构方面的表,去除了原开发人员复用的不知名项目相关的字段关联关系。
4)一些单词拼写错误。
此外,优化了一些表关联结构及业务:
1)商品与商品分类的关系,由一个商品只能属于一个分类,优化为允许属于多个分类,变为类似标签的概念。
2)商品的图片存储,由关联表的一对多存储,优化为在商品表通过一个字段存储 json 数组来保存图片URL,这一优化可以使后台相关代码简化许多。
3)若干字段存储不合理的问题,可能是由于开发过程中的需求调整,开发人员对增加的字段存储欠考虑,存储的位置和结构存在问题,我们在重构中进行了修正。
表和字段的命名方式,不管合不合理,毕竟我们是重构,不是推倒重做,所以没有太大变化,继承了过去的规则和方式,但是去掉了“jade_” 的表名前缀。
梳理数据库之后,我们为数据库建立了完整的外键关联关系,使用 Entity Framework 生成了实体模型:
第二步:迁移数据
在改造之后,需要将原数据库中的数据完全迁移过来,考虑到表结构发生了一定的变化,已无法简单的数据导出导入,我们专门编写了一系列的迁移脚本在上线前完成数据迁移工作。
第三步:.NET 版本后台框架的搭建
首先我们确立重构所要达到的目标:
1)完全兼容 Java 版本向 APP 端提供的 API 接口,不影响线上 APP 的正常使用。
2)针对后台管理功能所提供的 API 接口,使用更规范的方式独立实现,不继承 Java 版的后台 API 接口。
3)后台的全部重新实现,包括全部 UI,Java 版本的 UI 有很多缺陷一直被客户诟病,体验不佳。
4)以最小的代价完成此次重构,计划 2 个人,2个周末,共 8 人天的时间完成全部工作。
对以上问题,我们在开工前进行了简要的分析,难度并不大,但是工作量不小。接口的问题,我们定义两套,一套后台使用,一套 APP 使用,给 APP 使用的接口,只需参照 Java 版代码定义 DTO 对象,实现与数据库实体对象的映射关系即可,后台接口和整个后台管理端UI部分,在过去我做的 .NET Web 项目上进行大幅简化复用即可。
为了便于说明,我画了一张简要的结构图来说明两套 Api :
项目的实际规模并不大,且由于我们力求最小成本,所以如图所见,项目的结构也同样简单,本次改造工作无需追求技术体系的先进性,能够满足项目的要求即可。
左边后台到 Api ,再到 Core 有现成的代码可以复用,实现了出入参协议、鉴权等基本功能,只需要实现业务逻辑即可,右侧 AppApi 部分,则需要多一层协议转换工作,将原 Java 版本的出入参协议转换为 Core 要求的协议格式,而原 Java 版的 Api 接口协议并不统一,需要一个一个接口核查复刻。
后台解决方案结构如下:
CCPRestSDK
我们使用的短信平台的 SDK。
Jade.Core
业务逻辑层
Jade.Model
数据库实体模型
Jade.Model.Dto
用于前后台数据传输的对象定义,包括针对 Model 的传输对象定义和其它需要传递的对象定义。
Sheng.Kernal
基础类库,提供了如反射,HTTP请求,对象映射等等基础功能。在复用到本项目时经过了简化。
Sheng.Web.Infrastructure
用于 Web 项目的基础类库,提供了通用 Api 协议定义,控制器、DTO等共通的基础功能,在复用到本项目时经过了简化。
这里包括一个专门为此项目写的友盟推送实现,友盟官方没有提供 C# 版 SDK。
考虑到这个项目比较简单,我在这里不再对基本技术体系做太多赘述,而是通过两个简单的请求过程进行阐述,代码已经开源在了 Github 上,可以下载代码后根据下文进行查看。
Github: https://github.com/iccb1013/Jade.Net
Api 的请求过程
在 Areas 下提供了 Api 和 AppApi 两个区域提供接口,我们以 Api 下的 ProductController 为例,它向后台 UI 提*品相关的接口:
以 UpdateProduct 接口进行说明,此接口用于更新产品,此接口接收前端传入的商品信息,并更新数据库中的商品信息:
此接口的 RequestArgs 方法是接口 Controller 的基础 ApiBaseController 所提供的,用于把前端 Post 过来的内容反序列化成指定的对象。
Product_Info product = Mapper.Map<Product_Info>(args);
作用是将 Dto 对象映射为数据库实体对象,这里我们使用了开源组件 AutoMapper。
有关 AutoMapper 可以访问:http://automapper.org/
引用 AutoMapper 组件后,只需定义不同对象间的映射规则即可,可适用于绝大多数情况,Product_Info 的映射规则如下:
接口在完成对象映射后,调用 Core 中的方法来实现业务:
基本的 Entity Framework 操作,不作赘述。
这里有一个细节,是图中标出的 ShengMapper.SetValuesWithoutProperties 部分,这个方法把 传入的 product 中数据拷贝到从数据库取出来的 dbProduct 中,但是跳过2个指定的属性和所有的虚属性。
AutoMapper 不能定义同一个对象类型的映射规则,也不能灵活的在不同场景使用不同的规则,所以我写了 ShengMapper 用于处理这种情况。
ShengMapper 也是开源的:Github: https://github.com/iccb1013/Sheng.Mapper
此外可以留意到方法的返回对象是 NormalResult,这是一个 Core 层使用的一般返回对象:
相当于一个逻辑上不需要返回值的方法,但我们需要知道它的执行状态,如:
有一些开发人员爱用 Exception 来返回业务结果,这样做是非常不合适的,比如这里的 商品编码重复,他是一个业务操作的结果,并且这个结果是在我们的预期之内的,不是一个 程序异常。
用异常来返回业务操作结果有两个非常大的弊端,一是抛异常时非常影响性能,二是要区别对待真的程序异常和业务结果,也是十分麻烦的事情。总之,没有理由这么做。
NormalResult 还提供了一个重载,可以返回指定类型的对象结果:
当 Core 层完成业务操作时,Controller 层的 API 会通过一个 ApiResult 对象来封装 Api 接口的返回结果:
此处的 return ApiResult() 方法,是 Sheng.Web.Infrastructure 中的 BaseController 所提供的,可以处理大多数 Api 返回结果:
如果都不能满足,也可以手工 new 一个 ApiResult 返回:
Hint 只在部分特殊情况下使用,并不会为每种操作结果安排一个错误码,对于我们的项目来说,多传一些字节回去没有问题,但有些特殊场景,前端需要知道具体的情况针对性处理,这时我们才使用错误码。
Sheng.Web.Infrastructure 还提供了对于请求分页列表数据的通用协议:
GetListDataArgs 对象使用一个 ParametersContainer 来存储查询条件,它其实是一个键值对。避免为每一个查询定义强类型的查询入参对象,实在是太过麻烦了,也没有必要。
我们通过这样的方式来处理查询条件即可:
下面我们再看一个 AppApi 的说明,我们还以产品信息为例,它的 Api 定义:
在 AppApi 中,为了兼容既有的接口约定,做了许多转换工作:
把原 APP 接口的查询条件,转换为上文提到的 ParametersContainer:
这里把 APP 接口的列表查询入参,转换为我们过去定义好的 GetListDataArgs:
这里做一些字段转换时的特殊标记:
此外,原 Java 版在向 APP 提供接口时,提供给 APP 的 DTO 对象,十分诡异的和数据库模型不一致,比如数据库字段名有下划线,但是 DTO 传输模型没有,还有一些字段的命名则是完全不一样,我们利用 AutoMapper 来逐一映射:
至此,项目的结构已经完全清楚,剩下的全部是业务逻辑层的业务操作。
重构工作的完成的效果:
前端 UI 的实现方法:
前端 UI 及脚本库复用了我之前写过的 Web 项目, Asp.net MVC,结合使用了前后端分离和 Razor 两种方式。
Razor 引擎具有极高的开发效率,在做页面数据展示时非常的方便,借助 Razor 和 Asp.net MVC 的布局页和分布页技术,可以快速而有效的搭建页面框架。
如下图,定义了一个用于一般列表页面的布局页(模版):
可以轻松的看出页面定义了大体结构:标题,副标题,按钮,查询区,表格容器和分页容器。表格容器和分页容器并没有使用 Razor,而是在具体视图页通过一般前后端分离的方式用脚本进行处理。
这是一个一般列表页视图实现的例子,基于上面的布局页,代码量就只有不到100行,就实现了一个普通列表页面。
只需要初始化table,主要是定义这个页面中表格所具备的列,查询参数即可,另外在定义一个查询条件区,这个列表页面就完成了。所有共通的功能都写在了布局页和共享的脚本文件中。
编辑和查看页面也使用了同样的处理方式,不再赘述。
基于开发效率和实际项目需要考虑,我们这里没有使用重量级的前端开发框架,而是复用了过去我写的 js 脚本,这些脚本基于 jQuery 完成一些共通的功能来提高开发效率,如处理数据加载绑定,发起 Api 请求等操作。
common.js:
这里的 __getDto 和 _setDto 方法,搭配页面 HTML 的特殊标记,可以实现前端对象的自动生成和绑定:
在 HTML 标签中用 dtoproperty 属性标记出 DTO 对象的属性名后,使用 __getDto 方法即可自动生成前端对象。
使用 __setDto 方法,则可以快速把 Api 返回的对象,加载到前端控件中。
如上图所示,前端画面简单的保存,加载数据就完成了。
listViewCommon2.js,这是用于一般表页的脚本,基于这里的共通脚本,加上Razor 引擎的布局页功能,实现了不到 100 行 HTML 和 JS 代码即可完成一个列表页面,当然,如果追求技术上的更加完美,可以继续抽象,继续封装达到更好的效果:
editViewCommon.js ,这是用于一般编辑页面的脚本:
你可以从 Github 上下载代码之后在 Jade.Shell 的 Scripts 目前下查阅这些脚本。
整个项目从纯技术角度来说还有许多提高改进的空间,但我们现在是做项目,不是做研究做框架产品,我们可以在未来的项目中通过项目推进的方式一步一步的提炼和完善我们的开发模式和技术体系。
最终,2个人,2个周末,在大量复用过去代码的基础上,只消耗了8人天完成了本次改造工作。
Github: https://github.com/iccb1013/Jade.Net
本次工作之后的一点心得体会,主要是几个失误的地方:
1)前期将项目交给过去的同事来做,没有过多关注,导致了项目上线前遭遇许多问题,却得不到妥善解决,我个人秉承诚以待人的原则,用人不疑,疑人不用,这一点我想没有错,错的是我完全放手没有投入精力把控工程,除了报销吃喝费用外基本不参与,这是一个教训,无论何种情况,应该一定程度的参与并把控好各项主要工作。
2)立项时预估后台工作量只需1个人月,为了分担人员风险,我还是多付了一个人的费用安排2个人来做,但在人员使用上,没有做到风险规避。
3)过早的结清了人员费用,导致工作无法顺利推进,对于外包项目,费用结算一定要有计划性,包括预留尾款。
最后我想谈一谈技术人员的“人设”问题,这是我从此项目上深刻认识到的一个问题。
我做了超过十年的技术研发工作,但同时许多朋友说我不像一个程序员,我并不认为程序员一定要双肩包,开口只谈技术,相反随着年龄和阅历的增长,我一天比一天认识到业务、行业的重要性,我很早就知道做技术是为了什么,做技术不是为了做技术,而是为了服务我们的客户,服务社会,服务一切需要的人,我更关心的是我要做什么事情,我的目标是什么。一直以来我认为这是一个技术人员转变和提高的核心观念。
但是最近一到两年,我慢慢意识到,有时我们需要让自己契合对方心理上的某种“人设”,就这个玉雕工作室的项目来说,我和客户谈需求和业务比较多,吃饭喝酒比较多,加上不那么技术的形象(长头发,扎辫子),导致客户始终不认为我是一个技术人员,当然我也不太在意这一点,但是,后来我意识到一些问题。
对于这种小型外包项目,特别是还没有接下来之前,客户最在意的还是我们有没有技术实力做好,能不能给我们做,这里有一个角色带入的问题,这时我不是供职于大公司的项目经理服务于既有客户,扎辫子花衣服做需求也不是不可以,但是当时我是一个要接项目的人,在互相不了解的情况下,如何快速建立信任?最简单的办法是让自己契合对方心理上的“人设”:我就是你要找的人。
许多客户包括企业领导层,对技术人员都有自己所理解的“人设”,客户不懂技术,领导也许也不那么懂技术,他们要找有技术实力的人,怎么办呢,说白了,凭感觉。
玉雕这个项目直到原来 Java 版后台的两个开发人员撂挑子,客户都感叹他们技术好,客户是做工艺品的,和IT技术八杆子打不着边,为什么?这件事让我反思了很久,就是“人设”。
于是我剪了头发,换上衬衫,买一个瑞士军刀电脑包(笑)。以便于我在不同的时候有不同的人设。
当然更重要的是对于技术和业务的种种理念,在和不同的人表达的时候,要特别注意表达的方式和技巧,以及表达到什么度。对于水平比自己高的人,可以随意表达自己的想法,不要怕,但是遇到经验或能力不如自己的人时,要特别小心,因为对方可能无法体悟你的意思。
比如和同样一个做过十年项目的老鸟说一句:技术不是那么重要,也许双方可以会心一笑,重要的是彼此知道我们所说的技术不重要的点,技术不重要的度在哪里。但是如果和一个没有太多各种项目经验的人说这样的话,可能是不合适的,对方会不能理解,进行主观的判断你不行,因为你不技术。
唯一的办法是不要太个性,要契合别人的人设,不管他有没有道理。"木秀于林风必摧之",瑞士军刀电脑包该背还是要背(笑)。
本文联合作者:
曹旭升
QQ:279060597
Email:cao.silhouette@msn.com
http://blog.shengxunwei.com
范大宏
QQ:237194340
Email:whoarefan@gmail.com