CRM系统

思想:设计前端的一些页面,直接找插件,不要直接自己做。

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
2
3
产品名称:${name}
产品价格:${price}
设计作者:<#list users as user> ${user} </#list>
  • 创建测试类 FreemarkerTest.java
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
@Test
public void testss() throws IOException, TemplateException {
// 创建一个freemarker.templates.Configuration实例,它是存储 FreeMarker 应用级设置的核心部分
// 指定版本号
Configuration cfg = new Configuration(Configuration.VERSION_2_3_22);
// 设置模板目录
cfg.setDirectoryForTemplateLoading(new File("src/main/java/templates"));
// 设置默认编码格式
cfg.setDefaultEncoding("UTF-8");

//数据
Map<String, Object> product = new HashMap<>();
product.put("name", "Huwei P8");
product.put("price", "3985.7");
product.put("users", new String[]{"Tom", "Jack", "Rose"});

//从设置的目录中获得模板
Template temp = cfg.getTemplate("product.ftl");

//合并模板和数据模型
Writer out = new OutputStreamWriter(System.out);
temp.process(product, out);

//关闭
out.flush();
out.close();
}
-----------产品详情------------
产品名称:Huwei P8
产品价格:3985.7
设计作者: Tom Jack Rose
------------------------------

如果在项目中使用,在 mvc 中配置 FreemarkerConfigurer 和 FreemarkerViewResolver。
mvc.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- FreemarkerConfig 配置 -->
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<!-- 配置 freemarker 寻找模板的路径 -->
<property name="templateLoaderPath" value="/WEB-INF/view/"/>
<!-- 配置 freemarker 的文件编码 -->
<property name="defaultEncoding" value="UTF-8"/>
</bean>

<!-- 配置freemarker视图解析器 -->
<bean class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
<!-- 是否在 model 自动把 session 中的 attribute 导入进去 -->
<property name="exposeSessionAttributes" value="true"/>
<!-- 配置输出的HTML的 ContentType 属性 -->
<property name="contentType" value="text/html;charset=UTF-8"/>
<!-- 自动添加后缀 -->
<property name="suffix" value=".ftl"/>
</bean>

在解析 ftl 文件时,一旦${name} name值为空就会报错,也会停止解析

Freemarker 语法

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
<#if (entity.permissions)??>
<#list entity.permissions as p>
<option value="${p.id}">${p.name}</option>
</#list>
</#if>

FTL指令常用标签及语法
注意:使用freemaker,要求所有标签必须闭合,否则会导致freemaker无法解析。

freemaker注释:<#-- 注释内容 -->格式部分,不会输出

---------------------------------- 基础语法 ----------------------------------
1、字符输出
${emp.name?if_exists}      // 变量存在,输出该变量,否则不输出
${emp.name!}           // 变量存在,输出该变量,否则不输出
${emp.name?default("xxx")} // 变量不存在,取默认值xxx
${emp.name!"xxx"}       // 变量不存在,取默认值xxx

常用内部函数:
${"123<br>456"?html}   // 对字符串进行HTML编码,对html中特殊字符进行转义
${"str"?cap_first}    // 使字符串第一个字母大写
${"Str"?lower_case} // 将字符串转换成小写
${"Str"?upper_case} // 将字符串转换成大写
${"str"?trim} // 去掉字符串前后的空白字符

字符串的两种拼接方式拼接:
${"hello${emp.name!}"} // 输出hello+变量名
${"hello"+emp.name!} // 使用+号来连接,输出hello+变量名

可以通过如下语法来截取子串:
<#assign str = "abcdefghijklmn"/>
// 方法1${str?substring(0,4)} // 输出abcd
// 方法2${str[0]}${str[4]} // 结果是ae
${str[1..4]}     // 结果是bcde// 返回指定字符的索引${str?index_of("n")}

2、日期输出
${emp.date?string('yyyy-MM-dd')} //日期格式

3、数字输出(以数字20为例)
${emp.name?string.number}  // 输出20
${emp.name?string.currency} // ¥20.00
${emp.name?string.percent} // 20%
${1.222?int}            // 将小数转为int,输出1
<#setting number_format="percent"/> // 设置数字默认输出方式('percent',百分比)
<#assign answer=42/>          // 声明变量 answer 42
#{answer}          // 输出 4,200%
${answer?string}      // 输出 4,200%
${answer?string.number}   // 输出 42
${answer?string.currency} // 输出 ¥42.00
${answer?string.percent}  // 输出 4,200%
#{answer}         // 输出 42

