# shop **Repository Path**: paphae/shop ## Basic Information - **Project Name**: shop - **Description**: test - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 3 - **Forks**: 0 - **Created**: 2020-05-18 - **Last Updated**: 2024-12-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README --- title: javaWeb项目-仿小米商城 date: 2020-05-15 22:49:09 tags: 项目 categories: javaWeb --- ## 使用说明 本项目导入idea后,需要配置maven,添加Web框架,修改db.properties数据库信息。 运行环境: idea2019.3 mysql8.0 tomcat9.0.24 谷歌浏览器 ## 需求分析 ![仿小米商城](javaWeb%E9%A1%B9%E7%9B%AE-%E4%BB%BF%E5%B0%8F%E7%B1%B3%E5%95%86%E5%9F%8E/%E4%BB%BF%E5%B0%8F%E7%B1%B3%E5%95%86%E5%9F%8E.png) ## 数据库设计 (1)用户表user的详细设计如表3-1所示: 表3-1 用户表 | No. | 字段名 | 数据类型 | Def. | Pk | Fk | 备注 | | ---- | ------ | -------- | ---- | ---- | ---- | -------- | | 1 | uid | Int | | Y | | 用户id | | 2 | uname | varchar | | | | 用户名 | | 3 | pwd | varchar | | | | 密码 | | 4 | name | varchar | | | | 真实姓名 | | 5 | email | varchar | | | | 邮箱 | | 6 | sex | varchar | | | | 性别 | | 7 | level | varchar | | | | 登录 | | 8 | uimage | varchar | | | | 头像 | | 9 | birth | date | | | | 生日 | (2)商品表product的详细设计如表3-2所示: 表3-2 商品表 | No. | 字段名 | 数据类型 | Def. | Pk | Fk | 备注 | | ---- | ------------ | -------- | ---- | ---- | ---- | ---------- | | 1 | pid | int | | Y | | 商品id | | 2 | pname | varchar | | | | 商品名称 | | 3 | market_price | double | | | | 原价 | | 4 | shop_price | double | | | | 销售价格 | | 5 | pnumb | int | | | | 库存 | | 6 | pimage | varchar | | | | 商品图片 | | 7 | pdate | date | | | | 上架日期 | | 8 | is_hot | int | | | | 是否是热卖 | | 9 | pdesc | varchar | | | | 商品描述 | | 10 | pflag | int | | | | 上架下架 | | 11 | cid | varchar | | | | 类型id | (3)类型表category的详细设计如表3-3所示: 表3-3 类型表 | No. | 字段名 | 数据类型 | Def. | Pk | Fk | 备注 | | ---- | ------ | -------- | ---- | ---- | ---- | ------------------------------- | | 1 | cid | varchar | | Y | | 类型id 和商品表cid 逻辑外键关系 | | 2 | cname | varchar | | | | 类型名称 | (4)地址表address的详细设计如表3-4所示: 表3-4 地址表 | No. | 字段名 | 数据类型 | Def. | Pk | Fk | 备注 | | ---- | ----------- | -------- | ---- | ---- | ---- | ------------------- | | 1 | aid | int | | Y | | 地址id | | 2 | callname | varchar | | | | 联系人姓名 | | 3 | callphone | varchar | | | | 联系人手机号 | | 4 | calladdress | varchar | | | | 联系人地址 | | 5 | uid | int | | | | 与用户表中的uid一致 | (5)订单表order的详细设计如表3-5所示: 表3-5 订单表 | No. | 字段名 | 数据类型 | Def. | Pk | Fk | 备注 | | ---- | ------ | -------- | ---- | ---- | ---- | ------------------- | | 1 | oid | varchar | | Y | | 订单id | | 2 | odate | datetime | | | | 提交时间 | | 3 | ostatu | varchar | | | | 订单状态 | | 4 | total | double | | | | 总价 | | 5 | uid | int | | | | 用户表uid为外键关系 | | 6 | aid | int | | | | 逻辑外键地址id | (6)订单条目表orderitem的详细设计如表3-6所示: 表3-6 订单条目表 | No. | 字段名 | 数据类型 | Def. | Pk | Fk | 备注 | | ---- | ------ | -------- | ---- | ---- | ---- | -------- | | 1 | tid | int | | Y | | 条目id | | 2 | pid | int | | | | 商品id | | 3 | numb | int | | | | 购买数量 | | 4 | sum | double | | | | 小计 | | 5 | oid | varchar | | | | 订单id | # 商品信息模块 ## 商品一级菜单 在加载页面时,通过ajax与数据库交互,获得商品类别信息,并使用拼接html的方式将含有商品类别信息的html代码植入到一级菜单类别的父div中。 ```javascript $(function () { $.post("${pageContext.request.contextPath}/selectAllCategory.do",function (data) { var str = ""; $("#nav-category").html(str); },"json"); }); ``` ## 商品二级菜单 1 #### **实现效果**:当鼠标悬浮在一级菜单上时,显示该分类的所有商品信息 #### 实现方式(两种): ##### 第一种:实时查询 当鼠标悬浮在一级菜单上的某个类别时,触发ajax的鼠标悬浮事件,完成与数据库的交互。代码如下: ```javascript $(function () { $(document).on("mouseover",".category-hover",function(e){ var cid = $(this).attr("name"); var $pop = $($(this).children("div").get(0)); var $div1 = $($pop.children("div").get(0)); var $div2 = $($pop.children("div").get(1)); $.post("${pageContext.request.contextPath}/selectProductByCid.do",{cid:cid},function (data) { console.log(data); var str1 = "",str2 = ""; for (var i = 0 ; i
\"\"
"+data[i].pname+"
选购
"; }else if (i<10&&i>=5){ str2+="\t
"; } } } $div1.html(str1); $div2.html(str2); },"json"); }); }); ``` 返回的商品信息通过html拼接的方式,植入到触发事件的节点中。 优点:页面加载速度快 缺点:每次触发事件就会进行一次数据库交互,数据库压力较大,加载二级菜单的商品信息时不流畅。 解决方式:使用缓存机质,第一次查询时与数据库交互获得数据,存入缓存中,之后的查询只需要从缓存中获取数据即可,降低了数据库的压力。 代码bug:当鼠标悬浮在一级菜单的分类上时,拼接的html代码存在于DOM中,一旦鼠标离开一级菜单时,拼接的html代码从DOM中脱离。绑定到二级菜单的事件或者a链接失效。 ##### 第二种:一次性查询(加载页面时) 在加载页面时通过ajax进行数据库交互,查出所有分类的商品信息,分别放在商品类别数量的div中,使用相对定位relative将所有类别的二级菜单放在同一个区域,设置display:none,隐藏所有二级菜单,当鼠标悬浮在一级菜单的某个分类上时,通过jquery的$("div").attr("dispaly","block")方法,显示该分类的二级菜单 优点:只需要进行一次数据库交互,数据库的压力比较小 缺点:页面加载较慢,代码冗余量较高 ## 商品详情页面 3 在商品列表页面中点击某个商品,会通过a链接跳转到查询商品信息的servlet,a标签的地址拼接了商品id,在servlet中通过与数据库交互,查询出改商品id的详细信息。将商品详情信息存到request中,并转发到商品详情页面。java代码如下: ```java String pid = request.getParameter("pid"); Product product = productService.findbyid(Integer.parseInt(pid)); Cookie[] cookies = request.getCookies(); boolean flag =false; if(cookies!=null&&cookies.length>0){ for (Cookie cookie : cookies) { String name = cookie.getName(); if("history".equals(name)){ flag = true; String value = cookie.getValue(); value = URLDecoder.decode(value, "utf-8"); List products = JSON.parseArray(value, Product.class); boolean f = true; for (Product product1 : products) { if(Objects.equals(product1.getPid(), product.getPid())){ f = false; break; } } if (f){ products.add(product); } String string = JSON.toJSONString(products); string= URLEncoder.encode(string,"utf-8"); if(string.getBytes().length>4000){ products.remove(0); string = JSON.toJSONString(products); string= URLEncoder.encode(string,"utf-8"); } cookie.setValue(string); cookie.setMaxAge(60 * 60 * 24 * 30); response.addCookie(cookie); break; } } } if(cookies==null||cookies.length==0|| !flag){ List pros = new ArrayList<>(); pros.add(product); String string = JSON.toJSONString(pros); string= URLEncoder.encode(string,"utf-8"); Cookie cookie = new Cookie("history",string); cookie.setMaxAge(60 * 60 * 24 * 30); response.addCookie(cookie); } request.setAttribute("product",product); request.getRequestDispatcher("/xiangqing.jsp").forward(request,response); ``` 2 使用JSTL和EL表达式在商品详情页面中显示商品信息,html代码如下: ```html
${product.pname}
变焦双摄,4 轴防抖 / 骁龙835 旗舰处理器,6GB 大内存,最大可选128GB 闪存 / 5.15" 护眼屏 / 四曲面玻璃/陶瓷机身
${product.shop_price}
选择版本
选择颜色
${product.pdesc}
``` 使用jquery添加点击事件,实现选择商品规格功能,js代码如下: ```javascript $(".selectstyle").click(function () { $("#select-info").html(""); $("#select-price").html(""); var $span1 = $($(this).children("span").get(0)); var $span2 = $($(this).children("span").get(1)); $("#select-info").html($span1.html()); $("#select-price").html($span2.html()); $("#sum-price").html("总计:"+$span2.html()); }); $(".selectcolor").click(function () { var $span = $($(this).children("span").get(1)); var content = $("#select-info").html(); $("#select-info").html(content+$span.html()); }); ``` ## 商品分页查询 引用了bootstrap中的css和js文件 html代码如下: ```html
小米手机
${pro.market_price}
``` 首先需要创建一个包含分页信息的工具类,此处命名为PageBean,其中需要包含当前页,每页商品数量,商品总数量,末尾页,包含了商品类别id的url地址,还有包含商品信息的List集合等成员; 后台servlet中需要获取两个属性,当前商品列表的类别id,和搜索的关键词。然后根据这两个属性进行拼接sql语句,将查询得到的结果封装到PageBean工具类对象中,将PageBean对象存到request中,通过request转发到商品列表页面中。 java代码如下: ```java String cid = request.getParameter("cid"); String key = request.getParameter("key"); //拼接查询条件 String trem = ""; if(!(cid==null||"".equals(cid))){ trem+="and cid = "+cid; } if(!(key==null||"".equals(key))){ trem+=" and pname like '%"+key+"%'"; } String nowpage = request.getParameter("nowpage"); if(nowpage==null||"".equals(nowpage)){ nowpage="1"; } Integer eachpage = 5; PageBean pb = productService.findpage(Integer.valueOf(nowpage), eachpage, trem); String uri = request.getRequestURI()+"?"; String queryString = request.getQueryString(); if(!(queryString==null||"".equals(queryString))){ uri+=queryString; } if(uri.indexOf("&nowpage")>-1){ uri=uri.substring(0,uri.indexOf("&nowpage")); } pb.setUrl(uri); Cookie[] cookies = request.getCookies(); List products = null; if(cookies!=null&&cookies.length>0){ for (Cookie cookie : cookies) { String name = cookie.getName(); if("history".equals(name)){ String value = cookie.getValue(); value = URLDecoder.decode(value, "utf-8"); products = JSON.parseArray(value, Product.class); } } } if (products!=null){ request.setAttribute("history",products); } request.setAttribute("cid",cid); request.setAttribute("pb",pb); System.out.println(pb.getLists().toString()); if (request.getServletPath().equals("/admin/findallproduct.do")){ request.getRequestDispatcher("/admin/selectallproduct.jsp").forward(request,response); }else if (request.getServletPath().equals("/findProByCategory.do")){ request.getRequestDispatcher("/liebiao.jsp").forward(request,response); }else if (request.getServletPath().equals("/findproduct.do")){ request.getRequestDispatcher("/liebiao.jsp").forward(request,response); } ``` ## 图片轮播功能 4 用js实现的,设置了一个index索引表示当前轮播图片的索引,和一个滑过清除定时器,即设置一个计时器,比如设置3秒后显示下一张轮播图片,那么就会通过index++操作来进行翻页,显示图片单独定义一个方法,根据当前索引index来显示对应的图片,通过修改css样式的display属性为block,其他的图片设置为none,达到显示图片的效果。 当鼠标滑过轮播图片的时候,就会清除这个计时器,这时图片是一直停止在当前图片上面的,当鼠标离开轮播图片时,重新计时。 可以手动点击上一张,下一张来实现翻页,实现原理是,通过修改index的值,然后进行图片的显示。 js代码如下: ```javascript //封装一个代替getElementById()的方法 function byId(id){ return typeof(id) === "string" ? document.getElementById(id) : id; } //全局变量 var index = 0; var timer = null; var pics = byId("banner").getElementsByTagName("div"); var dots = byId("dots").getElementsByTagName("span"); var prev = byId("prev"); var next = byId("next"); var len = pics.length; var menu = byId("menu-content"); var menuItems = menu.getElementsByClassName("menu-item"); var subMenu = byId("sub-menu"), innerBox = subMenu.getElementsByClassName("inner-box"); console.log(len); function slideImg(){ var main = byId("main"); //滑过清除定时器,离开继续 main.onmouseover = function(){ //滑过清楚定时器 if (timer) { clearInterval(timer); } } main.onmouseout = function(){ timer = setInterval(function(){ index++; if (index >= len) { index = 0; } //切换图片 console.log(index); changeImg(); },3000); } // 自动在main上触发鼠标离开的事件 main.onmouseout(); //问题:事件相当于一个函数,可以直接调用? // for(var d = 0 ; d < len ; d++){ //function中d为最终值,所以不能用d来表示index的值 //给所有span添加一个id的属性,值为d,作为当前span的索引 dots[d].id = d; dots[d].onclick = function(){ //改变index为当前span的索引 index = this.id; //调用changeImg,切换图片 changeImg(); } } //下一张 next.onclick = function(){ index++; if (index >= len) { index = 0; } changeImg(); } //上一张 prev.onclick = function(){ index--; if (index < 0) { index = len - 1; } changeImg(); } //导航菜单 //遍历主菜单,且绑定事件 for(var m = 0 ; m < menuItems.length ; m++){ //非HTML关键字,如id 自定义属性要使用 setAttribute和getAttribute; menuItems[m].setAttribute("data-index",m); menuItems[m].onmouseover = function(){ //鼠标未划过主菜单时,隐藏所有子菜单 for(var j = 0 ; j < innerBox.length ; j++){ innerBox[j].style.display = 'none'; menuItems[j].style.background = 'none'; } //为每一个menu-item定义data-index属性,作为索引 var idx = this.getAttribute("data-index"); subMenu.className = 'sub-menu'; innerBox[idx].style.display = 'block'; menuItems[idx].style.background = 'rgba(0,0,0,0.1)'; } } menu.onmouseout = function(){ subMenu.className = "sub-menu hide"; } subMenu.onmouseover = function(){ subMenu.className = "sub-menu"; } subMenu.onmouseout = function(){ subMenu.className = "sub-menu hide"; } } //切换图片 function changeImg(){ //遍历所有的图片并隐藏 for(var i = 0 ; i < len ; i++){ pics[i].style.display = "none"; dots[i].className = ""; } //显示当前index对应的图片 pics[index].style.display = "block"; dots[index].className = "active"; } slideImg(); ``` ## 验证码功能 java工具类代码如下: ```java public class CaptcahCode { /** * 验证生成的方法 * @param response * @return */ public static String drawImage(HttpServletResponse response){ //1:定义以字符串的拼接的StringBuilder StringBuilder builder = new StringBuilder(); //准备产生4个字符串的随机数 for(int i=0;i<4;i++){ builder.append(randomChar()); } String code = builder.toString(); //2:定义图片的宽度和高度 int width = 70; int height = 25; //简历bufferedImage对象,制定图片的长度和宽度以及色彩 BufferedImage bi = new BufferedImage(width,height,BufferedImage.TYPE_3BYTE_BGR); //3:获取到 Graphics2D 绘制对象,开始绘制验证码 Graphics2D g = bi.createGraphics(); //4:设置文字的字体和大小 Font font = new Font("微软雅黑",Font.PLAIN,20); //设置字体的颜色 Color color = new Color(0,0,0); //设置字体 g.setFont(font); //设置颜色 g.setColor(color); //设置背景 g.setBackground(new Color(226,226,240)); //开始绘制对象 g.clearRect(0,0,width,height); //绘制形状,获取矩形对象 FontRenderContext context = g.getFontRenderContext(); Rectangle2D bounds = font.getStringBounds(code,context); //计算文件的坐标和间距 double x = (width - bounds.getWidth())/2; double y = (height - bounds.getHeight())/2; double ascent = bounds.getY(); double baseY = y - ascent; g.drawString(code,(int)x,(int)baseY); //结束绘制 g.dispose(); try { ImageIO.write(bi,"jpg",response.getOutputStream()); //刷新响应流 response.flushBuffer(); }catch(Exception ex){ ex.printStackTrace(); } return code; } /** * 算术表达式验证码 * * 1:干扰线的产生 * 2: 范围随机颜色,随机数 * * @param response * @return */ public static String drawImageVerificate(HttpServletResponse response){ //定义验证码的宽度和高度 int width = 100,height = 30; //在内存中创建图片 BufferedImage image = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB); //创建图片的上下文 Graphics2D g = image.createGraphics(); //产生随机对象,此随机对象主要用于算术表达式的数字 Random random = new Random(); //设置背景 g.setColor(getRandomColor(240,250)); //设置字体 g.setFont(new Font("微软雅黑", Font.PLAIN,22)); //开始绘制 g.fillRect(0,0,width,height); //干扰线的绘制,绘制线条到图片中 g.setColor(getRandomColor(180,230)); for(int i=0;i<10;i++){ int x = random.nextInt(width); int y = random.nextInt(height); int x1 = random.nextInt(60); int y1 = random.nextInt(60); g.drawLine(x,y,x1,y1); } //开始进行对算术验证码表达式的拼接 int num1 = (int)(Math.random()*10 + 1); int num2 = (int)(Math.random()*10 + 1); int fuhao = random.nextInt(3);//产生一个[0,2]之间的随机整数 //记录符号 String fuhaostr = null; int result = 0; switch (fuhao){ case 0 : fuhaostr = "+";result = num1 + num2;break; case 1: fuhaostr = "-";result = num1 - num2;break; case 2 : fuhaostr = "*";result = num1 * num2;break; //case 3 : fuhaostr = "/";result = num1 / num2;break; } //拼接算术表达式,用户图片显示。 String calc = num1 + " " + fuhaostr +" "+ num2 +" = ?"; //设置随机颜色 g.setColor(new Color(20+random.nextInt(110),20+random.nextInt(110),20+random.nextInt(110))); //绘制表达式 g.drawString(calc,5,25); //结束绘制 g.dispose(); try { //输出图片到页面 ImageIO.write(image,"JPEG",response.getOutputStream()); return String.valueOf(result); }catch (Exception ex){ ex.printStackTrace(); return null; } } /** * 范围随机颜色 * @param fc * @param bc * @return */ public static Color getRandomColor(int fc,int bc){ //利用随机数 Random random = new Random(); //随机颜色,了解颜色-Color(red,green,blue).rgb三元色 0-255 if(fc>255)fc = 255; if(bc>255)bc = 255; int r = fc+random.nextInt(bc-fc); int g = fc+random.nextInt(bc-fc); int b = fc+random.nextInt(bc-fc); return new Color(r,g,b); } /** * 此方法用户产生随机数字母和数字 * @return */ private static char randomChar(){ //1:定义验证需要的字母和数字 String string = "QWERTYUIOPASDFGHJKLZXCVBNM0123456789"; //2:定义随机对象 Random random = new Random(); return string.charAt(random.nextInt(string.length())); } } ``` jsp页面代码如下: ```jsp <%@ page import="com.zhongruan.util.CaptcahCode" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <% //1:清空浏览器缓存,目的是为了清空浏览器的缓存,因为浏览器 //会对网站的资源文件和图像进行记忆存储,如果被浏览器加载过的图片就记忆起来,记忆以后 //文件就不会和服务器在交互,如果我们验证不清空的话可能会造成一个问题就是:验证刷新以后没有效果。 response.setHeader("pragma","no-cache"); response.setHeader("cache-control","no-cache"); response.setHeader("expires","0"); //2:调用编写的生成验证码的工具 String code = CaptcahCode.drawImage(response); session.setAttribute("code",code); //3:如何解决getOutputStream异常问题 out.clear(); out = pageContext.pushBody(); %> ``` 用法: ```html 看不清?点我 ``` 当点击看不清?点我时,会触发一个点击事件,转发到code.jsp中调用工具类形成验证码并将验证码的值存到session中,在登陆界面通过引用code.jsp显示验证码,用户输入验证码的值,在后台获取到之后通过与session中的值进行对比,来判断验证码是否正确。 ## 购物车模块和订单模块 ``` 实现功能: 1. 实现登陆后跳转会上一个浏览的页面 写一个filter过滤登录和注册servlet, 把请求头refer写到session,成功后重定向到refer 2. 实现浏览记录显示在商品列表的下方,用户查看商品详情会添加到浏览记录 在查询商品详情的方法中, 如果浏览记录不存在,添加cookie, 否则将其取出转为List集合,如果当前商品不在List集合中则添加。 3. 购物车 利用session实现购物车功能,新增购物车和购物车条目的实体类 购物车中包含购物车条目的Map集合和购物车总额 购物车条目包含商品,商品数量,小计 对购物车的操作只需维护购物车实体类在session中的更新 4. 购物车页面 全选,利用jQuery实现 选择结算,未选择不能结算,利用jQuery判断是否选中,未选中阻止表单提交 修改商品数量,给数量添加失去焦点事件,利用Ajax修改购物车商品数量 5. 结算页面 利用UUID生成订单号,将订单条目添加到数据库,将已选择的商品从购物车删除 选择地址,给地址添加点击事件,选中地址后边框变色,以区别未选中的地址 添加地址,点击添加地址弹出模态框,输入信息后点击添加,利用Ajax添加到数据库 成功后回显到结算页面,利用jQuery添加到第一个地址之前 修改地址,和添加地址公用一个模态框,先把初始值传入到模态框,在利用Ajax进行从数据库修改地址 6. 订单页面 按照时间顺序,将最近订单排序,通过sql语句order by odate DESC排序 分页,通过limit查询几条订单显示,翻页按钮使用bootstrap组件完成 7. 订单详情 根据订单状态,动态显示不同的订单详情页 订单的操作有取消订单(未发货状态),确认收货(已发货状态),去评价(待评价状态) ```