【JAVA EE#6】【传智书城·源码阅读】后台管理模块:权限控制+页面分析+商品管理+销售榜单+订单管理+公告管理+项目结构思维导图

权限控制
普通用户只能访问client文件夹下面的jsp文件,对于没有权限操作的admin文件夹就会提示错误,而超级用户同时可以访问两者,一直很好奇这个权限限制怎么实现的。
在这里插入图片描述
原来在存在一个AdminPrivilegeFilter类继承自过滤器Fliter,获取Sessionrole属性对此进行的管制:

// 1 强制转换
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) resp;

		// 2判断是否具有权限
		User user = (User) request.getSession().getAttribute("user");

		if (user != null && "超级用户".equals(user.getRole())) {
			// 3.放行
			chain.doFilter(request, response);
			return;
		}

		response.sendRedirect(request.getContextPath() + "/error/privilege.jsp");

并在web.xml注册信息里面对所有访问进行了拦截:

<filter>
		<filter-name>adminPrivilegeFilter</filter-name>
		<filter-class>cn.itcast.itcaststore.web.filter.AdminPrivilegeFilter</filter-class>
</filter>
<filter-mapping>
		<filter-name>adminPrivilegeFilter</filter-name>
		<url-pattern>/admin/*</url-pattern>
</filter-mapping>

页面分析
按照代码逻辑,管理员登陆后只能进入home.jsp页面,并不像和普通用户一样可以进行购书操作的逻辑。页面由四个jsp组成,利用<frameset>标签划分各个<frame>搭建框架。body { SCROLLBAR-ARROW-COLOR: SCROLLBAR-BASE-COLOR:}语句控制滚动条颜色。
在这里插入图片描述
jsp代码:

<%@ page language="java" pageEncoding="UTF-8"%>
<html>
    <head>
		<meta http-equiv="Content-Language" content="zh-cn">
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <style>
		body
		{
			SCROLLBAR-ARROW-COLOR: #ffffff;  SCROLLBAR-BASE-COLOR: #dee3f7;
		}
    </style>
   </head>
<frameset rows="103,*,43" frameborder=0 border="0" framespacing="0">
  <frame src="${pageContext.request.contextPath}/admin/login/top.jsp" name="topFrame" scrolling="NO" noresize>
  <frameset cols="159,*" frameborder="0" border="0" framespacing="0">
		<frame src="${pageContext.request.contextPath}/admin/login/left.jsp" name="leftFrame" noresize scrolling="YES">
		<frame src="${pageContext.request.contextPath}/admin/login/welcome.jsp" name="mainFrame">
  </frameset>
  <frame src="${pageContext.request.contextPath}/admin/login/bottom.jsp" name="bottomFrame" scrolling="NO"  noresize>
</frameset>
</html>

top.jsp有两个功能,显示日期以及退出系统,日期显示利用document内置对象进行输出的,js语句不用<%>内嵌,而是语句块一样写在一起。

<font color="#000000">
	<script language="JavaScript">								
		tmpDate = new Date();
		date = tmpDate.getDate();
		month = tmpDate.getMonth() + 1;
		year = tmpDate.getFullYear();
		document.write(year);
		document.write("年");
		document.write(month);
		document.write("月");
		document.write(date);
		document.write("日 ");

		myArray = new Array(6);
		myArray[0] = "星期日"
		yArray[1] = "星期一"
		myArray[2] = "星期二"
		myArray[3] = "星期三"
		myArray[4] = "星期四"
		myArray[5] = "星期五"
		myArray[6] = "星期六"
		weekday = tmpDate.getDay();
		if (weekday == 0 | weekday == 6) {
			document.write(myArray[weekday])
		} else {
			document.write(myArray[weekday])
		};								
	</script> 
</font>

退出登录调用了自定义函数exitSys(),利用window内置对象window.top返回该页面的顶层窗口(不过实测,点击后并无反应,Session也没有失效)。

<script type="text/javascript">
	function exitSys() {
		var flag = window.confirm("确认退出系统吗?");
		if (flag) {
			window.top.open('', '_parent','',true);
			window.top.close();
		}
		//如果你使用的是firefox浏览器必须要做以下设置 
		//1、在地址栏输入about:config然后回车,警告确认 
		//2、在过滤器中输入”dom.allow_scripts_to_close_windows“,双击即可将此值设为true 即可完成了 

	}
</script>

商品管理
该模块有4个功能:添加书本、查询(包括所有书本+多条件查询)、编辑书本信息、删除该书本。
点击左侧商品管理,就会访问ListProductServlet,然后重定向到/admin/products/list.jsp,然后循环输出书本列表。页面根据jsp传递的type访问FindProductByIdServlet,该类得到用户类型为超级用户就会转向edit.jsp,普通用户就会转向info.jsp,点击删除时,就会转向DeleteProductServlet,同时调用页面自定义函数p_del()弹出确认框,
在这里插入图片描述
该jsp页面核心代码:

 <!--  循环输出所有商品 -->
	<c:forEach items="${ps}" var="p">
		<tr onmouseover="this.style.backgroundColor = 'white'"
			onmouseout="this.style.backgroundColor = '#F5FAFE';">
			<td style="CURSOR: hand; HEIGHT: 22px" align="center" width="200">${p.id }</td>
			<td style="CURSOR: hand; HEIGHT: 22px" align="center" width="18%">${p.name }</td>
			<td style="CURSOR: hand; HEIGHT: 22px" align="center" width="8%">${p.price }</td>
			<td style="CURSOR: hand; HEIGHT: 22px" align="center" width="8%">${p.pnum }</td>
			<td style="CURSOR: hand; HEIGHT: 22px" align="center">${p.category}</td>
			<td align="center" style="HEIGHT: 22px" width="7%">
			<a href="${pageContext.request.contextPath}/findProductById?id=${p.id}&type=admin">
			<img src="${pageContext.request.contextPath}/admin/images/i_edit.gif" border="0" style="CURSOR: hand"> </a>		
			</td>
			<td align="center" style="HEIGHT: 22px" width="7%">
			<a href="${pageContext.request.contextPath}/deleteProduct?id=${p.id}" onclick="javascript:return p_del()">
				<img src="${pageContext.request.contextPath}/admin/images/i_del.gif"
				width="16" height="16" border="0" style="CURSOR: hand">
			</a>
			</td>
			</tr>
	</c:forEach>

除此之外,还有多条件查询、编辑商品信息、增加商品信息的功能。核心代码:

// 多条件查询
	public List<Product> findProductByManyCondition(String id, String name,
			String category, String minprice, String maxprice)
			throws SQLException {
		List<Object> list = new ArrayList<Object>();
		String sql = "select * from products where 1=1 ";

		QueryRunner runner = new QueryRunner(DataSourceUtils.getDataSource());

		if (id != null && id.trim().length() > 0) {
			sql += " and id=?";
			list.add(id);
		}

		if (name != null && name.trim().length() > 0) {
			sql += " and name=?";
			list.add(name);
		}
		if (category != null && category.trim().length() > 0) {
			sql += " and category=?";
			list.add(category);
		}
		if (minprice != null && maxprice != null
				&& minprice.trim().length() > 0 && maxprice.trim().length() > 0) {
			sql += " and price between ? and ?";
			list.add(minprice);
			list.add(maxprice);
		}

		Object[] params = list.toArray();

		return runner.query(sql, new BeanListHandler<Product>(Product.class),
				params);
	}

多条件查询主要是对SQL摆弄,判断各个参数传入的情况加入到数组集合,再转成String数组传入QueryRunner查询,用到了ArraytoArray()方法,该方法返回一个Object[]对象。常用的集合转数组的方法。

// 修改商品信息
	public void editProduct(Product p) throws SQLException {
		//1.创建集合并将商品信息添加到集合中
		List<Object> obj = new ArrayList<Object>();
		obj.add(p.getName());
		obj.add(p.getPrice());
		obj.add(p.getCategory());
		obj.add(p.getPnum());
		obj.add(p.getDescription());
		//2.创建sql语句,并拼接sql
		String sql  = "update products " +
				      "set  name=?,price=?,category=?,pnum=?,description=? ";
		//判断是否有图片
		if (p.getImgurl() != null && p.getImgurl().trim().length() > 0) {
			sql += " ,imgurl=?";
			obj.add(p.getImgurl());
		}
		sql += " where id=?";
		obj.add(p.getId());		
		System.out.println(sql);		
		System.out.println(obj);
		//3.创建QueryRunner对象
		QueryRunner runner = new QueryRunner(DataSourceUtils.getDataSource());
		//4.使用QueryRunner对象的update()方法更新数据
		runner.update(sql, obj.toArray());
	}

在这里插入图片描述
增加商品主要逻辑集中在文件上传上面,代码很繁杂,步骤很多,主要用了DiskFileItemFactoryServletFileUploadIOUtilsFileItemFileFileUploadUtils这几个没见过的新类实现的,前5个都是org.apache.commons下面的第三方jar包,这个过程相当复杂,FileUploadUtils是我们自己封装的一个工具类,作用是截取真实的文件名,我也是不明其意,暂且记录着,用到的时候说不定就慢慢懂了:

// 创建javaBean,将上传数据封装.
		Product p = new Product();
		Map<String, String> map = new HashMap<String, String>();
		// 封装商品id
		map.put("id", IdUtils.getUUID());
		DiskFileItemFactory dfif = new DiskFileItemFactory();
		// 设置临时文件存储位置
		dfif.setRepository(new File(this.getServletContext().getRealPath(
				"/temp")));
		// 设置上传文件缓存大小为10m
		dfif.setSizeThreshold(1024 * 1024 * 10);
		// 创建上传组件
		ServletFileUpload upload = new ServletFileUpload(dfif);
		// 处理上传文件中文乱码
		upload.setHeaderEncoding("utf-8");
		try {
			// 解析request得到所有的FileItem
			List<FileItem> items = upload.parseRequest(request);
			// 遍历所有FileItem
			for (FileItem item : items) {
				// 判断当前是否是上传组件
				if (item.isFormField()) {
					// 不是上传组件
					String fieldName = item.getFieldName(); // 获取组件名称
					String value = item.getString("utf-8"); // 解决乱码问题
					map.put(fieldName, value);
				} else {
					// 是上传组件
					// 得到上传文件真实名称
					String fileName = item.getName();
					fileName = FileUploadUtils.subFileName(fileName);
					// 得到随机名称
					String randomName = FileUploadUtils
							.generateRandonFileName(fileName);
					// 得到随机目录
					String randomDir = FileUploadUtils
							.generateRandomDir(randomName);
					// 图片存储父目录
					String imgurl_parent = "/productImg" + randomDir;

					File parentDir = new File(this.getServletContext()
							.getRealPath(imgurl_parent));
					// 验证目录是否存在,如果不存在,创建出来
					if (!parentDir.exists()) {
						parentDir.mkdirs();
					}
					String imgurl = imgurl_parent + "/" + randomName;
					map.put("imgurl", imgurl);
					IOUtils.copy(item.getInputStream(), new FileOutputStream(
							new File(parentDir, randomName)));
					item.delete();
				}
			}
		} catch (FileUploadException e) {
			e.printStackTrace();
		}
		try {
			// 将数据封装到javaBean中
			BeanUtils.populate(p, map);
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		}
		ProductService service = new ProductService();
		try {
			// 调用service完成添加商品操作
			service.addProduct(p);
			response.sendRedirect(request.getContextPath()
					+ "/listProduct");
			return;
		} catch (AddProductException e) {
			e.printStackTrace();
			response.getWriter().write("添加商品失败");
			return;
		}

FileUploadUtils


public class FileUploadUtils {
	/**
	 * 截取真实文件名
	 * 
	 * @param fileName
	 * @return
	 */
	public static String subFileName(String fileName) {
		// 查找最后一个 \出现位置
		int index = fileName.lastIndexOf("\\");
		if (index == -1) {
			return fileName;
		}
		return fileName.substring(index + 1);
	}

	// 获得随机UUID文件名
	public static String generateRandonFileName(String fileName) {
		// 获得扩展名
		int index = fileName.lastIndexOf(".");
		if (index != -1) {
			String ext = fileName.substring(index);
			return UUID.randomUUID().toString() + ext;
		}
		return UUID.randomUUID().toString();
	}

	// 获得hashcode生成二级目录
	public static String generateRandomDir(String uuidFileName) {
		int hashCode = uuidFileName.hashCode();
		// 一级目录
		int d1 = hashCode & 0xf;
		// 二级目录
		int d2 = (hashCode >> 4) & 0xf;
		return "/" + d1 + "/" + d2;
	}
}