数字格式化插值可采用#{expr;format}形式来格式化数字,其中format可以是:
mX:小数部分最小X位
MX:小数部分最大X位
如下面的例子:
<#assign x=2.582/><#assign y=4/>#{x; M2} // 输出2.58
#{y; M2} // 输出4
#{x; m2} // 输出2.58
#{y; m2} // 输出4.0
#{x; m1M2} // 输出2.58
#{x; m1M2} // 输出4.0

4、申明变量
<#assign foo=false/>
// 声明变量,插入布尔值进行显示,注意不要用引号${foo?string("yes","no")}
// 当为true时输出"yes",否则输出"no"

申明变量的几种方式
<#assign name=value>
<#assign name1=value1 name2=value2 ... nameN=valueN>
<#assign same as above... in namespacehash>
<#assign name> capture this </#assign>
<#assign name in namespacehash> capture this </#assign>

5、比较运算符
表达式中支持的比较运算符有如下几个:= 或 == :判断两个值是否相等.
!= :判断两个值是否不等.
> 或 gt :判断左边值是否大于右边值
>= 或 gte :判断左边值是否大于等于右边值
< 或 lt :判断左边值是否小于右边值
<= 或 lte :判断左边值是否小于等于右边值
6、算术运算符
FreeMarker表达式中完全支持算术运算,FreeMarker支持的算术运算符包括:+, - , * , / , % 注意:
(1)、运算符两边必须是数字
(2)、使用+运算符时,如果一边是数字,一边是字符串,就会自动将数字转换为字符串再连接,如:${3 + "5"},结果是:35
7、逻辑运算符
逻辑运算符有如下几个:逻辑与:&&逻辑或:||逻辑非:!逻辑运算符只能作用于布尔值,否则将产生错误
8、FreeMarker中的运算符优先级如下(由高到低排列):
①、一元运算符:!
②、内建函数:?
③、乘除法:*, / , %
④、加减法:- , +
⑤、比较:> , < , >= , <= (lt , lte , gt , gte)
⑥、相等:== , = , !=
⑦、逻辑与:&&
⑧、逻辑或:||
⑨、数字范围:..实际上,我们在开发过程中应该使用括号来严格区分,这样的可读性好,出错少
9、if 逻辑判断(注意:elseif 不加空格)
<#if condition>
...
<#elseif condition2>
...
<#elseif condition3>
...
<#else>
...
</#if>

if 空值判断
// 当 photoList 不为空时<#if photoList??>...</#if>
值得注意的是,${..}只能用于文本部分,不能用于表达式,
下面的代码是错误的:
<#if ${isBig}>Wow!</#if><#if "${isBig}">Wow!</#if>
// 正确写法<#if isBig>Wow!</#if>

10、switch (条件可为数字,可为字符串)
<#switch value>
<#case refValue1>
....
<#break>
<#case refValue2>
....
<#break>
<#case refValueN>
....
<#break>
<#default>
....
</#switch>
11、集合 & 循环
// 遍历集合:
<#list empList! as emp>
${emp.name!}
</#list>
// 可以这样遍历集合:
<#list 0..(empList!?size-1) as i>
${empList[i].name!}
</#list>

// 与jstl循环类似,也可以访问循环的状态。

empList?size    // 取集合的长度
emp_index:     // int类型,当前对象的索引值
emp_has_next: // boolean类型,是否存在下一个对象
// 使用<#break>跳出循环
<#if emp_index = 0><#break></#if>

// 集合长度判断
<#if empList?size != 0></#if> // 判断=的时候,注意只要一个=符号,而不是==

<#assign l=0..100/> // 定义一个int区间的0~100的集合,数字范围也支持反递增,如100..2
<#list 0..100 as i>   // 等效于java for(int i=0; i <= 100; i++)
  ${i}
</#list>

// 截取子集合:
empList[3..5] //返回empList集合的子集合,子集合中的元素是empList集合中的第4-6个元素

// 创建集合:
<#list ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期天"] as x>

// 集合连接运算,将两个集合连接成一个新的集合
<#list ["星期一","星期二","星期三"] + ["星期四","星期五","星期六","星期天"] as x>

// 除此之外,集合元素也可以是表达式,例子如下:
[2 + 2, [1, 2, 3, 4], "whatnot"]
/ seq_contains:判断序列中的元素是否存在
<#assign x = ["red", 16, "blue", "cyan"]>
${x?seq_contains("blue")?string("yes", "no")} // yes
${x?seq_contains("yellow")?string("yes", "no")} // no
${x?seq_contains(16)?string("yes", "no")} // yes
${x?seq_contains("16")?string("yes", "no")} // no

