Fork me on GitHub

tmall_ssm项目结构

项目结构

项目名称

tmall_ssm

java源代码包结构

pojo 实体类
mapper Mapper类
interceptor 拦截器
controller 控制层
service Service层
test 测试类
util 工具类
comparator 比较类

web目录

css css文件
img 图片资源
js js文件
admin 后台管理用到的jsp文件
fore 前台展示用到的jsp文件
include 被包含的jsp文件

典型场景

购物车

立即购买

购买流程

点击购买,会在OrderItem表中插入一条数据,这条数据会表示:
1、pid 购买的商品id
2、oid 这个订单项还没有生成对应的订单,即还在购物车中
3、uid 用户uid
4、number 购买了几件产品

如果未登录,点击购买之前会弹出模态登录窗口。
登录之后,点击立即购买,会访问tmall_ssm/forebuyone?pid=&num=,并带上了产品id和购买数量。

OrderItemService新增方法listByUser
OrderItemServiceImpl新增方法listByUser的实现。
通过上个步骤访问地址forebuyone,调用ForeController.buyone()
获取参数pid、num,根据pid获取产品对象p,从session中获取用户对象user。
接下来就是新增订单项OrderItem,新增订单项要考虑两个情况。
1、如果已经存在这个产品对应的OrderItem,并且还没有生成订单,即还在购物车中。那么就应该在对应的OrderItem基础上,调整数量。

1) 基于用户对象user,查询没有生成订单的订单项集合。
2) 遍历这个集合。
3) 如果产品是一样的话,就进行数量追加
4)获取这个订单项的id

2、如果不存在对应的OrderItem,那么就新增一个订单项OrderItem

1) 生成新的订单项
2)设置数量,用户和产品
3)插入到数据库
4)获取这个订单项的id

最后,基于这个订单项id,客户端跳转到结算页面/forebuy

模态登录窗口

立即购买和加入购物车这两个按钮的监听是放在imgAngInfo.jsp页面中。
这两个按钮都会通过JQuery的.get方法,用异步Ajax的方式访问forecheckLogin,获取当前是否登录状态,如果返回的不是success即表明是未登录状态,那么就会打开登录的模态窗口: $("#loginModal").modal('show');

在上一步的Ajax访问路径/forecheckLogin会导致ForeController.checkLogin()方法被调用。获取session中的user对象,如果不为空,即表示已经登录,返回字符串success;如果为空,即表示未登录,返回字符串fail

modal.jsp页面被footer.jsp所包含,所以每个页面都是加载了的。通过imgAndInfo.jsp页面中的购买按钮或者加入购物车按钮显示出来。点击登录按钮时,使用imgAndInfo.jsp页面中的Ajax代码进行登录验证。

在上一步modal.jsp中,点击了登录按钮之后,访问路径/foreloginAjax,导致ForeController.loginAjax()方法被调用。获取账号密码,通过账号密码获取User对象,如果User对象为空,那么就返回fail字符串,如果User对象不为空,那么就把User对象放在session中,并返回success字符串。

结算页面

结算流程

在购买页面中,客户端跳转到路径/forebuy?oiid=,导致ForeController.buy方法被调用。
1、通过字符串数组获取参数oiid。
为社么为什么这里要用字符串数组试图获取多个oiid,而不是int类型仅仅获取一个oiid? 因为根据购物流程环节与表关系,结算页面还需要显示在购物车中选中的多条OrderItem数据,所以为了兼容从购物车页面跳转过来的需求,要用字符串数组获取多个oiid。
2、准备一个泛型是OrderItem的集合ois
3、根据前面步骤获取的oiids,从数据库中取出OrderItem对象,并放入ois集合中
4、累计这些ois的价格总数,赋值在total上
5、把订单项集合放在session的属性 “ois” 上
6、把总价格放在 model的属性 “total” 上
7、服务端跳转到buy.jsp
1

buyPage.jsp中,遍历出订单项集合ois中的订单项数据,显示总金额。

加入购物车

加入购物车流程

在产品页面点击加入购物车时,在OrderItem表中插入一条数据,把界面上的加入购物车按钮变成灰色并且不可点击。

如果未登录,那么点击加入购物车会弹出模态登陆窗口,登录之后,点击加入购物车,会使用Ajax一步访问地址/foreaddCart?pid=&num=,并带上了产品id和购买数量num。

上一步访问地址/foreaddCart调用ForeController.addCart()addCart()方法和立即购买中的ForeController.buyone()步骤做的事情是一样的,区别在于返回不一样。
1、获取参数pid,num。
2、根据pid获取产品对象p
3、从session中获取用户对象user

接下来就是新增订单项OrderItem, 新增订单项要考虑两个情况:
1、如果已经存在这个产品对应的OrderItem,并且还没有生成订单,即还在购物车中。那么就应该在对应的OrderItem基础上,调整数量。

1) 基于用户对象user,查询没有生成订单的订单项集合。
2) 遍历这个集合。
3) 如果产品是一样的话,就进行数量追加
4)获取这个订单项的id

2、如果不存在对应的OrderItem,那么就新增一个订单项OrderItem

1) 生成新的订单项
2)设置数量,用户和产品
3)插入到数据库
4)获取这个订单项的id

ForeController.buyone()客户端跳转到结算页面不同的是,最后返回字符串”success”, 表示添加成功。

查看购物车页面