销售榜单
该模块只有一个销售榜单的文件下载功能,主要实现在DownloadServlet中,文件下载后是csv后缀的excel文档,其实只是一个"商品名称"+"销售数量"文本文档,牵涉到文件下载就要用到IO流了,打开一个PrintWriter对象,println()函数写入,flush()强制输出close()关闭,有趣的是,这个流对象是直接由Respose给出,在拿到流对象之前,就已经在Respose设置好了相关ContentTypeHeader、文件名、文件编码CharacterEncoding参数。
在这里插入图片描述
在这里插入图片描述

String year = request.getParameter("year");
		String month = request.getParameter("month");
		ProductService service = new ProductService();
		List<Object[]> ps = service.download(year,month);
		//开始设置参数
		String fileName=year+"年"+month+"月销售榜单.csv";	
		response.setContentType(this.getServletContext().getMimeType(fileName));
		response.setHeader("Content-Disposition", "attachement;filename="+new String(fileName.getBytes("GBK"),"iso8859-1"));		
		response.setCharacterEncoding("gbk");		
		PrintWriter out = response.getWriter();
		out.println("商品名称,销售数量");
		for (int i = 0; i < ps.size(); i++) {
			Object[] arr=ps.get(i);
			out.println(arr[0]+","+arr[1]);			
		}
		out.flush();
		out.close();