// seq_index_of:第一次出现的索引
<#assign x = ["red", 16, "blue", "cyan", "blue"]>
${x?seq_index_of("blue")} // 2

// sort_by:排序(升序)
<#list movies?sort_by("showtime") as movie></#list>

// sort_by:排序(降序)
<#list movies?sort_by("showtime")?reverse as movie></#list>

// 具体介绍:
// 不排序的情况:
<#list movies as moive>
  <a href="${moive.url}">${moive.name}</a>
</#list>
//要是排序,则用
<#list movies?sort as movie>
  <a href="${movie.url}">${movie.name}</a>
</#list>

// 这是按元素的首字母排序。若要按list中对象元素的某一属性排序的话,则用
<#list moives?sort_by(["name"]) as movie>
  <a href="${movie.url}">${movie.name}</a>
</#list>

//这个是按list中对象元素的[name]属性排序的,是升序,如果需要降序的话,如下所示:
<#list movies?sort_by(["name"])?reverse as movie>
  <a href="${movie.url}">${movie.name}</a>
</#list>
12、Map对象
// 创建map
<#assign scores = {"语文":86,"数学":78}>

// Map连接运算符
<#assign scores = {"语文":86,"数学":78} + {"数学":87,"Java":93}>

// Map元素输出
emp.name // 全部使用点语法
emp["name"] // 使用方括号