访问地址/forecart调用ForeController.cart()方法。
1、通过session获取当前用户, 所以一定要登录才能访问,否则拿不到用户对象,会报错。
2、获取为这个用户关联的订单集合ois
3、把ois放在model中
4、服务端跳转到cart.jsp

register.jsp相仿cart.jsp也包含了header.jsp, top.jsp, simpleSearch.jsp, footer.jsp等公共页面。中间是产品业务页面cartPage.jsp

cartPage.jsp中遍历订单项集合ois,即可。

购物车页面操作

购物车页面和服务端的交互主要是三个
1、增加、减少某种产品的数量
2、删除某种产品
3、选中产品后,提交到结算页面

调整订单数量

点击增加或者减少按钮后,根据cartPage.jsp 中的js代码,会通过Ajax访问/forechangeOrderItem路径,导致ForeController.changeOrderItem()方法被调用
1、判断用户是否登录
2、获取pid和number
3、遍历出用户当前所有的未生成订单的OrderItem
4、根据pid找到匹配的OrderItem,并修改数量后更新到数据库
5、返回字符串”success”

删除订单项

点击删除按钮后,根据 cartPage.jsp 中的js代码,会通过Ajax访问/foredeleteOrderItem路径,导致ForeController.deleteOrderItem方法被调用
1、判断用户是否登录
2、获取oiid
3、删除oiid对应的OrderItem数据
4、返回字符串”success”

提交到结算页面

在选中了购物车中的任意商品之后,结算按钮呈现可点击状态。
点击之后,提交到结算页面,并带上(多个)被选中的OrderItem对象的id/forebuy?oiid=&oiid=
之后的流程就进入了前面的结算页面

订单状态流转

生成订单

通过立即购买或者购物车的提交到结算页面进入结算页面,然后点击提交订单。
提交订单后,在数据库中生成一条Order记录。不仅如此,订单项的oid字段也会被设置为这条Order记录的id。

在applicationContext.xml最后增加事务管理配置。
因为增加订单行为需要同时修改两个表
1、为Order表新增数据
2、修改OrderItem表
所以需要进行事务管理,否则当新增了Order表的数据,还没来得及修改OrderItem的时候出问题,比如突然断电,那么OrderItem的数据就会是脏数据了。

在OrderService中新增方法add(Order c, List ois)
在OrderServiceImpl中实现add(Order o, List ois)方法,该方法通过注解进行事务管理。@Transactional(propagation= Propagation.REQUIRED,rollbackForClassName="Exception")

提交订单访问路径/forecreateOrder, 在ForeController中调用createOrder
1、从session中获取user对象
2、通过参数Order接受地址,邮编,收货人,用户留言等信息
3、根据当前时间加上一个4位随机数生成订单号
4、根据上述参数,创建订单对象
5、把订单状态设置为等待支付
6、从session中获取订单项集合 ( 在结算功能的ForeController.buy() 13行,订单项集合被放到了session中 )
7、把订单加入到数据库,并且遍历订单项集合,设置每个订单项的order,更新到数据库
8、统计本次订单的总金额
9、客户端跳转到确认支付页forealipay,并带上订单id和总金额

确认支付

1、在上一步客户端跳转到路径/forealipay方法,导致PageController.alipay()方法被调用。 alipay()没做什么事情,就是服务端跳转到了alipay.jsp页面。
2、alipay.jsp :
register.jsp 相仿,alipay.jsp也包含了header.jsp, top.jspfooter.jsp 等公共页面。
中间是确认支付业务页面 alipayPage.jsp
3、alipayPage.jsp:
显示总金额,并且让确认支付按钮跳转到页面 /forepayed页面,并带上oid和金额

支付成功页

1、在上一步确认访问按钮提交数据到/forepayed,导致ForeController.payed方法被调用
1.1 获取参数oid
1.2 根据oid获取到订单对象order
1.3 修改订单对象的状态和支付时间
1.4 更新这个订单对象到数据库
1.5 把这个订单对象放在model的属性”o”上
1.6 服务端跳转到payed.jsp
2、payed.jsp
register.jsp 相仿,payed.jsp也包含了header.jsp, top.jsp, simpleSearch.jspfooter.jsp 等公共页面。
中间是支付成功业务页面 payedPage.jsp
3、payedPage.jsp
显示订单中的地址,邮编,收货人,手机号码等等

后台发货

当订单状态是waitDelivery的时候,就会出现发货按钮
1、发货按钮链接跳转到admin_order_delivery
2、OrderController.delivery()方法被调用
2.1 注入订单对象
2.2 修改发货时间,设置发货状态
2.3 更新到数据库
2.4 客户端跳转到admin_order_list页面

确认收货

1、点击确认收货后,访问地址/foreconfirmPay
2、ForeController.confirmPay()方法被调用
2.1 获取参数oid
2.2 通过oid获取订单对象o
2.3 为订单对象填充订单项
2.4 把订单对象放在request的属性”o”上
2.5 服务端跳转到 confirmPay.jsp

3、confirmPay.jsp
register.jsp 相仿,confirmPay.jsp也包含了header.jsp, top.jsp, simpleSearch.jspfooter.jsp 等公共页面。
中间是订单确认业务页面 confirmPayPage.jsp
4、confirmPayPage.jsp
显示订单的创建时间,付款时间和发货时间,以及订单号,收款人信息等
遍历订单项集合,显示其中的产品图片,产品标题,价格,数量,小计,总结信息
最后提供确认支付按钮,提交到/foreorderconfirmed路径

