电商项目

1 创建项目

1.1 项目名称

TeduStore

1.2 Tomcat Runtime

1.3 添加依赖

  • spring-webmvc
  • spring-jdbc
  • commons-dbcp
  • mysql-connector
  • mybatis
  • mybatis-spring
  • jstl
  • jackson-core
  • jackson-annotations
  • jackson-databind
  • junit

1.4 Spring的基本配置

将原来的项目中的applicationContext.xml复制到此次的项目中,改名为spring-mvc.xml,在这个配置文件中,配置注解驱动和视图解析器ViewResolver。

1.5 配置web.xml

配置CharacterEncodingFilter和DispatcherServlet。

注意:在配置DispatcherServlet时,初始化参数读取的配置文件需要修改为spring-*.xml。

1.6 静态页面

1.7 创建控制器类

创建cn.tedu.store.controller.MainController,使用@Controller和@RequestMapping("/main")进行注解,添加处理"/index.do"请求的方法public String showIndex(),使用@RequestMapping("/index.do")进行注解,该方法直接返回"index"。
打开spring-mvc.xml文件,配置组件扫描,扫描的包:cn.tedu.store.controller。

1.8 测试运行

找到webapp/web/index.html文件,在该文件的最前部添加必要的JSP声明,将其扩展名改为.jsp。
将项目部署到Tomcat服务器。
打开浏览器,输入 http://SERVER:PORT/PROJECT/main/index.do 检查页面是否可以正常打开。

2 准备数据库

2.1 创建数据库

CREATE DATABASE tedu_store;

2.2 切换使用数据库

USE tedu_store;

2.3 创建用户数据表

CREATE TABLE t_user (
	id INT AUTO_INCREMENT,
	username VARCHAR(16) NOT NULL UNIQUE, 
	password VARCHAR(16) NOT NULL, 
	phone VARCHAR(20) NOT NULL UNIQUE, 
	email VARCHAR(50) NOT NULL UNIQUE, 
	disabled INT DEFAULT 0,
	created_user VARCHAR(16),
	created_time DATETIME,
	modified_user VARCHAR(16),
	modified_time DATETIME,
	PRIMARY KEY (id)
);

2.4 测试添加并查询数据

通过INSERT和SELECT语法进行测试

3 配置数据库连接

3.1 配置resources/db.properties

url=xxx
driver=xxx
user=xxx
password=xxx
initSize=xxx
maxSize=xxx

3.2 配置连接池

在resources下创建新的Spring配置文件:spring-dao.xml
location=“classpath:db.properties” />


	
	
	... ...

3.3 测试连接池是否可用

在test下创建测试时使用的类Tester,在该类中添加测试数据库连接的方法public void testDbcpConnection(),并使用@Test进行注解。

@Test
public void testDbcpConnection() {
	String file = "spring-dao.xml";
	AbstractApplicationContext ctx
		= new ClassPathXmlApplicationContext();
	BasicDataSource ds 
		= ctx.getBean("dataSource", BasicDataSource.class);
	System.out.println(ds.getUrl());
	System.out.println(ds.getDriverClassName());
	// ... 输出剩下的配置的属性
	ctx.close();
}

4 通过Mybatis实现增加数据

4.1 创建User实体类

创建cn.tedu.store.bean(entity).User类。

在这个类中添加与数据表完全一致的所有属性,其中,数据表中设计为INT的对应Integer,VARCHAR对应String,DATETIME对应java.util.Date。

对于created_user这种多个单词组成、使用下划线分隔、均小写的名字,在类中的属性名应该去除下划线,后续的单词首字母改成大写,例如createdUser。

在这个类中所有的属性都应该是private的,并且通过开发工具自动生成GET/SET方法。

通过开发工具基于所有属性自动生成equals()方法和hashCode()方法。

通过开发工具添加无参数的构造方法和带全部参数的构造方法。

通过开发工具自动生成toString()方法。

实现Serialiazable接口,并通过开发工具自动生成序列化ID。

4.2 设计DAO接口

创建cn.tedu.store.mapper.UserMapper接口,并在接口中声明void createUser(User user)抽象方法

4.3 配置接口的映射的XML文件

在resources文件夹下创建mappers文件夹,然后,从此前的项目中复制一个映射文件到mappers中并改名为UserMapper.xml,清空原有的文件的配置。

添加节点并配置:


	
		INSERT INTO t_user (
			username, password, phone, 
			email, disabled, 
			created_user, created_time, 
			modified_user, modified_time
		) VALUES (
			#{username}, #{password}, #{phone}, 
			#{email}, #{disabled}, 
			#{createdUser}, #{createdTime},
			#{modifiedUser}, #{modifiedTime}
		)
	

4.4 配置spring-dao.xml

配置MapperScannerConfigurer和SqlSessionFactoryBean对应的节点,其中,MapperScannerConfigurer需要配置扫描映射的接口文件的包名,SqlSessionFactoryBean需要配置dataSource和mapperLocations。


	



	
	

4.5 测试

通过Spring获取UserMapper对象,调用createUser(User user)方法,测试增加数据,并在Mysql控制台查询以检查增加是否成功。

5 在持久层开发根据用户名查询用户的功能

5.1 在UserMapper接口中添加方法

User findUserByUsername(String username)

5.2 配置映射

此次select节点中可以不配置参数类型。

由于数据表的字段名和实体类的属性名有4个不一致的名称,所以,这4个字段需要设置别名。


5.3 测试

同上

6 开发注册业务

6.1 创建业务接口

创建cn.tedu.store.service.UserService接口,并添加抽象方法void register(User user)和User findUserByUsername(String username)

6.2 创建业务实现类

创建cn.tedu.store.service.UserServiceImpl类,实现以上接口,并使用@Service(“userService”)进行注解。

在类中声明private UserMapper userMapper;并使用@Resource进行注解。

先完成findUserByUsername()方法,再完成register()方法。

6.3 配置组件扫描到service包

添加新的Spring配置文件spring-service.xml,在这个配置文件中,配置组件扫描cn.tedu.store.service

6.4 测试

同上

7 添加其它相关功能

7.1 在持久层实现检查邮箱和手机是否被注册

在UserMapper接口中,添加Integer getRecordCountByEmail(@Param(“email”) String email)、Integer getRecordCountByPhone(@Param(“phone”) String phone)

7.2 配置映射

在UserMapper.xml中,添加2个select节点对应以上2个方法,例如:


7.3 在业务层也添加对应的功能

在UserService接口中,添加抽象方法boolean checkPhoneExists(String phone)和boolean checkEmailExists(String email)

在UserServiceImpl类中实现以上抽象方法

7.4 测试

测试以上2个功能

***** 经验总结 *****

1 使用Spring时,尽量不要使用5.?版本,凡是依赖的jar包名称中含有spring字样的,都应该使用相同的版本!

2 对于实体类,把属性声明完了以后,应该把Eclipse(或其它专业的开发工具)的Source菜单中的Generate系列方法全部执行一次!

3 通常在开发一个项目时,推荐先开发持久层的处理,即DAO,然后开发业务层,即Service,再开发控制器,即Controller,最后完成View。

8 开发“检查用户名是否存在”的控制器层功能

