思想:设计前端的一些页面,直接找插件,不要直接自己做。
FreeMarker
FreeMarker,其实就是一个静态的网页生成工具,它本身不依托任何容器(web,服务器),将业务数据按照模板内容生成静态网页。
FreeMarker最初的设计,是用来在 MVC 模式的 Web 开发框架中生成 HTML 的。没有被绑定到 Servlet、HTML 或者任意 Web 相关的东西上。
为什么使用 FreeMarker
这里就拿传统的 jsp 来比较。我们知道 jsp 文件是需要 Tomcat等服务器容器去解析并且会变成字节码文件的,最终是由字节码去输出页面。
思考,哪些隐患?
- jsp 不够纯粹,其内部既有 Java 代码,又有前端代码,而且如果前端程序员想要调试页面,必须在后端环境下去调试。并且协同开发时,后台和前端同时开发 jsp 文件会有冲突等等问题。
- jsp 中的代码是会在内存中的 Code 区域占用内存的,如果页面代码非常多,会导致 Code 内存溢出。
FreeMarker 模板不会编译成字节码对象,不会占用 PermGen 空间,可以理解为不会占用内存区域存放 Code 的空间。所以前端工程师开发页面不需要后台环境,专注数据如何展示。
Freemarker 就像复制粘贴,替换。完工。专注于替换值,所以性能比较好。
FreeMarker 的简单实用
- 导入组件
spring-context-support
,选择对应版本的freemarker
- 在 java 下创建 templates Package,存放所有的 freemarker 模板,并创建模板文件
product.ftl
product.ftl
1 | 产品名称:${name} |
- 创建测试类 FreemarkerTest.java
1 |
|
如果在项目中使用,在 mvc 中配置 FreemarkerConfigurer 和 FreemarkerViewResolver。
mvc.xml
1 | <!-- FreemarkerConfig 配置 --> |
在解析 ftl 文件时,一旦${name} name值为空就会报错,也会停止解析
Freemarker 语法
1 | <#if (entity.permissions)??> |
输出中间含有 , 的list
1 | <#list ["hello","world"] as word> |
分页插件 Pagehelper
mysql 中,分页的 sql 是 limit 做的,一旦 model 多了复杂了,就会想到使用 mybatis 的逆向工程来生成响应的 po 和 mapper,但是同时会带来弊端,分页就不好解决了。可以手动修改,可是一般来说,逆向生成的文件一般都不会去动。所以需要使用一个分页插件解决。
1 | <!-- mybatis分页插件 --> |
在 Mybatis 配置拦截器插件
applicationContext.xml
1 | <!-- 3.创建 sqlSEssion --> |
相当于在 mybatis.xml 添加
1 | <plugins> |
1 |
|
模态框
使用模态框完成添加和修改操作
1 | <#--部门编辑模态框--> |
js 代码
1 | <script> |
模态框显示其他页面
jQuery 消息提示框插件
jquery.messager.js
,处理在逻辑执行完以后的提示操作
- confirm(e, v, f) 带回调方法,带两个按钮
- alert(e, v) 不带回调方法,带一个按钮
- popup(e) 没有按钮
1 | // 删除 |
数据绑定
使用场景,当从数据库中拉取数据时,就已经将数据内容拿到了,此时在做一些当前页面回显数据的时候,如果还要到后台去数据库再拿一遍相同的数据,实属浪费。因此使用数据绑定,将拿到的数据绑定到事件上,在触发事件时就可以拿到当前数据。
data-json
- 在按钮标签上加入 data-json 属性
list.ftl
1 | <a class="btn btn-info btn-xs btn-info btnUpdate" href="javascript:;" data-json='${entity.jsonStr!}'> |
${entity.jsonStr!} 表示调用对象的 getJsonStr 方法,将返回值赋值给 data-json
1 | false) (serialize = |
注意:需要加 @JSONField(serialize = false) 注解,表示该属性(getXxx也表示属性) 不会被序列化,不然会出现栈溢出,自己体会为什么(底层序列化会通过 getXxx 去获取属性的值拼接字符串)。
最后页面中的 html 标签就带有了具体值的 data-json 属性了
1 | <a class="btn btn-info btn-xs btn-info btnUpdate" href="javascript:;" data-json="{"id":1,"name":"天宫部","sn":"IOS-8859-1"}""> |
异步请求
在执行一些请求时,需要根据响应结果做一些事情,这时候直接访问后台资源刷新页面就不是特别合适。最好使用异步请求,即只通过 Ajax 请求,获取数据,而不重新生成页面。
这时就可以通过后台响应的结果信息使用 js 做一些操作。
解决方案:
- 自己手动使用 ajax 发送异步请求。详情见 Ajax实现登录
- 使用 jquery-form 插件
使用 jquery-form 插件需要引入库<script type="text/javascript" src="/js/plugins/jquery-form/jquery.form.js"></script>
情况一:表单自带提交按钮
1 | $(function() { |
情况二:表单不带有提交按钮,而是普通按钮,需要手动提交
1 | $(function() { |
Shiro 权限框架
统一的异常处理
方式一:SpringMVC 的统一异常处理。
mvc.xml
1 | <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"> |
方式二:使用 ControllerAdvice,来自定义统一的异常
1 |
|
批量删除
1 | $(function () { |
思路,前端页面,
- 如果没有选择,给出提示
- 如果选择了,提示用户确认
- 获取要删除的数据 id
- 发送 ajax 请求(携带删除数据的 id)
后端:
先删关系,再删员工
1 | <delete id="deleteFromEmpRoleBatch"> |
导入/导出(excel)
框架:
- 阿里出品的 easyExcel
- easyPoi(注解工具)
其中涉及的文件上传与下载的知识,请查看文章文件上传与下载
数据管理:字典
系统中管理业务数据的字典。数据的分类:字典目录(system_dictionary),字典目录明细(system_dictionary_item)
字典目录主要存放要描述一个对象的一系列的行为或者状态的抽象概念。
比如我们如果要描述一个人,那么字典目录应该为:收入、职业、文凭、住址(城市)、年龄等等。其中,我们需要对数据进行规范,因此有了字典目录明细的概念,我们会对职业数据规范,你只能从医生、老师、司机、老板、学生等等词语中选择,对数据进行规范有利于后期对数据进行处理、分析等。
再比如,如果要描述一次销售行为,字典目录应该为,来源、意向程度、收款类型、客户重要程度、交流方式、跟进情况等等。而目录可以有明细。
说白了,字典的作用就是规范数据。将以后我们可能会使用到的数据进行规范。字典目录和字典目录明细是一对多关系。
客户管理
这里的客户管理,指的是销售员工对应的客户,也就是说这些客户是这名销售拉过来的。
CRM 最终要的类目就是客户管理了,客户虽然只有一张表,但是对客户分类和描述需要一些辅助,目前的 CRM 练手项目中,对客户进行分类为 潜在客户、客户池、失败客户、正式客户、流失客户。
潜在客户表示为有意向的客户,但没有继续深入。
失败客户表示潜在客户谈崩了,进入失败客户列表,那谈成功了,就会进入正式客户列表。
在正式客户列表中,最终没有交易成功,或者不可抗力因素造成交易失败的客户就是流失客户了。
客户池比较特殊,因为潜在客户中有些客户比较难搞,可能大半年了还未交易,负责该客户的员工不想再跟进了,于是把该客户置入了客户池。客户池的员工所有其他员工应都可见。
建表:
id name gender age tel qq job_id 这些都是用户属性。
source_id 来源:客户从哪来的
seller_id 员工id,哪个销售的客户
inputUser_id 是谁创建的用户
input_time 创建时间
status 状态 0 潜在客户、1 客户池、2 失败客户、3 正式客户、4 流失客户
潜在客户操作
默认客户添加时为潜在客户
潜在客户中的操作:编辑、跟进、移交、修改状态
- 跟进,销售为了维持用户,需要持续关注用户,可能需要2天后给用户打个电话,在跟进历史中就可以查询到。
- 移交 指把客户从一个员工交给另一个员工处理。需要 客户id,新老员工id,操作员和操作时间,原因等。其记录应当在移交历史中可查。
客户池
销售搞不定的客户会存入客户池中
客户池操作,吸纳,移交
- 吸纳 将失败用户或者客户池中的客户重新回炉,本质上还是移交,
- 移交,指把客户从一个员工交给另一个员工处理。需要 客户id,新老员工id,操作员和操作时间,原因等。其记录应当在移交历史中可查。
报表
编写分组查询的 sql
1 | SELECT |
将查询结果封装起来,方式一 使用 JavaBean 来封装,
二 使用 map 集合封装(比较简单,推荐)
List<Map(String, Object)>
——> 一个 JavaBean 对象 ———-> 一行数据,每个 map<String, Object>
存放其中某行,列为 key 的值。
设计sql
初始 sql 分组查询
1 | mysql> SELECT DATE_FORMAT(c.input_time,'%Y-%m-%d'), COUNT(c.id) FROM customer c LEFT JOIN employee seller on c.seller_id = seller.id |
报表基本是分组查询应用,但我们需要可变的参数的分组查询。有2个基本点:
- GROUP BY 后面的内容应该和查询内容一致,比如,我要按日期将用户分组,则如上述查询,哪个日期下有那几个客户。再如,按销售人员分组那么 sql 就变成如下
1 | mysql> SELECT seller.name, COUNT(c.id) FROM customer c LEFT JOIN employee seller on c.seller_id = seller.id |
所以,select 后面查询的数据,应该跟按什么分组一致,所以使用一个别名来记录分组和查询内容,sql就变成了
1 | SELECT #{groupType} groupType, COUNT(c.id) FROM customer c LEFT JOIN employee seller on c.seller_id = seller.id |
之后的高级查询,其实就是 where 语句的拼接了。
1 | <select id="queryCustomerCountByCondition" resultType="java.util.Map"> |
${}
和 #{}
因为我们需要拼接 sql 而不仅仅传值,因此 sql 的 select 后面使用 ${}
,会将字符串表达的语句直接和原 sql 拼接成一条执行
设计查询 queryObject
以往,都是用 javaBean 封装数据库查出来的数据,由于报表不同于其他数据,报表数据通常是一个集合,而且只用一次,我们不会用数据库去存储,因此也不会单独建立一个 javabean 来封装。所以使用 map 封装。
由上面的 sql 可以看出,一个 map<String, Object>
对象对应一行数据,map 中每个 key 都是列名(column),value 则是列对应的 value。而数据集合是由很多行数据组成的,所以可以将 resultType 定义为 map,最后的结果集使用 List<Map>
来封装。
由于报表数据类目是随机的,所以 queryObject 需要给出指定的类目数组(groupTypeDisplayValues),不同的类目对应的 sql 是不一样的,所以还需要给出对于某个类目,能返回 sql 的属性(groupTypeValue)。而由于页面数据的回显问题,需要进行数据类目的比较,所以我认为使用常数标注类目,所以最终代码如下:
1 | public class QueryChart extends QueryKeyword { |
模板视图代码关键位置代码如下:
1 | <div class="form-group"> |
柱状图和饼状图
前端的图形视图采用 echarts中的模型插件。
主要的点是对数据的封装和数据结构的理解,
1 | option = null; |
第一个柱状视图需要的数据大致为,柱子总高度以及当前列柱高。很显然,data,应该是可比较的柱高,而 dataAxis 就是描述了。这里需要注意的是 data 需要的数据类型是个 js 数组。而如果按以往封装数据的话,是个 java 的 List 类型的数据,显然不能够直接赋值。必须转成 js 可以识别的 json 对象。所以需要如下代码:
1 | "/customerReportForms_bar") ( |
饼状图同理。
1 | <!DOCTYPE html> |