确认收货成功

通过上一步最后的确认支付按钮,提交到路径/foreorderConfirmed,导致ForeController.orderConfirmed()方法被调用
1、ForeController.orderConfirmed() 方法
1.1 获取参数oid
1.2 根据参数oid获取Order对象o
1.3 修改对象o的状态为等待评价,修改其确认支付时间
1.4 更新到数据库
1.5 服务端跳转到orderConfirmed.jsp页面
2、orderConfirmed.jsp
register.jsp 相仿,orderConfirmed.jsp也包含了header.jsp, top.jsp, simpleSearch.jspfooter.jsp 等公共页面。
中间是确认收货成功业务页面 orderConfirmedPage.jsp
3、orderConfirmedPage.jsp
显示”交易已经成功,卖家将收到您的货款。”

评价

评价产品页面

通过点击评价按钮,来到路径/forereview,导致ForeController.review()方法被调用
1、ForeController.review()
1.1 获取参数oid
1.2 根据oid获取订单对象o
1.3 为订单对象填充订单项
1.4 获取第一个订单项对应的产品,因为在评价页面需要显示一个产品图片,那么就使用这第一个产品的图片了
1.5 获取这个产品的评价集合
1.6 为产品设置评价数量和销量
1.7 把产品,订单和评价集合放在request上
1.8 服务端跳转到 review.jsp
2、review.jsp
register.jsp 相仿,review.jsp也包含了header.jsp, top.jsp, simpleSearch.jsp
footer.jsp 等公共页面。
中间是产品业务页面 reviewPage.jsp
3、reviewPage.jsp
reviewPage.jsp中显示产品图片,产品标题,价格,产品销量,产品评价数量,以及订单信息等。
同时还显示出了该产品所有的评价,但是默认是隐藏的

提交评价

在评价产品页面点击提交评价,就把数据提交到了/foredoreview路径,导致ForeController.doreview方法被调用
1、ForeController.doreview()
1.1 获取参数oid
1.2 根据oid获取订单对象o
1.3 修改订单对象状态
1.4 更新订单对象到数据库
1.5 获取参数pid
1.6 根据pid获取产品对象
1.7 获取参数content (评价信息)
1.8 对评价信息进行转义,道理同注册ForeController.register()
1.9 从session中获取当前用户
1.10 创建评价对象review
1.11 为评价对象review设置 评价信息,产品,时间,用户
1.12 增加到数据库
1.13.客户端跳转到/forereview: 评价产品页面,并带上参数showonly=true
2、reviewPage.jsp
reviewPage.jsp中,当参数showonly==true,那么就显示当前产品的所有评价信息

CRUD

增加(create)

读取查询(Retrieve)

1、首先浏览器上访问路径 /admin_category_list
2、tomcat根据web.xml上的配置信息,拦截到了/admin_category_list,并将其交由DispatcherServlet处理。
3、DispatcherServlet 根据springMVC的配置,将这次请求交由CategoryController类进行处理,所以需要进行这个类的实例化
4、在实例化CategoryController的时候,注入CategoryServiceImpl
5、在实例化CategoryServiceImpl的时候,又注入CategoryMapper
6、根据ApplicationContext.xml中的配置信息,将CategoryMapper和CategoryMapper.xml关联起来了。
7、这样就拿到了实例化好了的CategoryController,并调用list方法
8、在list方法中,访问CategoryService,并获取数据,并把数据放在”cs”上,接着服务端跳转到listCategory.jsp去
9、最后在listCategory.jsp 中显示数据
2

更新(update)

删除(delete)

分页

新增Page这个类专门为分页提供必要信息
属性:
    int start; 开始位置
    int count; 每页显示的数量
    int total; 总共有多少条数据
    String param; 参数
方法:
    getTotalPage根据每页显示的数量count以及总共有多少条数据total,计算出总共有多少页
    getLast计算出最后一页的数值是多少
    isHasPreviouse判断是否有前一页
    isHasNext判断是否有后一页

CategoryMapper.xml中,提供带分页的查询语句和获取总数的sql语句
CategoryMapper.java中提供一个支持分页的查询方法list(Page page)和获取总数的方法total
CategoryService.java中,提供一个支持分页的查询方法list(Page page)和获取总数的方法list
CategoryService.java中,提供一个支持分页的查询方法list(Page page)和获取总数的方法total
CategoryController.java中,为方法list增加参数Page用于获取浏览器传递过来的分页信息
1、categoryService.list(page); 获取当前页的分类集合
2、通过categoryService.total(); 获取分类总数
3、通过page.setTotal(total); 为分页对象设置总数
4、把分类集合放在”cs”中
5、把分页对象放在 “page“ 中
6、跳转到listCategory.jsp页面

在listCategory.jsp中加入adminPage.jsp

adminPage.jsp中,分页超链的效果,用的Bootstrap的分页效果来制作。
对边界进行了处理,没有上一页或下一页的时候,对应超链处于不可点击状态。hasPreviouse会导致isHasPreviouse()方法被调用。

逆向工程