13、FreeMarker支持如下转义字符:
\" :双引号(u0022)
\' :单引号(u0027)
\\ :反斜杠(u005C)
\n :换行(u000A)
\r :回车(u000D)
\t :Tab(u0009)
\b :退格键(u0008)
\f :Form feed(u000C)
\l :<
\g :>
\a :&
\{ :{
\xCode :直接通过4位的16进制数来指定Unicode码,输出该unicode码对应的字符.

如果某段文本中包含大量的特殊符号,FreeMarker提供了另一种特殊格式:
可以在指定字符串内容的引号前增加r标记,在r标记后的文件将会直接输出.看如下代码:
${r"${foo}"} // 输出 ${foo}
${r"C:/foo/bar"} // 输出 C:/foo/bar
14、include指令
// include指令的作用类似于JSP的包含指令:
<#include "/test.ftl" encoding="UTF-8" parse=true>

// 在上面的语法格式中,两个参数的解释如下:
encoding="GBK" // 编码格式
parse=true    // 是否作为ftl语法解析,默认是true,false就是以文本方式引入,
注意:在ftl文件里布尔值都是直接赋值的如parse=true,而不是parse="true"

15、import指令
// 类似于jsp里的import,它导入文件,然后就可以在当前文件里使用被导入文件里的宏组件
<#import "/libs/mylib.ftl" as my>
// 上面的代码将导入/lib/common.ftl模板文件中的所有变量,
交将这些变量放置在一个名为com的Map对象中,
"my"在freemarker里被称作namespace

16、compress 压缩
// 用来压缩空白空间和空白的行
<#compress>
...
</#compress>
<#t> // 去掉左右空白和回车换行

<#lt>// 去掉左边空白和回车换行

<#rt>// 去掉右边空白和回车换行

<#nt>// 取消上面的效果
17、escape,noescape 对字符串进行HTML编码
// escape指令导致body区的插值都会被自动加上escape表达式,
但不会影响字符串内的插值,只会影响到body内出现的插值,
使用escape指令的语法格式如下:
<#escape x as x?html>
  First name: ${firstName}
<#noescape>Last name: ${lastName}</#noescape>
  Maiden name: ${maidenName}
</#escape>

// 相同表达式
First name: ${firstName?html}
Last name: ${lastName}
Maiden name: ${maidenName?html}

---------------------------------- 高级语法 ----------------------------------
1、global全局赋值语法
<#global name=value>

<#global name1=value1 name2=value2 ... nameN=valueN>

<#global name>
  capture this
</#global>
// 利用这个语法给变量赋值,那么这个变量在所有的namespace中是可见的,
如果这个变量被当前的assign语法覆盖如<#global x=2><#assign x=1>在当前页面里x=2将被隐藏,
或者通过${.globals.x} 来访问
2、setting 语法
// 用来设置整个系统的一个环境
locale // zh_CN 中文环境
number_format
boolean_format
date_format , time_format , datetime_format
time_zone
classic_compatible
// 例1:
<#setting number_format="percent"/> // 设置数字默认输出方式('percent',百分比)

// 例2:
// 假如当前是匈牙利的设置,然后修改成美国
${1.2} // 输出1,2
<#setting locale="en_US">
${1.2} // 输出1.2,因为匈牙利是采用", "作为十进制的分隔符,美国是用". "
3、macro宏指令
例子1:

<#-- 定义宏 -->
<#macro test foo bar="Bar" baaz=-1>
  Text: ${foo}, ${bar}, ${baaz}
</#macro>

<#-- 使用宏 -->
<@test foo="a" bar="b" baaz=5*5/> // 输出:Text: a, b, 25
<@test foo="a" bar="b"/>     // 输出:Text: a, b, -1
<@test foo="a" baaz=5*5-2/>     // 输出:Text: a, Bar, 23
<@test foo="a"/> // 输出:Text: a, Bar, -1
例子2:

<#-- 定义一个循环输出的宏 -->
<#macro list title items>
  ${title}
  <#list items as x>
    *${x}
  </#list>
</#macro>

<#-- 使用宏 -->
<@list items=["mouse", "elephant", "python"] title="Animals"/>
// 输出Animals *mouse *elephant *python
例子3:

<#-- 嵌套宏 -->
<#macro border>
  <table>
    <#nested>
  </table>
</#macro>

<#-- 嵌套宏使用 -->
<@border>
  <tr><td>hahaha</td></tr>
</@border>
输出结果:
<table>
  <tr><td>hahaha</td></tr>
</table>
例子4:在nested指令中使用循环变量时,可以使用多个循环变量,看如下代码:
<#-- 循环嵌套宏 --><#macro repeat count>  
<#list 1..count as x>
<#nested x, x/2, x==count>
// 使用nested指令时指定了三个循环变量  
</#list></#macro><#-- 使用宏 -->
<@repeat count = 4; c, halfc, last>  
${c}. ${halfc}<#if last> Last!</#if></@repeat>
// 输出结果:// 1. 0.5// 2. 1// 3. 1.5// 4. 2 Last!

freemarker 宏嵌套nested 的使用:http://blog.sina.com.cn/s/blog_7e5699790100z59g.html

4、结束macro指令
// return指令用于结束macro指令
<#-- 创建宏 -->
<#macro book>
  spring
  <#return>
  j2ee
</#macro>

<#-- 使用宏 -->
<@book />
// 上面的代码输出:spring,而j2ee位于return指令之后,不会输出.

输出中间含有 , 的list

1
2
3
<#list ["hello","world"] as word>
<span>${word}</span><#if word_has_next>,</#if>
</#list>

分页插件 Pagehelper


PageHelper

mysql 中,分页的 sql 是 limit 做的,一旦 model 多了复杂了,就会想到使用 mybatis 的逆向工程来生成响应的 po 和 mapper,但是同时会带来弊端,分页就不好解决了。可以手动修改,可是一般来说,逆向生成的文件一般都不会去动。所以需要使用一个分页插件解决。

1
2
3
4
5
6
<!-- mybatis分页插件 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.6</version>
</dependency>

在 Mybatis 配置拦截器插件

applicationContext.xml

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
<!-- 3.创建 sqlSEssion -->
<bean class="org.mybatis.spring.SqlSessionFactoryBean" id="mySqlSession">
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations" value="classpath:mappers/*"/>
<property name="typeAliasesPackage" value="cn.lizhaoloveit.crm"/>
<property name="configurationProperties">
<value>
lazyLoadingEnabled=true
aggressiveLazyLoading=false
lazyLoadTriggerMethods=equals,clone,hashCode
</value>
</property>
<!-- 注意其他配置 -->
<property name="plugins">
<array>
<bean class="com.github.pagehelper.PageInterceptor">
<property name="properties">
<!--当pageNum<=0时,将pageNum设置为1-->
<!--当pageNum>pages时,将pageNum设置为pages-->
<value>
reasonable=true
</value>
</property>
</bean>
</array>
</property>
</bean>

相当于在 mybatis.xml 添加

1
2
3
4
5
6
7
8
<plugins>
<!-- com.github.pagehelper为PageHelper类所在包名 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!--当pageNum<=0时,将pageNum设置为1-->
<!--当pageNum>pages时,将pageNum设置为pages-->
<property name="reasonable" value="true"/>
</plugin>
</plugins>
1
2
3
4
5
6
7
8
9
@Override
@Transactional(readOnly = true)
public PageInfo<Department> gets(QueryName qn) {
PageHelper.startPage(qn.getPageNum(), qn.getPageSize());
List<Department> list = departmentMapper.selectAll(qn);
PageHelper.startPage(qn.getPageNum(), qn.getPageSize());
Integer totalCount = departmentMapper.totalCount(qn);
return new PageInfo<>(list, totalCount);
}

模态框


使用模态框完成添加和修改操作

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
<#--部门编辑模态框-->
<div class="modal fade" id="editModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span> </button>
<h4 class="modal-title">部门编辑</h4>
</div>
<div class="modal-body"> <!-- 后面是我们填充的内容 -->
<form class="form-horizontal" action="/department/saveOrUpdate" method="post" id="editForm">
<input type="hidden" name="pageNum" id="pageNum" value=${qn.pageNum}>
<input type="hidden" value="" name="id">
<div class="form-group" >
<label class="col-sm-3 control-label">部门名称:</label>
<div class="col-sm-6">
<input type="text" class="form-control" name="name" value="" placeholder="请输入部门名称">
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">部门编号:</label>
<div class="col-sm-6">
<input type="text" class="form-control" name="sn" value="" placeholder="请输入部门编号">
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary btnSubmit" >保存</button>
<button type="button" class="btn btn-default" data-dismiss="modal" >取消</button>
</div>
</div>
</div>
</div>

js 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
$(function () {
// 保存回显
$('.btnUpdate').click(function () {
// 打开 modal 之前加载数据
var data = $(this).data('json')
// console.log($('#editForm input[name="id"]').attr('name'));
$('#editForm input[name="id"]').val(data.id)
$('#editForm input[name="name"]').val(data.name)
$('#editForm input[name="sn"]').val(data.sn)
$('#editModal').modal('show')
})
// 添加
$('.btnSubmit').click(function () {
$('#editForm').submit()
})
})
</script>

模态框显示其他页面

jQuery 消息提示框插件

jquery.messager.js,处理在逻辑执行完以后的提示操作

  • confirm(e, v, f) 带回调方法,带两个按钮
  • alert(e, v) 不带回调方法,带一个按钮
  • popup(e) 没有按钮
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 删除
$(function () {
// 删除
$(".btn_delete").click(function () {
var url = $(this).data("url");
var formId = $(this).data("formid");
$.messager.confirm("温馨提示", "你确定要删除此数据吗?", function () {
// 删除
$.get(url, function (data) {
if (data.success) {
// 删除成功
$.messager.confirm("温馨提示", "删除成功", function () {
$('#' + formId).submit()
})
} else {
//删除失败
$.messager.popup(data.msg);
}
});
});
})
})

数据绑定


使用场景,当从数据库中拉取数据时,就已经将数据内容拿到了,此时在做一些当前页面回显数据的时候,如果还要到后台去数据库再拿一遍相同的数据,实属浪费。因此使用数据绑定,将拿到的数据绑定到事件上,在触发事件时就可以拿到当前数据。

data-json

  • 在按钮标签上加入 data-json 属性

list.ftl

1
2
3
<a class="btn btn-info btn-xs btn-info btnUpdate" href="javascript:;" data-json='${entity.jsonStr!}'>
<span class="glyphicon glyphicon-pencil"></span> 编辑
</a>

${entity.jsonStr!} 表示调用对象的 getJsonStr 方法,将返回值赋值给 data-json

1
2
3
4
@JSONField(serialize = false)
public String getJsonStr() {
return JSON.toJSONString(this);
}

注意:需要加 @JSONField(serialize = false) 注解,表示该属性(getXxx也表示属性) 不会被序列化,不然会出现栈溢出,自己体会为什么(底层序列化会通过 getXxx 去获取属性的值拼接字符串)。

最后页面中的 html 标签就带有了具体值的 data-json 属性了

1
2
3
<a class="btn btn-info btn-xs btn-info btnUpdate" href="javascript:;" data-json="{"id":1,"name":"天宫部","sn":"IOS-8859-1"}"">
<span class="glyphicon glyphicon-pencil"></span> 编辑
</a>

异步请求


在执行一些请求时,需要根据响应结果做一些事情,这时候直接访问后台资源刷新页面就不是特别合适。最好使用异步请求,即只通过 Ajax 请求,获取数据,而不重新生成页面。

这时就可以通过后台响应的结果信息使用 js 做一些操作。

解决方案:

  1. 自己手动使用 ajax 发送异步请求。详情见 Ajax实现登录
  2. 使用 jquery-form 插件

使用 jquery-form 插件需要引入库
<script type="text/javascript" src="/js/plugins/jquery-form/jquery.form.js"></script>

情况一:表单自带提交按钮

1
2
3
4
5
6
7
8
9
$(function() {
$("#testForm").ajaxForm(function(data){
console.debug(data)
})
})

<form id="testForm" action="/text.do" method="post">
<input type="submit" value="提交">
</form>

情况二:表单不带有提交按钮,而是普通按钮,需要手动提交

1
2
3
4
5
6
7
8
9
10
11
12
13
$(function() {
$("#btn_test").click(function(){
$("#testForm").submit();
})

$("#testForm").ajaxForm(function(data){
console.debug(data)
})
})

<form id="testForm" action="/text.do" method="post">
<input type="submit" value="提交">
</form>

Shiro 权限框架


Shiro权限框架章节

统一的异常处理


方式一:SpringMVC 的统一异常处理。

mvc.xml

1
2
3
4
5
6
7
8
9
10
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<!-- 定义默认的异常处理页面 -->
<property name="defaultErrorView" value="common/error"/>
<!-- 定义需要特殊处理的异常,使用类名或完全路径名作为 Key,异常页作为值 -->
<property name="exceptionMappings">
<value>
org.apache.shiro.authz.UnauthorizedException=common/nopermission
</value>
</property>
</bean>

方式二:使用 ControllerAdvice,来自定义统一的异常

1
2
3
4
5
6
7
8
@ControllerAdvice
public class MyExceptionAdvice {
@ExceptionHandler(RuntimeException.class)
public String handleException(HttpServletResponse response, Model model, RuntimeException e) {
model.addAttribute("exception", "出错了");
return "common/error";
}
}

批量删除


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
$(function () {
$('#checkAll').change(function () {
$('.selectOne').prop('checked', $(this).prop('checked'))
})

$('.btn_batchDelete').click(function () {
// 1.如果没有选择 给出提示
if ($('.selectOne:checked').length == 0) {
$.messager.alert("温馨提示", "请选择要删除的数据");
return;
}
// 如果选择了,确认提示
$.messager.confirm("温馨提示", "您确定要删除所选项吗?", function () {
// 获取要删除数据的id
var checks = $('.selectOne:checked');
var ids = []; // 创建数组
$.each(checks, function (index, checkBox) {
ids.push($(checkBox).data('id'))
})
// 发送 ajax请求(携带要删除数据的 id)
$.get('/employee/batchDelete', {ids: ids}, function (data) {
if (data.success) {
$.messager.confirm("温馨提示", "删除成功", function () {
$('#searchForm').submit()
})
} else {
$.messager.alert("温馨提示", "删除失败")
}
})
})
})

<td><input type="checkbox" class="cb selectOne" data-id="${entity.id}"></td>

思路,前端页面,

  1. 如果没有选择,给出提示
  2. 如果选择了,提示用户确认
  3. 获取要删除的数据 id
  4. 发送 ajax 请求(携带删除数据的 id)

后端:

先删关系,再删员工

1
2
3
4
5
6
7
8
9
10
11
12
<delete id="deleteFromEmpRoleBatch">
delete from emp_role where emp_id in
<foreach collection="array" open="(" close=")" separator="," item="empId">
#{empId}
</foreach>
</delete>
<delete id="deleteBatch">
delete from employee where id in
<foreach collection="array" open="(" close=")" separator="," item="id">
#{id}
</foreach>
</delete>

导入/导出(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
WHERE
c.status = 0
GROUP BY
DATE_FORMAT(c.input_time, '%Y-%m-%d')

+---------------------------------------+-------------+
| DATE_FORMAT(c.input_time, '%Y-%m-%d') | count(c.id) |
+---------------------------------------+-------------+
| 2018-07-01 | 1 |
+---------------------------------------+-------------+
| 2018-08-03 | 3 |
+---------------------------------------+-------------+
| 2018-09-28 | 2 |
+---------------------------------------+-------------+
| 2019-02-01 | 1 |
+---------------------------------------+-------------+

将查询结果封装起来,方式一 使用 JavaBean 来封装,

二 使用 map 集合封装(比较简单,推荐)

List<Map(String, Object)> ——> 一个 JavaBean 对象 ———-> 一行数据,每个 map<String, Object> 存放其中某行,列为 key 的值。

设计sql

初始 sql 分组查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
WHERE c.`status` = 0
GROUP BY DATE_FORMAT(c.input_time,'%Y-%m-%d');
+--------------------------------------+-------------+
| DATE_FORMAT(c.input_time,'%Y-%m-%d') | COUNT(c.id) |
+--------------------------------------+-------------+
| 2018-07-01 | 1 |
| 2018-08-03 | 3 |
| 2018-09-28 | 2 |
| 2019-02-01 | 1 |
| 2019-09-01 | 8 |
| 2019-09-02 | 1 |
+--------------------------------------+-------------+
6 rows in set (0.00 sec)

报表基本是分组查询应用,但我们需要可变的参数的分组查询。有2个基本点:

  • GROUP BY 后面的内容应该和查询内容一致,比如,我要按日期将用户分组,则如上述查询,哪个日期下有那几个客户。再如,按销售人员分组那么 sql 就变成如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> SELECT seller.name, COUNT(c.id) FROM customer c LEFT JOIN employee seller on c.seller_id = seller.id
WHERE c.`status` = 0
GROUP BY seller.name;
+-------+-------------+
| name | COUNT(c.id) |
+-------+-------------+
| admin | 1 |
| will | 5 |
| 李朝 | 2 |
| 李朝2 | 2 |
| 李朝3 | 1 |
| 李朝4 | 1 |
| 李朝5 | 3 |
| 李朝6 | 1 |
+---------+----------+

所以,select 后面查询的数据,应该跟按什么分组一致,所以使用一个别名来记录分组和查询内容,sql就变成了

1
2
3
SELECT #{groupType} groupType, COUNT(c.id) FROM customer c LEFT JOIN employee seller on c.seller_id = seller.id
WHERE c.`status` = 0
GROUP BY groupType

之后的高级查询,其实就是 where 语句的拼接了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<select id="queryCustomerCountByCondition" resultType="java.util.Map">
SELECT ${groupType} groupType,
COUNT(c.id) number
FROM customer c
LEFT JOIN employee seller on c.seller_id = seller.id
WHERE c.status = #{status}
<if test="keyword != null">
and (seller.name like concat('%', #{keyword}, '%') OR seller.email like concat('%', #{keyword}, '%'))
</if>
<if test="beginDate != null">
and c.input_time &gt; #{beginDate}
</if>
<if test="endDate != null">
and c.input_time &lt; #{endDate}
</if>
GROUP BY ${groupType}
</select>

${}#{} 因为我们需要拼接 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
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
public class QueryChart extends QueryKeyword {
private int status = 0;
private Date beginDate;
private Date endDate;
public final static int GROUPTYPE_EMPLOYEE = 0;
public final static int GROUPTYPE_YEAR = 1;
public final static int GROUPTYPE_MONTH = 2;
public final static int GROUPTYPE_DAY = 3;
private int groupType = GROUPTYPE_EMPLOYEE;
private int[] groupTypes = {0, 1, 2, 3};
private String[] groupTypeDisplayValues = {"员工", "年", "月", "日"};

public Date getEndDate() {
return DateUtil.getEndDate(endDate);
}

public String getGroupTypeValue() {
switch (groupType) {
case GROUPTYPE_EMPLOYEE: return "seller.name";
case GROUPTYPE_YEAR: return "DATE_FORMAT(c.input_time,'%Y')";
case GROUPTYPE_MONTH: return "DATE_FORMAT(c.input_time,'%Y-%m')";
case GROUPTYPE_DAY: return "DATE_FORMAT(c.input_time,'%Y-%m-%d')";
}
return "";
}
}

模板视图代码关键位置代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<div class="form-group">
<label for="status">分组类型:</label>
<select class="form-control" id="groupType" name="groupType">
<#list qo.groupTypes as k>
<option value="${k}"
<#if k == qo.groupType>
selected
</#if>
>
${qo.groupTypeDisplayValues[k]}
</option>
</#list>
</select>
<button id="btn_query" class="btn btn-primary">
<span class="glyphicon glyphicon-search"></span> 查询
</button>
<button type="button" class="btn btn-info chart_btn" data-url="/reportForms/customerReportForms_bar">
<span class="glyphicon glyphicon-stats"></span> 柱状图
</button>
<button type="button" class="btn btn-warning chart_btn" data-url="/reportForms/customerReportForms_pie">
<span class="glyphicon glyphicon-dashboard"></span> 饼状图
</button>
</div>

柱状图和饼状图

前端的图形视图采用 echarts中的模型插件。
主要的点是对数据的封装和数据结构的理解,

1
2
3
4
5
6
7
8
9
option = null;
var dataAxis = ${groupTypes}; // = ['1', '2'...]
var data = ${numbers}; // = [1, 2, 3]
var yMax = ${maxNumber}; // = 500
var dataShadow = [];

for (var i = 0; i < data.length; i++) {
dataShadow.push(yMax);
}

第一个柱状视图需要的数据大致为,柱子总高度以及当前列柱高。很显然,data,应该是可比较的柱高,而 dataAxis 就是描述了。这里需要注意的是 data 需要的数据类型是个 js 数组。而如果按以往封装数据的话,是个 java 的 List 类型的数据,显然不能够直接赋值。必须转成 js 可以识别的 json 对象。所以需要如下代码:

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
@RequestMapping("/customerReportForms_bar")
@RequiresPermissions(value = {"reportForms:customerReportForms_bar", "客户报表柱状图"}, logical = Logical.OR)
public String customerReportForms_bar(Model model, @ModelAttribute("qo") QueryChart qo) {
List<Map<String, Object>> maps = customerChartService.getsCustomerCountByCondition(qo);
List<Object> groupTypes = new ArrayList<>();
List<Object> numbers = new ArrayList<>();
Long count = 0l;
for (Map<String, Object> map : maps) {
Long number = (Long) map.get("number");
groupTypes.add(map.get("groupType"));
numbers.add(number);
if (count < number) count = number;
}
model.addAttribute("groupTypes", JSON.toJSONString(groupTypes));
model.addAttribute("numbers", JSON.toJSONString(numbers));
model.addAttribute("maxNumber", Math.ceil(count + count * 0.3));
return "reportForms/customerReportForms_bar";
}

@RequestMapping("/customerReportForms_pie")
@RequiresPermissions(value = {"reportForms:customerReportForms_pie", "客户报表柱状图"}, logical = Logical.OR)
public String customerReportForms_pie(Model model, @ModelAttribute("qo") QueryChart qo) {
List<Map<String, Object>> maps = customerChartService.getsCustomerCountByCondition(qo);
model.addAttribute("maps", JSON.toJSONString(maps));
return "reportForms/customerReportForms_pie";
}

饼状图同理。

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
70
71
72
73
74
75
76
77
78
79
80
81
<!DOCTYPE html>
<html style="height: 100%">
<head>
<meta charset="utf-8">
</head>
<body style="height: 100%; margin: 0">
<div id="container" style="height: 100%"></div>
<script type="text/javascript" src="/js/echarts/echarts.min.js"></script>
<script type="text/javascript" src="/js/echarts/echarts-gl.min.js"></script>
<script type="text/javascript" src="/js/echarts/ecStat.min.js"></script>
<script type="text/javascript" src="/js/echarts/dataTool.min.js"></script>
<script type="text/javascript" src="/js/echarts/china.js"></script>
<script type="text/javascript" src="/js/echarts/world.js"></script>
<script type="text/javascript" src="/js/echarts/bmap.min.js"></script>
<script type="text/javascript" src="/js/echarts/simplex.js"></script>
<script type="text/javascript">
var dom = document.getElementById("container");
var myChart = echarts.init(dom);
var app = {};
option = null;
var data = genData(50);

option = {
title : {
text: '潜在客户新增报表',
subtext: '根据销售或时间统计',
x:'center'
},
tooltip : {
trigger: 'item',
formatter: "{a} <br/>{b} : {c} ({d}%)"
},
legend: {
type: 'scroll',
orient: 'vertical',
right: 10,
top: 20,
bottom: 20,
data: data.legendData,

selected: data.selected
},
series : [
{
name: '姓名',
type: 'pie',
radius : '55%',
center: ['40%', '50%'],
data: data.seriesData,
itemStyle: {
emphasis: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};


function genData(count) {
var seriesData = [];
${maps}.forEach(function (map) {
seriesData.push({
name: map.groupType,
value: map.number
});
})

return {
seriesData: seriesData
};
}
;
if (option && typeof option === "object") {
myChart.setOption(option, true);
}
</script>
</body>
</html>
文章作者: Ammar
文章链接: http://lizhaoloveit.cn/2019/08/24/CRM%E7%B3%BB%E7%BB%9F/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Ammar's Blog
打赏
  • 微信
  • 支付宝

评论