8.1 整合

在Spring的配置文件中配置DataSourceTransactionManager


	



8.2 规划请求与响应

请求路径:/user/checkUsername.do

请求类型:GET

请求参数:username=admin

响应结果类型:ResponseResult

响应结果:如果正确,则state=1,message=“当前用户名可以正常使用”;如果错误,则state=-1,message=“当前用户名已经被注册”。

8.3 创建通用响应结果类

public class ResponseResult {
	public static final int STATE_OK = 1;
	public static final int STATE_ERROR = -1;

	private int state;
	private String message;
	private T data;

	// SET / GET、equals()、hashCode、Constructor、toString
}

8.4 编写Controller类

创建cn.tedu.store.controller.UserController类,并使用@Controller进行注解。

为类添加@RequestMapping("/user")注解。

在类中添加private UserService userService变量,并使用@Resource注解。

添加public ResponseResult checkUsername(String username)方法,该方法使用@RequestMapping("/checkUsername.do")和@ResponseBody。

在方法内部,通过userService的findUserByUsername()方法判断用户名是否存在,然后返回匹配的ResponseResult。

8.5 测试

在浏览器地址栏中输入 http://SERVER:PORT/PROJECT/user/checkUsername.do?username=admin

9 异步检查用户名是否被注册

9.1 准备注册界面的jsp

在webapp/web下找到register.html,在文件内部添加JSP声明的代码,然后将扩展名改为.jsp

9.2 设置请求路径能转发到register.jsp

在UserController中添加public String showRegister()方法,注解请求路径@RequestMapping(“register.do”),方法内不需要添加其它代码,直接返回"register"即可。

9.3 测试访问register.do

http://SERVER:PORT/PROJECT/user/register.do

9.4 删除不需要使用的原有代码

在register.jsp文件中,大概在169行左右,找到“/**发起异步GET请求……”注释,然后将其后的ajax请求相关代码删除(包括注释的步骤的1、2、3、4和“//处理响应消息”)。

9.5 提交ajax请求检查用户名是否存在

找到“/**发起异步GET请求……”注释,编写ajax请求:

$.ajax({
	"url":"checkUsername.do",
	"data":"username=" + data, // 此处data变量是前序代码中已经声明并赋值的,表示用户输入的用户名的值
	"type":"GET",
	"datatype":"json",
	"success":function(obj) {
		var css = obj.state == 1 ? "msg-success" : "msg-error";
		$("#uname").next().attr("class", css);
		$("#uname").next().text(obj.message);
	}
});

或者:
$.ajax({
“url” :“checkUsername.do”,
“data”:“username=” + data,
“type”:“GET”,
“datatype”:“json”,
“success”:function(obj) {
var css = obj.state == 1 ? “msg-success” : “msg-error”;
var hintTag = $("#uname").next();
hintTag.attr(“class”, css);
hintTag.text(obj.message);
}
});

9.6 测试

打开/user/register.do,输入用户名,观察右侧的提示

10 完成前端检查手机和邮箱

10.1 完成服务器端Controller中检查手机和邮箱

在UserController中添加检查手机的方法:public ResponseResult checkPhone(String phone),该方法使用@ResponseBody和@RequestMapping("/checkPhone.do")进行注解。

请求路径:/user/checkPhone.do

请求类型:GET

请求参数:phone=130xxxxx

响应结果类型:ResponseResult

响应结果:如果正确,则state=1,message=“当前手机号可以正常使用”;如果错误,则state=-1,message=“当前手机号已经被注册”。

在UserController中添加检查邮箱的方法:public ResponseResult checkEmail(String email),该方法使用@ResponseBody和@RequestMapping("/checkEmail.do")进行注解。

请求路径:/user/checkEmail.do

请求类型:GET

请求参数:[email protected]

响应结果类型:ResponseResult

响应结果:如果正确,则state=1,message=“当前邮箱可以正常使用”;如果错误,则state=-1,message=“当前邮箱已经被注册”。

10.2 测试

http://SERVER:PORT/PROJECT/user/checkPhone.do?phone=13800xxxx

http://SERVER:PORT/PROJECT/user/[email protected]

10.3 完成前端页面提交异步请求,检查手机号和邮箱

在register.jsp中,通过Ctrl+F查找“/**发起异步GET请求”,删除原有的AJAX请求代码,然后自行添加基于jQuery的AJAX请求,验证手机号和邮箱

10.4 测试

打开http://SERVER:PORT/PROJECT/user/register.do页面,输入手机号和邮箱,观察提示是否正确。

11 完成注册

11.1 在UserService中添加注册业务

在UserService接口中定义新的注册方法:

void register(String username, String password, 
	String phone, String email);

在UserServiceImpl中实现新的注册方法:

public void register(String username, String password, 
	String phone, String email);
	User user = new User();
	Date now = new Date();

	user.setUsername(username);
	user.setPassword(password)
	user.setPhone(phone);
	user.setEmail(email);

	user.setDisabled(0);
	user.setCreatedTime(now);
	user.setCreatedUser("System");
	user.setModifiedTime(now);
	user.setModifiedUser("System");

	register(user);
}

11.2 完成Controller中接收注册的请求

在Controller中添加接收注册请求的方法:

@RequestMapping("/handleRegister.do")
@ResponseBody
public ResponseResult handleRegister(
	@RequestParam("uname") String username, 
	@RequestParam("upwd") String password, 
	String phone, String email) {
	// ...

	// 处理业务
	userService.register(username, password, phone, email);
	
	// ...
}

11.3 在前端页面中通过ajax请求完成注册

在register.jsp中,约147行位置,删除setTimeout()函数代码,替换为ajax提交请求。

var data = $("#form-register").serialize();
$.ajax({
	"url":"handleRegister.do",
	"type":"POST",
	"data":data,
	"datatype":"json",
	"success":function(obj) {
		if (obj.state == 1) {
			window.location = "login.do";
		} else {
			alert("注册失败!出现未知错误!请联系管理员!" + obj);
		}
	}
});

11.4 测试

通过前端页面注册新账号,如果注册成功,此时正确的响应应该是404错误页面,并且浏览器的地址栏的资源地址应该是login.do,通过mysql控制台可以看到新的账号已经添加到数据表记录中。

12 登录

12.1 开发登录的Service

在UserService接口中定义登录的方法

/**
 * @return 如果登录成功,返回登录成功的用户的信息,
 * 	如果登录失败,则返回null
 */
User login(String username, String password);

在UserServiceImpl实现类中,基于已有findUserByUsername()方法,实现以上login()方法

12.2 开发登录的Controller

在UserController类中添加处理登录请求的方法

@RequestMapping("/handleLogin.do")
@ResponseBody
public ResponseResult handleLogin(
	@RequestParam("lname") String username,
	@RequestParam("lwd") String password,
	HttpSession session) {
	// ...
	// TODO 处理Session
}

12.3 完成登录的JSP

打开login.jsp,在约152行,找到登录的AJAX代码,删除,然后自行添加基于jQuery登录的AJAX代码。
如果登录成功,重定向到"…/main/index.do"。

***** 经验总结 *****