MybatisGenerator插件是Mybatis官方提供的,这个插件存在一个BUG,即第一次生成了CategoryMapper.xml后,再次运行会导致CategoryMapper.xml生成重复内容,而影响正常的运行。
resouces下创建generatorConfig.xml文件,其目的就是为了正确使用本插件而提供必要的配置信息。
1、使用OverlsMergeablePlugin插件<plugin type="com.how2java.tmall.util.OverIsMergeablePlugin" />
2、在生成的代码中,不提供注释。如果提供注释,生成出来的代码,看上去乱。
3、指定链接数据库的账号和密码,既然是逆向工程,肯定要先链接到数据库。<jdbcConnection driverClass="com.mysql.jdbc.Driver" connectionURL="jdbc:mysql://localhost/tmall_ssm" userId="root" password="admin">
4、指定生成的pojo, mapper, xml文件的存放位置。
5、生成对应表及类名。

在generatorConfig.xml中,加入所有的table表,运行MybatisGenerator,需要把today变量修改为今天。
和手动编写的区别在于id类型 从基本类型id变成了Integer。

1
2
id: 基本类型
Integer: 引用类型

属性管理

属性管理流程

Property.java实体类已经和其他所有的实体类一起,在逆向工程这个环节就一起自动生成好了。不过仅仅有自动生成的实体类代码,还不足以支撑业务需要,所以在Property基础上增加了一个Category字段。

PropertyService.java中,提供CRUD一套。因为在业务上需要查询某个分类下的属性,所以list方法会带上对应分类的id。

新增PropertyService.xml实现PropertyService对应的方法。通过调用自动生成的PropertyMapper就可以实现大部分方法。值得注意的是查询的时候用到了辅助查询类: PropertyExample。这一行表示查询cid字段example.createCriteria().andCidEqualTo(cid);

控制器类PropertyController.java用于映射不同路径的访问。

查询页面listProperty.jsp
编辑页面editProperty.jsp

查询功能

查询地址admin_property_list映射的是PropertyController的list()方法。
1、获取分类 cid,和分页对象page
2、通过PageHelper设置分页参数
3、基于cid,获取当前分类下的属性集合
4、通过PageInfo获取属性总数
5、把总数设置给分页page对象
6、拼接字符串”&cid=”+c.getId(),设置给page对象的Param值。 因为属性分页都是基于当前分类下的分页,所以分页的时候需要传递这个cid
7、把属性集合设置到 request的 “ps” 属性上
8、把分类对象设置到 request的 “c” 属性上。 ( 这个c有什么用呢? 在后面步骤的 其他-面包屑导航 中会用于显示分类名称)
9、把分页对象设置到 request的 “page” 对象上
10、服务端跳转到admin/listProperty.jsp页面
11、在listProperty.jsp页面上使用c:forEach 遍历ps集合,并显示

增加功能

1、在listProperty.jsp页面提交数据的时候,除了提交属性名称,还会提交cid
2、在PropertyController通过参数Property 接受注入
3、通过propertyService保存到数据库
4、客户端跳转到admin_property_list,并带上参数cid

编辑功能

1、在PropertyController的edit方法中,根据id获取Property对象
2、根据properoty对象的cid属性获取Category对象,并把其设置在Property对象的category属性上
3、把Property对象放在request的 “p” 属性中
3、服务端跳转到admin/editProperty.jsp
4、在editProperty.jsp中显示属性名称
5、在editProperty.jsp中隐式提供id和cid( cid 通过 p.category.id 获取)

修改功能

1、在PropertyController的update方法中获取Property对象
2、借助propertyService更新这个对象到数据库
3、客户端跳转到admin_property_list,并带上参数cid

删除功能

1、在PropertyController的delete方法中获取id
2、根据id获取Property对象
3、借助propertyService删除这个对象对应的数据
4、客户端跳转到admin_property_list,并带上参数cid

面包屑导航

在属性管理页面的页头,有一个面包屑导航
第一个连接跳转到admin_category_list
第二个连接跳转到admin_property_list?cid=
样式用的是Bootstrap面包屑导航

分页

这里的分页比分类管理中的分页多了一个参数cid。
1、在PropertyController.list() 方法中,把&cid= 参数设置到在page对象的param属性上page.setParam("&cid="+c.getId());
2、在adminPage.jsp页面中通过${page.param}取出这个参数

产品管理

产品管理流程

Product.java在自动生成的基础上增加category属性。
新增ProductService,提供CRUD一套。
新增ProductServiceImpl,提供CRUD一套。get和list方法都会把取出来的Product对象设置上对应的category。
准备ProductController.java类。
查询页面listProduct.jsp
编辑页面editProduct.jsp

查询功能讲解

查询地址admin_product_list映射的是ProductController的list()方法
1、获取分类 cid,和分页对象page
2、通过PageHelper设置分页参数
3、基于cid,获取当前分类下的产品集合
4、通过PageInfo获取产品总数
5、把总数设置给分页page对象
6、拼接字符串”&cid=”+c.getId(),设置给page对象的Param值。 因为产品分页都是基于当前分类下的分页,所以分页的时候需要传递这个cid
7、把产品集合设置到 request的 “ps” 产品上
8、把分类对象设置到 request的 “c” 产品上。 ( 这个c有什么用呢? 在 其他-面包屑导航 中会用于显示分类名称)
9、把分页对象设置到 request的 “page” 对象上
10、服务端跳转到admin/listProduct.jsp页面
11、在listProduct.jsp页面上使用c:forEach 遍历ps集合,并显示

增加功能讲解

1、在listProduct.jsp提交数据的时候,除了提交产品名称,小标题,原价格,优惠价格,库存外还会提交cid
2、在ProductController中获取Product对象,并插入到数据库
3、客户端跳转到admin_product_list,并带上参数cid

编辑功能讲解