ProductDao底层代码里:

// 销售榜单
	public List<Object[]> salesList(String year, String month)
			throws SQLException {
		String sql = "SELECT products.name,SUM(orderitem.buynum) totalsalnum FROM orders,products,orderItem WHERE orders.id=orderItem.order_id AND products.id=orderItem.product_id AND orders.paystate=1 and year(ordertime)=? and month(ordertime)=? GROUP BY products.name ORDER BY totalsalnum DESC";
		QueryRunner runner = new QueryRunner(DataSourceUtils.getDataSource());
		return runner.query(sql, new ArrayListHandler(), year, month);
	}

QueryRunner不仅可以以javaBean的数据形式返回查询结果,还可以以数组的形式返回数据,这样每一项各自有一段名称和销售总数的数据。

订单管理
该模块下有3个功能:查询所有订单(包括多条件查询)、查看订单详情、删除订单。
就循环输出所有订单和对应用户信息,用到了匿名内部类实现了ResultSetHandler<List<Order>>接口,重写了handle(ResultSet rs)方法,循环遍历结果返回List<Order>Dao这样的写法还用了很多,比如多条件查询。现在还不太了解,借下大佬总结,知道这么一个东西可以这样用:
【DButils学习之】利用ResultSetHandler各实现类来处理查询结果

