【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上面,武裝自己吧!

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