1、在ProductController的edit方法中,根据id获取product对象
2、根据product对象的cid产品获取Category对象,并把其设置在product对象的category产品上
3、把product对象放在request的 “p” 产品中
3、服务端跳转到admin/editProduct.jsp
4、在editProduct.jsp中显示产品名称
5、在editProduct.jsp中隐式提供id和cid( cid 通过 p.category.id 获取)

修改功能讲解

1、在ProductController的update方法中获取Product对象
2、借助productService更新这个对象到数据库
3、客户端跳转到admin_product_list,并带上参数cid

删除功能讲解

1、在ProductController的delete方法中获取id
2、根据id获取Product对象
3、借助productService删除这个对象对应的数据
4、客户端跳转到admin_product_list,并带上参数cid

产品图片管理

图片管理流程

ProductImage.java直接使用自动生成的,没有变化。

创建ProductImageService,提供CRUD。同时提供两个常量,分别表示单个图片和详情图片:

1
2
String type_single = "type_single";
String type_detail = "type_detail";

除此之外,还提供了根据产品id和图片类型查询的list方法。

1
List list(int pid, String type);

创建ProductImageServiceImpl,实现CRUD相关方法。关于list方法,使用了ProductImageExample类,这样的写法表示同时匹配pid和type。

1
2
3
example.createCriteria()
.andPidEqualTo(pid)
.andTypeEqualTo(type);

ProductImageController提供了list, add和delete方法。edit和update没有提供相关业务,所以无。

新增listProductImage.jsp

查询功能

通过产品页面的图片管理访问ProductImageController的list()方法
1、获取参数pid
2、根据pid获取Product对象
3、根据pid对象获取单个图片的集合pisSingle
4、根据pid对象获取详情图片的集合pisDetail
5、把product 对象,pisSingle ,pisDetail放在model上
6、服务端跳转到admin/listProductImage.jsp页面
7、在listProductImage.jsp,使用c:forEach 遍历pisSingle
8、在listProductImage.jsp,使用c:forEach 遍历pisDetail

增加功能

增加产品图片分单个和详情两种,其区别在于增加所提交的type类型不一样。
这里就对单个的进行讲解,详情图片的处理同理。
首先, 在listProductImage.jsp准备一个form,提交到admin_productImage_add。
<form method="post" class="addFormSingle" action="admin_productImage_add" enctype="multipart/form-data">
接着在ProductImageController的add()方法中进行处理
1、通过pi对象接受type和pid的注入
2、借助productImageService,向数据库中插入数据。
3、根据session().getServletContext().getRealPath( “img/productSingle”),定位到存放单个产品图片的目录
除了productSingle,还有productSingle_middle和productSingle_small。 因为每上传一张图片,都会有对应的正常,中等和小的三种大小图片,并且放在3个不同的目录下
4、文件命名以保存到数据库的产品图片对象的id+”.jpg”的格式命名
5、通过uploadedImageFile保存文件
6、借助ImageUtil.change2jpg()方法把格式真正转化为jpg,而不仅仅是后缀名为.jpg
7、再借助ImageUtil.resizeImage把正常大小的图片,改变大小之后,分别复制到productSingle_middle和productSingle_small目录下。
8、处理完毕之后,客户端条跳转到admin_productImage_list?pid=,并带上pid。

详情图片做的是一样的事情,区别在于复制到目录productDetail下,并且不需要改变大小。
ImageUtil.resizeImage 使用了swing自带的修改图片大小的API,属于底层工作原理。

删除功能

点击删除超链,进入ProductImageController的delete方法
1、获取id
2、根据id获取ProductImage 对象pi
3、借助productImageService,删除数据
4、如果是单个图片,那么删除3张正常,中等,小号图片
5、如果是详情图片,那么删除一张图片
6、客户端跳转到admin_productImage_list地址

调整

回到产品管理,在产品列表页面,是没有图片的。因为当时还没有产品图片管理功能,现在支持了,所以需要对Product做一些调整。

在Product.java新增属性private ProductImage firstProductImage;

在ProductService.java新增方法void setFirstProductImage(Product p);

在ProductServiceImpl.java中新增方法setFirstProductImage(Product p): 根据pid和图片类型查询出所有的单个图片,然后把第一个取出来放在firstProductImage上。
增加方法setFirstProductImage(List ps): 给多个产品设置图片。
在get方法中调用setFirstProductImage(Product p): 为单个产品设置图片。
在list方法中调用setFirstProductImage(List ps): 为多个产品设置图片。

在listProduct.jsp中增加如下来显示图片:

1
2
3
<c:if test="${!empty p.firstProductImage}">
<img width="40px" src="img/productSingle/${p.firstProductImage.id}.jpg">
</c:if>

产品展示

前台无需登录

从前台模块之间的依赖性,以及开发顺序的合理性来考虑,把前台功能分为了无需登录即可使用的功能,和需要登录才能访问的功能。

前台需要登录

接着是需要登录的前台功能。这部分功能基本上都是和购物相关的。

首页

从首页展示需求分析上来看:
1、在横向导航栏上提供4个分类连接
2、在纵向导航栏上提供全部17个分类连接
3、当鼠标移动到某一个纵向分类连接的时候,显示这个分类下的推荐产品列表
4、按照每种分类显示5个商品的方式,显示所有17种分类

在Category.java中新增两个瞬间自动products和productsByRow

1
2
List<Product> products;
List<List<Product>> productsByRow;