1 如果某次判断的目标是得到一个值,并且只有2种可能,使用三元表达式(条件表达式)即可,无须使用if或switch语句。

2 java.lang.Void是一个占位符类,这个类本身没有实际的使用意义,仅用于表达“我不在乎这个数据类型”的含义。

3 在使用Spring MVC时,如果已经添加jackson相关依赖,在Controller中使用@ResponseBody注解的方法内返回某个对象,会自动转换为JSON格式的字符串。

13 登录时即时检查用户名是否存在

13.1 实现Controller中处理登录时验证用户名的请求

在Controller中添加以下方法:

@RequestMapping("/checkLoginUsername.do")
@ResponseBody
public ResponseResult 
	checkLoginUsername(String username) {
	// ...
}

该方法可以基于此前开发的checkUsername()方法进行改动!即复制粘贴后再调整,无须全部重新编写。

13.2 前端页面即时检查用户名是否存在

在login.jsp的约127行位置,添加ajax异步请求。

13.3 测试

14 登录时记住用户名和密码

14.1 修改“记住用户名密码”函数名称

在login.jsp中,搜索“//记住用户名密码”,找到对应的Save()函数,将其改名为saveCookie()函数

14.2 删除原有的调用saveCookie()函数

在原有的代码中,登录的提交按钮中调用了“记住用户名和密码”功能的Save()函数:


应该将此处的onclick删除!

14.3 在登录成功后调用saveCookie()函数

修改提交异步登录的代码,当登录成功后,在跳转到主页(window.location = “…/main/index.do”;)之前,调用saveCookie()函数。

14.4 检查判断checkbox选中是否有效

在原Save()方法中,会判断checkbox是否选中,使用的代码是:

