原标题:纳税服务系统七(投诉管理模块)【显示投诉信息、处理回复、我要投诉、Quartz自动受理、统计图FusionCharts】
tags: 纳税服务系统项目
投诉受理管理模块
接下来,就是来开发我们的投诉受理管理模块了.....我们来看看原型图与需求吧:
查询用户提交的投诉信息,可以根据投诉部门(部门A/B)、投诉时间段、状态进行查询。在列表信息中展示投诉标题、被投诉部门、被投诉人、投诉时间、状态(待受理、已受理、已失效)、操作;其中操作栏内内容为“处理”,点击“处理”则在打开的查询页面中查看具体的投诉信息并且可以多次回复投诉信息;一旦回复则说明已受理该投诉。
投诉详细信息:在本页面中首先要明显地展示出当前投诉是否已经受理;然后再显示投诉人信息、被投诉信息、受理信息(历史受理信息)三部分内容,并且在页面中可以无限次的对本次受理进行回复。投诉人信息包括:是否匿名投诉、投诉人单位、投诉人姓名、投诉人手机,*如果是匿名投诉,则不显示投诉人单位、姓名并对手机号中间4位号码使用号代替。被投诉信息包括:投诉时间、被投诉部门、被投诉人、投诉标题、投诉内容。受理信息:如果有多次回复则将多次的回复信息显示,显示内容包括回复时间、回复部门、回复人、受理回复内容;可以再次回复。**
根据上面两张原型图以及文字说明,我们可以发现:一个投诉信息可对应多个回复。
在“工作主页”中点击“我要投诉”进入页面,添加内容包括:投诉标题、被投诉部门(部门A/B)、被投诉人、投诉详情、是否匿名投诉。
关键在于匿名投诉的那一部分,我们该怎么写....
统计:根据年度将相应年度的每个月的投诉数进行统计,并以图表的形式展示在页面中;在页面中可以选择查看当前年度及其前4年的投诉数。在页面中可以选择不同的年度,然后页面展示该年度的曲线统计图。
这个统计图,大概也需要用到组件来生成出来的吧???
自动投诉受理:在每个月月底最后一天对本月之前的投诉进行自动处理;将投诉信息的状态改为 已失效。在后台管理中不能对该类型投诉进行回复。
自动投诉受理??在每个月的最后一天判断投诉信息,程序对其自动受理。。
投诉受理开发
我们首先来画一个流程图看看它的大概思路是怎么样的:
Hibernate逆向工程我们经过上面的分析,知道了:一个投诉信息可对应多个回复。是一对多的关系。我们下面使用powerdesginer来画出它的概念数据模型图
生成物理模型图:
生成数据库表:
/*==============================================================*/
/* DBMS name: MySQL 5.0 */
/* Created on: 2017/6/12 19:06:20 */
/*==============================================================*/
drop table if exists complain;
drop table if exists complain_reply;
/*==============================================================*/
/* Table: complain */
/*==============================================================*/
create table complain
(
comp_id varchar(32) not null,
comp_company varchar(100),
comp_name varchar(20),
comp_mobile varchar(20),
is_NM bool,
comp_time datetime,
comp_title varchar(200) not null,
to_comp_name varchar(20),
to_comp_dept varchar(100),
comp_content text,
state varchar(1),
primary key (comp_id)
);
/*==============================================================*/
/* Table: complain_reply */
/*==============================================================*/
create table complain_reply
(
reply_id varchar(32) not null,
comp_id varchar(32) not null,
replyer varchar(20),
reply_dept varchar(100),
reply_time datetime,
reply_content varchar(300),
primary key (reply_id)
);
alter table complain_reply add constraint FK_comp_reply foreign key (comp_id)
references complain (comp_id) on delete restrict on update restrict;
生成实体与配置文件:
Intellij idea下生成出来的映射文件是没有对应的关联关系的。也就是说:一对多或多对多的关系,它是不会帮你自动生成的【好像是这样子的】。。。因此,需要我们自己添加Set【如果需要】
-
其实是可以的,只是我当时候不知道而已。
- 添加Set集合和状态的常量
package zhongfucheng.complain.entity;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* Created by ozc on 2017/6/12.
*/
public class Complain {
private String compId;
private String compCompany;
private String compName;
private String compMobile;
private Byte isNm;
private Timestamp compTime;
private String compTitle;
private String toCompName;
private String toCompDept;
private String compContent;
private String state;
//状态
public static String COMPLAIN_STATE_UNDONE = "0";
public static String COMPLAIN_STATE_DONE = "1";
public static String COMPLAIN_STATE_INVALID = "2";
public static Map<String, String> COMPLAIN_STATE_MAP;
static {
COMPLAIN_STATE_MAP = new HashMap<String, String>();
COMPLAIN_STATE_MAP.put(COMPLAIN_STATE_UNDONE, "待受理");
COMPLAIN_STATE_MAP.put(COMPLAIN_STATE_DONE, "已受理");
COMPLAIN_STATE_MAP.put(COMPLAIN_STATE_INVALID, "已失效");
}
//添加set集合
private Set complainReplies = new HashSet(0);
public Set getComplainReplies() {
return complainReplies;
}
public void setComplainReplies(Set complainReplies) {
this.complainReplies = complainReplies;
}
public String getCompId() {
return compId;
}
public void setCompId(String compId) {
this.compId = compId;
}
public String getCompCompany() {
return compCompany;
}
public void setCompCompany(String compCompany) {
this.compCompany = compCompany;
}
public String getCompName() {
return compName;
}
public void setCompName(String compName) {
this.compName = compName;
}
public String getCompMobile() {
return compMobile;
}
public void setCompMobile(String compMobile) {
this.compMobile = compMobile;
}
public Byte getIsNm() {
return isNm;
}
public void setIsNm(Byte isNm) {
this.isNm = isNm;
}
public Timestamp getCompTime() {
return compTime;
}
public void setCompTime(Timestamp compTime) {
this.compTime = compTime;
}
public String getCompTitle() {
return compTitle;
}
public void setCompTitle(String compTitle) {
this.compTitle = compTitle;
}
public String getToCompName() {
return toCompName;
}
public void setToCompName(String toCompName) {
this.toCompName = toCompName;
}
public String getToCompDept() {
return toCompDept;
}
public void setToCompDept(String toCompDept) {
this.toCompDept = toCompDept;
}
public String getCompContent() {
return compContent;
}
public void setCompContent(String compContent) {
this.compContent = compContent;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Complain complain = (Complain) o;
if (compId != null ? !compId.equals(complain.compId) : complain.compId != null) return false;
if (compCompany != null ? !compCompany.equals(complain.compCompany) : complain.compCompany != null)
return false;
if (compName != null ? !compName.equals(complain.compName) : complain.compName != null) return false;
if (compMobile != null ? !compMobile.equals(complain.compMobile) : complain.compMobile != null) return false;
if (isNm != null ? !isNm.equals(complain.isNm) : complain.isNm != null) return false;
if (compTime != null ? !compTime.equals(complain.compTime) : complain.compTime != null) return false;
if (compTitle != null ? !compTitle.equals(complain.compTitle) : complain.compTitle != null) return false;
if (toCompName != null ? !toCompName.equals(complain.toCompName) : complain.toCompName != null) return false;
if (toCompDept != null ? !toCompDept.equals(complain.toCompDept) : complain.toCompDept != null) return false;
if (compContent != null ? !compContent.equals(complain.compContent) : complain.compContent != null)
return false;
if (state != null ? !state.equals(complain.state) : complain.state != null) return false;
return true;
}
@Override
public int hashCode() {
int result = compId != null ? compId.hashCode() : 0;
result = 31 * result + (compCompany != null ? compCompany.hashCode() : 0);
result = 31 * result + (compName != null ? compName.hashCode() : 0);
result = 31 * result + (compMobile != null ? compMobile.hashCode() : 0);
result = 31 * result + (isNm != null ? isNm.hashCode() : 0);
result = 31 * result + (compTime != null ? compTime.hashCode() : 0);
result = 31 * result + (compTitle != null ? compTitle.hashCode() : 0);
result = 31 * result + (toCompName != null ? toCompName.hashCode() : 0);
result = 31 * result + (toCompDept != null ? toCompDept.hashCode() : 0);
result = 31 * result + (compContent != null ? compContent.hashCode() : 0);
result = 31 * result + (state != null ? state.hashCode() : 0);
return result;
}
}
- 在配置文件下添加set,设置级联、懒加载
<set name="complainReplies" inverse="true" cascade="save-update,delete" lazy="false" >
<key>
<column name="comp_id" length="32" not-null="true" />
</key>
<one-to-many class="zhongfucheng.complain.entity.ComplainReply" />
</set>
编写dao、service、action
编写dao、service、action都非常简单。记得要把模块的配置文件加载到总配置文件中!
ComplainAction代码如下:
public class ComplainAction extends BaseAction {
/*************注入Service************************/
@Autowired
private ComplainService complainServiceImpl;
/************数据自动封装,给出setter和getter*************************/
private Complain complain;
public Complain getComplain() {
return complain;
}
public void setComplain(Complain complain) {
this.complain = complain;
}
/************Action中7大方法*************************/
//抛出Action异常
public String listUI() throws ServiceException, UnsupportedEncodingException {
//把状态的集合带过去
QueryHelper queryHelper = new QueryHelper(Complain.class, "c");
//当前页数没有值,那么赋值为1
if (currentPageCount == 0) {
currentPageCount = 1;
}
//把状态带过去给JSP页面
ActionContext.getContext().getContextMap().put("complainStateMap", Complain.COMPLAIN_STATE_MAP);
pageResult = complainServiceImpl.getPageResult(queryHelper,currentPageCount);
return "listUI";
}
}
导入对应的JSP页面.....得到的效果如下:
条件查询
我们来看一下条件查询有几个:可以根据投诉的标题、投诉的时间、投诉的状态进行查询。
对于投诉标题和投诉的状态我们都可以很容易地拿到条件:
//根据complain标题是否为null来判断是否是条件查询。如果complain为空,那么是查询所有。
if (complain != null) {
if (org.apache.commons.lang.StringUtils.isNotBlank(complain.getCompTitle())) {
selectCondition = URLDecoder.decode(complain.getCompTitle(),"UTF-8");
complain.setCompTitle(selectCondition);
queryHelper.addCondition(" c.compTitle like ? ", "%" + complain.getCompTitle() + "%");
}
//投诉状态并不需要编码,因为它并不是中文。
if (org.apache.commons.lang.StringUtils.isNotBlank(complain.getState())) {
queryHelper.addCondition(" c.state like ? ", "%" + complain.getState() + "%");
}
}
那么根据投诉时间来进行查询,我们要怎么做呢???我们约定时间是这样的格式:yyyy-MM-dd HH:mm。这样的格式Struts2默认是不支持解析的,那么我们怎么获取呢???
有的同学可能会想到类型转换器,我们在Struts2的时候的确是学过类型转换器。。。但是呢,这种方法并不是最好的。我们可以使用DateUtils工具类来得到日期数据!
//先判断时间,通过时间进行筛选后,再进行like模糊查询。那么性能会好一些
if (StringUtils.isNotBlank(startTime)) {
startTime = URLDecoder.decode(startTime,"UTF-8");
queryHelper.addCondition(" c.compTime >= ? ", DateUtils.parseDate(startTime, new String[]{"yyyy-MM-dd HH:mm"}));
}
if (StringUtils.isNotBlank(endTime)) {
endTime = URLDecoder.decode(endTime,"UTF-8");
queryHelper.addCondition(" c.compTime <= ? ", DateUtils.parseDate(endTime, new String[]{"yyyy-MM-dd HH:mm"}));
}
我们在JSP页面上也使用datepicker组件来让用户选择日期:
投诉时间:<s:textfield id="startTime" name="startTime" cssClass="s_text" cssStyle="width:160px;" readonly="true" onfocus="WdatePicker({'skin':'whyGreen','dateFmt':'yyyy-MM-dd HH:mm'});"/>
-
<s:textfield id="endTime" name="endTime" cssClass="s_text" cssStyle="width:160px;" readonly="true" onfocus="WdatePicker({'skin':'whyGreen','dateFmt':'yyyy-MM-dd HH:mm'});"/>
受理回复
提供处理受理的UI界面。根据id查找投诉的全部信息。
//提供受理的UI
public String dealUI() {
//把状态传递过去
ActionContext.getContext().getContextMap().put("complainStateMap", Complain.COMPLAIN_STATE_MAP);
//得到想要受理的记录
if (complain != null) {
complain = complainServiceImpl.findObjectById(complain.getCompId());
}
return "dealUI";
}
在处理受理的JSP页面上要把投诉的id给发送给Action处理。不然在保存信息的时候,就会把投诉信息丢失了!。在Action中通过id重新查找回投诉的信息!
//受理
public String deal() {
//修改投诉信息的处理状态
if (complain != null) {
//查找到信息
complain = complainServiceImpl.findObjectById(complain.getCompId());
//如果状态是已处理了,那么就不用再修改了
if (!complain.getState().equals(Complain.COMPLAIN_STATE_DONE)) {
complain.setState(Complain.COMPLAIN_STATE_DONE);
}
}
//保存回复的信息
if (reply != null) {
//更新回复的日期
reply.setReplyTime(new Timestamp(new Date().getTime()));
//把回复信息添加到投诉信息中【关联关系】
reply.setComplain(complain);
complain.getComplainReplies().add(reply);
}
//级联更新
complainServiceImpl.update(complain);
return "list";
}
显示回复信息
我们在处理投诉的时候,应该把回复的历史信息给处理的人看.....
把回复的信息遍历出来。
<s:iterator value="complain.complainReplies" status="st">
<tr>
<%--得到所有回复的信息--%>
<td colspan="2">
<fieldset style="border: solid 1px #c0c0c0;margin-top:5px;"><legend style="color:green;font-weight:bold;">
回复<s:property
value="#st.count"/> </legend>
<div style="width:100%; text-align:center;color:#ccc;maring-top:5px;">
回复部门:<s:property value="replyDept"/>
回复人:<s:property value="replyer"/>
回复时间:<s:date name="replyTime" format="yyyy-MM-dd HH:mm"/>
</div>
<div style="width:100%;maring-top:10px;font-size:13px;padding-left:5px;"><s:property value="replyContent"/></div>
</fieldset>
</td>
</tr>
</s:iterator>
现在有一个问题,就是我们使用的是set集合,它所有的回复信息并不是按照顺序来排列的。我们可以在hbm配置文件中指定我们set集合的顺序:
那么在显示的时候,我们的回复顺序就不会被搞乱了。
匿名投诉
在需求中,我们已经看到了,如果投诉的人是匿名投诉的,那么我们不能显示该投诉人的名字、部门。他的号码应该设置成137**2342类型的**。
其实我们只要在显示对应值的前面判断该投诉人是否是匿名投诉就行了。
<td>
<s:if test="%{complain.isNm==0}">
<%--138****2342类型--%>
<s:property value="complain.compMobile"/>
</s:if>
<s:else>
<s:property value="%{complain.compMobile.substring(0,3)+'****'+complain.compMobile.substring(7,11)}"/>
</s:else>
</td>
我要投诉二级联动
用户可以在首页上通过“我要投诉”超链接对工作人员进行投诉..当然了,用户点击“我要投诉”超链接的时候,应该在新的页面上给出对应的页面,所以指定target为“_blank”..
我们在指定部门的时候,下拉菜单应该在后台给出对应的的员工。这就需要我们用到ajax进行二级菜单的二级联动了。
我们在返回JSON格式有两种方式:第一种就是没有使用Struts2框架的时候,
使用三个开发包commons-beanutils-1.8.0,ezmorph-1.0.6,json-lib-2.3-jdk15。使用JSONObject对象来构建JSON字符串,使用流对象返回给浏览器。
我们如果使用了Struts2框架的话,直接导入:struts2-json-plugin-2.3.20这么一个开发包,并且在配置文件中指定继承json-defalut包,返回的类型是JSON的话。那么Struts2框架就会自动帮我们在该Action中所拥有getter方法的属性就生成JSON格式返回给浏览器。。。当然了,我们可能不想Struts2把全部带有getter的属性都生成JSON返回给浏览器,我们只要在返回JSON类型上指定参数root,就可以指定生成哪一个属性自动生成JSON字符串返回给浏览器了。
当然了,无论是没有使用Struts2框架,还是有使用Struts2框架,我们都是有过Demo的。详情请参考博文:http://blog.csdn.net/hon_3y/article/details/72468761和http://blog.csdn.net/hon_3y/article/details/72480126
另外,我们手动访问Acttion给出对应的参数,就可以看到服务器返回的JSON是什么了。最后我们使用HiJson这样的工具,就可以把返回的JSON进行格式化。
那么,我们的代码是这样的:
使用ajax返回服务器。
function doSelectDept() {
var $dept = $("#toCompDept option:selected").val();
//初始化清空
if($dept =="0"){
$("#toCompName").empty();
}
$.ajax({
type: "post",
url: "${basePath}sys/home_getUserJson.action",
data: {"dept":$dept},
dataType: "json",
success: function (data) {
if("success" == data.msg){
var toCompName = $("#toCompName");
toCompName.empty();
$.each(data.userList, function(index, user){
toCompName.append("<option value='" + user.name + "'>" + user.name + "</option>");
});
} else {alert("获取被投诉人列表失败!");}
},
error: function () {
alert("失败咯")
}
});
}
使用一个Map集合装载这些数据,Struts2自动把Map集合的数据转成是JSON格式的,返回给浏览器。
private Map<String, Object> return_map;
public Map<String, Object> getReturn_map() {
return return_map;
}
public String getUserJson() {
//得到带过来的dept
String dept = ServletActionContext.getRequest().getParameter("dept");
if (dept != null) {
//根据部门查询所有的员工
QueryHelper queryHelper = new QueryHelper(User.class, "u");
queryHelper.addCondition(" u.dept like ? ", "%" +dept);
//2、根据部门查询用户列表
return_map = new HashMap();
return_map.put("msg", "success");
return_map.put("userList", userServiceImpl.findObjects(queryHelper));
}
return "success";
}
我要投诉保存信息
我们在投诉的内容上添加上富文本框,让用户可以在文本域上传上图片....
加上一个富文本框是非常简单的,只要导入对应的js文件,在textarea上写上ueditor的id就可以完成效果了。。。
<script type="text/javascript" charset="utf-8" class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="${basePath}js/ueditor/ueditor.config.js"></script>
<script type="text/javascript" charset="utf-8" class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="${basePath}js/ueditor/ueditor.all.min.js"> </script>
<script type="text/javascript" charset="utf-8" class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="${basePath}js/ueditor/lang/zh-cn/zh-cn.js"></script>
<script type="text/javascript">
//配置ueditor的根路径
var UEDITOR_HOME_URL = "${basePath}js/ueditor/";
var ue = UE.getEditor('editor');
</script>
<td><s:textarea id="editor" name="comp.compContent" cssStyle="width:90%;height:160px;"/></td>
再次观察我们的投诉页面,表单里面的值只有是被投诉人的信息,投诉人的信息是没有的。于是我们在表单中把投诉人的信息通过隐藏域将其添加进去....
<s:hidden name="comp.compCompany" value="%{#session.SYS_USER.dept}"></s:hidden>
<s:hidden name="comp.compName" value="%{#session.SYS_USER.name}"></s:hidden>
<s:hidden name="comp.compMobile" value="%{#session.SYS_USER.mobile}"></s:hidden>
那我们保存“我要投诉”信息的流程应该是怎么样的呢???为了达到更好的用户体验,我们应该先把提示用户数据已经保存起来了,然后刷新父窗口,接着把“我要投诉”本页面给关闭了。这样用户看起来,就觉得他的操作已经是成功了!
下面是整个“我要投诉”操作的时序图:
代码:
public void saveComplain() {
try {
if (comp != null) {
//把投诉的缺少的信息补全
comp.setState(Complain.COMPLAIN_STATE_UNDONE);
comp.setCompTime(new Timestamp(new Date().getTime()));
//调用service保存
complainServiceImpl.save(comp);
//告诉浏览器保存信息成功了。
ServletActionContext.getResponse().getWriter().write("success");
}
} catch (IOException e) {
e.printStackTrace();
}
}
提示用户已经投诉成功,把父窗口刷新,本页面关闭。
function saveComplain() {
$.ajax({
url: "${basePath}sys/home_saveComplain.action",
/*将整个表单的属性转成是JSON*/
data: $("form").serialize(),
type: "post",
success: function (backdata) {
if(backdata == "success"){
//告诉用户,保存成功了。
alert("投诉成功!!!");
//把父窗口刷新
window.opener.parent.location.reload(true);
//把本页面关闭
window.close();
}
},
error:function () {
alert("保存投诉信息失败了!");
}
});
}
投诉受理的三圈问题
在信息管理模块的时候,我们就提出了三圈的问题了。何为三圈问题呢???就是当我们使用条件查询出数据的时候,再对查询出的数据进行操作【修改、保存】,当保存完之后回到列表显示页面上的时候,查询条件就会丢失掉了。也就是说,我们原来查询出的数据不见了。
首先,我们在Action中使用两个变量把有可能成为查询条件的变量记住:
/************三圈问题数据回显*************************/
private String compTitle;
private String state;
public String getCompTitle() {
return compTitle;
}
public void setCompTitle(String compTitle) {
this.compTitle = compTitle;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
接着,在跳转到处理投诉页面的JSP上的时候,把查询条件的数据查询出来,把它赋值给变量。然后使用request域对象把数据发给JSP页面
//把查询条件带过去给JSP页面
ActionContext.getContext().getContextMap().put("compTitle", complain.getCompTitle());
ActionContext.getContext().getContextMap().put("state", complain.getState());
ActionContext.getContext().getContextMap().put("startTime", startTime);
ActionContext.getContext().getContextMap().put("endTime", endTime);
然后在处理投诉页面的JSP上,通过隐藏域把数据给回Action。。Action在重定向到listUI页面的时候,就通过配置文件,把参数带过去:
<!--返回列表展示页面,重定向到列表展示-->
<result name="list" type="redirectAction">
<param name="actionName">complain_listUI</param>
<param name="complain.state">${state}</param>
<param name="complain.compTitle">${compTitle}</param>
<param name="endTime">${startTime}</param>
<param name="startTime">${startTime}</param>
<param name="encode">true</param>
</result>
这样一来,我们的查询条件就没有丢失了。当我们操作完数据的时候,我们的查询出来的数据还是原来那部分。
Quartz自动受理
回到我们的需求:
自动投诉受理:在每个月月底最后一天对本月之前的投诉进行自动处理;将投诉信息的状态改为 已失效。在后台管理中不能对该类型投诉进行回复。
这个需求需求我们要怎么弄呢????要在每个月底最后一天对本月之前的投诉进行自动处理。。。。
记得我们以前在学习Java基础的时候学过了一个Timer这么一个类,可以用规定的频率来执行我们的代码。。。使用起来是非常简单的:
但是呢,要精确到每个月的月底,这就需要我们人为去判断时间了,这就非常不方便了。
因此,我们引入了另一个非常好用的框架:Quartz
Quartz
这是一个优秀的开源任务调度框架“quartz”,可以简单理解成他是Timer的升级版....Spring集成了该框架...
快速入门我们要使用它,就要导入其开发包:quartz-1.8.6.jar,和Spring对其支持的开发包:org.springframework.context.support-3.0.2.RELEASE
使用它的步骤是十分简单的,可分成三个步骤:
1、制定任务信息 bean
- ① 设置执行对象
- ② 设置执行对象中对应的执行方法
- ③ 是否可以同步执行
2、制定任务执行时机(执行触发器) bean
-
2.1、简单触发器(SimpleTrigger)
- ① 设置任务详细
- ② 设置任务延迟执行时间
- ③ 设置任务执行频率
-
2.2、任务触发器(CronTrigger)
- ① 设置任务详细
- ② 设置执行时机(cronExpression)
- cronExpression:秒 分 时 日 月 周 年(可选)
3、设置任务调度工厂 bean
- 设置触发器们
jobDetail任务详细信息
1、jobDetail 任务详细信息;包括调用哪个类;类中的哪个方法;执行时是否可并行执行任务。
<!-- 1、制定任务信息信息 -->
<bean id="jobDetail1" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<!-- 设置执行对象 -->
<property name="targetObject" ref="quartzTask"></property>
<!-- 设置执行对象中对应的执行方法 -->
<property name="targetMethod" value="doSimpleTriggerTask"></property>
<!-- 是否可以同步执行;不可同步执行 -->
<property name="concurrent" value="false"></property>
</bean>
<bean id="jobDetail2" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<!-- 设置执行对象 -->
<property name="targetObject" ref="quartzTask"></property>
<!-- 设置执行对象中对应的执行方法 -->
<property name="targetMethod" value="doCronTriggerTask"></property>
<!-- 是否可以同步执行;不可同步执行 -->
<property name="concurrent" value="false"></property>
</bean>
trigger任务调度触发器
2、trigger 任务调度触发器;主要用于定义jobDetail什么时候执行。触发器最常用的有两种:简单触发器SimpleTrigger 和 任务触发器CronTrigger 。SimpleTrigger和jdk的timer类似,只能指定任务执行以什么样的频率执行,但无法制定精确的执行时间。CronTrigger则既可以执行简单触发器所制定的以频率来执行的时间,也可以制定复杂的时间计划来执行。
<!-- 2、制定任务执行时机(任务执行触发器) -->
<bean id="simplerTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean">
<!-- 设置任务详细 -->
<property name="jobDetail" ref="jobDetail1"></property>
<!-- 设置任务延迟执行时间 ;延迟2秒执行-->
<property name="startDelay" value="2000"></property>
<!-- 设置任务执行频率;执行频率为每4秒执行一下 -->
<property name="repeatInterval" value="2000"></property>
</bean>
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
<!-- 设置任务详细 -->
<property name="jobDetail" ref="jobDetail2"></property>
<!-- 设置任务执行时机,cron表达式 -->
<property name="cronExpression" value="* * * 18c * ?"></property>
</bean>
这里我们着重要了解表达式怎么写:秒 分 时 日 月 周 年【日和周不能同时出现】
-
*字符可以用于所有字段
- 在“分”字段中设为"*"表示"每一分钟"的含义。
-
'?' 字符可以用在“日”和“周几”字段. 它用来指定 '不明确的值'. 这在你需要指定这两个字段中的某一个值而不是另外一个的时候会被用到。在后面的例子中可以看到其含义。
-
'-' 字符被用来指定一个值的范围,比如在“小时”字段中设为"10-12"表示"10点到12点".
-
',' 字符指定数个值。比如在“周几”字段中设为"MON,WED,FRI"表示"the days Monday, Wednesday, and Friday".
-
'/' 字符用来指定一个值的的增加幅度. 比如在“秒”字段中设置为"0/15"表示"第0, 15, 30, 和 45秒"。而 "5/15"则表示"第5, 20, 35, 和 50". 在'/'前加"*"字符相当于指定从0秒开始. 每个字段都有一系列可以开始或结束的数值。对于“秒”和“分”字段来说,其数值范围为0到59,对于“小时”字段来说其为0到23, 对于“日”字段来说为0到31, 而对于“月”字段来说为1到12。"/"字段仅仅只是帮助你在允许的数值范围内从开始"第n"的值。 因此对于“月”字段来说"7/6"只是表示7月被开启而不是“每六个月”, 请注意其中微妙的差别。
-
'L'字符可用在“日”和“周几”这两个字段。它是"last"的缩写, 但是在这两个字段中有不同的含义。例如,“日”字段中的"L"表示"一个月中的最后一天" —— 对于一月就是31号对于二月来说就是28号(非闰年)。而在“周几”字段中, 它简单的表示"7" or "SAT",但是如果在“周几”字段中使用时跟在某个数字之后, 它表示"该月最后一个星期×" —— 比如"6L"表示"该月最后一个周五"。当使用'L'选项时,指定确定的列表或者范围非常重要,否则你会被结果搞糊涂的。
-
'W' 可用于“日”字段。用来指定历给定日期最近的工作日(周一到周五) 。比如你将“日”字段设为"15W",意为: "离该月15号最近的工作日"。因此如果15号为周六,触发器会在14号即周五调用。如果15号为周日, 触发器会在16号也就是周一触发。如果15号为周二,那么当天就会触发。然而如果你将“日”字段设为"1W", 而一号又是周六, 触发器会于下周一也就是当月的3号触发,因为它不会越过当月的值的范围边界。'W'字符只能用于“日”字段的值为单独的一天而不是一系列值的时候。
-
'L'和'W'可以组合用于“日”字段表示为'LW',意为"该月最后一个工作日"。
-
'#' 字符可用于“周几”字段。该字符表示“该月第几个周×”,比如"6#3"表示该月第三个周五( 6表示周五而"#3"该月第三个)。再比如: "2#1" = 表示该月第一个周一而 "4#5" = 该月第五个周三。注意如果你指定"#5"该月没有第五个“周×”,该月是不会触发的。
-
'C' 字符可用于“日”和“周几”字段,它是"calendar"的缩写。它表示为基于相关的日历所计算出的值(如果有的话)。如果没有关联的日历, 那它等同于包含全部日历。“日”字段值为"5C"表示"日历中的第一天或者5号及其以后",“周几”字段值为"1C"则表示"日历中的第一天或者周日及其以后"。
- 对于“月份”字段和“周几”字段来说合法的字符都不是大小写敏感的。
schedulerFactory 任务调度工厂
schedulerFactory 任务调度工厂;用于调度各个任务触发器。
任务调度工厂可以调度多个任务同时进行
<!-- 3、设置调度工厂 -->
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<!-- <ref bean="simplerTrigger"/> -->
<ref bean="cronTrigger"/>
</list>
</property>
</bean>
自动受理
回到我们的需求,我们已经大概了解了Quartz这么一个开源框架使用了。我们就可以在每个月的最后一天中去执行我们对应的代码就行了。
- 时机:
- *每个月月底最后一天;cronExpression:10 10 2 L ?**
- 执行什么内容:
- 对本月之前的待受理投诉进行自动处理;将投诉信息的状态改为 已失效
- 如何执行:
- *本月之前的待受理投诉:select from Complain where 投诉时间 < 本月1号0时0分0秒**
在Spring配置文件中加入Quartz框架....
我们在Service中执行对应的代码:
<!-- 1、制定任务信息信息 -->
<bean id="jobDetail1" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<!-- 设置执行对象 -->
<property name="targetObject" ref="complainServiceImpl"></property>
<!-- 设置执行对象中对应的执行方法 -->
<property name="targetMethod" value="doTask"></property>
<!-- 是否可以同步执行;不可同步执行 -->
<property name="concurrent" value="false"></property>
</bean>
<!-- 2、制定任务执行时机(任务执行触发器) -->
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
<!-- 设置任务详细 -->
<property name="jobDetail" ref="jobDetail1"></property>
<!-- 设置任务执行时机,cron表达式 -->
<property name="cronExpression" value="10 10 2 L * ?"></property>
</bean>
<!-- 3、设置调度工厂 -->
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="cronTrigger"/>
</list>
</property>
</bean>
</beans>
查询出未处理和是本月前的记录,修改成是失效的。
@Override
public void doTask() {
//查询所有待受理的信息
QueryHelper queryHelper = new QueryHelper(Complain.class, "c");
queryHelper.addCondition(" c.state=?", Complain.COMPLAIN_STATE_UNDONE);
//只要在本月之前
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.DAY_OF_MONTH, 1);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
queryHelper.addCondition(" c.compTime <?", calendar.getTime());
//拿到本月之前所有未处理的数据
List<Complain> complains = findObjects(queryHelper);
//将数据全部改成是失效的了。
for (Complain complain : complains) {
complain.setState(Complain.COMPLAIN_STATE_INVALID);
}
}
}
如果已经失效了,那么我们就不让管理员对其进行回复了。
<s:if test="state!=2"> <a href="javascript:doDeal('<s:property value='compId'/>')">受理</a></s:if>
统计图Fusionchart
我们在投诉模块中还有一个功能没有实现:
统计:根据年度将相应年度的每个月的投诉数进行统计,并以图表的形式展示在页面中;在页面中可以选择查看当前年度及其前4年的投诉数。在页面中可以选择不同的年度,然后页面展示该年度的曲线统计图。
我们到目前为止是没有学过任何的统计图的工具的,那么我们要怎么解决这个功能呢???我们有另外的组件来把统计图显示出来:FusionCharts
FusionCharts 是使用javascript 实现统计图表的js组件;其官网地址:http://www.fusioncharts.com。
具体的是怎么操作的可以看官方文档,我们以项目的需求来完成对应的功能就行了。
FusionCharts使用 FusionCharts安装
首先,我们要把对应的JS文档加入到我们的项目中:
- “fusioncharts.js”
- “fusioncharts.charts.js”
- 和相关主题文件复制到项目的js/fusioncharts文件夹。
我们只要根据修改Demo的值就可以实现出我们想要的效果了。
<!DOCTYPE HTML>
<html>
<head>
<%@include file="/common/header.jsp" %>
<title>年度投诉统计图</title>
<script type="text/javascript" class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="${basePath}js/fusioncharts.js"></script>
<script type="text/javascript" class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="${basePath}js/fusioncharts.charts.js"></script>
<script type="text/javascript" class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="${basePath}js/themes/fusioncharts.theme.fint.js"></script>
<script>
FusionCharts.ready(function () {
var revenueChart = new FusionCharts({
"type": "column2d",
"renderAt": "chartContainer",
"width": "500",
"height": "300",
"dataFormat": "json",
"dataSource": {
"chart": {
"caption": "Monthly revenue for last year",
"subCaption": "Harry's SuperMart",
"xAxisName": "Month",
"yAxisName": "Revenues (In USD)",
"theme": "fint"
},
"data": [
{
"label": "Jan",
"value": "420000"
},
{
"label": "Feb",
"value": "810000"
},
{
"label": "Mar",
"value": "720000"
},
{
"label": "Apr",
"value": "550000"
},
{
"label": "May",
"value": "910000"
},
{
"label": "Jun",
"value": "510000"
},
{
"label": "Jul",
"value": "680000"
},
{
"label": "Aug",
"value": "620000"
},
{
"label": "Sep",
"value": "610000"
},
{
"label": "Oct",
"value": "490000"
},
{
"label": "Nov",
"value": "900000"
},
{
"label": "Dec",
"value": "730000"
}
]
}
});
revenueChart.render();
})
</script>
</head>
<body>
<br>
<s:select id="year" list="{2015}" onchange="doAnnualStatistic()"></s:select>
<br>
<div id="chartContainer"></div>
</body>
</html>
需求分析
再次回到我们的需求原型图,我们看看是怎么样的:
根据不同的年份,就显示出不同的统计图数据.....这明显就用到了ajax技术。 因此可以确定下来,我们的前端就是用ajax进行交互,渲染出对应的统计图的。
我们的后端就是根据不同的年份,去获取不同的年份每个月的数据,返回给浏览器...
前端分析我们的需求是得让我们显示近5年的统计图...于是下拉框是我们近5年的....
我们虽然是可以把option中的数据写死,但是呢,如果过了一年的话,那么我们的数据是不会同步的。当我在2017年写的时候,到2018年,页面显示还是2017年的数据....所以这明显是不合理的....
要想近5年是动态产生的,就不能够把数据写死....于是我们可以在JSP页面上得到当前年的值,根据当前年就非常容易推出近5年的数据了...
于是我们又可以使用到Calendar这个日历类了...
在JSP页面得到当前年的数据,并装载到list集合中
Calendar calendar = Calendar.getInstance();
//得到当前年的值
int year = calendar.get(Calendar.YEAR);
//把年份用一个集合装载
List yearList = new ArrayList();
//获取近5年的值
for(int i =0; i<5; i++) {
yearList.add(i, year--);
}
request.setAttribute("yearList", yearList);
request.setAttribute("year", year);
在Struts的select标签中把这个集合迭代出来
<s:select id="year" list="#request.yearList" onchange="doAnnualStatistic()"></s:select>
下面是我们的效果:
接着,我们发现FusionCharts这个组件,想要把数据显示在统计图表中,我们的JSON数据的格式是需要这样的:
还有值得注意的地方是,一进入页面中需要加载当前年度的投诉统计数
<script>
//页面一加载就执行方法
$(function () {
doAnnualStatistic();
});
//根据年份获取投诉数
function doAnnualStatistic() {
//获取当前年份
var $year = $("#year option:selected").val();
//一进来,如果没有选择任何的年数,就显示当前年份的
if($year=="" || $year==undefined) {
$year = "${year}";
}
//2、统计年度投诉数据并展示图表
$.ajax({
url: "${basePath}complain/complain_getAnnualStatisticData.action",
type: "post",
dataType: "json",
data: {"year",$year},
success: function (backData) {
if(backData!=null && backData!=""){
var revenueChart = new FusionCharts({
"type": "line",
"renderAt": "chartContainer",
"width": "600",
"height": "400",
"dataFormat": "json",
"dataSource": {
"chart": {
"caption": "年度统计投诉数",
"xAxisName": "月 份",
"yAxisName": "投 诉 数",
"theme": "fint"
},
"data":backData.chartData
}
});
revenueChart.render();
}
},
error:function () {
alert("统计投诉数失败!");
}
});
}
</script>
后端分析
我们的后端就是根据年份,获取对应的值,返回一个JSON格式给浏览器,那就行了...
但是呢,我们还有其他的细节需要考虑:今年是2017年7月,但是在查询年度投诉数是要把整个年的信息查询出来,8-12月的投诉数肯定是没有的。那么我们会将还没到的时间设置成“”,如果在2016年的某月是没有投诉数的,我们应该将其替换成0,而不是“"....
在action中,我们得获取到用户传递过来的年份,我们调用service、dao层的方法获取该年度对应每个月的投诉数,转换成JSON格式输出就行了。
我们知道前端需要的JSON格式是一个对象数组,最终目的就是数组:Struts2框架在最后解析的时候,会把集合解析成是数组。对象数组在java编程语言就是List集合中嵌套着Map集合。
在后端中,还有一个难点,就是我们的SQL语句该怎么写????我们要从数据库查询的是该年份每个月的投诉数....
通过该年而查询每个月,我们可以很快地想到要用到分组查询。但是还有一个问题,我们在进行分组查询的时候,如果表中是没有1月或2月等数组的话,分组查询出来的数据是没有这些月份的。而我们的统计图是需要所有月份的数据的。咋看一下,我们是需要把查询出来的数据做循环判断,得看看有没有该月份,如果没有该月份还得把数据填充进去。。还得判断该月份是不是本年度的....这样想一下就觉得麻烦了......
select month(comp_time) as '月份',count(*) '总数'
from complain
where year(comp_time)=?
group by month(comp_time)
再次回到前面分析的,如果本年度的月份还没有到,那么将该月的数据设置为“”,如果是其他年份的的月份查出的数据为null,那么我们应该把这些月份的投诉数设置为0而不是”“.....
但是呢,我们现在有一个办法,可以在查询的时候,不管该月份有没有数据,都得显示出来....这就是左外连接
于是我们自己手动生成一张拥有12个月份的数据表,跟我们的投诉表进行左外连接...
select imonth, count(comp_id)
from t_month left join complain on imonth=month(comp_time)
and year(comp_time)=2017
group by imonth
order by imonth;
上面的sql语句的查询效率是有点低的,我们改造一下,改成是子查询:
select imonth,c2
from t_month left join (select month(comp_time) c1, count(comp_id) c2 from complain where year(comp_time)=? group by month(comp_time)) t
on imonth = c1
order by imonth;
代码实现
dao层根据年份查询出每个月份的投诉数据
/**
*
* @param year 根据年获取数据
* @return 返回的是一个列表数组
*/
@Override
public List<Object[]> getAnnualStatisticByYear(int year) {
//拼接SQL语句
StringBuffer buffer = new StringBuffer();
buffer.append(" SELECT imonth,c2 ")
.append(" FROM t_month")
.append(" LEFT JOIN (SELECT month(comp_time) c1, count(comp_id) c2 FROM complain WHERE YEAR(comp_time)=? GROUP BY MONTH(comp_time)) t")
.append(" ON imonth = c1")
.append(" ORDER BY imonth;");
SQLQuery sqlQuery = getSession().createSQLQuery(buffer.toString());
sqlQuery.setParameter(0, year);
List<Object[]> list = sqlQuery.list();
return list;
}
service层拿到Dao层的数据,判断是否是本年度的,如果是本年度的,那么还没有到的月份的数据就设置为”“,如果已经过的了月份,如果没有数据就设置为0.
返回一个List集合嵌套着Map集合,就可以给前台解析了。
@Override
public List getAnnualStatisticByYear(int year) {
List<Object[]> annualStatisticByYear = complainDao.getAnnualStatisticByYear(year);
List<Map> returnList = new ArrayList<>();
//得到本年度和本月份
int curYear = Calendar.getInstance().get(Calendar.YEAR);
//Calerdar月份从0开始,
int curMonth = Calendar.getInstance().get(Calendar.MONTH)+1;
//使用Map集合装载着数据
Map<String,Object> map = null;
for (Object[] objects : annualStatisticByYear) {
map = new HashedMap();
//得到月份
Integer month = Integer.valueOf(objects[0] + "");
map.put("label", month + "月");
if (curYear == year) { //是本年度,那么看看月份是否大于本月份
if (month > curMonth) {
//将数据设置为""
map.put("value", "");
} else {
if (objects[1] != null) {
map.put("value", objects[1]);
} else {
map.put("value", "0");
}
}
}else {//不是本年度
if (objects[1] != null) {
map.put("value", objects[1]);
} else {
map.put("value", "0");
}
}
returnList.add(map);
}
return returnList;
}
action层把service层的数据封装到Map集合中,嵌套ajax解析Map集合,得到的就是对象数组了。
//返回JSON格式的数据,这里我们就直接用Struts2框架来返回对应的数据就行了。
public String getAnnualStatisticData() {
//获取用户传递过来的年份
String str_year = ServletActionContext.getRequest().getParameter("year");
if (str_year != null) {
int year = Integer.valueOf(str_year);
//根据年份去获取每个月的投诉数
map.put("msg", "success");
map.put("chartData", complainServiceImpl.getAnnualStatisticByYear(year));
}
return "getAnnualStatisticData";
}
前台把年份提交给Action,解析出后台返回的数据,渲染成折线图....
function doAnnualStatistic() {
//获取当前年份
var $year = $("#year option:selected").val();
//一进来,如果没有选择任何的年数,就显示当前年份的
if($year=="" || $year==undefined) {
$year = "${year}";
}
//2、统计年度投诉数据并展示图表
$.ajax({
url: "${basePath}complain/complain_getAnnualStatisticData.action",
type: "post",
dataType: "json",
data: {"year":$year},
success: function (backData) {
if(backData!=null && backData!=""){
var revenueChart = new FusionCharts({
"type": "line",
"renderAt": "chartContainer",
"width": "600",
"height": "400",
"dataFormat": "json",
"dataSource": {
"chart": {
"caption": "年度统计投诉数",
"xAxisName": "月 份",
"yAxisName": "投 诉 数",
"theme": "fint"
},
"data":backData.chartData
}
});
revenueChart.render();
}
},
error:function () {
alert("统计投诉数失败!");
}
});
}
总结
- 在条件查询的时候,尽量把like字段的数据放在后边,以提高我们的查询性能!
- 使用DateUtils可以把字符串解析成对象
- 只要页面上的数据没有的话,要么是通过域对象把数据带过去的。要么就在请求的时候我们手动设置的。
- 在hbm配置文件中指定我们set集合的顺序:set集合也可以按照一定的顺序来展示
- 在Struts2指定name为root的话,我们可以指定哪个属性是返回JSON格式的。很多情况下,我们并不需要把所有的属性都返回JSON格式的。
- 得到更好的用户体验,我们可以先提示用户操作成功了,然后把页面刷新一下,最后关闭该页面。
- 以一定频率来指定某段代码的话,我们第一时间是Timer对象,可是我们现在是每个月的前一天来执行某一段代码,现在就有点难度了。
- 因此,我们想要了有什么其他的组件帮我们来完成这个任务。于是有了quartz
- 我们使用它只要配置三样东西
- 任务详情
- 任务执行周期[频率]
- 任务调度工厂
- 我们要是使用到日历的话,我们应该使用Calendar这个日历类
- 由于我们展示的是近五年的数据,我们不能写死的。因此我们可以在后台使用Calendar类来找到进五年的年份,返回给页面做展示。
- 在Demo中我们就可以发现需要的JOSN类型是数组加每个对象。对应我们Java中的集合+Map。
- 我们需要统计每个月的投诉数,第一反应想到的是分组函数。但是,分组函数仅仅会把我们存在投诉的月份展示出来。要想把整年的月份都展示出来,此时就用到我们的外连接查询了!
- 但还有一个条件:如果还没到的月份我们应该设置为“”,而已经到的月份,如果没有投诉数,就设置为0
- 因此,我们拿到dao返回的数据,还要我们进行对日历的判断。
如果文章有错的地方欢迎指正,大家互相交流。
共同学习,写下你的评论
评论加载中...
作者其他优质文章