products比较好理解,代表一个分类下有多个产品。
productByRow这个属性的类型是List>productByRow。即一个分类又对应多个List,提供这个属性,是为了在首页竖状导航的分类名称右边显示推荐产品列表。
一个分类会对应多行产品,而一行产品里又有多个产品记录。
为了实现界面上这个功能,为Category类设计了List<List<Product>>productsByRow这样一个集合属性。

在ProductService中新增三个方法

1
2
3
public void fill(List<Category> categorys);
public void fill(Category category);
public void fillByRow(List<Category> categorys);

在ProductServiceImpl为ProductService中新增加的三个方法提供实现。
1、为分类填充产品集合public void fill(Category cateogry)
2、为多个分类填充产品集合public void fill(List<Category> categorys)
3、为多个分类填充推荐产品集合,即把分类下的产品集合,按照8个为一行,拆成多行,以利于后续页面上进行显示。public void fillByRow(List<Category> categorys);

在home()方法映射首页访问路径/forehome
1、查询所有分类
2、为这些分类填充产品集合
3、为这些分类填充推荐产品集合
4、服务端跳转到home.jsp

home.jsp

home.jsp分成header.jsp, top.jsp, search.jsp, homePage.jsp, footer.jsp

header.jsp

1、声明为HTML5格式<!DOCTYPE html>
2、让浏览器以UTF显示中文,本页面的中文文字采用UTF-8编码,启动EL表达式

1
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" isELIgnored="false"%>

3、引入标准标签库

1
2
3
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix='fmt' %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>

c通常用于条件判断和遍历
fmt用于格式化日期和货币
fn用于校验长度
4、引入bootstrap和jquery

1
2
3
<script src="js/jquery/2.0.0/jquery.min.js"></script>
<link href="css/bootstrap/3.3.6/bootstrap.min.css" rel="stylesheet">
<script src="js/bootstrap/3.3.6/bootstrap.min.js"></script>

5、自定义前端样式

1
<link href="css/fore/style.css" rel="stylesheet">

除了轮播和图标,前端几乎没用Bootstrap中的样式,所有的自定义样式都在这个文件里。
6、各种自定义函数和事件监听

top.jsp

置顶导航页面
这里会根据用户是否登录,决定是否显示退出按钮,或者登录注册按钮,以及购物车中的商品数量。

serach.jsp

这里会从request的属性”cs”中获取到分类集合,并取第5个到第8个,一共4个来显示。

homePage.jsp

这里是首页业务页面,在homePage中要显示如下内容:
1、天猫超市连接右侧有4个分类数据
2、竖状导航栏显示17个分类数据
3、每个分类又再对应不同的推荐产品集合
4、中部会显示17个分类
5、每个分类又显示前5款产品
6、每款产品又会显示第一张图片,标题,价格等信息。

homePage.jsp中包含categoryAndcarousel.jsp分类和轮播,这个jsp包含categoryMenu.jsp竖状分类菜单, productsAsideCategorys.jsp竖状分类菜单右侧的推荐产品列表, carousel.jsp轮播; homepageCategoryProducts.jsp总体17种分类以及每个分类对应的5个产品。

categoryAndcarousel.jsp

categoryAndcarousel.jsp 页面利用ForeController传递过来的数据,天猫国际几个字旁边显示4个分类超链
另外包含了其他3个页面:
categoryMenu.jsp
productsAsideCategorys.jsp
carousel.jsp

categoryMenu.jsp

categoryMenu.jsp 显示左侧的竖状分类导航

productsAsideCategorys.jsp

productsAsideCategorys.jsp 进行了三层遍历
1、先取出每个分类
2、然后取出每个分类的productsByRow集合
3、根据productsByRow集合,取出每个产品,把产品的subTitle信息里的第一个单词取出来显示。

轮播部分,都是静态的页面,没有用到服务端数据

homepageCategoryProducts.jsp

homepageCategoryProducts.jsp 进行了2次遍历
1、遍历所有的分类,取出每个分类对象
2、遍历分类对象的products集合,取出每个产品,然后显示该产品的标题,图片,价格等信息

产品页

Product.java

在Product.java中增加如下属性:
1、单个产品图片集合private List<ProductImage> productSingleImages;
2、详情产品图片集合private List<ProductImage> productDetailImages;
3、销量private int saleCount;
4、累计评价private int reviewCount;

Review.java

修改自动生成的评价类Review,增加User属性: private User user;

ReviewService.java

增加ReviewService,提供CURD以及通过产品获取评价方法:

1
2
3
List list(int pid);
int getCount(int pid);

User.java

修改User, 增加一个getAnonymousName方法用于在显示评价者的时候进行匿名显示

ReviewServiceImpl.java

增加ReviewServiceImpl,实现对应方法

OrderItemService.java

修改OrderItemService,增加根据产品获取销售量的方法: int getSaleCount(int pid);

OrderItemServiceImpl.java

修改OrderItemServiceImpl,实现getSaleCount方法

ProductService.java

修改ProductService,增加为产品设置销量和评价数量的方法:

1
2
void setSaleAndReviewNumber(Product p);
void setSaleAndReviewNumber(List<Product> ps);

ProductServiceImpl.java

修改ProductServiceImpl,实现为产品设置销量和评价数量的方法:

1
2
void setSaleAndReviewNumber(Product p);
void setSaleAndReviewNumber

ForeController.product()

