摘要
本网站为男装定制网,实现西装在线定制的功能。本网站将用户分为匿名用户、注册客户、生产部门、配送部门、管理部门、系统管理员6个角色,不同的角色有不同的功能和职责。本网站用javaee实现,具体采用了spring、struts、hibernate、spring security等框架,数据库使用了mysql。本网站还整合了一个discuz论坛。
一、概述
时间:本项目由个人独立完成。从需求分析到编码实现、完成各种文档,历时约3个月(主要在寒假内完成)。
粗略统计程序规模(仅包括主站中自己写的代码):java代码(*.java):7743行;jquery代码(*.js):2473行;css(*.css):5165行;freemarker模板文件(*.ftl):7220行
二、需求分析
本系统的权限分为6个:
1.匿名用户:
① 可以注册、登录;
② 可以搜索、浏览产品,可以个性化定制产品;
③ 可以使用购物车存放产品;
④ 可以修改购物车中产品的数量、定制信息、尺寸信息等;
⑤ 可以浏览网站的其他各种信息;
⑥ 可以使用在线交流功能;
⑦ 可以浏览定制论坛中的各种信息。
2.注册客户:
① 拥有匿名用户的全部功能;
② 可以下订单,可以查看账户的历史订单信息;
③ 可以设置账户的体型信息和各种尺寸信息(在账户中设置了体型信息和各种尺寸信息后,在登录的状态下定制产品时,录入尺寸页面和录入体型信息页面会自动加载本账户中保存的信息);
④ 可以设置账户的基本资料、联系方式;
⑤ 可以修改密码;
⑥ 可以添加、删除、编辑配送地址信息(账户中设置了一个或多个配送地址信息后,在创建订单页面会自动加载这些地址信息,可以选择这些已有的地址,也可以重新创建新地址,重新创建的新地址将会自动保存到用户账户中)。
3.生产部门
① 可以查看等待生产的订单的详细信息(订单号、订单状态、产品的名称、数量、产品的个性化设置信息、尺寸信息、体型信息);
② 可以对等待生产的订单点击开始生产;
③ 可以查看正在生产中的订单的详细信息;
④ 可以查看生产完毕的订单的详细信息。
4.配送部门
① 可以查看等待配送的订单的详细信息(订单号、订单状态、产品的名称、数量、订单的配送地址);
② 可以对等待配送的订单点击发货;
③ 可以查看已经配送的订单的详细信息。
5.管理部门
① 可以对网站的定制项目和定制系列进行编辑、添加、删除、上移、下移等操作(网站的导航栏中“定制目录”的下拉菜单就是从数据库中取到这些信息,并按照它们的顺序显示出来的);
② 可以添加、编辑、删除产品。(产品的信息包括产品名称、编号、价格、产品所属的定制项目和定制系列、本品包含、尺寸设置、产品列表页面的展示图、产品详细信息页面的背景图、产品描述等)。
6.系统管理员
① 可以查看注册客户的详细信息;
② 可以查看业务日志信息(哪个用户,在什么时间,什么地点(IP地址),做了什么事情);
③ 可以查看各部门人员的详细信息;
④ 可以编辑各部门人员的详细信息;
⑤ 可以添加、删除部门人员用户;
三、整体设计和功能
1、系统目标:
① 良好的客户体验
② 数据的完整性、正确性
③ 系统的稳定性、安全性
④ 系统的可扩展、可维护性
2、开发环境:
服务器端:
操作系统:windows
Web服务器:tomcat6.0、httpd2.2
Java开发包:jdk1.6
php5.3
Discuz7.2
Jms provider:activeMQ5.4
Javamail1.4
视图层:freemarker2.3.16
数据库:mysql 5.1.45
浏览器端:
js:jQuery1.4.2
Css:css2
浏览器:IE7及其以上、firefox、chrome等。
分辨率:1024*768
Web开发框架:spring3.04 + struts2.1.8 + hibernate3.5.1 + spring serurity3.1.0
主要的开发工具:Eclipse、Photoshop、Firebug
3、系统物理部署图:
① 本系统用javaEE开发,使用了spring、struts、hibernate框架;
② 使用spring security进行权限管理;
③ 视图层使用了freemarker模板技术;
④ 使用javamail发送邮件,当用户注册、创建订单、订单开始生产、订单生产完毕、订单已成功发货的时候,系统会自动给客户发送邮件,由于发送邮件是一件耗时的动作,所以采用异步的方式,当要发送邮件时,只给jms服务器发送一条请求发送邮件的消息,然后立即返回,提高了交互速度,发送邮件的任务由jms服务器以异步的方式完成。
⑤ 本系统整合了一个开源的论坛系统discuz7.2,这个论坛是用php做的,因此要整合tomcat服务器、apache服务器和php解释器;所有的请求都先交给apache服务器,然后静态页面、静态资源(css、js、图片等)由apache本身处理,php页面调用php解释器处理,jsp、servlet、action则交给tomcat服务器处理。
⑥ 数据库管理系统使用了mysql,分了两个库,suitdiy和suitdiy_discuz,分别是主站和discuz论坛的数据库。
4、系统用例图:
(其他省略。。。)
5、活动图
(其他省略。。。)
四、数据库设计
1、概况:数据库管理系统使用了mysql。数据库中用到了存储过程、触发器、事件等。
2、概念设计:E-R图
3、创建触发器(trigger):
触发器是强制实施数据库中数据完整性的主要机制之一。为维护数据完整性,本系统创建了若干触发器实现保留已定义约束和应用规则的目的。在用户对指定表执行更新或删除操作时,系统自动执行相应触发器中的SQL语句,实现较复杂的完整性控制。
① item表的触发器item_update:
功能:管理人员删除item表中记录时(实际的操作是设置item_enabled字段为0),将其下关联的style记录从item中删除(实际的操作是设置style_item_enabled为0),同时判断style记录是否被完全删除(即style_item_enabled和style_serious_enabled是否全部为0),若是,则将所有购物车中对应的style记录删除。
创建:
CREATE DEFINER=`root`@`localhost` TRIGGER `item_update` AFTER UPDATE ON `item` FOR EACH ROW begin
if new.item_enabled = 0 then
update style set style_item_enabled = 0 where item_id = new.item_id;
delete from product_temp where style_id in(select style_id from style where item_id = new.item_id and style_item_enabled = 0 and style_serious_enabled = 0);
end if;
end
② serious表的触发器serious_update:
功能:管理人员删除serious表中记录时(实际的操作是设置serious_enabled字段为0),将其下关联的style记录从serious中删除(实际的操作是设置style_serious_enabled为0),同时判断style记录是否被完全删除(即style_item_enabled和style_serious_enabled是否全部为0),若是,则将所有购物车中对应的style记录删除。
创建:
CREATE DEFINER=`root`@`localhost` TRIGGER `serious_update` AFTER UPDATE ON `serious` FOR EACH ROW begin
if new.serious_enabled = 0 then
update style set style_serious_enabled = 0 where serious_id = new.serious_id;
delete from product_temp where style_id in(select style_id from style where serious_id = new.serious_id and style_item_enabled = 0 and style_serious_enabled = 0);
end if;
end
③ session_temp表的触发器delete_st:
功能:定时任务清理session_temp表中客户端浏览器的跟踪信息记录后,删除对应的product_temp表中对应的的产品信息(即购物车中的信息)。
创建:
CREATE DEFINER=`root`@`localhost` TRIGGER `delete_st` AFTER DELETE ON `session_temp` FOR EACH ROW delete from product_temp where session_id = old.id
4、创建存储过程(procedure):
存储过程可被多个程序多次调用,同时加快了系统的运行速度,提高数据安全性。
序号 | 名称 | 创建 | 注释 |
1 | createSessionTemp | DELIMITER $$ | 添加一个客户端浏览器跟踪记录 |
2 | createUser | DELIMITER $$ | 创建一个用户 |
3 | getEnabledStylesByItemId | DELIMITER $$ | 获取某个定制项目下所有有效的产品 |
4 | getEnabledStylesBySeriousId | DELIMITER $$ | 获取某个定制系列下所有有效的产品 |
5 | getLimitedLogs | DELIMITER $$ | 分页查询业务日志 |
6 | getOrderByOrderNo | DELIMITER $$ | 通过订单号查询订单 |
7 | getUsersByRole | DELIMITER $$ | 查询某个角色下的所有用户 |
8 | saveBusinessLog | DELIMITER $$ | 保存业务日志 |
9 | searchEnabledStylesByName | DELIMITER $$ | 根据用户输入的关键字搜索匹配的产品。搜索的字段包括产品名称、产品所属的系列的名称、产品所属的定制项目名称。 |
10 | updateSessionTemp | DELIMITER $$ | 更新客户端浏览器跟踪记录的最后访问时间,如果没有对应id的记录,则插入一条记录。 |
5、创建事件(event,定期运行):
功能:每天零点执行一次,检查session_temp表中的记录(客户端浏览器的跟踪信息),如果记录的最后更新时间距离现在超过14天,则删除之。
创建:
CREATE EVENT `session_temp_check` ON SCHEDULE EVERY 1 DAY STARTS '2011-01-01 00:00:00' ON COMPLETION NOT PRESERVE ENABLE DO delete from session_temp where to_days(now()) - to_days(last_update) > 14
6、数据字典
(共10个表)
① business_log表
字段 | 数据类型 | 键 | 允许为空 | 注释 |
id | varchar(255) | 主键 | NO | 日志id号 |
session_id | varchar(255) |
| NO | 用户浏览器id号 |
user_id | varchar(255) |
| YES | 注册用户id号 |
ip | varchar(255) |
| NO | ip地址 |
action_target | varchar(255) |
| NO | 操作对象 |
action_type | varchar(255) |
| NO | 操作类型 |
action_content | varchar(255) |
| NO | 操作内容 |
time | datetime |
| NO | 时间 |
(其他省略。。。)
五、系统详细设计和实现
1、系统的逻辑架构设计:
整个项目采用MVC模式,分层设计提高了系统的易维护性、可扩展性,减少代码的耦合程度。
4、模块详细设计:
(1)、业务日志模块:
① 定义一个BaseAction.java类,所有的action都继承这个类。
在BaseAction中定义了一些公共的方法,子action只需调用即可。
businessLog方法就是在BaseAction中定义的一个方法,它负责记录业务日志。子类action中调用这个方法,并传递相应的业务信息,实现了记录业务日志的功能:
public void businessLog(String actionTarget,String actionType,String actionContent) throws SuitdiyException{
String userId = getUserIdFromUserInfo();
Map<String,Object> session = ActionContext.getContext().getSession();
String sessionId = (String) session.get("sessionId");
String ip = getIpAddr(ServletActionContext.getRequest());
Date time = new Date(System.currentTimeMillis());
suitdiyService.businessLog(sessionId, userId , ip,
actionTarget, actionType, actionContent, time);
}
② 业务逻辑层:
public void businessLog(String sessionId, String userId, String ip,
String actionTarget, String actionType, String actionContent,
Date time) throws SuitdiyException {
try {
businessLogDao.save(sessionId, userId, ip, actionTarget,
actionType, actionContent, time);
} catch (Exception e) {
e.printStackTrace();
throw new SuitdiyException("记录日志过程中出错~");
}
}
③ dao层:
public void save(final String id, final String userId, final String ip,
final String actionTarget, final String actionType,
final String actionContent, final Date time) {
getHibernateTemplate().executeWithNativeSession(
new HibernateCallback<Object>() {
public Object doInHibernate(Session session) {
session.doWork(new Work() {
public void execute(Connection connection)
throws SQLException {
CallableStatement cs = connection
.prepareCall("{call saveBusinessLog(?,?,?,?,?,?,?,?)}");
cs.setString(1, UUID.randomUUID().toString());
cs.setString(2, id);
cs.setString(3, userId);
cs.setString(4, ip);
cs.setString(5, actionTarget);
cs.setString(6, actionType);
cs.setString(7, actionContent);
cs.setTimestamp(8, new java.sql.Timestamp(time
.getTime()));
cs.execute();
}
});
return null;
}
});
}
(2)、购物车模块:
① 设计思路:
匿名用户即可以使用购物车,与账户无关;
购物车中产品信息的保存采用session+cookie的方式实现。购物车中产品信息保存在服务器端,客户端的cookie只保存购物车的id号。
产品信息不保存在cookie中的原因是,第一,cookie容量大小有限制,本网站产品信息包括了各种定制信息、尺寸信息、体型信息等,很容易就超过了这个限制;第二,cookie会被包含在每次的请求的请求头信息中,cookie内容多了会占用带宽资源。
数据库设计了两个表session_temp和product_temp,用来保存购物车及其产品的信息,如下所示:
Session_temp(id,last_update)
Product_temp(id,session_id,... ... )
具体的实现流程是,当客户端提交请求后,服务器端会首先判断session中是否有sessionId(自己设置的,用来唯一地识别客户端浏览器的id号,与购物车一一对应,可以看做是购物车id号),如果有,取出对应的id值,从Product_temp表中取出对应的产品信息,返回;如果没有,则判断cookie中是否有_suitdiy_session_id(自己设置的,用来唯一地识别客户端浏览器的id号,session中的sessionId值就是这个值的复制),如果有,则将这个id号放入session中(名字为sessionId),同时插入Session_temp中,返回;如果cookie中没有id号,则重新创建一个id号(使用uuid,全球唯一),放入session和cookie中并插入session_temp表中,返回。
_suitdiy_session_id这个id号没有用服务器端的sessionId,而是自己生成的uuid,原因是,sessionId在服务器重启的情况下可能会产生重复,造成混乱。
另外,购物车信息初始情况下是隐藏的,可以采用异步方式加载购物车信息,提高页面响应速度。在页面加载完成后,客户端发送一个ajax请求,获取购物车的信息。
② 存在的问题及解决方案:客户端删除cookie后,服务器端购物车的信息就成为了废物,网站点击量大了,会产生巨大的垃圾,占用了资源。解决方法是,在session_temp表中设置字段last_update,用户每次访问时,都更新这个字段;然后在数据库中设置一个定期执行的任务,每天晚上零点执行一次,判断last_update的值距离现在是否已经超过一定期限(期限可以根据实际运行情况进行调整),如果超过,删除,同时删除Product_temp表中对应的产品记录。
③ 购物车的行为:在客户端没有删除浏览器cookie的情况下,并且在距离最后一次访问本站的时间没有超过一定期限的情况下,服务器能永久地跟踪客户端特定的浏览器,其购物车信息不会丢失(业务日志中记录了浏览器的行为,为用户行为分析提供了依据)。
④ 代码实现:
1) 页面加载完成后,提交ajax请求获取购物车信息,并初始化购物车的交互行为的js代码: function InitCart(){
$('#detail_cart').hide();
var time = new Date().getTime();
var url = 'cart.action';
$('#detail_cart').load(url,function(){ $('#cart_num').html($('#detail_cart .count').html());
});
$('#menu_cart').click(function(){
$('#search_input').slideUp();
$('#detail_cart').slideDown('normal');
return false;
});
$('body').click(function(){
$('#detail_cart').slideUp('normal');
});
$('#detail_cart').click(function(event){
event.stopPropagation();
});
$('#detail_cart a#delete').click(function(){
if(confirm('确定要删除吗?')){
return true;
}
else{
return false;
}
});
}
2) 处理cart.action请求的action代码:
a、BaseAction中的判断逻辑:(BaseAction实现了Preparable接口,prepare方法是Preparable接口的方法,它在action的任何方法执行之前执行,保证了判断逻辑的执行。)
public void prepare() throws SuitdiyException {
Map<String, Object> session = ActionContext.getContext().getSession();
sessionId = (String)session.get("sessionId");
if(null != sessionId && !sessionId.equals("")){
suitdiyService.updateSessionTemp(sessionId);
}
else{
Cookie[] cookies = ServletActionContext.getRequest().getCookies();
if(null != cookies){
for(Cookie c:cookies){
if(c.getName().equals("_suitdiy_session_id")){
sessionId = c.getValue();
}
}
}
if(null != sessionId && !sessionId.equals("")){
session.put("sessionId", sessionId);
suitdiyService.updateSessionTemp(sessionId);
}
else{
sessionId = UUID.randomUUID().toString();
suitdiyService.createSessionTemp(sessionId);
session.put("sessionId", sessionId);
Cookie c = new Cookie("_suitdiy_session_id",sessionId);
c.setPath("/");
c.setMaxAge(999999999);
ServletActionContext.getResponse().addCookie(c);
}
}
}
b、子action获取购物车信息:
public class CartAction extends BaseAction{
protected List<Map<String,String>> cartgoods = new ArrayList<Map<String,String>>();
public String execute() throws SuitdiyException{
cartgoods = suitdiyService.getCartgoodsBySessionId(sessionId);
return SUCCESS;
}
public List<Map<String, String>> getCartgoods() {
return cartgoods;
}
public void setCartgoods(List<Map<String, String>> cartgoods) {
this.cartgoods = cartgoods;
}
}
3)业务逻辑代码
public void updateSessionTemp(String sessionId) throws SuitdiyException {
sessionTempDao.updateSessionTemp(sessionId);
}
public List<Map<String, String>> getCartgoodsBySessionId(String sessionId)
throws SuitdiyException {
try {
List<Map<String, String>> cartgoods = new ArrayList<Map<String, String>>();
List<ProductTemp> pts = productTempDao.getBySessionId(sessionId);
Map<String, String> countMap = new HashMap<String, String>();
countMap.put("count", new Integer(pts.size()).toString());
cartgoods.add(countMap);
int iMax = (pts.size() <= 4) ? pts.size() : 4;
for (int i = 0; i < iMax; i++) {
ProductTemp pt = pts.get(i);
Style s = pt.getStyle();
Map<String, String> cg = new HashMap<String, String>();
cg.put("productTempId", pt.getId());
cg.put("styleName", s.getName());
cg.put("frontPicture", s.getFrontPicture());
cg.put("price", s.getPrice().toString());
cg.put("count", pt.getCount().toString());
String diy = s.getDiyOptions();
if (null != diy && !diy.equals("")) {
cg.put("hasDiy", "yes");
} else {
cg.put("hasDiy", "no");
}
Blob measure = pt.getMeasurements();
if (null != measure && measure.length() > 0) {
cg.put("measureEmpty", "false");
} else {
cg.put("measureEmpty", "true");
}
cartgoods.add(cg);
}
return cartgoods;
} catch (Exception e) {
e.printStackTrace();
throw new SuitdiyException("获取购物车信息时出现错误~");
}
}
4)dao层代码
public void updateSessionTemp(final String sessionId) {
getHibernateTemplate().executeWithNativeSession(
new HibernateCallback<Object>() {
public Object doInHibernate(Session session) {
session.doWork(new Work() {
public void execute(Connection cnn)throws SQLException {
CallableStatement cs = cnn.prepareCall("call updateSessionTemp(?)");
cs.setString(1, sessionId);
cs.execute();
}
});
return null;
}
});
}
public List<ProductTemp> getBySessionId(String sid){
return (List<ProductTemp>)getHibernateTemplate().find("from ProductTemp pt where pt.sessionTemp.id = ?",sid);
}
5)调用的存储过程:
DELIMITER $$
DROP PROCEDURE IF EXISTS `suitdiy`.`updateSessionTemp`$$
CREATE DEFINER=`root`@`localhost` PROCEDURE `suitdiy`.`updateSessionTemp`(IN `sessionId` varchar(255))
BEGIN
if (select count(id) from session_temp where id = sessionId) = 0 then
insert into session_temp(id,last_update) values(sessionId,now());
else
update session_temp set last_update = now() where id = sessionId;
end if;
END $$
DELIMITER ;
(其他省略。。。)
六、关键问题分析与解决
1、购物车采用session + cookie的方式实现。
2、管理人员在删除产品、定制项目、定制系列时,实际的操作不能直接删除。因为过去可能有对应的订单,订单对应的数据库表的外键指向这些产品、定制项目、定制系列对应的表,会出现删除异常。如果级联删除对应的订单记录,那么用户发现自己以前的订单没了,大事不妙。解决的方法是,在产品、定制项目、定制系列对应的表中设置一个字段enabled,用来标识一条记录是否被删除。删除时,仅仅把这个字段设置为false;查询时,加上一个条件enabled = true ;不影响订单继续使用这些记录。
3、在注册、创建订单、订单开始生产、订单生产完毕、订单已经发货后,系统会自动给用户发送一封邮件通知。这个用jms和javamail实现。发送邮件的任务由jms服务器以异步的方式完成,提高系统交互速度,提高客户体验。
4、本网站整合了一个开源的论坛系统,discuz7.2。由于这个论坛是用php做的,所以需要apache服务器。本系统整合了tomcat服务器和apache服务器。
5、struts中action的问题:有些页面使用了struts2校验框架后,如果校验失败,则立即返回,而execute方法没有执行,造成一些数据没有被初始化。解决方法有多种:
① 在action中重写validate方法,将初始化动作放在这个方法里面;
② 让action实现preparable接口,将初始化动作放在prepare方法里面。
6、本网站使用spring security框架进行权限管理。默认的userDetails实现类只包含username、password和权限等信息,通过提供自定义的userDetailsService类,使用自定义的UserInfo类,达到扩展信息的目的。
7、由于定制信息的条目比较多(大约有34个),如果每个条目对应一个字段,会造成数据库表的字段太多而不利于管理、维护,所以,本项目中,将所有的定制信息保存到java中的HashMap对象中,然后将其序列化后保存为数据库的一个Blob类型的字段。