权限控制
普通用户只能访问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上面,武装自己吧!