public List<Order> findAllOrder() throws SQLException {
		//1.创建sql
		String sql = "select orders.*,user.* from orders,user where user.id=orders.user_id order by orders.user_id";
		//2.创建QueryRunner对象
		QueryRunner runner = new QueryRunner(DataSourceUtils.getDataSource());
        //3.返回QueryRunner对象query()方法的查询结果
		return runner.query(sql, new ResultSetHandler<List<Order>>() {			
			public List<Order> handle(ResultSet rs) throws SQLException {
				//创建订单集合
				List<Order> orders = new ArrayList<Order>();
                //循环遍历订单和用户信息
				while (rs.next()) {
					Order order = new Order();
					order.setId(rs.getString("orders.id"));
					order.setMoney(rs.getDouble("orders.money"));
					order.setOrdertime(rs.getDate("orders.ordertime"));
					order.setPaystate(rs.getInt("orders.paystate"));
					order.setReceiverAddress(rs.getString("orders.receiverAddress"));
					order.setReceiverName(rs.getString("orders.receiverName"));
					order.setReceiverPhone(rs.getString("orders.receiverPhone"));
					orders.add(order);

					User user = new User();
					user.setId(rs.getInt("user.id"));
					user.setEmail(rs.getString("user.email"));
					user.setGender(rs.getString("user.gender"));
					user.setActiveCode(rs.getString("user.activecode"));
					user.setIntroduce(rs.getString("user.introduce"));
					user.setPassword(rs.getString("user.password"));
					user.setRegistTime(rs.getDate("user.registtime"));
					user.setRole(rs.getString("user.role"));
					user.setState(rs.getInt("user.state"));
					user.setTelephone(rs.getString("user.telephone"));
					user.setUsername(rs.getString("user.username"));
					order.setUser(user);
				}
				return orders;
			}
		});

查看订单详情,具体在普通用户和管理员之间实现了一个转向,底层Dao的逻辑多了一个订单子项的Dao层运用,写法上大同小异没什么新的知识,研究的意义不大:

//1.获取用户类型
		String type=request.getParameter("type");		
		//2.得到要查询的订单的id
		String id = request.getParameter("id");
		//3.根据id查找订单
		OrderService service = new OrderService();
		Order order = service.findOrderById(id);
        //4.将查询出的订单信息添加到request作用域中
		request.setAttribute("order", order);
		//5.如果用户类型不为null,则请求转发到view.jsp页面,否则转发到orderInfo.jsp页面
		if(type!=null){
			request.getRequestDispatcher("/admin/orders/view.jsp").forward(request, response);
			return;
		}
		request.getRequestDispatcher("/client/orderInfo.jsp").forward(request, response);
	}