if ($("#ck_rmbUser").attr("checked")) {

此次调用的attr()函数很可能无法正确获取到checkbox的选中状态,需要将调用的函数调整为prop()函数,即:

if ($("#ck_rmbUser").prop("checked")) {

或者,也可以使用is()函数进行判断:

if ($("#ck_rmbUser").is(":checked")) {

注意:如果使用is()函数,在参数字符串中,checked字样的左侧需要添加冒号。

15 调整主页(index.jsp)顶部菜单

15.1 规划目标

如果没有登录,顶部菜单保持与设计时的静态页面一致;如果已经登录,则在“我的收藏”左侧显示当前登录的用户的用户名,用户名链接到的资源是/user/user_center.do,并且最右侧不再显示“登录”,而是替换为“退出”,“退出”链接到的资源是/user/logout.do

15.2 在服务端处理登录时处理Session

在UserController中的handleLogin()方法中添加HttpSession session参数,然后在方法体中通过参数session添加数据:

session.setAttribute("uid", ???);
session.setAttribute("username", ???);

15.3 在服务端开发“退出”的功能

@RequestMapping("/logout.do")
public String logout(HttpSession session) {
	session.invalidate();
	return "redirect:../user/login";
}

15.4 在前端页面根据Session判断如何显示

检查Maven依赖中是否已经添加jstl,如果没有,则添加。

在index.jsp中,在最顶部添加引入标签库:

<% @taglib prefix="c" uri="...." %>

找到顶部菜单的html代码,在第1项之前判断是否登录并显示用户名:


	
  • ${sessionScope.username} |
  • 并且在最后一项的“登录”处进行判断,仅当没有Session信息时显示“登录”,并调整此处链接到的地址:

    
    	
  • 登录
  • 在“登录”之前或之后添加“退出”,仅当存在Session信息时显示:

    
    	
  • 退出
  • 15.5 测试

    16 显示用户中心页面

    16.1 设计需求

    请求路径:/user/user_center.do

    原静态页面:personage.html

    16.2 准备JSP文件

    打开现有的某个.jsp文件,复制最顶部的JSP声明代码,粘贴到personage.html文件的顶部,然后将personage.html文件的名称修改为user_center.jsp

    16.3 配置服务器端接收请求并转发到JSP

    在UserController中,添加public String showUserCenter()方法,并使用@RequestMapping("/user_center.do"),方法的返回值是"user_center"。

    17 在user_center.jsp中默认显示当前登录的用户的信息

    17.1 在Controller中转发当前登录的用户的信息

    在现有的showUserCenter()方法中,添加ModelMap参数:

    public String showUserCenter(ModelMap modelMap) {	
    

    在方法内部,通过userService对象的findUserByUsername()方法查询当前登录的用户的完整信息,其中,调用方法时所需的用户名是session.getAttribute(“username”).toString(),然后将找到的用户数据(User对象)添加到ModelMap中:

    modelMap.addAttribute("user", user);
    

    17.2 将用户信息显示在网页中

    显示的语法:${user.username }

    显示数据的位置:从约113行起

    18 提取共用的页面顶部代码

    18.1 把已经确定的顶部代码保存到专门的文件中

    新建header.jsp文件,在该文件顶部添加正确的JSP声明,然后添加引入JSTL标签库。

    打开index.jsp文件,复制“页面顶部”的代码(参考注释,约第20行起至第48行),粘贴到header.jsp中。

    最终,在header.jsp中只会有JSP声明、引入JSTL标签库和“页面顶部”代码,不要保留html标签或body标签等其它代码。

    18.2 修改index.jsp和user_center.jsp

    打开index.jsp,删除文件中“页面顶部”的代码,添加使用jstl导入header.jsp

    
    

    再打开user_center.jsp,处理方式同上。

    19 执行修改用户个人数据

    19.1 持久层

    在cn.tedu.store.mapper.UserMapper接口中添加修改用户个人数据的接口:

    void updateUserInfo(
    	@Param("id") Integer id,
    	@Param("username") String username,
    	@Param("phone") String phone,
    	@Param("email") String email);
    

    在main\resource\mappers\UserMapper.xml中添加映射

    
    	UPDATE 
    		t_user 
    	SET 
    		username=#{username}, 
    		phone=#{phone}, 
    		email=#{email} 
    	WHERE 
    		id=#{id}
    
    

    19.2 业务层

    在UserService接口中设计抽象方法:

    void updateUserInfo(Integer id, String username,
    	String phone, String email);
    

    在UserServiceImpl实现抽象方法:

    public void updateUserInfo(
    	Integer id, String username,
    	String phone, String email) {
    	// 根据session中的username查询用户数据user
    	// 如果此次提交的username为null,
    	// 则使用数据库中查到的user中的用户名作为修改记录的值
    	// 邮箱也是同样的处理方式
    }
    

    19.3 Controller

    请求路径:update_user_info.do

    请求类型:GET

    请求参数:username=xx&phone=xx&email=xx

    响应结果:ResponseResult

    实现:在UserController中添加public ResponseResult updateUserInfo(String username, String phone, String email, HttpSession session)方法,使用@RequestMapping(“update_user_info.do”)和@ResponseBody进行注解,在方法内,先从Session中取出当前登录的用户uid,然后结合用户提交的3个数据一起调用userService中的方法,以执行更新数据库记录,然后返回结果。

    测试

    19.x JSP

    ***** 经验总结 *****

    1 如何处理高度相似的SQL语句

    在本次案例中,根据手机号查询用户是否存在,和根据邮箱查询用户是否存在,甚至根据用户名查询用户是否存在,可以使用极为相似的SQL语句:

    SELECT * FROM t_user WHERE username=?
    SELECT * FROM t_user WHERE phone=?
    SELECT * FROM t_user WHERE email=?
    

    对于这种需求的处理方式可以是:

    1 编写3种不同的查询

    2 在XML中通过进行判断,这种做法只需要写1个SQL即可

    3 基于本项目中用户名、手机号、邮箱都是唯一的,可以使用1个SQL但是给出3个参数即可:SELECT * FROM t_user WHERE username=? OR phone=? OR email=?

    以上3种方式中,优先使用第1种,这种做法的缺点是开发量略多,优点在于执行效果很高,对于此次业务非常简单、SQL代码简短的场景来说,这种做法很方便!第2种做法的缺点是执行效率略低,但是很灵活,更加适用于更复杂的SQL语句,最不推荐的是第3种,这种方式的要求很高,必须传递3个参数,而且每次传递的参数中应该只有1个有效值和2个null值,这样得到的SQL语句中会存在username=null这样的查询条件,而且,如果同时传递2个或3个有效值时,可能查询结果就不是期望的结果!

    2 什么样的数据放在Session中

    1 当前用户的唯一标识,通常是用户的id

    2 高频率使用的数据,例如多个页面都需要显示用户名的话,则把用户名也保存在Session中

    3 在多次请求中都需要使用的数据,即使使用频率不高,但是也可以先使用Session存储,使用完毕后再清除

    	DAO -> Service -> Controller -> JSP
    
    	User u = userService.findUserByUsername(session.getAttribute("username").toString());
    

    20 完成修改个人资料

    20.1 修改前端页面,删除不需要的部分

    删除“头像”和“性别”相关的前端代码:打开user_center.jsp文件,在源代码中查找关键字“我的头像”,即可找到相关代码,然后删除。

    20.2 修改前端页面,使之可以提交表单

    user_center.jsp中,按下Ctrl+O打开Outline,展开标签,点击任意的标签,然后找到引用的personage.css,按下Ctrl键的同时鼠标单击源代码中该文件的名称,以打开personage.css,然后在personage.css文件中,查找“.save”,为这套标签规则中添加“display: block;”

    修改“保存更改”按钮的代码:

    
    

    找到“用户名”、“绑定电话”和“绑定邮箱”的3个输入框(input标签),均指定id属性,以便于后续提交时获取输入框中的值。

    user_center.jsp的末尾部分,添加新的

    无论是新增操作,还是编辑操作,都会打开遮罩,即调用showMask()函数,新增操作时,调用会使用0作为参数,编辑操作时,会使用数据记录的id作为参数,所以,在showMask()函数刚运行时,先把参数用于给全局变量进行赋值,即:

    function showMask(id) {
    	actionId = id;
    	// ...
    }
    

    后续提交表单的postForm()也可以访问到这个全局变量,则修改现有的postForm()函数,判断actionId的值,从而决定提交到的URL,如果是编辑操作,还应该在原本提交的数据的基础之上,再添加提交id:

    // 序列化表单中的数据,即拿到所有数据
    var data = $("#form-recv-info").serialize();
    // 确定表单提交到的路径
    var url;
    // 根据全局变量actionId判断当前是新增操作还是编辑操作
    if (actionId == 0) {
    	// 新增
    	url = "${pageContext.request.contextPath}/address/handle_add_address.do";
    } else {
    	// 编辑
    	url = "${pageContext.request.contextPath}/address/handle_update_address.do";
    	data += "&id=" + actionId;
    }
    // 调用ajax()函数提交请求并处理
    // ... ...
    

    34 调整个人信息相关界面的左侧菜单

    1. 提取左侧菜单的代码

    打开user_center.jsp,找到之间的代码,并剪切。

    新建left_side_menu.jsp文件,用于管理左侧菜单的代码,删除全部代码,并粘贴刚才复制的代码。

    复制user_center.jsp顶部的JSP声明和引用的JSTL的2条语句,并粘贴到left_side_menu.jsp的顶部。

    2. 调整现有的JSP文件

    user_center.jsp原本编写左侧菜单代码的位置添加:

    
    

    还有user_password.jspaddress.jsp中,也删除原有代码,替换为上面的导入语句。

    3. 调用左侧菜单中各菜单项的超链接

    修改“地址管理”、“我的信息”和“安全管理”这3个超链接的地址。

    建议使用绝对路径表示,并与UserController中的配置保持一致。

    4.

    35 完成“设为默认”的超链接点击及修改数据功能

    35.1 持久层

    在AddressMapper.java接口中声明2个方法:

    void setDefaultAddress(
    	@Param("id") Integer id,
    	@Param("uid") Integer uid);
    
    void cancelAllDefaultAddress(Integer uid);
    

    在AddressMapper.xml中配置以上2个方法的映射:

    
    	UPDATE 
    		t_address 
    	SET 
    		is_default=1 
    	WHERE 
    		id=#{id} AND uid=#{uid}
    
    
    
    	UPDATE 
    		t_address 
    	SET 
    		is_default=0 
    	WHERE 
    		uid=#{uid}
    
    

    35.2 业务层

    在AddressService接口中声明“设为默认”的抽象方法:

    void setDefaultAddress(Integer id, Integer uid);
    

    在AddressServiceImpl实现类实现以上方法:

    @Override
    public void setDefaultAddress(Integer id, Integer uid) {
    	addressMapper.cancelAllDefaultAddress(uid);
    	addressMapper.setDefaultAddress(id, uid);
    }
    

    36.3 控制器层

    在AddressController中声明处理“设为默认”请求的方法:

    @RequestMapping("/set_default.do")
    @ResponseBody
    public ResponseResult setDefaultAddress(
    	Integer id, HttpSession session) {
    
    }
    

    36.4 前端界面

    首先,定义提交请求的函数:

    function setDefaultAddress(id) {
    	var url = "........";
    	$.ajax({
    		"url": url,
    		"data": "id=" + id,
    		"type": "GET",
    		"dataType": "json",
    		"success": function(obj) {
    			// 刷新列表
    		}
    	});
    }
    

    然后,修改HTML部分(在JS代码定义的模版中)的“设为默认”的点击响应:

    onclick="setDefaultAddress(%id_val%)"
    

    36.5 测试

    正常测试

    ***** 经验总结 *****

    什么情况下会访问不到Session

    在正常情况下,通过HttpSession对象调用了setAttribute()后,在后续的Controller运行过程中都可以正确的获取到Session中的数据,如果获取不到,可能是因为:

    1. 浏览器不支持Cookie

    2. 访问超时(当前操作和前序操作间隔时间太长)

    3. 操作过程中曾经关闭浏览器

    4. 服务器曾经关闭或重启过

    通过Java读取数据库并将数据写入到Excel中

    1 使用逗号分隔值文件

    将从数据库中读到的List集合进行遍历,然后结合IO技术各某个扩展名为.csv的文件中写入即可:

    id,username,password
    1,chengheng,1234
    2,liucs,5678
    

    关键在于目标Excel文件中各单元格的数据之间使用逗号进行分隔。

    这种做法的缺陷在于最终的文件扩展名是.csv,且这种文件的内容是没有任何格式的!即不能设置字段样式,也不可以使用公式,或者设置数据类型等。

    2 使用Apache POI

    POI是Apache官方提供的用于访问Office文档的框架,可以访问Word、Excel、PPT……

    3 使用其它第三方框架

    37 优化“删除收货人”的逻辑

    37.1 持久层

    在AddressMapper.java接口中声明以下抽象方法:

    /**
     * 获取当前登录的用户的第1条记录的ID
     * @param uid 当前登录的用户的uid
     * @return 当前登录的用户的第1条记录的ID
     */
    Integer getFirstRecordId(Integer uid);
    
    /**
     * 获取当前登录的用户的数据的数量
     * @param uid 当前登录的用户的uid
     * @return 当前登录的用户的数据的数量
     */
    Integer getRecordCountByUid(Integer uid);
    

    在AddressMapper.xml中配置映射,配置时,2个对应的节点以配置读取数据。

    39.3 业务层

    创建cn.tedu.store.service.GoodsCategoryService接口,并在接口中声明获取数据的方法:

    List getGoodsCategoryList(Integer page);
    

    创建cn.tedu.store.service.GoodsCategoryServiceImpl实现类,实现以上接口,该类使用@Serivce("goodsCategoryService")进行注解,在类中,声明@Resource private GoodsCategoryMapper goodsCategoryMapper;用于访问持久层,重写抽象方法时,调用该对象的匹配的方法即可。

    39.4 控制器层

    创建cn.tedu.store.controller.GoodsCategoryController类,并使用@Controller@RequestMapping("/goods_category")进行注解,添加@Resource private GoodsCategoryService goodsCategoryService对象,然后在类中添加方法:

    @RequestMapping("/list.do")
    public String showGoodsCategoryList(
    	Integer page,
    	ModelMap modelMap) {
    	List data;
    
    	data = goodsCategoryService.getGoodsCategoryList(page);
    	modelMap.addAttribute("data", data);
    
    	return "goods_category_list";
    }	
    

    39.5 前端页面

    创建goods_category_list.jsp文件,并在顶部添加配置以使用JTSL标签库,在中,创建一个表格:

    
    	

    40 完成商品信息列表的测试

    ***** 经验总结 *****

    怎么写设计逻辑

    1 不会写代码时,用中文写,写得差不多了,再翻译成程序代码。

    2 在设计逻辑时,尽量的考虑各种可能,避免设计的逻辑不完整,后续可能出现BUG。

    3 在设计逻辑时,一次性考虑得非常完整并不太现实,但是,至少考虑数据最极端的状况!

    4 业务逻辑不一定有绝对的正确或错误,合理,就是好的。

    解决MySQL中可能存在的乱码问题

    1 查看MySQL使用的编码

    在MySQL控制台中,输入\s查看状态,在显示端口号的上方会有4条编码信息,通常应该4条都是utf8,如果不是,则可能存在乱码的风险。

    2 查看哪些配置使用的其它编码

    在MySQL控制台中,输入show variables like '%character%';以查看哪些MySQL变量设置了哪种编码

    3 重新设置编码格式

    找出值不是utf8的变量,然后通过以下指定重新设置:

    set character_set_xx=utf8;
    

    将除了filesystem变量以外的其它变量的值都设置为utf8即可。

    注意:以上修改的是运行时的变量,退出后再重新进入控制台则无效。

    -------------- 草稿 --------------

    哪些功能对“设为默认”可能产生影响?
    删除:如果删除的数据是“设为默认”的那一条,则,将当前登录的用户的第1条收货人数据设置为“默认”
    
    Integer getFirstRecordId(Integer uid);
    SELECT id FROM t_address WHERE uid=#{uid} ORDER BY id LIMIT 0,1
    
    Integer getRecordCountByUid(Integer uid)
    SELECT COUNT(id) FROM t_address WHERE uid=#{uid}
    
    AddressService中:
    public void delete(Integer id, Integer uid) {
    	// 根据id获取要删除的数据的完整信息
    	// 这个过程必须在执行删除之前
    	// 否则后续将无法判断此次删除的数据是否是默认收货人
    	Address addr = getAddressById(id, uid);
    
    	// 执行删除
    
    	// 如果删除后数据量为0,则无须再考虑将哪条设置为默认
    	if (addressMapper.getRecordCountByUid(uid) == 0) {
    		// 结束
    		return;
    	}
    
    	// 如果刚才删除的数据是默认收货人,还需要设置新默认收货人
    	if (addr.isDefault()) {
    		// 获取当前登录的用户的第1条数据的id
    		Integer newDefaultId = addressMapper.getFirstRecordId(uid);
    
    		// 将某数据设置为默认
    		setDefaultAddress(newDefaultId, uid);
    	}
    }
    

    41 显示主页中推荐的3个电脑商品

    41.1 要求

    只显示笔记本电脑,只需要3条数据,数据根据优先级排列,优先级值越大表示优先级更高。

    41.2 持久层

    持久层处理当前业务的数据存取方法为:

    List getGoodsByCategory(
    	@Param("categoryId") Integer categoryId, 
    	@Param("offset") Integer offset,
    	@Param("pageCount") Integer pageCount
    );
    

    配置持久层的映射。

    41.3 业务层

    声明业务层接口中的抽象方法:

    List getGoodsByCategory(
    	Integer categoryId, 
    	Integer offset, 
    	Integer pageCount);
    

    在实现类中,通过goodsMapper完成以上方法。

    41.4 控制器层

    在首页对应的MainController中,找到已有的showIndex()方法,在该方法中读取首页需要显示的数据,封装到ModelMap参数中,最后,将请求转发给index.jsp

    41.5 前端页面

    通过EL表达式和JSTL显示数据

    42 在首页显示“电脑,办公”的分类

    42.1 分析

    经查询,“电脑,办公”的是一级分类,id是161,归属该分类的有7个二级分类,只需要根据sort_order的顺序排列方式取出前3个,此次的查询结果是“电脑整机”,“电脑配件”,“外设产品”,对应的id分别是:162,171,186。

    然后根据这些二级分类再查询对应的三级分类。

    以上需要进行4次查询,查询结果使用4个List集合表示,然后全部转发给JSP页面。

    42.2 持久层

    在GoodsCategoryMapper.java接口中:

    List getGoodsCategoryListByParentId(
    	@Param("parentId") Integer parentId,
    	@Param("offset") Integer offset,
    	@Param("pageCount") Integer pageCount
    );
    

    在GoodsCategoryMapper.xml中配置映射:

    SELECT 
    	XX 
    FROM  
    	t_goods_category  
    WHERE 
    	parent_id=#{parentId} 
    	AND status=1 
    ORDER BY 
    	sort_order ASC 
    LIMIT #{offset}, #{pageCount}
    

    42.3 业务层

    在GoodsCategoryService接口中添加:

    List getGoodsCategoryListByParentId(
    	Integer parentId, 
    	Integer offset, 
    	Integer pageCount
    );
    

    在GoodsCategoryServiceImpl实现类中实现以上方法

    42.4 控制器层

    在MainController的showIndex()方法中,先获取parent_id=161的3个分类信息:

    List categories161 = goodsCategoryService.getGoodsCategoryListByParentId(161, 0, 3);
    

    然后根据以上得到的3个二级分类,获取对应的三级分类:

    List subCategories1 = goodsCategoryService.getGoodsCategoryListByParentId(categories161.get(0).getId(), 0, 50);
    List subCategories2 = goodsCategoryService.getGoodsCategoryListByParentId(categories161.get(1).getId(), 0, 50);
    List subCategories3 = goodsCategoryService.getGoodsCategoryListByParentId(categories161.get(2).getId(), 0, 50);
    

    以上使用了3个List集合表示3个三级分类,当然,也可以只使用1个更大的List将这3个三级分类存储起来:

    List> subCategories 
    	= new ArrayList;
    

    然后,遍历之前获取的3个二级分类,来确定所有三级分类的数据:

    int i = 0;
    for (GoodsCategory goodsCategory : categories161) {
    	subCategories = goodsCategoryService.getGoodsCategoryListByParentId(goodsCategory.getId(), 0, 50);
    	i++;
    }
    

    然后,将获取到的数据添加到ModelMap中以转发。

    42.5 前端界面

    在index.jsp中显示数据。

    43 根据分类id显示商品列表

    43.1 持久层

    将GoodsMapper.java接口中已有的getGoodsListByCategory()方法改名为getGoodsListCategoryId(),此次改动只是为了增加方法名称的识别度,没有功能上的调整。

    注意:GoodsMapper.xml映射文件中的id也一并调整

    将GoodsMapper.xml中的配置中,SQL语句最后的LIMIT部分使用动态SQL进行处理:

    
    	LIMIT #{offset}, #{pageCount}
    
    

    由于持久层的方法名称发生了变化,就会导致业务层的调用出错,所以,使用同样的方式对业务层、控制器层进行调整。

    43.2 业务层

    可参考第42步

    43.3 控制器层

    可参考第42步

    动态SQL

    使用动态SQL可以使用“编程”的方式处理MyBatis中的SQL映射的XML文件,使得开发过程中,不用反复创建高度相似的SQL映射!

    例如在MyBatis中的SQL映射中,使用对某个条件进行判断,就可以动态的决定最终生成的SQL语句是否包括SQL语句中的某个部分!

    但是,对于简单的SQL语句而言,其实使用动态SQL并没有太大的优势,毕竟动态SQL希望解决的问题是:“简化开发,便于统一管理”,如果SQL语句本身就很简单,就没有必要使用动态SQL,因为一旦使用了动态SQL,意味着生成SQL语句会更耗时(虽然这个耗时的量非常小),同时,同一个动态SQL可能可以应用于多个应用场景,则命名方面就会更加泛化,无法精准的对应到某个业务,使得程序的可阅读性会降低!

    由于在开发项目时,都会使用特定的项目结构,例如MVC,使得项目中各个接口、类都负责对应的功能,例如Mapper/DAO是用于处理持久层,而Service是处理业务的,Controller是控制器……与数据库中的数据访问对应的就是Mapper/DAO,它的定位是处理数据,并不是关心业务,真正应该处理业务的是Service!

    所以,如果动态SQL比较简单,不过多的干涉业务,则可以使用动态SQL,如果动态SQL中尝试表现的业务比较复杂,则应该不使用动态SQL,而是在Service中对业务进行管理!!!

    动态SQL的目标是1次配置却可以扩展出更多的功能,而并不是判断或验证甚至更加复杂的业务!

    在网页中标签中的alt和title属性

    alt属性用于:当图片无法加载时,显示alt属性的值,该值是一段文字

    title属性用于:当鼠标悬停在图片上时,显示这段文字

    因为这2个属性的值都只在特定情况下显示,如果没有出现这些情况,是看不到这些文字的!

    但是,强烈推荐配置这2个属性,特别是动态数据的图片的这2个属性(也就是说例如素材图片就真的不用配置这个属性了,而商品等对于站点有价值的数据的图片都应该配置)。

    之所以这样做,是因为在网页中源代码中体现了例如“联想笔记本”的字样后,更加利于站点被搜索引擎根据“联想笔记本”关键字搜索得到!!!

    基于以上的思想,还需要另外注意一个问题:并不是所有的数据都应该通过ajax来获取,虽然通过ajax方式在有些场景中可以提高用户体验,并且看似很有格调,但是,通过ajax获取的数据都是变量,其具体的值不会体现在网页的源代码中,也就是说这样的数据是无利用SEO的。

    更多详情请参见百度的SEO文档。

    #{categoryId}:分类ID,163表示笔记本电脑
    #{offset}:翻页数据偏移量
    #{pageCount}:每页显示的数据量
    
    SELECT
    	???
    FROM
    	t_goods
    WHERE
    	category_id = #{categoryId} 
    	AND num > 0 
    	AND status = 1
    ORDER BY 
    	priority DESC 
    LIMIT #{offset}, #{pageCount}
    

    读取某个商品分类中的商品的数量

    1 持久层

    在GoodsMapper.java接口中声明:

    Integer getRecordCountByCategoryId(Integer categoryId);
    

    在GoodsMapper.xml配置映射:

    SELECT COUNT(id) FROM t_goods
    WHERE 
    	category_id=#{categoryId}
    	AND status=1 AND num>0
    

    2 业务层

    (直接写)

    3 控制器

    4 前端页面

    在前端页面中显示分页信息

    根据商品ID读取商品信息

    1 持久层

    在GoodsMapper.java中创建新的方法,用于根据id获取商品信息:

    Goods getGoodsById(Integer id);
    

    另外,还要根据以上方法找到的商品信息中的item_type去查找相同品类、具体参数略不同的相关商品:

    List getGoodsListByItemType(String itemType);
    

    在GoodsMapper.xml配置以上2个方法对应的映射:

    
    
    
    

    2 业务层

    (标准流程)

    3 控制器层

    GoodsController中添加处理“显示商品详情”请求的方法:

    @RequestMapping("/details.do")
    public String showGoodsDetails(
    	@RequestParam("id") Integer goodsId,
    	ModelMap modelMap) {
    	// 通过Service读取id对应的商品
    	// 通过Service根据以上商品的itemType读取同品类商品列表
    
    	// 测试输出以上2项数据
    
    	return "";
    }
    

    4 前端页面

    找到product_details.html,在该文件中添加JSP声明,然后替换顶部(headers),并将文件重命名为goods_details.jsp

    将数据通过EL表达式替换到页面中。

    通过商品分类的ID/父级ID获取商品分类信息

    1 分析

    该功能用于“商品详情”页的上方的路径导航,即“首页 > 电脑 -> 笔记本”这类导致

    2 持久层

    GoodsCategoryMapper.java接口中:

    GoodsCategory getGoodsCategroyById(Integer id);
    

    GoodsCategoryMapper.xml中配置映射

    3 业务层

    (标准流程)

    4 前端页面

    GoodsController中,声明GoodsCategoryService对象

    @Resource
    private GoodsCategoryService goodsCategoryService;
    

    然后,在现有的showGoodsDetails()方法中:

    在已经读取到商品信息后,根据商品信息中的categoryId获取所属的三级分类信息:

    GoodsCategory goodsCategory3 = goodsCategoryService
    	.getGoodsCategroyById(
    		goods.getCategoryId());
    

    根据三级分类信息找到所属的二级分类:

    GoodsCategory goodsCategory2 = goodsCategoryService
    	.getGoodsCategroyById(
    		goodsCategory3.getParentId());
    

    再根据二级分类信息找到所属的一级分类:

    GoodsCategory goodsCategory1 = goodsCategoryService
    	.getGoodsCategroyById(
    		goodsCategory2.getParentId());
    

    最后,将以上3个分类的数据添加到ModelMap中以转发,并在前端页面中显示。

    Goods
    [
    id=10000008, categoryId=163, itemType=燃 7000经典版, title=戴尔Dell 燃700R1605银色, sellPoint=仅上海,广州,沈阳仓有货!预购从速!, price=4549, num=99999, barcode=null, image=/images/portal/11DELLran7000R1605Ssilvery/collect.png, status=1, priority=32, createdTime=null, createdUser=null, modifiedTime=null, modifiedUser=null
    ]
    [
    Goods
    [id=10000008, title=戴尔Dell 燃700R1605银色, num=99999],
    Goods
    [id=10000007, title=戴尔Dell 燃700金色, num=99999]
    ]

    46 预备知识点:MYSQL中的触发器

    46.1 常用操作

    CREATE DATABASE

    CREATE TABLE

    INSERT / UPDATE/ DELETE / SELECT

    46.2 Trigger(触发器)

    当数据操作满足特定的情况,会自动执行指定的另一套SQL指令。

    例如存在部门信息表:

    t_department
    ID	NAME
    1	Java
    2	PHP
    3	UI
    
    CREATE TABLE t_department (
    	id int auto_increment,
    	name varchar(50),
    	primary key(id)
    ) DEFAULT CHARSET=UTF8;
    
    INSERT INTO t_department (name) VALUES ('JAVA');
    INSERT INTO t_department (name) VALUES ('PHP');
    INSERT INTO t_department (name) VALUES ('UI');
    

    并且存在员工信息表:

    t_employee
    ID	NAME	DEPARTMENT_ID
    1	JACK	2
    2	TOM		3
    3	JIM		3
    4	BILLY	1
    5	DAVID	2
    
    CREATE TABLE t_employee (
    	id int auto_increment,
    	name varchar(50),
    	department_id int,
    	primary key(id)
    ) DEFAULT CHARSET=UTF8;
    
    INSERT INTO t_employee (name, department_id) VALUES ('JACK', 2);
    INSERT INTO t_employee (name, department_id) VALUES ('TOM', 3);
    INSERT INTO t_employee (name, department_id) VALUES ('JIM', 3);
    INSERT INTO t_employee (name, department_id) VALUES ('BILLY', 1);
    INSERT INTO t_employee (name, department_id) VALUES ('DAVID', 2);
    

    当需要删除某条部门信息表中的数据时,员工信息表中的数据也应该有相关的处理,例如删除ID=2的部门之后,在员工信息表中,可以将原来DEPARTMENT_ID=2的部门的员工设置新的部门信息DEPARTMENT_ID=0

    使用触发器可以解决当某张表中的数据被执行了INSERT/DELETE/UPDATE操作时产生关联操作。

    使用触发器的示例:

    CREATE TRIGGER xx		// 创建触发器并命名
    AFTER				// AFTER可以被替换为BEFORE
    DELETE			// 可以是INSERT/DELETE/UPDATE中的任何一种
    ON table 			// 原来的操作是对哪张数据表的操作
    FOR EACH ROW		// 在MYSQL中是固定的
    BEGIN				// 准备开始执行另一张表的数据操作
    操作				// 对另一张表的操作,可以是多条SQL语句
    END				// 结束
    

    在使用触发器,原来的操作类型和BEFORE/AFTER会共同构成6种触发条件:

    BEFORE INSERT
    BEFORE UPDATE
    BEFORE DELETE
    AFTER INSERT
    AFTER UPDATE
    AFTER DELETE
    

    每张数据表中的触发器只能配置以上每种条件中各1种,即同一张数据表最多存在6种触发器。

    在创建触发器时,可以在满足触发条件后执行多条SQL指令,即以上代码格式中的“操作”可以是多条指令,如果确实需要有多条指令,这些指令应该使用分号(;)进行分隔,但是,当编写“操作”时,创建触发器的语句尚未结束,一旦执到“操作”中的分号时,MYSQL就会开始尝试创建触发器,就会导致创建失败!

    在MYSQL中,使用DELIMITER指令可以改变结束标识,即默认情况下,MYSQL中把分号作为每条语句的结束,但是,可以更改!例如:

    DELIMITER $
    

    一旦执行以上语句后,MYSQL只会将$作为结束标识,而不再使用分号!

    需要注意的是:一旦改变了结束标识,在创建完触发器,应该还原为原来的结束标识:

    DELIMITER ;
    

    针对以上列举的模拟数据和模拟需求,创建触发器应该是:

    DELIMITER $
    CREATE TRIGGER trigger_after_delete_department
    AFTER DELETE ON t_department
    FOR EACH ROW
    BEGIN
    	UPDATE t_employee SET department_id=0 WHERE department_id=old.id;
    END $
    DELIMITER ;
    

    以上示例代码中,触发条件对应的“操作”中,old是触发执行条件时,被操作的那条数据。

    47 购物车之添加数据的功能

    47.1 规划数据表

    根据设计好的静态页面规划购物车的数据表:

    CREATE TABLE t_cart (
    	id int auto_increment,
    	user_id int comment '用户id',
    	goods_id int comment '商品id',
    	goods_title varchar(100) comment '商品名称/标题',
    	goods_image varchar(500) comment '商品的图片',
    	goods_price int(20) comment '商品单价',
    	num int comment '购买数量',
    	primary key(id)
    );
    

    规划完成后,创建数据表。

    47.2 设计实体类

    创建cn.tedu.store.bean.Cart实体类

    public class Cart {
    	private Integer id;
    	private Integer userId;
    	private Integer goodsId;
    	private String goodsTitle;
    	private String goodsImage;
    	private Integer goodsPrice;
    	private Integer num;
    	// 无参和全参构造方法
    	// SET/GET
    	// hashCode和equals
    	// toString
    	// Serializable
    }
    

    47.3 持久层

    创建cn.tedu.store.mapper.CartMapper.java接口,声明:

    void add(Cart cart);
    

    创建resources\mappers\CartMapper.xml配置文件,并添加配置:

    
    	INSERT INTO t_cart 
    		(...)
    	VALUES
    		(...)
    
    

    47.4 业务层

    (标准流程)

    47.5 控制器层

    将接收到AJAX请求,所以,响应数据类型应该是ResponseResult。

    创建cn.tedu.store.controller.CartController,添加必要的注解,然后添加处理请求的方法:

    @Resource
    private CartService cartService;
    
    @RequestMapping("add.do")
    @ResponseBody
    public ResponseResult handleAddCart(
    	Cart cart, HttpSession session) {
    	// 从session获取uid并封装到cart中
    	cartService.add(cart);
    	// 返回
    }
    

    47.6 前端页面

    在选择购买数量的部分的源代码调整为:

    
    

    数量:

    然后调整“加入购物车”的按钮ID:

    
    	
    	加入购物车
    
    

    最后,添加Javascript部分代码:

    
    

    48 显示购物车

    48.1 持久层

    CartMapper.java接口中添加方法:

    List getCartList(Integer uid);
    

    CartMapper.xml中配置映射

    
    

    48.2 业务层

    (标准流程)

    48.3 控制器层

    CartController中声明处理请求的方法:

    @RequestMapping("/list.do")
    public String showCartList(
    	HttpSession session, ModelMap modelMap) {
    	// 获取当前登录的用户ID
    	Integer uid = getUidFromSession(session);
    	// 根据用户ID获取购物车列表
    	List carts = cartService.getCartList(uid);
    	// 将数据封装到ModelMap中以转发
    	modelMap.addAttribute("carts", carts);
    	// 执行转发
    	return "cart_list";
    }
    

    48.4 前端页面

    cart.html调整为cart_list.jsp,源代码需要在顶部添加JSP声明和使用JSTL,并引用header.jsp

    cart_list.jsp中找到显示购物车数据的代码部分,使用JSTL循环此前转发过来的carts,并将数据填充到界面中。

    关于form中的隐藏域

    设置标签为type="hidden"表示隐藏域。

    隐藏域的特点是在界面上完全不显示,但是,隐藏域的namevalue可以随着整个form的提交(submit)一起把这些数据提交到服务器。

    通常会通过JSP来确定隐藏域中的值,或通过Javascript代码来控制其中的值。

    使用隐藏域的典型需求就是编辑相关数据,通常编辑数据时会根据数据的id进行显示,当提交时也需要根据id最终执行数据库的update语句,由于显示编辑界面时,并没有哪个输入框或者相关控件表示数据的id,却又需要提交这个数据,所以,通常会在显示界面时,就把编辑的数据的id放在隐藏域中,最终该id值就可以随着form的所有数据一并提交。

    49 创建订单

    49.1 规划数据表

    规划订单的数据表:

    CREATE TABLE t_order (
    	id int auto_increment,
    	user_id 		int,
    	recv_person 	varchar(16)		comment '收货人姓名',
    	recv_phone		varchar(50)		comment '收货人手机号',
    	recv_district 	varchar(50)		comment '收货人省市区',
    	recv_addr		varchar(50)		comment '收货人详细地址',
    	recv_addr_code	char(6)			comment '收货人邮编',
    	price			int				comment '订单中的商品总价',
    	status			int				comment '订单状态',
    	order_time		datetime		comment '下单时间'
    	goods_count		int				comment	'商品总数'
    	primary key(id)
    );
    

    以下是订单中的商品表:

    CREATE TABLE t_order_item (
    	id 				int auto_increment,
    	order_id		int				comment '订单id'
    	goods_id 		int 			comment '商品id',
    	goods_title 	varchar(100) 	comment '商品名称/标题',
    	goods_image 	varchar(500) 	comment '商品的图片',
    	goods_price 	int(20) 		comment '商品单价',
    	num 			int 			comment '购买数量',
    	primary key(id)
    );
    

    综上,订单信息需要通过至少2张数据表才可以实现!

    49.2 实体类

    创建以上2张数据表对应的实体类:OrderOrderItem

    49.3 持久层

    创建cn.tedu.store.mapper.OrderMapper接口,并添加抽象方法:

    void add(Order order);
    
    void add(OrderItem orderItem);
    

    OrderMapper.xml中配置以上2个方法的映射。

    49.4 业务层

    创建cn.tedu.store.service.OrderService接口,并添加抽象方法:

    void add(Order order);
    
    void add(OrderItem orderItem);
    
    void createOrder(
    	Order order, List orderItems);
    

    创建cn.tedu.store.service.OrderServiceImpl实现类,实现以上抽象方法:

    @Resource
    private OrderMapper orderMapper;
    @Resource
    private OrderItemMapper orderItemMapper;
    
    public void add(Order order) {
    	// ....
    }
    
    public void add(OrderItem orderItem) {
    	// ....
    }
    
    /**
     * 创建订单
     */
    @Transcational
    public void createOrder(
    	Order order, List orderItems) {
    	// 第1步:增加订单表中的数据
    	this.add(order);
    	// 第1步:获取刚增加的数据的id
    	Integer orderId = order.getOrderId();
    
    	// 第2步:增加订单商品表中的数据
    	for(int  = 0; i < orderItems.size(); i++) {
    		// 为每条订单商品数据添加刚才生成的订单ID
    		orderItems.get(i).setOrderId(orderId);
    		// 增加数据
    		this.add(orderItems.get(i));
    
    		// 消库存
    	}
    }
    

    基于SSM的事务

    准备工作

    在Spring的XML配置文件中必须配置TransactionManager和注解驱动:

    
    	
    	
    
    
    
    
    

    使用事务

    应该保证在Service中存在与DAO/Mapper中对应的方法,即在DAO/Mapper中有什么方法,在Service中就存在直接调用它们的方法。

    当某个方法需要以事务的方式执行,则可以使用@Transactional进行注解,则在这个方法执行的所有数据库的增删改操作都会以事务的方式执行,即一系列的操作将整体成功,或整体失败!

    关于@Transactional注解,还可以用于注解整个类,则这个类中所有的方法都会以事务的方式执行,当然,这种做法是不推荐的!原因有:a)并不是所有方法都需要是以事务的方式执行的,例如单条增删改的操作,或者查询的操作;b)该注解还可以做其它配置,每个方法的配置可以不同!

    最后,Spring也严重不推荐使用@Transactional对接口或抽象方法进行注解!

    事务的传播

    在使用@Transactional注解时,可以配置注解参数,其中,propagation参数就是用于配置其传播特性的!语法为:

    @Transactional(propagation=TransactionDefinition.PROPAGATION_SUPPORTS)
    

    该配置的属性取值为枚举值,推荐使用TransactionDefinition中以PROPAGATION_作为前缀的常量,取值有:

    PROPAGATION_REQUIRED
    PROPAGATION_SUPPORTS
    PROPAGATION_MANDATORY
    PROPAGATION_REQUIRES_NEW
    PROPAGATION_NOT_SUPPORTED
    PROPAGATION_NEVER
    PROPAGATION_NESTED
    

    如果没有配置propagation,则默认值是PROPAGATION_SUPPORTS,表示支持事务,当原本存在事务时,当次的执行将加入到事务中。

    以上各PROPAGATION_值的意义可参考TransactionDefinition的注释,或查阅资料。

    小结

    如果需要使用事务,先在Spring的XML文件中配置。

    在编写Service类时,应该创建与DAO/Mapper中匹配所有方法,与其保持一致!然后,当需要以事务的方法执行时,另使用某个方法调用Service自身的方法来执行,同时,使用@Transactional进行注解!

    你可能感兴趣的:(电商项目)

    id parent_id name status sort_order is_parent
    ${category.id} ${category.parentId} ${category.name} ${category.status} ${category.sortOrder} ${category.isParent}