5 系统详细设计
5.1 系统接口以及Action的抽取
由于各模块块之间存在一些几乎相同的操作,例如对于POJO对象的增加、删除、更改、查询操作,如果每次都重写,将会十分麻烦,因此,我们将这些方法形成接口,封装起来,并且使用泛型的方法,让它得到复用,之后,我们再为这些接口实现统一的实现方法。
首先,我们新建BaseService接口,封装保存、更新、删除、根据id查询和查询所有数据5个方法。
public interfaceBaseService<T> {
void save(T t);
void update(T t);
void delete(int id);
T get(int id);
List<T> query();
}
新建BaseServiceImp实现BaseService,并在构造方法中获取子类的泛型对象类型,并且封装一个获取Hibernate中当前Session的getSession方法,然后,重写相应的方法。
ParameterizedType type = (ParameterizedType) this.getClass()
.getGenericSuperclass();
clazz = (Class) type.getActualTypeArguments()[0];
@Override
public void delete(int id) {
String hql = "delete from " + clazz.getSimpleName() +" where id=:id";
getSession().createQuery(hql).setInteger("id", id).executeUpdate();
}
@Override
public T get(int id) {
return (T)getSession().get(clazz, id);
}
@Override
public List<T>query() {
String hql = "from " + clazz.getSimpleName();
returngetSession().createQuery(hql).list();
}
最后,是对Action的抽取。为BaseAction实现RequestAware、SessionAware和ApplicationAware三个接口,用于实现对请求级、会话级和应用级的缓存数据存储。重写相应方法,新建request、session和application三个键值对,分别用于存储相应需求的缓存数据。同时,实现ModelDriven<T>接口,重写getModel方法,提供T类型的model对象,方便我们操作POJO,以后在请求中直接获取要操作的对象,使用泛型让子类实现时返回其类型,方便model进行转化。
将所有实现了BaseService的Service接口在BaseAction中定义。并且,BaseAction提供FileImage对象,方便存储用户发布商品时上传的照片。提供jsonList列表对象、InputStream流对象以及jsonMap映射表对象,分别用于在Action中返回列表形式的json对象、返回流信息和返回键值对形式的json信息。提供ids字符串,用于接收管理员要删除的相应数据的编号集合。提供page和rows两个字符串,用于记录页码和每页的数据量,实现后台分页查询功能。
以上所有BaseAction提供的对象,都是用protected声明,方便子类继承,并且提供get和set方法,方便Spring调用。
5.2 前台显示模块的设计与实现
前台显示模块分为游客前台显示界面和注册用户显示界面两种,分别如下图5-1和5-2所示。服务器会在启动时,自动读取数据库中的所有商品种类和部分商品信息,如果此类商品是热点商品,那么,取出此种类下最新的四条数据显示在主页面,并且每小时,系统会自动刷新,重新读取数据,并且刷新界面。实现方法如下:
首先,在web.xml中添加监听器,用于初始化数据。
<listener>
<listener-class>cn.it.listener.InitDataListener</listener-class>
</listener>
然后,新建InitDataListener类,实现ServletContextListener接口,重写contextInitialized方法。
public void contextInitialized(ServletContextEvent event) {
context = WebApplicationContextUtils.getWebApplicationContext(event
.getServletContext());
productTimerTask= (ProductTimerTask) context.getBean("productTimerTask");
productTimerTask.setApplication(event.getServletContext());
new Timer(true).schedule(productTimerTask,0, 1000*60*60);
}
新建ProductTimerTask工具类,继承TimerTask类,重写run方法,并且提供Servlet上下文对象application和商品、种类和订单三个接口对象。在run方法中,我们根据商品类别是否是热点查询返回一个商品类别数组,遍历数组,通过商品类别的编号查询出相应类别下最新上架的四条商品数据,并且,将这四条商品数据,加入bigList数组。查询商品所有类别,加入typeList数组。然后,我们将bigList和typeList通过application的setAttribute方法,加入缓存。
在jsp页面中,我们通过applicationScope获取bigList和typeList列表,并通过<c:forEach>标签遍历数组。由于bigList存放也是list,因此,我们需要嵌套<c:forEach>进行二次循环读取。当我们点击商品图片或者种类时,触发相应action分别返回商品的具体信息界面和此类别下所有商品信息页面。
类别列表:
<c:forEachitems="${applicationScope.typeList}"
var="list" varStatus="status">
<a href="product_queryByType.action?
id=${status.count}">${list.type}</a>
</c:forEach>
商品列表:
<c:forEachitems="${applicationScope.bigList}" var="list">
...
<h2>${list[0].category.type}</h2>
<c:forEachitems="${list}" var="product">
<ul><li> <a href="${shop}/product_get.action?id=${product.id}">
<imgsrc="${shop}/image/${product.pic}" /></a>
<div>...</div> </li></ul>
</c:forEach>
</c:forEach>
图5-1游客前台显示界面
图5-2 注册用户前台显示界面
5.3 用户注册模块的设计与实现
在游客前台首页点击“注册”按钮之后。进入注册界面。在注册界面,用户需要填写登录名,密码,真实姓名,性别,邮箱以及电话等基本信息,如图5-3所示。其中,我们使用JQuery-Validate来实现对输入数据的验证功能。其中除邮箱之外,其他数据都是必填的,当输入登录名之后,系统会返回action请求去后台查询当前输入的用户名是否存在,密码的长度被限制在6到12位,确认密码必须和密码保持一致。如果验证不正确,将在数据最后提示自定义的错误信息,即将错误信息,自定义显示到表格当前行的最后一列中。在后台的action中,除了保存用户信息之外,还需要为此用户新建一个权限,添加权限记录,并且将用户信息加入session,方便之后提取当前用户信息。
前台界面:
$(function() {
$("#ff").validate({
onKeyup : true,
errorPlacement:function(error,element){
$(element).parent().next().html(error);
},
rules : {
login : {
required : true,
remote : {
url : 'user_loginValidate.action',
type : "post"
}
},
pwd : {
required : true,
rangelength : [ 6, 12 ]
},
cpwd : {
required : true,
equalTo : "#pwd"
},
…
},
messages : {
login : {
required : "请输入登录名",
remote : "登录名已存在"
},…
}
});
});
</script>
后台:
publicString register() {
userService.save(model);
session.put("user", model);
Ulimit limit = new Ulimit();
limit.setAddComment(true);
limit.setAnsComment(true);
limit.setUser(model);
limitService.save(limit);
return "loginindex";
}
publicString loginValidate() {
List<User> userList =userService.query();
if (userService.loginValidate(model,userList)) {
inputStream = newByteArrayInputStream("false".getBytes());
} else {
inputStream = newByteArrayInputStream("true".getBytes());
}
return "stream";
}
图5-3用户注册页面
5.4 用户登录模块的设计与实现
点击游客前台界面的“登录”按钮,进入用户登录界面,如图5-4所示。在用户登录界面中输入账号和密码之后,提交action请求,后台查询用户名密码是否正确,如果正确,进入注册用户前台界面,如果不正确,刷新当前界面,显示登录错误信息。
由于系统支持游客选购商品,但在结账时需要账户信息,所以,点击登录之后,会出现两种情况的跳转。一种是直接返回注册用户前台界面,另一种,则是返回账单确定界面。我们使用url地址重定位的方式解决这个问题。
首先,我们需要在web.xml中添加过滤器。
<filter>
<filter-name>userFilter</filter-name>
<filter-class>cn.it.filter.UserFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>userFilter</filter-name>
<url-pattern>/user/*</url-pattern>
</filter-mapping>
新建一个过滤器,继承Filter,重写doFilter方法。获取当前action请求要去的url地址,用于订单确认界面在内网中,因此,我们要将url地址改写成send_*_*的格式,手动将请求转发到内网下,并将地址保存到goUrl中,存储进request中。当然,如果用户还没有登录,我们直接转发原请求即可。
@Override
publicvoid doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException,ServletException {
HttpServletRequest req =(HttpServletRequest) request;
HttpServletResponse res =(HttpServletResponse) response;
String goUrl = req.getServletPath();
String param = req.getQueryString();
String[] split =goUrl.split("/");
String name = split[2].substring(0,split[2].lastIndexOf("."));
goUrl = "/send_" + split[1] +"_" + name + ".action";
if (param != null) {
goUrl += goUrl + "?" +param;
}
req.getSession().setAttribute("goUrl",goUrl);
if (req.getSession().getAttribute("user")== null) {
req.getSession().setAttribute("error","非法请求,请登录!");
res.sendRedirect(req.getContextPath()+ "/ulogin.jsp");
} else {
res.sendRedirect(req.getContextPath()+ goUrl);
}
}
图5-4用户登录界面
5.5 账户管理模块的设计与实现
用户登录之后,在注册用户前台,可以点击“账户管理”进入账户管理界面,如图5-5所示,在此界面中,我们可以修改我们当前用户的密码。首先,我们输入旧密码,当点击更新按钮之后,系统会取出session中存储的用户信息进行比对,如果一致,则将用户信息中的密码更改,并且通过Service保存POJO对象。反之,不保存。更新成功之后,返回游客前台界面。
图5-5 用户账户管理界面
5.6 商品发布模块的设计与实现
用户登陆后,点击“发布商品”进入商品发布界面,如图5-6所示。按照提示输入相应的商品信息,同样利用JQuery-Validation进行验证,商品价格必须为数字且大于0,商品图片的后缀名必须为jpeg、jpg或者png,所有字段都是必填字段。当点击“发布”之后,action保存商品信息,但此时“推荐”和“有效”均为false,当后台管理员审核之后,才有可能在前台显示。
图5-6 商品发布界面
5.7 购物车模块的设计与实现
用户登陆之后,点击“购物车”,进入购物车管理界面,如图5-7所示。此界面中显示当前用户目前已加入购物车中的商品,并且计算总共的价格。最右边的叉叉,点击之后,会通过商品id,从购物车中删除当前选中的商品。点击下方的“继续购物”按钮,返回注册用户前台继续选购商品,点击“清空购物车”按钮,清空当前购物车中所有购物项,即新建一个Forder放入session中。
图5-7购物车界面
5.8 订单确认模块的设计与实现
当用户确定要购买购物车中的商品时,点击“结账”按钮,在判定已登录情况下会返回订单确认界面,如图5-8所示。页面会显示当前购物车中的购物项信息,用户填写订单的相应信息之后,点击“确认无误,购买”,就可以进入支付界面,同时,我们保存用户的订单信息到数据库,当用户支付完成之后,我们将订单的状态从“未支付”改为“已支付”。同时,我们将当前Forder放入application中的oldForderSet中,方便在订单管理模块对订单进行操作。
由于同一个用户可能选购不同用户发布的商品,所以,我们需要在保存订单时,将不同用户发布的商品进行分类,形成不同的订单保存。同样的,由于,我们在页面中输入的数据,出入后台会保存到model中,而model中保存的sorderSet和我们获取的sorderSet并不是一个对象,我们需要新建一个Forder,然后将所有的值赋给这个Forder,最后保存这个Forder对象。
图5-8 订单确认界面
5.9 订单管理模块的设计与实现
用户登陆之后,点击“订单管理”进入订单管理界面,如图5-9所示。在订单管理界面中,会显示当前用户下,购买的并且已发货的订单以及购买的并且已经付款的订单。显示订单的编号,以及此订单编号下的购物项的具体信息。这些信息都是在用户登录时,根据用户的信息,查询application中的oldForderSet得到的,然后,在页面中通过<c:forEach>遍历展示。
当点击“确认收货”或者“确认发货”之后,后台根据oldForderSet中的信息,以及当前用户的信息,查询到其中相应的Forder,然后更改他们的订单状态,如果是“确认收货”则将订单状态从3变为4,如果是“确认发货”,则将订单状态从2变为3。然后更新相应的Forder对象,重新将oldForderSet放入application中,方便下次使用。
图5-9 订单管理界面
5.10 留言及回复模块的设计与实现
用户在登陆后,可以点击相应的商品图片,进入商品详细介绍页面,如图5-10所示,点击“留言”按钮,可以进入留言界面进行留言。用户留言时,如果此商品的发布用户是当前用户,用户的留言不会被保存。
图5-10 用户留言界面
在注册用户首页中,点击“留言回复”按钮,进入留言回复界面,如图5-11所示。此页面会显示当前用户留言的回复信息,以及其他用户对本用户发布商品的留言信息。用户在输入回复信息之后,点击“回复”按钮即可。同时,从session中获取留言列表,删除当前留言记录,并设置此对象的回复字段为true,更新留言记录,保存新的留言列表到session中。
图5-11 用户留言回复界面
5.11 后台模块的设计与实现
后台模块主要实现类别管理、商品管理、用户管理、留言管理和权限管理等功能。由于这些模块的功能相似,我们选择最具典型的类别管理进行说明,其他的模块不再进行说明。
首先,后台我们使用easyUI的框架进行搭建。分为三部分,左边显示后台系统的所有功能,当点击某一个功能时,页面进行Ajax请求跳转,在中间部分显示相应的界面,如果请求的界面存在,则选中请求的界面,如果不存在,则新建一个页面,并跳转到此界面。
代码如下:
$(function() {
$("a[title]").click(function(){
var text = $(this).text();
var href = $(this).attr("title");
if ($("#tt").tabs("exists", text)){
$("#tt").tabs("select", text);
} else {
$("#tt").tabs("add",
{ closable : true,
title : text,
content :
'<iframe title='+ text +'src='+ href +' frameboder="0"
width="100%"height="100%"/>'
});
}});
});
类别管理模块主要实现管理员对商品类别的增加,删除,更新操作,如图5-12所示。这个界面主要就是存放一个datagrid组件,将后台查询的数据以jsonMap的形式传到前台,其中包含总的记录数以及json数据。
点击“添加类别”,出现添加类别界面,如图5-13所示,同时会锁住全屏。实现方法为,在父页面中隐藏一个窗体,在子页面中调用父页面的窗体并显示,其中我们用<iFrame>将我们自己做的界面放入窗体中,实现窗体的复用。
点击“更新类别”按钮时,必须选择一条记录,不能不选择,也不能选择多条记录。之后跟新建一样出现“更新类别”界面,如图5-14所示,并且通过Ajax请求实现回写。
点击“删除类别”按钮,页面执行ajax请求,将选中的一条或者多条记录的id打包成ids返回给action,service根据ids删除相应的记录,刷新界面。
在搜索栏中输入要查询的类别,点击搜索按钮,发出ajax请求,根据查询类别名称模糊查询相应记录,从后台以jsonList的格式传到前台。
实现代码:
$(function() {$("#dg").datagrid(
{url : 'category_queryToJson.action', type : ""},
idField : 'id',
...
pageSize : 5,
pageList : [ 5, 10, 15, 20 ],
toolbar : [{
iconCls : 'icon-add',
text : '添加类别',
handler : function() {
parent.$("#win").window({
title : "添加类别",
...
content : '<iframe src="send_category_save.action" … />'
});}},'-',
{ iconCls : 'icon-edit',
text : '更新类别',
handler : function() {
var rows = $('#dg').datagrid('getSelections');
if(rows.length!=1){
$.messager.show({...});
}else{
parent.$("#win").window({
title : "更新类别",
...
content : '<iframesrc="send_category_update.action " … />'
});}}},'-',
{iconCls : 'icon-remove',
text : '删除类别',
handler : function() {
var rows = $('#dg').datagrid(
if (rows.length== 0) {
$.messager.show({
...
});} else {
$.messager.confirm('删除确认对话框','是否要删除选中的记录?',
function(r) {
if (r) {
var ids ="";
for (var i = 0;i < rows.length; i++) {
ids +=rows[i].id+ ",";
}
ids =ids.substring(0,ids.lastIndexOf(","));
$.post("category_deleteByIds.action",{ids: ids},
function(result){
if (result =="true") {
$('#dg').datagrid('uncheckAll');
$('#dg').datagrid('reload');
} else {...
});} },"text");}});
}}}, '-',
{text : "<input id='ss' name='type'/>"} ],
columns : [ [
{field : 'xyz',
checkbox : true
},
{field : 'id',
title : '编号',
width : 200
}, ... ] ]});
$("#ss").searchbox({
searcher : function(value, name) {
$('#dg').datagrid('load',{type : value});
},
prompt : '请输入要查询的类别'});
});
其他的四个模块中,商品管理模块主要实现对商品信息的更新和删除操作,用户管理模块主要实现对用户信息的更新和删除操作,留言管理模块主要实现对留言信息的删除操作,权限管理主要实现对用户权限的更新操作。
图5-12 后台类别管理界面
图5-13 添加类别界面
图5-14 更新类别界面