删除订单项时,一个订单Order对应一组订单子项OrderItem(以List形式存放),当删除Order时,OrderItem就没有对应的成员变量,就会出错,所以先删其子项再删订单本身,到最后终于利用上一开篇的DataSourceUtils定义的事务管理方法,作用是,为了安全着想,如果订单项或订单删除时任一出错的话,删除事务回滚,操作失败:
OrderService中:

//根据id删除订单 管理员删除订单
	public void delOrderById(String id) {			
		try {
			DataSourceUtils.startTransaction();//开启事务
			oidao.delOrderItems(id);
			odao.delOrderById(id);
		} catch (SQLException e) {
			e.printStackTrace();
			try {
				DataSourceUtils.rollback();
			} catch (SQLException e1) {
				e1.printStackTrace();
			}
		}finally{
			try {
				DataSourceUtils.releaseAndCloseConnection();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}		
	}

DataSourceUtils中:

/**
	 * 开启事务
	 * @throws SQLException
	 */
	public static void startTransaction() throws SQLException {
		Connection con = getConnection();
		if (con != null)
			con.setAutoCommit(false);
	}
	/**
	 * 从ThreadLocal中释放并且关闭Connection,并结束事务
	 * @throws SQLException
	 */
	public static void releaseAndCloseConnection() throws SQLException {
		Connection con = getConnection();
		if (con != null) {
			con.commit();
			tl.remove();
			con.close();
		}
	}
	/**
	 * 事务回滚
	 * @throws SQLException 
	 */
	public static void rollback() throws SQLException {
		Connection con = getConnection();
		if (con != null) {
			con.rollback();
		}
	}

公告管理
该模块有添加公告、查询所有通告、编辑信息、删除通告子功能。
调用了ListNoticeServlet,打开服务层获取所有通告,设置到Request参数中,jsp页面获取显示,之前用烂了的套路的套路,也没啥探讨的。

NoticeService nService = new NoticeService();
		List<Notice> notices = nService.getAllNotices();
		req.setAttribute("notices", notices);
		req.getRequestDispatcher("/admin/notices/list.jsp").forward(req, resp);

整理下,整个项目结构如下,jsp文件那块没有完善,主要集中在后台逻辑上,一清二楚,方便理解,后续放上原文件,感兴趣的可以下载:
在这里插入图片描述
完完整整的一大套业务,着实庞大,若不是阅读自己写恐怕没几个月是不可能完成的,写惯了Android小项目的我总算开了大眼界,jsp和Servlet技术这么原始纯粹的技术实现大项目起来费劲的地方还是有的,正式宣布我要开始入门web开发了,到时毕业设计不懂后端什么也做不成,这还是非常有必要的。还有寒假的两个项目开发任务可算完成了(郭霖书本的天气预报+传智书城),偷懒了这么久挺惭愧的,eclipse再见,再也不想用你了难受。

接下来专门学习后端技术,学会写写API,应用在App上面,武装自己吧!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章