權限控制
普通用戶只能訪問client
文件夾下面的jsp文件,對於沒有權限操作的admin
文件夾就會提示錯誤,而超級用戶同時可以訪問兩者,一直很好奇這個權限限制怎麼實現的。
原來在存在一個AdminPrivilegeFilter
類繼承自過濾器Fliter
,獲取Session
的role
屬性對此進行的管制:
// 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
查詢,用到了Array
的toArray()
方法,該方法返回一個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());
}
增加商品主要邏輯集中在文件上傳上面,代碼很繁雜,步驟很多,主要用了DiskFileItemFactory
、ServletFileUpload
、IOUtils
、FileItem
、File
、FileUploadUtils
這幾個沒見過的新類實現的,前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
設置好了相關ContentType
、Header
、文件名、文件編碼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上面,武裝自己吧!