访问地址/foreproduct?pid=
导致ForeController.product() 方法被调用
1、获取参数pid
2、根据pid获取Product 对象p
3、根据对象p,获取这个产品对应的单个图片集合
4、根据对象p,获取这个产品对应的详情图片集合
5、获取产品的所有属性值
6、获取产品对应的所有的评价
7、设置产品的销量和评价数量
8、把上述取值放在request属性上
9、服务端跳转到 “product.jsp” 页面

product.jsp

与 register.jsp 相仿,product.jsp也包含了header.jsp, top.jsp, simpleSearch.jsp, footer.jsp 等公共页面。
中间是产品业务页面 productPage.jsp

productPage.jsp

productPage.jsp 又由3个页面组成
1、imgAndInfo.jpg
单个图片和基本信息
2、productReview.jsp
评价信息
3、productDetail.jsp
详情图片

imgAndInfo.jsp

1、左侧显示5张单个图片
1.1 默认显示第一张图片

1
<img src="img/productSingle/${p.firstProductImage.id}.jpg" class="bigImg">

1.2 5张小图片

1
2
3
<c:forEach items="${p.productSingleImages}" var="pi">
<img src="img/productSingle_small/${pi.id}.jpg" bigImageURL="img/productSingle/${pi.id}.jpg" class="smallImage">
</c:forEach>

2、右边显示基本信息
2.1 标题和小标题

1
2
3
4
5
6
<div class="productTitle">
${p.name}
</div>
<div class="productSubTitle">
${p.subTitle}
</div>

2.2 原始价格和促销价

1
2
<fmt:formatNumber type="number" value="${p.originalPrice}" minFractionDigits="2"/>
<fmt:formatNumber type="number" value="${p.promotePrice}" minFractionDigits="2"/>

2.3 销量和累计评价

1
2
<div>销量 <span class="redColor boldWord"> ${p.saleCount }</span></div>
<div>累计评价 <span class="redColor boldWord"> ${p.reviewCount}</span></div>

2.4 库存

1
<span>库存${p.stock}</span>

productReview.jsp

借助c:forEach遍历request中的reviews

productDetail.jsp

如图所示,productDetail.jsp做了两件事
1、显示属性值

1
2
3
<c:forEach items="${pvs}" var="pv">
<span>${pv.property.name}: ${fn:substring(pv.value, 0, 10)} </span>
</c:forEach>

2、显示详情图片

1
2
3
<c:forEach items="${p.productDetailImages}" var="pi">
<img src="img/productDetail/${pi.id}.jpg">
</c:forEach>

搜索查询

每个页面都包含了搜索的jsp,首页和搜索结果页包含的是search.jsp,其他页面包含的是simpleSearch.jsp
这两个页面都提供了一个form,提交数据keyword到foresearch这个路径。

通过search.jsp或者simpleSearch.jsp提交数据到路径 /foresearch, 导致ForeController.search()方法被调用
1、获取参数keyword
2、根据keyword进行模糊查询,获取满足条件的前20个产品
3、为这些产品设置销量和评价数量
4、把产品结合设置在model的”ps”属性上
5、服务端跳转到 searchResult.jsp 页面

在ProductService.java中增加search方法
在ProductServiceImpl中实现search方法,通过关键字进行模糊查询。

与 register.jsp 相仿,searchResult.jsp 也包含了header.jsp, top.jsp, search.jsp, footer.jsp 等公共页面。
中间是搜索结果业务页面 searchResultPage.jsp。
在search.jsp中,又把参数keyword显示在输入框中。<input name="keyword" type="text" value="${param.keyword}" placeholder="时尚男鞋 太阳镜 ">

searchResultPage.jsp 本身没做什么。。。。 直接包含了 productsBySearch.jsp

productsBySearch.jsp 显示产寻结果:
1、遍历ps,把每个产品的图片,价格,标题等信息显示出来
2、如果ps为空,则显示 “没有满足条件的产品”

注册

register.jsp

与首页的home.jsp相仿,register.jsp也包含了header.jsp,top.jsp,footer.jsp等公共页面。
不同的是,没有使用首页的search.jsp,而是用的简单一点的 simpleSearch.jsp
中间是注册业务页面registerPage.jsp

PageController.java

register.jsp 是放在WEB-INF目录下的,是无法通过浏览器直接访问的。 为了访问这些放在WEB-INF下的jsp,准备一个专门的PageController类,专门做服务端跳转。 比如访问registerPage,就会服务端跳转到WEB-INF/jsp/fore/register.jsp 去,这样就解决了无法被访问的问题。
这个类很简单,就是单纯用来做服务端跳转用的

simpleSearch.jsp

与首页的search.jsp不太一样的是,这个搜索栏要更简单一些,并且左右分开。

registerPage.jsp

注册页面的主体功能,用于提交账号密码。 在提交之前会进行为空验证,以及密码是否一致验证。
这段代码用于当账号提交到服务端,服务端判断当前账号已经存在的情况下,显示返回的错误提示 “用户名已经被使用,不能使用”

1
2
3
4
<c:if test="${!empty msg}">
$("span.errorMessage").html("${msg}");
$("div.registerErrorMessageDiv").css("visibility","visible");
</c:if>

UserService.java

UserService新增加isExist(String name)方法

UserServiceImpl.java

UserServiceImpl 新增isExist(String name)的实现,判断某个名称是否已经被使用过了。

ForeController.register()

registerPage.jsp 的form提交数据到路径 foreregister,导致ForeController.register()方法被调用

