体育商城项目总结
[TOC]
一、项目概述
👉体育商城系统 https://git.yuencode.cn/jiaxiaoyu/sportshop
前台预览:http://sportshop.yuencode.cn/
账号:mike 123456
后台预览:http://sportshop-admin.yuencode.cn/
账号:admin 123456
1)技术架构
使用技术:
thymeleaf 3.0.11
:渲染html动态页面。
servlet
mysql 5.7.26
:数据库,存取数据。
使用框架:
系统架构:采用MVC设计模式
,MVC
模式代表 Model-View-Controller
(模型-视图-控制器) 模式。这种模式用于应用程序的分层开发。
Model(模型) - 模型代表一个存取数据的对象或 JAVA POJO。它也可以带有逻辑,在数据变化时更新控制器。
View(视图) - 视图代表模型包含的数据的可视化。
Controller(控制器) - 控制器作用于模型和视图上。它控制数据流向模型对象,并在数据变化时更新视图。它使视图与模型分离开。
2)系统用例图
3)系统功能模块
4)项目架构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <sportshop> ├<front> ---前台 │ ├<src> │ │ ├druid.properties ---数据库配置文件 │ │ ├<cn> │ │ │ ├<yuencode> │ │ │ │ ├<sportshop> │ │ │ │ │ ├<dao> ---数据库操作层 │ │ │ │ │ ├<dto> ---数据传输对象 │ │ │ │ │ ├<pojo> ---JavaBean实体对象 │ │ │ │ │ ├<service> ---服务层,为Servlet层提供服务 │ │ │ │ │ ├<servlet> ---Servlet层,与页面进行直接交互 │ │ │ │ │ ├<util> ---工具类 │ ├<web> │ │ ├<templates> ---Thymeleaf渲染的html模板 │ │ ├<upload> ---上传文件路径 │ │ ├<WEB-INF> │ │ │ ├web.xml │ │ │ ├<lib> ---依赖库 ├<manage> ---后台 │ ├<src> │ │ ├druid.properties ---数据库配置文件 │ │ ├<cn> │ │ │ ├<yuencode> │ │ │ │ ├<manage> │ │ │ │ │ ├<dao> ---数据库操作层 │ │ │ │ │ ├<dto> ---数据传输对象 │ │ │ │ │ ├<filter> ---过滤器,对请求进行拦截放行 │ │ │ │ │ ├<pojo> ---JavaBean实体对象 │ │ │ │ │ ├<service> ---服务层,为Servlet层提供服务 │ │ │ │ │ ├<servlet> ---Servlet层,与页面进行直接交互 │ │ │ │ │ ├<util> ---工具类 │ ├<web> │ │ ├<upload> ---上传文件路径 │ │ ├<WEB-INF> │ │ │ ├web.xml │ │ │ ├<lib> ---依赖库 │ │ │ ├<templates> ---Thymeleaf渲染的html模板
二、数据库设计
1、t_admin管理员用户表
字段名
类型
长度
是否为NULL
注释
userid
int
11
否
ID
username
varchar
50
是
用户名
userpwd
varchar
50
是
密码
2、t_catelog商品类别表
字段名
类型
长度
是否为NULL
注释
catelog_id
int
11
否
分类ID
catelog_name
varchar
50
是
类别名称
catelog_miaoshu
varchar
50
是
类别描述
3、t_goods商品表
字段名
类型
长度
是否为NULL
注释
goods_id
int
11
否
商品ID
goods_name
varchar
20
是
商品名称
goods_miaoshu
varchar
300
是
商品描述
goods_pic
varchar
50
是
商品图片
market_price
int
11
是
市面价格
mall_price
int
11
是
商城价格
catelog_id
int
11
是
分类编号
stock_num
int
11
是
库存量
goods_address
varchar
20
否
发货地
enter_date
date
是
上架时间
4、t_order订单表
字段名
类型
长度
是否为NULL
注释
order_id
varchar
50
否
订单编号
order_time
datetime
否
下单时间
order_zhuangtai
int
11
是
订单状态
order_jine
int
11
是
订单总金额
order_address
varchar
50
是
收货地址
order_pay
varchar
20
是
支付方式
order_userid
int
11
是
用户ID
5、t_orderitem订单商品项表
字段名
类型
长度
是否为NULL
注释
orderitem_id
int
11
否
订单项ID
order_id
varchar
50
否
订单编号
goods_id
int
11
否
商品ID
goods_num
int
11
是
商品数量
6、t_user会员用户表
字段名
类型
长度
是否为NULL
注释
user_id
int
11
否
用户ID
user_name
varchar
20
是
用户名
user_pwd
varchar
30
是
密码
user_realname
varchar
30
是
真实姓名
user_address
varchar
50
是
用户地址
user_sex
char
1
是
性别
user_tel
char
11
是
电话
user_email
varchar
30
是
邮箱
user_qq
varchar
20
是
QQ
三、技术要点
3.1 登录验证
使用到了session
与filter
。
原理:通过验证seesion
中是否存在用户信息来识别用户是否登录。
难点1:@WebFilter("/*")
通配符拦截所有请求,静态资源css、js、png等也会被拦截,需要对这些资源进行放行。
难点1解决方法:定义一个数组存放放行的请求,每次请求被过滤器拦截后,执行遍历数组操作,若包含放行请求则放行。
难点2:通过request.getRequestURL()
或request.getRequestURI()
获取的请求路径只是返回的数据格式不同,请都包含请求参数,而项目中不同页面的访问常常使用action
参数进行识别。
难点2解决方法:通过使用request.getQueryString()
方法获取到请求参数,并拼接成完整的路径。
@WebFilter("/*")
通配符拦截所有请求,静态资源css、js、png等也会被拦截。
过滤器完整代码:
LoginFilter.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package cn.yuencode.manage.filter;@WebFilter("/*") public class LoginFilter implements Filter { @Override public void init (FilterConfig filterConfig) throws ServletException { System.out.println("过滤器初始化" ); } @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; String requestURL = request.getQueryString() == null ? request.getRequestURL().toString() : (request.getRequestURL().toString() + "?" + request.getQueryString()); String[] ignoreUrl = {".js" , ".css" , ".png" , ".jpeg" , ".jpg" , ".ico" , ".otf" , "ttf" , "svg" , "index.do?action=toLogin" ,"authCode.action" ,"admin.do?action=login" }; boolean flag = true ; for (String s : ignoreUrl) { if (requestURL.contains(s)) { flag = false ; break ; } } if (flag) { HttpSession session = request.getSession(); Object admin = session.getAttribute("admin" ); if (null == admin) { response.sendRedirect(request.getContextPath() + "/index.do?action=toLogin" ); } else { filterChain.doFilter(request, response); } } else { filterChain.doFilter(request, response); } } @Override public void destroy () { } }
四、问题汇总
4.1 关于mysql中datetime类型字段的问题
在本地环境中,通过JdbcUtils
工具类返回的map
中order_time
字段类型为Timestrap
,如图4.1.1。
图4.1.1
通过查看源码ResultSetImpl.class
的getObject
实现方法,发现它是根据得到的数据库字段类型,进行格式转换后存在Map
中后再返回。
图4.1.2
图4.1.3
图4.1.4
将项目打包部署到线上后出现如下图4.1.5异常,说明从JdbcUtil
工具类中从数据库取出来的order_time
格式为LocalDateTime
。
图4.1.5
图4.1.7
一看mysql-connector-java-8.0.23.jar
的源码,果然它把数据库中的datetime
转换为了LocalDateTime
,如图4.1.8
图4.1.8
将本地环境中的mysql-connector-java-5.1.20.jar
只保留mysql-connector-java-8.0.23.jar
如预期一样,出现了格式转换异常的问题。
(未发现问题原因之前的解决方案)最终解决方案,如抛出类型转换异常,对异常进行处理将LocalDateTime
转换为Timestrap
类型。
想实现点击发货
按钮后,弹出对话框填写物流信息。如图4.2.1
图4.2.1
通过使用pintuer
框架中的对话框https://www.pintuer.com/documents/pintuer/1.x/javascript.html#dialog ,发现弹出对话框中的写的<input>
标签中value
值获取一直为空字符串,查看文档发现没有其它任何说明。
看了一下js源码,发现它是将页面中写的对话框布局加上一些其它的布局后,再读取配置的标签属性,再去动态生成这个对话框的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 $showdialogs=function (e ) { var trigger=e.attr("data-toggle" ); var getid=e.attr("data-target" ); var data=e.attr("data-url" ); var mask=e.attr("data-mask" ); var width=e.attr("data-width" ); var detail="" ; var masklayout=$('<div class="dialog-mask"></div>' ); if (width==null ){width="80%" ;} if (mask=="1" ){ $("body" ).append(masklayout); } detail='<div class="dialog-win" style="position:fixed;width:' +width+';z-index:11;">' ; if (getid!=null ){detail=detail+$(getid).html();} if (data!=null ){detail=detail+$.ajax({url :data,async :false }).responseText;} detail=detail+'</div>' ; var win=$(detail); win.find(".dialog" ).addClass("open" ); $("body" ).append(win); var x=parseInt ($(window ).width()-win.outerWidth())/2 ; var y=parseInt ($(window ).height()-win.outerHeight())/2 ; if (y<=10 ){y="10" } win.css({"left" :x,"top" :y}); win.find(".dialog-close,.close" ).each(function ( ) { $(this ).click(function ( ) { win.remove(); $('.dialog-mask' ).remove(); }); }); masklayout.click(function ( ) { win.remove(); $(this ).remove(); }); };
后来发现,提交对话框表单还需要订单id字段。通过使用框架自带的逻辑无法实现将对应行订单的id传到对话框中去,边重写了框架方法,自定义了显示对话框的函数,自定义函数中通过对对话框的显示与隐藏实现,而不是移除对话框元素与动态生成对话框,所以就不存在动态生成的input元素无法获取其value值的问题了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 function showdialog (orderId ) { let win = $(".dialog-win" ); win.find("input[name=order_id]" ).val(orderId) win.find(".dialog" ).addClass("open" ); var masklayout=$('<div class="dialog-mask"></div>' ); $("body" ).append(masklayout); var x=parseInt ($(window ).width()-win.outerWidth())/2 ; var y=parseInt ($(window ).height()-win.outerHeight())/2 ; if (y<=10 ){y="10" } win.css({"left" :x,"top" :y}); win.find(".dialog-close,.close" ).each(function ( ) { $(this ).click(function ( ) { win.find(".dialog" ).removeClass("open" ) $('.dialog-mask' ).remove(); }); }); masklayout.click(function ( ) { win.find(".dialog" ).removeClass("open" ) $(this ).remove(); }); } $('#submitForm' ).click(function ( ) { let win = $(".dialog-win" ); let logisticsSn = win.find("input[name=logisticsSn]" ).val() if (logisticsSn){ $(".dialog-win form" ).submit() }else { alert("请填写物流单号" ) } })
4.3 使用pintuer表单验证后,提交无反应
问题描述:在商品添加页面中引入<script src="js/pintuer.js"></script>
,使用了表单验证。添加必填验证的input框都为绿色验证通过,但是点击保存
按钮无任何反应。
问题原因:
在商品添加页面上为了回显错误提示消息,且让消息文本为红色,给提示的div
标签添加了check-error
属性,如下:
1 2 3 4 5 <div class ="form-group" > <div class ="check-error" > <div class ="tips" > [[${errMsg}]]</div > </div > </div >
通过查看pintuer.js
源码如图4.3.1,发现check-error
属性是用来进行表单验证不通过时的提示消息的class属性。而当提交表单时,会判断带check-error
属性的标签数量,如果数量为0,则进行提交。我上面自己写了一个带check-error
class属性的标签,自然就无法进行表单提交了。
图4.3.1
4.4 获取参数值时,只能获取到表单提交过来的第一个键值对。
检查发现表单中添加了enctype="multipart/form-data"
解决方式,要么去掉enctype="multipart/form-data"
或者在Servlet
中添加注解@MultipartConfig
五、项目收获
5.1 mysql数据库驱动中getObject方法的实现逻辑
通过获取数据库中的字段类型,根据类型转化成相应的类型后向上转型为Object
在返回,具体源码在mysql驱动中com.mysql.jdbc.ResultSetImpl
的实现类中。
5.2 响应文件提供给浏览器下载
注意要配置响应头Content-Disposition
Servlet方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 protected void downloadPic (HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { String picUrl = ParameterUtils.getStringParameter(request, "url" , "" ); String url = request.getServletContext().getRealPath(picUrl); File file = new File(url); ServletOutputStream outputStream = response.getOutputStream(); response.setHeader("Content-Disposition" , "attachment;fileName=" + URLEncoder.encode(file.getName(), "UTF-8" )); InputStream in = new FileInputStream(file); byte [] buf = new byte [1024 * 8 ]; int len = 0 ; while ((len = in.read(buf)) != -1 ) { outputStream.write(buf, 0 , len); } outputStream.close(); in.close(); }
5.3 购物车数据存储
定义购物车的类Cart
,包含cartItemMap
的LinkedHashMap
的变量以键值对的形式存储购物项CartItem
,在Cart
类中定义添加商品、移除商品、清空购物车、获取总金额、获取商品列表等方法。
在CartItem
购物项中,包含商品信息Goods goods
、商品数量Integer quantity
、提供了其set、get
方法,同时定义了获取商品总金额(商品数量*商品价格)的方法getTotalPrice()
。
Cart.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 public class Cart { private final Map<Integer, CartItem> cartItemMap = new LinkedHashMap<>(); public void addItem (Goods goods, Integer quantity) { Integer goods_id = goods.getGoods_id(); CartItem cartItem = cartItemMap.get(goods_id); if (cartItem == null ) { cartItem = new CartItem(); cartItem.setGoods(goods); cartItem.setQuantity(quantity); cartItemMap.put(goods_id, cartItem); } else { cartItem.setQuantity(cartItem.getQuantity() + quantity); } } public List<CartItem> getCartItems () { Collection<CartItem> values = cartItemMap.values(); return new ArrayList<>(values); } public Integer getTotalPrice () { Integer totalPrice = 0 ; Collection<CartItem> cartItems = cartItemMap.values(); for (CartItem item : cartItems) { totalPrice += item.getPrice(); } return totalPrice; } public boolean changeQuantity (Integer goodsId, Integer quantity) { CartItem item = cartItemMap.get(goodsId); item.setQuantity(quantity); return true ; } public boolean clearCart () { cartItemMap.clear(); return true ; } public boolean removeCartItem (Integer goodsId) { cartItemMap.remove(goodsId); return true ; } }
CartItem.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class CartItem { private Goods goods; private Integer quantity; private Integer price; public Goods getGoods () { return goods; } public void setGoods (Goods goods) { this .goods = goods; } public Integer getQuantity () { return quantity; } public void setQuantity (Integer quantity) { this .quantity = quantity; } public Integer getPrice () { return goods.getMall_price() * this .quantity; } }
5.4 多个数据库操作启动事务进行处理
在订购商品生成订单的业务功能中,需要对三个表t_order
、t_orderitem
、t_goods
,分别新增一条订单记录、将所有的订单项插入、更新商品的库存量。使用事务将三个操作合并成一个操作,一旦某个操作出现异常,则其他操作也同时不会生效,保证了操作的原子性、数据的完整性。
需要注意的是:
关闭自动提交
手动获取数据库连接,进行相关操作
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 @Override public Integer insertOrder (Order order, List<Orderitem> orderitems) { Connection conn = null ; PreparedStatement pstmt = null ; try { conn = JdbcUtil.getConnection(); conn.setAutoCommit(false ); String sql1 = "INSERT INTO t_order(order_id,order_time,order_zhuangtai,order_jine,order_address,order_pay," + "order_userid) VALUES(?,?,?,?,?,?,?)" ; pstmt = conn.prepareStatement(sql1); pstmt.setString(1 , order.getOrder_id()); pstmt.setTimestamp(2 , order.getOrder_time()); pstmt.setInt(3 , order.getOrder_zhuangtai()); pstmt.setInt(4 , order.getOrder_jine()); pstmt.setString(5 , order.getOrder_address()); pstmt.setString(6 , order.getOrder_pay()); pstmt.setInt(7 , order.getUser().getUser_id()); pstmt.executeUpdate(); pstmt.close(); String sql2 = "INSERT INTO t_orderitem(order_id,goods_id,goods_num) " + "VALUES(?,?,?)" ; pstmt = conn.prepareStatement(sql2); for (Orderitem item : orderitems) { pstmt.setString(1 , order.getOrder_id()); pstmt.setInt(2 , item.getGoods().getGoods_id()); pstmt.setInt(3 , item.getGoods_num()); pstmt.addBatch(); } pstmt.executeBatch(); pstmt.close(); String sql3 = "UPDATE t_goods SET stock_num=stock_num-? WHERE goods_id=?" ; pstmt = conn.prepareStatement(sql3); for (Orderitem item : orderitems) { pstmt.setInt(1 , item.getGoods_num()); pstmt.setInt(2 , item.getGoods().getGoods_id()); pstmt.addBatch(); } pstmt.executeBatch(); pstmt.close(); conn.commit(); return 1 ; } catch (SQLException e) { e.printStackTrace(); try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } return -1 ; } finally { try { pstmt.close(); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }
5.5 数据库使用批处理执行sql
使用pstmt.addBatch()
添加到批处理,当所有的语句都准备好了后,使用pstmt.executeBatch()
执行批处理,最后关闭pstmt
。
1 2 3 4 5 6 7 8 9 10 11 String sql3 = "UPDATE t_goods SET stock_num=stock_num-? WHERE goods_id=?" ; pstmt = conn.prepareStatement(sql3); for (Orderitem item : orderitems) { pstmt.setInt(1 , item.getGoods_num()); pstmt.setInt(2 , item.getGoods().getGoods_id()); pstmt.addBatch(); } pstmt.executeBatch(); pstmt.close();
5.6 状态值格式化的两种方法
后端处理,在实体类中增加格式化状态值的方法,例如,订单的实体类中添加方法getOrderStatus()
,方法中对private int order_zhuangtai
状态的变量进行格式化后返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Order { private String order_id; private Timestamp order_time; private int order_zhuangtai; private int order_jine; private String order_address; private String order_pay; private User user; public String getOrderStatus () { switch (this .order_zhuangtai) { case 1 : return "待发货" ; case 2 : return "已发货" ; case 3 : return "已完成" ; default : return "未知状态" + this .order_zhuangtai; } } }
在页面中,直接调用getOrderStatus
方法获取状态。
前端处理,在页面中通过js、模板引擎等对状态值进行判断输出。
1 2 3 4 5 6 <div th:switch ="${item.order_zhuangtai}" > <td th:case ="1" > <span class ="tag" > 待发货</span > </td > <td th:case ="2" > <span class ="tag bg-blue" > 已发货</span > </td > <td th:case ="3" > <span class ="tag bg-green" > 已完成</span > </td > <td th:case ="*" > <span class ="tag bg-red" > 未知状态[[${item.order_zhuangtai}]]</span > </td > </div >
5.7 Servlet上传文件
注意要在form
标签添加enctype
属性<form enctype="multipart/form-data"></form>
与添加注解@MultipartConfig
若未添加属性enctype
,则会抛出如图异常。
若添加了enctype
属性而未添加注解@MultipartConfig
,则request.getParameter()
获取不到值(从第二个开始)。
以下为实现文件上传的主要代码,其他逻辑代码被省略。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @MultipartConfig @WebServlet("/goods.do") public class GoodsServlet extends ViewBaseServlet { protected void add (HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { Goods goods = new Goods(); Part goods_pic = request.getPart("goods_pic" ); if (goods_pic == null || goods_pic.getSubmittedFileName().isEmpty()) { request.setAttribute("errMsg" , "请添加商品图片" ); super .processTemplate("goods/add" , request, response); return ; } String fileName = UUID.randomUUID().toString().replace("-" , "" ); String fileType = goods_pic.getSubmittedFileName().substring(goods_pic.getSubmittedFileName().lastIndexOf("." )); goods_pic.write(request.getServletContext().getRealPath("upload" ) + File.separator + fileName + fileType); goods.setGoods_pic("upload/" + fileName + fileType); goodsService.save(goods); } }
5.8 replace与replaceAll的区别
replace(char oldChar, char newChar)
:寓意为:返回一个新的字符串,它是通过用 newChar
替换此字符串中出现的所有oldChar
得到的。
replace(CharSequence target, CharSequence replacement)
:寓意为:使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。
replaceAll(String regex, String replacement)
:寓意为:使用给定的replacement
替换此字符串所有匹配给定的正则表达式的子字符串。
replaceFirst(String regex, String replacement)
:使用给定的 replacement
替换此字符串匹配给定的正则表达式的第一个子字符串。
1 2 3 4 5 6 7 8 9 10 public class SportshopTest { public static void main (String[] args) { String s = UUID.randomUUID().toString(); System.out.println(s); System.out.println(s.replace('-' ,'#' )); System.out.println(s.replace("-" , "" )); System.out.println(s.replaceAll("-" , "" )); System.out.println(s.replaceFirst("-" ,"" )); } }
运行结果:
注意: replace与replaceAll都是替换所有的匹配值,replaceFirst才是仅替换第一个匹配值。只是replace的参数是char与CharSequence,而replaceAll参数为regex(正则表达式)与replacement。
六、项目运行效果
1、前台
2、后台
完成日期:2022年6月22日 jiaxiaoyu