项目简介
**项目地址:**https://github.com/newbee-ltd/newbee-mall
**项目介绍:**newbee-mall 项目是一套电商系统,包括 newbee-mall 商城系统及 newbee-mall-admin 商城后台管理系统,基于 Spring Boot 2.X 及相关技术栈开发。 前台商城系统包含首页门户、商品分类、新品上线、首页轮播、商品推荐、商品搜索、商品展示、购物车、订单结算、订单流程、个人订单管理、会员中心、帮助中心等模块。后台管理系统包含数据面板、轮播图管理、商品管理、订单管理、会员管理、分类管理、设置等模块。
经笔者验证,是一个值得一看的Spring项目!
项目运行
想要运行该项目需要进行相应的配置:
- 安装Mysql
如果是Mac电脑可以参考:Mac电脑安装mysql。
安装好Mysql后新建一个数据库,并运行项目文件夹下的newbee-mall-schema.sql文件,将数据插入数据库,可参考:如何执行sql文件插入数据库。
- 配置Mysql
修改application.properties文件中的mysql数据库名为自己的数据库名:
spring.datasource.name=newbee(自己的数据库名) spring.datasource.username=root spring.datasource.password=12345678
- 解压img,修改路径
将项目路径下的upload.zip解压,并记录下路径;
在common/constansts中修改路径:
//public final static String FILE_UPLOAD_DIC = "/opt/image/upload/";//上传文件的默认url前缀,根据部署设置自行修改 public final static String FILE_UPLOAD_DIC = "/Users/xiuyi/Desktop/javaer/newbee-mall/src/main/resources/upload/";
- 运行、访问
运行成功之后,打开网页,访问:localhost:28089即可。
项目中还出现问题可以参考:如何运行new-bee。
项目框架
项目整体框架如下:
其中,
- common:定义一些公用字段名;
- util:提供一些静态帮助方法。如邮箱格式验证、MD5码生成等;
- config:WebMvcConfigure的配置,主要设置拦截器以及资源文件;
- dao:跟数据层的交互接口,获取货品信息、用户信息、订单信息等;项目使用的数据库是Mybatis,通过XML配置dao接口跟数据库的映射关系;
- Interceptor:拦截器的具体实现,完成身份验证、购物车数量实时更新;
- controller:MVC框架中的控制器,根据web端的请求调用相应的service层进行逻辑处理;
- entity:项目中的常用字段类;
- service:服务层,由控制器进行调用;
功能细分
这个部分对项目的各个功能进行进一步的分析,捋清楚调用关系并补充一些细节。
其实这是一个典型的MVC项目,即Model-View-Controller,根据对应的客户端请求,由Spring MVC调度相应的Controller进行处理,Controller调用service进行业务逻辑处理,而后交给Thymeleaf模板引擎对结果Entity进行渲染,将处理结果返回给用户,从而显示在浏览器上,整个流程如下:
商城首页
首页,即访问:网页地址/index(或/,或/),获取新蜂商城首页。
在IndexController中,当接收到来自客户端“/”的请求时,访问数据库去获取分类数据、轮播图、新品、推荐数据,经由模板引擎(用的是thymeleaf)生成html返回给客户端:
@GetMapping({"/index", "/", "/index.html"}) public String indexPage(HttpServletRequest request) { List<NewBeeMallIndexCategoryVO> categories = newBeeMallCategoryService.getCategoriesForIndex(); if (CollectionUtils.isEmpty(categories)) { return "error/error_5xx"; } List<NewBeeMallIndexCarouselVO> carousels = newBeeMallCarouselService.getCarouselsForIndex(Constants.INDEX_CAROUSEL_NUMBER); List<NewBeeMallIndexConfigGoodsVO> hotGoodses = newBeeMallIndexConfigService.getConfigGoodsesForIndex(IndexConfigTypeEnum.INDEX_GOODS_HOT.getType(), Constants.INDEX_GOODS_HOT_NUMBER); List<NewBeeMallIndexConfigGoodsVO> newGoodses = newBeeMallIndexConfigService.getConfigGoodsesForIndex(IndexConfigTypeEnum.INDEX_GOODS_NEW.getType(), Constants.INDEX_GOODS_NEW_NUMBER); List<NewBeeMallIndexConfigGoodsVO> recommendGoodses = newBeeMallIndexConfigService.getConfigGoodsesForIndex(IndexConfigTypeEnum.INDEX_GOODS_RECOMMOND.getType(), Constants.INDEX_GOODS_RECOMMOND_NUMBER); request.setAttribute("categories", categories);//分类数据 request.setAttribute("carousels", carousels);//轮播图 request.setAttribute("hotGoodses", hotGoodses);//热销商品 request.setAttribute("newGoodses", newGoodses);//新品 request.setAttribute("recommendGoodses", recommendGoodses);//推荐商品 return "mall/index"; }
注意到上述Controller的返回值是“mail/index”注意到上述Controller的返回值是“mail/index”——这是因为Spring Boot集成的Thymeleaf默认配置会自动渲染**classpath:templates/网页名。**Thymeleaf的详细教程可参考Thymeleaf详解。
因此在使用Thymeleaf模板引擎时,编写Controller只需要返回指定的网页名即可,而不用想WebMVC一样,通过ModelView来进行渲染:
@RequestMapping(value = "/greeting") public ModelAndView test(ModelAndView mv) { mv.setViewName("/greeting"); mv.addObject("title","欢迎使用Thymeleaf!"); return mv; }
用户注册、登录
点击注册按钮,即跳转至用户登录界面:
这两个按钮对应的路径为/rigister和/login,处理请求的是mall/PersonalController,如果是获取登录、注册页面,则调用相应的模板引擎进行渲染:
@GetMapping({"/login", "login.html"}) public String loginPage() { return "mall/login"; } @GetMapping({"/register", "register.html"}) public String registerPage() { return "mall/register"; }
如果是获取登录、注册的表单,则应该调用相应的post方法,以登录为例:
@PostMapping("/login") @ResponseBody public Result login(@RequestParam("loginName") String loginName, @RequestParam("verifyCode") String verifyCode, @RequestParam("password") String password, HttpSession httpSession) { if (StringUtils.isEmpty(loginName)) { return ResultGenerator.genFailResult(ServiceResultEnum.LOGIN_NAME_NULL.getResult()); } if (StringUtils.isEmpty(password)) { return ResultGenerator.genFailResult(ServiceResultEnum.LOGIN_PASSWORD_NULL.getResult()); } if (StringUtils.isEmpty(verifyCode)) { return ResultGenerator.genFailResult(ServiceResultEnum.LOGIN_VERIFY_CODE_NULL.getResult()); } String kaptchaCode = httpSession.getAttribute(Constants.MALL_VERIFY_CODE_KEY) + ""; if (StringUtils.isEmpty(kaptchaCode) || !verifyCode.toLowerCase().equals(kaptchaCode)) { return ResultGenerator.genFailResult(ServiceResultEnum.LOGIN_VERIFY_CODE_ERROR.getResult()); } String loginResult = newBeeMallUserService.login(loginName, MD5Util.MD5Encode(password, "UTF-8"), httpSession); //登录成功 if (ServiceResultEnum.SUCCESS.getResult().equals(loginResult)) { //删除session中的verifyCode httpSession.removeAttribute(Constants.MALL_VERIFY_CODE_KEY); return ResultGenerator.genSuccessResult(); } //登录失败 return ResultGenerator.genFailResult(loginResult); }
值得注意的是,在处理登录时,会先对用户名和密码进行基本的格式验证,而后交由数据库进行验证。
商品详情页
以HUAWEI mate30 Pro为例:
其控制器为mail/GoodsController,通过访问数据库获取商品数据,放进模板引擎进行渲染:
@GetMapping("/goods/detail/{goodsId}") public String detailPage(@PathVariable("goodsId") Long goodsId, HttpServletRequest request) { if (goodsId < 1) { return "error/error_5xx"; } NewBeeMallGoods goods = newBeeMallGoodsService.getNewBeeMallGoodsById(goodsId); if (goods == null) { NewBeeMallException.fail(ServiceResultEnum.GOODS_NOT_EXIST.getResult()); } if (Constants.SELL_STATUS_UP != goods.getGoodsSellStatus()) { NewBeeMallException.fail(ServiceResultEnum.GOODS_PUT_DOWN.getResult()); } NewBeeMallGoodsDetailVO goodsDetailVO = new NewBeeMallGoodsDetailVO(); BeanUtil.copyProperties(goods, goodsDetailVO); goodsDetailVO.setGoodsCarouselList(goods.getGoodsCarousel().split(",")); request.setAttribute("goodsDetail", goodsDetailVO); return "mall/detail"; }
商品搜索
商品搜索时,其控制器也是mail/GoodsController,通过关键词去数据库中搜索属性对应的商品
并显示。这里还有分页设计,限制每一页的显示货品个数,同时还有排序属性:
另外,可以看出这里的搜索栏目与首页的分类栏目所用的接口是一样,只不过搜索时是借助keyword,而分类使用的是category。
@GetMapping({"/search", "/search.html"}) public String searchPage(@RequestParam Map<String, Object> params, HttpServletRequest request) { if (StringUtils.isEmpty(params.get("page"))) { params.put("page", 1); } params.put("limit", Constants.GOODS_SEARCH_PAGE_LIMIT); //封装分类数据 if (params.containsKey("goodsCategoryId") && !StringUtils.isEmpty(params.get("goodsCategoryId") + "")) { Long categoryId = Long.valueOf(params.get("goodsCategoryId") + ""); SearchPageCategoryVO searchPageCategoryVO = newBeeMallCategoryService.getCategoriesForSearch(categoryId); if (searchPageCategoryVO != null) { request.setAttribute("goodsCategoryId", categoryId); request.setAttribute("searchPageCategoryVO", searchPageCategoryVO); } } //封装参数供前端回显 if (params.containsKey("orderBy") && !StringUtils.isEmpty(params.get("orderBy") + "")) { request.setAttribute("orderBy", params.get("orderBy") + ""); } String keyword = ""; //对keyword做过滤 去掉空格 if (params.containsKey("keyword") && !StringUtils.isEmpty((params.get("keyword") + "").trim())) { keyword = params.get("keyword") + ""; } request.setAttribute("keyword", keyword); params.put("keyword", keyword); //搜索上架状态下的商品 params.put("goodsSellStatus", Constants.SELL_STATUS_UP); //封装商品数据 PageQueryUtil pageUtil = new PageQueryUtil(params); request.setAttribute("pageResult", newBeeMallGoodsService.searchNewBeeMallGoods(pageUtil)); return "mall/search"; }
其实这几个环节中商品搜索部分是我最感兴趣的:它是怎么实现搜索的呢?
笔者测试了一下搜索系统,结果比较准确,难不成还集成了搜索引擎?好奇心驱使之下遂继续往下面挖,直到:
<select id="findNewBeeMallGoodsListBySearch" parameterType="Map" resultMap="BaseResultMap"> select <include refid="Base_Column_List"/> from tb_newbee_mall_goods_info <where> <if test="keyword!=null and keyword!=''"> and (goods_name like CONCAT('%',#{keyword},'%') or goods_intro like CONCAT('%',#{keyword},'%')) </if> <if test="goodsCategoryId!=null and goodsCategoryId!=''"> and goods_category_id = #{goodsCategoryId} </if> <if test="goodsSellStatus!=null"> and goods_sell_status = #{goodsSellStatus} </if> </where> <if test="orderBy!=null and orderBy!=''"> <choose> <when test="orderBy == 'new'"> <!-- 按照发布时间倒序排列 --> order by goods_id desc </when> <when test="orderBy == 'price'"> <!-- 按照售价从小到大排列 --> order by selling_price asc </when> <otherwise> <!-- 默认按照库存数量从大到小排列 --> order by stock_num desc </otherwise> </choose> </if> <if test="start!=null and limit!=null"> limit #{start},#{limit} </if> </select>
从上述的Mybatis的动态Sql语句可以看出,整个商品搜索系统其实都是由底层的Mysql数据库提供的,而所谓的商品搜索也只是通过Like加%通配符的方式与商品名和商品简介进行匹配而已。
而这种搜索方式遇到模糊名词或者错误名词就难过了,什么都搜不出来,而且以通配符的方式进行搜索当数据量大的场景下,效率极其低。在真实的电商场景中,商品搜索是一个极其重要的组件,一般是基于倒排索引数据库(ES、OpenSearch等)的搜索引擎来提供商品搜索服务。
订单系统
购物车相关的逻辑控制都在mall/shoppingcartController中,暂不赘述:
顺带一提,支付的时候还会进行地址的验证,很棒!
PS:在一个健壮的系统中,在合适的地方进行数据验证能够有效地防止错误。
后台管理系统
后台管理系统可以看让商家对自己商品进行管理(用户名:admin,密码:123456),诸如增加商品、设置轮播图、修改排序等等:
后台管理系统可以进行商品管理、订单管理、会员管理等等,如果说之前的系统是对数据库的查询动手的话,那么这个环节主要是实现对数据库的修改。(现在的开发只要会CRUD就不怕没饭吃啊)
这个后台管理系统简直神了!
可以说前端、数据库、动态网页等技术全都用上了,很有必要仔细的学习!
总结
这个开源项目的内容包含了太多可以学习的知识:
- Spring Boot的配置
- Spring MVC的使用
- Thymeleaf模板引擎
- Mybatis的使用
- Mysql的配置
- Interceptot拦截器
- 身份验证
- kaptcha验证码生成
- AdminLTE仪表盘****等知识
可以说要完全吃透这一个项目需要的准备知识很多,但是收获也多!
这个开源项目确实很不错,谢谢十三老师的分享!