1、通过参数User获取浏览器提交的账号密码
2、通过HtmlUtils.htmlEscape(name);把账号里的特殊符号进行转义
3、判断用户名是否存在
3.1 如果已经存在,就服务端跳转到reigster.jsp,并且带材错误提示信息
3.2 如果不存在,则加入到数据库中,并服务端跳转到registerSuccess.jsp页面

注为什么要用 HtmlUtils.htmlEscape? 因为有些同学在恶意注册的时候,会使用诸如 这样的名称,会导致网页打开就弹出一个对话框。 那么在转义之后,就没有这个问题了。
注: model.addAttribute(“user”, null); 这句话的用处是当用户存在,服务端跳转到register.jsp的时候不带上参数user, 否则当注册失败的时候,会在原本是“请登录”的超链位置显示刚才注册的名称。 可以试试把这一条语句注释掉观察这个现象

登录

login.jsp

与register.jsp相仿,login.jsp也包含了header.jsp,footer.jsp等公共页面。
中间是登录业务页面loginPage.jsp

UserService.java

增加get(String name, String password)方法

UserServiceImpl.java

增加User get(String name, String password) 方法

loginPage.jsp

登陆业务页面,用于向服务器提交账号和密码.
1、与registerPage.jsp类似的,用于显示登录密码错误

1
2
3
4
<c:if test="${!empty msg}">
$("span.errorMessage").html("${msg}");
$("div.loginErrorMessageDiv").show();
</c:if>

2、其他js函数是用于为空验证

ForeController.login()

loginPage.jsp的form提交数据到路径 forelogin,导致ForeController.login()方法被调用

1、获取账号密码
2、把账号通过HtmlUtils.htmlEscape进行转义
3、根据账号和密码获取User对象
3.1 如果对象为空,则服务端跳转回login.jsp,也带上错误信息,并且使用 loginPage.jsp 中的办法显示错误信息
3.2 如果对象存在,则把对象保存在session中,并客户端跳转到首页”forehome”
注 为什么要用 HtmlUtils.htmlEscape? 因为注册的时候,ForeController.register(),就进行了转义,所以这里也需要转义。有些同学在恶意注册的时候,会使用诸如 这样的名称,会导致网页打开就弹出一个对话框。 那么在转义之后,就没有这个问题了。

退出

ForeController.logout()

通过访问退出路径: http://127.0.0.1:8080/tmall_ssm/forelogout
导致ForeController.logout()方法被调用
1、在session中去掉”user” session.removeAttribute("user");
2、客户端跳转到首页: return "redirect:forehome";

登录状态拦截器

查看购物车页面有个问题,必须简历在已经登录的状态之上。如果没有登录,而访问地址: http://127.0.0.1:8080/tmall_ssm/forecart会导致异常发生。

解决思路: 在查看购物车之前,应该进行登录操作,但是又不能确保用户一定会记得登录,那么怎么办呢?
准备一个过滤器,当访问那些需要登录才能做的页面的时候,进行是否登录的判断,如果不通过,那么就跳转到login.jsp页面去,提示用户登录。

哪些页面需要登录?哪些页面不需要呢?
a. 不需要登录也可以访问的
如:注册,登录,产品,首页,分类,查询等等
b. 需要登录才能够访问的
如:购买行为,加入购物车行为,查看购物车,查看我的订单等等

LoginInterceptor.java

新建一个过滤器LoginInterceptor ,根据解决思路中
哪些页面需要登录?哪些页面不需要呢?
a. 不需要登录也可以访问的
如:注册,登录,产品,首页,分类,查询等等
b. 需要登录才能够访问的
如:购买行为,加入购物车行为,查看购物车,查看我的订单等等
不需要登录也可以访问的已经确定了,但是需要登录才能够访问,截止目前为止还不能确定,所以这个过滤器就判断如果不是注册,登录,产品这些,就进行登录校验
1、准备字符串数组 noNeedAuthPage,存放哪些不需要登录也能访问的路径
2、获取uri
3、去掉前缀/tmall_ssm
4、如果访问的地址是/fore开头
4.1 取出fore后面的字符串,比如是forecart,那么就取出cart
4.2 判断cart是否是在noNeedAuthPage
4.2 如果不在,那么就需要进行是否登录验证
4.3 从session中取出”user”对象
4.4 如果对象不存在,就客户端跳转到login.jsp
4.5 否则就正常执行

springMVC.xml

在springMVC.xml中新增对登陆状态拦截器的配置

ForeController对createOrder进行事务管理

实现add(Order o, List ois)方法,该方法通过注解进行事务管理
@Transactional(propagation= Propagation.REQUIRED,rollbackForClassName="Exception")

设计模式

MVC

MVC设计模式贯穿于整个后台与前台功能开发始末
模型(Model)、视图(View)、控制器(Controller)

重构

通过发现问题,分析问题,解决问题的三部曲,进行了各种角度的重构。经历这样一个重构过程提高开发效率,降低维护成本
分页方式 ,分类逆向工程 ,所有逆向工程

统一的分页查询简化开发

所有的后台都使用同一个分页机制,仅仅需要一份简化的adminPage.jsp即满足了各种分页功能的需求,简化了开发,提升了开发速度。

模块化JSP设计

从大的JSP文件中,通过JSP包含关系抽象出多个公共文件,并把业务JSP按照功能,设计为多个小的JSP文件,便于维护和理解

Your support will encourage me to continue to create!