电商项目

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,在这个配置文件中,配置注解驱动<mvc:annotation-driven />和视图解析器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
<util:properties id=“dbConfig”
location=“classpath:db.properties” />

<bean id="dataSource"
	class="xxx.apache.xxx.BasicDataSource">
	<property name="url" 
		value="#{dbConfig.url}" />
	<property name="driverClassName" 
		value="#{dbConfig.driver}" />
	... ...
</bean>

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,清空原有的文件的配置。

添加节点并配置:

<mapper namespace="cn.tedu.store.mapper.UserMapper">
	<insert id="" 是否自动生成id 自动生成的id的字段名 参数类型>
		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}
		)
	</insert>
</mapper>

4.4 配置spring-dao.xml

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

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
	<property name="basePackage" 
		value="cn.tedu.store.mapper" />
</bean>

<bean id="sqlSessionFactory" 
	class="org.mybatis.spring.SqlSessionFactoryBean">
	<property name="dataSource" 
		ref="dataSource" />
	<property name="mapperLocations" 
		value="classpath:mappers/*.xml" />
</bean>

4.5 测试

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

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

5.1 在UserMapper接口中添加方法

User findUserByUsername(String username)

5.2 配置映射

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

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

<select id="" 返回结果类型>
	SELECT 
		id, username, password, 
		phone, email, disabled,
		created_user createdUser, 
		created_time createdTime,
		modified_user modifiedUser, 
		modified_time modifiedTime  
	FROM 
		t_user 
	WHERE
		username=#{username}
</select>

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个方法,例如:

<select id="getRecordCountByEmail"
	resultType="java.lang.Integer">
	SELECT 
		COUNT(id) 
	FROM 
		t_user 
	WHERE 
		email=#{email}
</select>

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

<bean id="transactionManager"
	class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
	<property name="dataSource" 
		ref="dataSource" />
</bean>

<tx:annotation-driven 
	transaction-manager="transactionManager" />

8.2 规划请求与响应

请求路径:/user/checkUsername.do

请求类型:GET

请求参数:username=admin

响应结果类型:ResponseResult

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

8.3 创建通用响应结果类

public class ResponseResult<T> {
	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=xxxx@tedu.cn

响应结果类型:ResponseResult

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

10.2 测试

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

http://SERVER:PORT/PROJECT/user/checkEmail.do?email=xxx@tedu.cn

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<Void> 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<Void> 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<Void> 
	checkLoginUsername(String username) {
	// ...
}

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

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

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

13.3 测试

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

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

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

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

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

<input class="button_login" type="button" value="登录" id="bt-login" onclick="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项之前判断是否登录并显示用户名:

<c:if test="${sessionScope.uid != null}">
	<li>
		<a href="${pageContext.request.contextPath}/user/user_center.do">${sessionScope.username}</a>
		<b>|</b>
	</li>
</c:if>

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

<c:if test="${sessionScope.uid == null}">
	<li><a href="${pageContext.request.contextPath}/user/login.do">登录</a></li>
</c:if>

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

<c:if test="${sessionScope.uid != null}">
	<li><a href="${pageContext.request.contextPath}/user/logout.do">退出</a></li>
</c:if>

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

<c:import url="header.jsp"></c:import>

再打开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 id="updateUserInfo">
	UPDATE 
		t_user 
	SET 
		username=#{username}, 
		phone=#{phone}, 
		email=#{email} 
	WHERE 
		id=#{id}
</update>

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;”

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

<div>
	<a class="save" href="###" onclick="doSave()">保存更改</a>
</div>

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

user_center.jsp的末尾部分,添加新的<script>标签,定义doSave()函数,在函数中,获取3个输入框中的值,后续将通过ajax方式提交表单。

20.3 通过ajax提交表单

在doSave()函数中,提交ajax请求

20.4 测试

通过页面正常访问,测试完整的修改功能

21 修改密码

21.1 持久层

UserMapper.java接口中声明修改密码的方法:

void updatePassword(
	@Param("id") Integer uid, 
	@Param("password") String newPassword);

UserMapper.xml映射文件中添加修改密码的配置:

<update id="updatePassword">
	UPDATE 
		t_user 
	SET 
		password=#{password} 
	WHERE 
		id=#{id}
</update>

21.2 业务层

UserService接口中声明修改密码的方法:

int updatePassword(
	Integer uid, 
	String oldPassword,
	String newPassword);

UserServiceImpl实现类中实现以上方法:

public int updatePassword(
	Integer uid, String oldPassword, String newPassword) {
	// 根据uid获取用户数据
	User user = userMapper.findUserById(uid);

	// 判断oldPassword和刚才获取的数据中的密码是否一致
	if (user.getPassword.equals(oldPassword)) {
		// 如果一致,则通过持久层修改密码
		userMapper.updatePassword(uid, newPassword);
		// 返回“成功”
		return 1;
	} else {
		// 如果不一致,不允许修改密码,返回“失败”
		return -1;
	}
}

21.3 控制器

请求路径:update_password.do

请求类型:POST

请求参数:old_password=xx&new_password=xx

响应结果:ResponseResult<Void>

UserController.java中,添加处理修改密码请求的方法:

@RequestMapping(method=RequestMethod.POST,
	value="update_password.do")
@ResponseBody
public ResponseResult<Void> handleUpdatePassword(
	@RequestParam(name="old_password") String oldPassword,
	@RequestParam(name="new_password") String newPassword,
	HttpSession session) {
	// 声明结果
	ResponseResult<Void> rr = new ResponseResult<Void>();

	// 从Session中获取当前登录的用户的id
	Integer uid = Integer.valueOf(
		session.getAttribute("uid").toString());
	// 调用userService执行,并获取结果
	int state = userService.updatePassword(
		uid, oldPassword, newPassword);
	// 处理结果
	String message = state == 1 ? "修改密码成功" : "修改密码失败!原密码错误";
	rr.setState(state);
	rr.setMessage(message);

	// 返回结果
	return rr;
}

21.4 前端页面

先找到index.jsp文件,复制该文件中的顶部声明的代码到personal_password.html,然后复制index.jsp的顶部菜单代码到personal_password.html,再将personal_password.html重命名为user_password.jsp

在UserController中添加方法,使得请求可以转发到user_password.jsp

@RequestMapping("/user_password.do")
public String showUpdatePassword() {
	return "user_password";
}

打开user_password.jsp,为3个输入密码的输入框分配id,id值可以自由设计。

参考此前的 20.2 步骤,使得输入框下方的“保存更改”可以提交POST类型的ajax请求,并实现。

21.5 测试

通过前端页面测试,然后通过控制台查看数据库中的数据是否已经更改!

22 地址管理 – 遮罩

22.1 分析 – 遮罩层

半透明:半透明的层是一个纯色的背景设置了半透明效果的,需要添加css属性:

background: #000;
opacity: 0.5;

并不是所有的浏览器都支持该属性,以Firefox为例,需要设置:

-moz-opacity: 0.5;

以上2个属性可以同时设置,浏览器对于不支持的属性会无视。

半透明的区域需要多大:与网页的正文尺寸保持一致:

$(document).width();
$(document).height();

由于网页的正文尺寸是动态的值,所以,不可以通过CSS来确定整个遮罩层的尺寸,只能通过Javascript来设置!

$("#mask").css("width": $(document).width());
$("#mask").css("height": $(document).height());

或者:

$("#mask").css({
	"width": $(document).width(),
	"height": $(document).height()
});

如何使得遮罩层挡住当前页面:遮罩层与当前页面的所有内容均不是同一层的内容!使用绝对定位:

position: absolute;

同时,确定该层的显示位置:

left: 0; top: 0;

然后,还要保证遮罩层一定是最上层的(当前HTML页面中已经存在z-index为1000的层)!

z-index: 9527;

显示遮罩层:

$("#mask").show();

隐藏遮罩层:

$("#mask").hide();

注意事项:如果position=absolute的层是某个position=relative的子级标签层,则absolute的层会以relative层的各顶点作为参考决定自己的位置!

22.2 分析 – 内容层

基本方式同遮罩层。

注意:内容层与遮罩层,从源代码来看,是没有父子级嵌套的两个标签!也就是说:千万不要把内容层做成遮罩层的子级标签!!!

内容层是浮于遮罩层的上方的!例如此前遮罩层z-index: 1001,则内容层的z-index值必须大于1001!

内容层的尺寸:可以以固定值,或者参考内容的宽度显示80%左右,如果使用百分比,缺点是内容层可能太宽,或者太窄,优点是绝对不会超出范围!如果使用固定值,对于分辨率较低的屏幕而言,可能显示的区域会太大,而不美观!综合来看,现在的显示器的分辨率通常高于页面的设计尺寸,为了保证内容的显示,推荐使用固定值!此次案例中,推荐宽度约820px,推荐高度约420px。

内容层的位置:一旦某个标签是position:absolute的,它将是独立的一层,在没有某个父级是position:relative时,它会以屏幕的顶点作为参考决定自身的位置,即通过top、bottom、left、right这4个属性中的其中2个来调节:

$("popup_content").css({
	"left": ($(window).width() - 820) / 2,
	"top": ($(window).height() - 420) / 2
});

22.3 修改addressAdmin.html文件

先创建2个<div>标签,分别是id=maskid=popup_content,这2个标签推荐放在<body>的末尾。

在源文件末尾添加<script>标签,自定义showMask()函数和hideMask()函数。

将原有的<form>的所有代码(含<form>自身)剪切到popup_content中。

在地址列表的下方添加自定义按钮,用于“新建收货人信息”,并且,点击按钮时显示遮罩层(id=mask)和内容层(id=popup_content)。

调整内容层的布局,添加标题显示和取消按钮。

在自行调整界面时,保证界面易于操作,并且整齐即可。

23 开发省、市、区的数据查询

23.1 数据表

进入MySQL控制台,应用项目的数据库(use tedu_store;),然后导入SQL脚本文件:

source d:\t_dict.sql

导入完成后,可以通过show tables;查看数据表,也可以通过desc t_dict_provinces;查看表结构,或通过select * from t_dict_areas limit 0,20查看数据,以检查导入的数据。

23.2 持久层:Mapper

编写实体类:

cn.tedu.store.bean.dict.Province
cn.tedu.store.bean.dict.City
cn.tedu.store.bean.dict.Area

实体类中的属性可参考对应的数据表。例如:

public class City {
	private Integer id;
	private String provinceCode;
	private String cityCode;
	private String cityName;
	// 自动生成:SET/GET、无参构造方法、全参构造方法、equals与hashCode、toString
	// 实现Serializable接口,并自动生成id
}

创建持久层接口:cn.tedu.store.mapper.DictMapper,在该接口中声明以下抽象方法:

List<Province> getProvinceList();
List<City> getCityList(String provinceCode);
List<Area> getAreaList(String cityCode);

配置持久层映射:将原UserMapper.xml复制一份,改名为DictMapper.xml,删除原有的配置代码,然后配置根节点<mapper>namespace属性,再配置与接口对应的3个<select>节点,例如:

<select id="getCityList" 
	resultType="cn.tedu.store.bean.dict.City">
	SELECT 
		id, city_code cityCode, city_name cityName 
	FROM 
		t_dict_cities  
	WHERE 
		province_code=#{provinceCode}
</select>

23.3 业务层:Service

创建业务层接口:cn.tedu.store.service.DictService,然后声明抽象方法:

List<Province> getProvinceList();
List<City> getCityList(String provinceCode);
List<Area> getAreaList(String cityCode);

创建业务层实现类:cn.tedu.store.service.DictServiceImpl,实现以上接口,并使用@Service("dictService")进行注解,在类中,添加属性private DictMapper dictMapper,该属性使用@Resource进行注解,在实现抽象方法时,均调用dictMapper对象的对应方法即可。

23.4 控制器

创建控制器类:cn.tedu.store.controller.DictController,使用@Controller@RequestMapping("/dict")进行注解。

在控制器类中声明属性:private DictService dictService;,该属性使用@Resource进行注解。

在控制器类中声明3个处理查询数据请求的方法:

@ResponseBody
@RequestMapping("/get_province_list.do")
public ResponseResult<List<Province>> getProvinceList() {
}

@ResponseBody
@RequestMapping("/get_city_list.do")
public ResponseResult<List<City>> getCityList(
	String provinceCode) {
}

@ResponseBody
@RequestMapping("/get_area_list.do")
public ResponseResult<List<Area>> getAreaList(
	String cityCode) {
}

23.5 测试

通过在浏览器地址栏中输入请求路径和参数来进行测试

24 加载省列表

24.1 准备JSP页面

打开某个已经完成的JSP页面,例如index.jsp,复制顶部的声明代码,然后打开addressAdmin.html,将复制的代码替换到html文件中,并且,从index.jsp中复制引用header.jsp的代码替换到html文件中,然后,将html文件改名为address.jsp

24.2 实现转发到address.jsp

在UserController中,添加访问address.jsp页面的请求处理方法:

@RequestMapping("/address.do")
public String showAddress() {
	return "address";
}

24.3 调整选择省市区的html部分的代码

调整结果如下:

<!--收货人所在城市等信息-->
<div data-toggle="distpicker" class="address_content">
	<span class="red">*</span>
	<span class="kuan">
	省份:</span>
	<select id="province"></select> 
	城市:
	<select id="city"></select> 
	区/县:
	<select id="area"></select>
</div>

24.4 编写函数获取省列表并填充到第1个下拉菜单中

function getProvinceList() {
	$.ajax({
		"url": "${pageContext.request.contextPath}/dict/get_province_list.do",
		"type": "GET",
		"dataType": "json",
		"success": function(obj) {
			$("#province").empty();
			var op;
			for (var i = 0; i < obj.data.length; i++) {
				op = "<option value="
					+ obj.data[i].provinceCode
					+ ">"
					+ obj.data[i].provinceName
					+ "</option>";
				$("#province").append(op);
			}
		}
	});
}

24.5 绑定事件到以上函数

推荐“弹出遮罩时”运行以上函数,即在showMask()中调用以上函数!

24.6 测试

通过页面操作进行测试

25 实现省市区的联动

25.1 实现省和市的二级联动

编写Javascript函数,用于根据省的code获取市的列表:

function getCityList() {
	var provinceCode = $("#province").val();
	$.ajax({
		"url": "${pageContext.request.contextPath}/dict/get_city_list.do",
		"data": "provinceCode=" + proviceCode,
		"type": "GET",
		"dataType": "json",
		"success": function(obj) {
			$("#city").empty();
			var op;
			for (var i = 0; i < obj.data.length; i++) {
				op = $("<option>")
					.val(obj.data[i].cityCode)
					.text(obj.data[i].cityName);
				$("#city").append(op);
			}
		}
	});
}

为省的下拉菜单的onchange事件绑定以上函数

<select id="province" onchange="getCityList()">

或者

$(function() {
	$("#province").change(function() {
		getCityList();
	});
});

25.2 实现市和区的二级联动

参考25.1

25.3 测试

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

1 对于某个程序部分的功能而言,应该先检查数据库与数据表,然后开发持久层,再开发业务层,再开发控制器,最后处理前端页面!

2 对于某个模块而言,应该先完成增加数据的操作,再完成查询数据的操作,通常是查询数据的列表,然后再完成修改或删除。# 26 保存用户的收货地址信息

26.1 规划数据表:t_address

id			int auto_increment	主键ID
uid			int				归属用户	
recv_person		varchar(16)			收货人
recv_province	varchar(6)			省(邮编)
recv_city		varchar(6)			市(邮编)
recv_area		varchar(6)			区(邮编)
recv_district	varchar(50)			省市区(名称)
recv_addr		varchar(100)		详细地址
recv_phone		varchar(16)			手机号
recv_tel		varchar(16)			固定电话
recv_addr_code	varchar(6)			邮编
recv_name		varchar(16)			地址名称(家、公司……)
is_default 		int				是否为默认地址

26.2 创建数据表

推荐在记事本中草拟创建数据表的SQL语句,然后在MySQL控制台创建数据表,并通过desc t_address;检查数据表结构

CREATE TABLE t_address (
	id int auto_increment,
	uid int not null,
	recv_person varchar(16),
	recv_province varchar(6),
	recv_city varchar(6),
	recv_area varchar(6),
	recv_district varchar(50),
	recv_addr varchar(100),
	recv_phone varchar(16),
	recv_tel varchar(16),
	recv_addr_code varchar(6),
	recv_name varchar(16),
	is_default int,
	primary key(id)
) DEFAULT CHARSET=utf8;

26.3 增加字典数据的处理

在cn.tedu.store.mapper.DictMapper持久层添加以下方法及在resources/mappers/DictMapper.xml中配置映射

String getProvinceNameByCode(String provinceCode);
String getCityNameByCode(String cityCode);
String getAreaNameByCode(String areaCode);

并在DictService业务层接口(cn.tedu.store.service.DictService)和实现类(cn.tedu.store.service.DictServiceImpl)中均添加同样的方法

26.4 持久层

创建实体类:

package cn.tedu.store.bean;

public class Address {
	// 对应数据表的所有属性
	// 无参和全参构造方法
	// SET/GET方法
	// equals()与hashCode()
	// toString()
	// 实现Serializable并添加ID
}

设计持久层接口及抽象方法:

package cn.tedu.store.mapper;

public interface AddressMapper {

	void addAddress(Address address);

}

处理持久层映射:resources/mappers/AddressMapper.xml

26.5 业务层

创建业务层接口及抽象方法:

package cn.tedu.store.service;

public interface AddressService {

	void addAddress(Address address);

}

创建业务层实现类并使用@Service("addressService")注解:

package cn.tedu.store.service;

@Service("addressService")
public class AddressServiceImpl 
	implements AddressService {

	public void addAddress(Address address) {
		// 实现代码
	}

}

在实现类声明处理持久层的对象:

@Resource
private AddressMapper addressMapper;

注意:由Controller给出的Address对象中将不包含收货人的“省市区名称”,则在当前Service的方法中需要进行处理,大致如下:

String provinceName = dictService.getProvinceNameByCode(address.getRecvProvince());
String cityName = dictService.getCityNameByCode(address.getRecvCity());
String areaName = dictService.getAreaNameByCode(address.getRecvArea());

String district = provinceName + ", " + cityName + ", " + areaName;

address.setRecvDistrict(district);

所以,在当前Service中,还需要声明:

@Resource
private DictService dictService;

26.5 控制器

创建cn.tedu.store.controller.AddressController,使用@Controller@RequestMapping("/address")注解:

package cn.tedu.store.controller;

@Controller
@RequestMapping("/address")
public class AddressController {

}

在控制器中声明对应的Service

@Resource
private AddressService addressService;

添加处理“增加收货人信息”请求的方法:

@RequestMapping(value="/handle_add_address.do",
	method=RequestMethod.POST)
@ResponseBody
public ResponseResult<Void> handleAddAddress(
	Address address) {
	// 注意:此时参数address中并不包含recv_district
	// 即不包含“省市区名称”
	// 但是,Controller不进行处理
	// 而是由后续的Service处理!
	
	addressService.addAddress(address);
}

以上功能暂不测试

27 完成“增加收货人信息”功能

27.0 前序的问题

在DictMapper.xml中新增的3个查询的<select>节点需要配置resultType="java.lang.String"

在AddressController中的handleAddAddress()方法中添加参数HttpSession session:

public ResponseResult<Void> handleAddAddress(
		Address address,
		HttpSession session)

并且,在方法内部通过session对象获取当前登录的用户的uid,然后将uid设置到address参数中。

27.1 处理前端界面的表单

form必须设置id。

form下的表单标签都必须设置name属性,且name属性值必须与服务器端的Address类中的属性名保持一致。

27.2 添加前端提交表单的函数

<script>中定义提交表单的函数:

function postForm() {
}

为表单中的提交按钮绑定单击事件到该函数。

27.3 通过ajax提交表单

先序列化表单中的数据,然后提交ajax请求:

function postForm() {
	// 序列化表单中的数据,即拿到所有数据
	var data = $("#form-recv-info").serialize();
	// 确定表单提交到的路径
	var url = "${pageContext.request.contextPath}/address/handle_add_address.do";
	// 调用ajax()函数提交请求并处理
	$.ajax({
		"url": url,
		"data": data,
		"type": "POST",
		"dataType": "json",
		"success": function(obj) {
			// 关闭遮罩
			hideMask();
			// 测试期提示
			alert(obj.message);
			// 待续:刷新列表
		}
	});
}

28 获取当前登录的用户的收货人数据

28.1 持久层

在AddressMapper.java接口中声明获取数据的方法:

List<Address> getAddressList(Integer uid);

配置AddressMapper.xml映射:

<select id="getAddressList" 
	resultType="cn.tedu.store.bean.Address">
	SELECT 
		所有字段,注意取别名,例如recv_addr需要设置别名为recvAddr,通过别名与实体类中的属性名保持一致 
	FROM 
		t_address 
	WHERE 
		uid=#{uid}
</select>

28.2 业务层

在AddressService接口中声明抽象方法:

List<Address> getAddressList(Integer uid);

在AddressServiceImpl实现类中实现以上方法。

28.3 控制器

在AddressController中添加处理“获取当前登录的用户的收货人列表”请求的方法:

@ResponseBody
@RequestMapping("/list.do")
public ResponseResult<List<Address>> 
	getAddressList(HttpSession session) {
}

然后实现以上方法。

28.4 测试

直接在浏览器输入请求路径后观察返回的JSON是否正确。

注意:需要先登录!

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

1 在项目中,时间与空间是很难两全的,有时候需要牺牲空间,换取时间,以保证程序的运行效率。

2 在数据库的使用中,应该尽量避免出现数据冗余,但是,刻意的冗余存储可能会提升运行效率,也是一种牺牲空间,换取时间的作法。

3 做项目时,应该尽量多分析,再写代码。

29 处理“显示收货人列表”的前端页面

29.1 分析

分析原有静态页面中对应的HTML代码,调整为:

<div class="address_list_item address_list_item_active">
	<span class="addr_name addr_name_active">办公室</span>
	<span class="recv_person">小刘老师</span>
	<span class="recv_addr_full">北京市朝阳区潘家园旧货市场1号铺</span>
	<span class="recv_phone">13800138008</span>
	<span class="op">
		<a href="###">编辑</a>
		<a href="###">删除</a>
	</span>
	<span class="is_default">设为默认</span>
</div>

并在common.css中添加对应的样式(参考原有的样式):

.address_list_item {
	height: 36px;
    margin-bottom: 10px;
    border: 1px solid transparent;
	background: #e8e8e8;
}

.address_list_item:after {
	display: inline-block;
    clear: both;
    content: "";
}

.address_list_item_active {
	border: 1px solid #2ea8ee;
}

.address_list_item .addr_name,
.address_list_item .recv_person,
.address_list_item .recv_addr_full,
.address_list_item .recv_phone,
.address_list_item .op,
.address_list_item .is_default {
	float: left;
    text-align: center;
    height: 36px;
    line-height: 36px;
    overflow: hidden;
}

.address_list_item .addr_name {
    width:103px;
    color: #fff;
}

.address_list_item .addr_name_active {
    background: #2ea8ee;
}

.address_list_item .addr_name_default  {
    background: #aaa;
}

.address_list_item .recv_person {
    width:76px;
}

.address_list_item .recv_addr_full {
    width:312px;
}

.address_list_item .recv_phone {
    width:100px;
}

.address_list_item .op {
    width: 93px;
    cursor: pointer;
}

.address_list_item .is_default {
    width: 94px;
}

.address_list_item .op a {
	color: #2ea8ee;
	text-decoration: none;
}

.address_list_item .op a:hover {
	color: #2aa3ea;
	text-decoration: underline;
}

分析原有页面的目的是读懂HTML部分的代码,保证后续的替换。

29.2.0 调整现有的代码

在原有的“ ”区域,在<div class="rs_content">下级原来就存在“ ”,新增与标题同级的<div>标签用于显示数据:

<!-- 数据 -->
<div id="address_list"></div>

并删除原有的模拟数据。

在第29.1步中的“设为默认”原本为:

<span class="is_default">设为默认</span>

调整为:

<span class="is_default">
	<a href="###">设为默认</a>
</span>

29.2.1 在Javascript中定义读取并显示数据的请求函数

function showAddressList() {
	var url = "${pageContext.request.contextPath}/address/list.do";
	$.ajax({
		"url": url,
		"type": "GET",
		"dataType": "json",
		"success": function(obj) {
			// 清除当前列表数据
			$("#address_list").empty();

			var addr; // 循环过程中每次循环到的收货人信息的数据
			var htmlString; // 每条数据对应的html源代码

			for (var i = 0; i < obj.data.length; i++) {
				addr = obj.data[i];

				// 注意:
				// 由于Javascript根据分号或换行来判断语句是否结束
				// 所以,以下字符串在编写代码时需要调整为在同1行
				htmlString = '<div class="address_list_item address_list_item_active">
					<span class="addr_name addr_name_active">' + addr.recvName + '</span>
					<span class="recv_person">' + addr.recvPerson + '</span>
					<span class="recv_addr_full">' + addr.recvDistrict + ' ' + addr.recvAddr + '</span>
					<span class="recv_phone">' + addr.recvPhone + '</span>
					<span class="op">
						<a href="###id=' + addr.id + '">编辑</a>
						<a href="###id=' + addr.id +'">删除</a>
					</span>
					<span class="is_default">
						<a href="###id=' + addr.id + '">设为默认</a>
					</span>
				</div>';

				// 将当前数据的html代码添加到列表中
				$("#address_list").append(htmlString);
			}
		}
	});
}

29.3 增加数据后刷新列表

在原postForm()函数中,去除测试使用的alert(),新增调用以上showAddressList()函数,以实现:增加收货人信息后列表自动刷新。

29.4 测试

仍然需要先登录,后测试。

当前代码中,如果弹出表单中填写过数据,后续再次弹出时,不会是空白表单,该问题下一步处理。

30 删除收货人信息

30.1 持久层

在AddressMapper.java中新增删除的方法:

void delete(
	@Param("id") Integer id, 
	@Param("uid") Integer uid);

在AddressMapper.xml中配置映射。

30.2 业务层

在AddressService接口中声明删除的方法:

void delete(Integer id, Integer uid);

并在AddressServiceImpl类中实现以上方法。

30.3 控制器层

在AddressController中添加处理“删除收货人信息”请求的方法:

@RequestMapping("/delete.do")
@ResponseBody
public ResponseResult<Void> delete(
	Integer id, HttpSession session) {

}

30.4 JSP

在Javascript,创建deleteAddress(id)函数,在函数的最初始位置,调用confirm()函数用于确认是否要删除数据,并获取此次确认的返回值,如果为false,则直接return不再向后执行,如果为true可以不处理,让程序自然向后执行。

当确认之后,通过ajax请求删除,删除完成后调用showAddressList()函数刷新列表。

31 获取指定ID的收货人信息

31.1 持久层

在AddressMapper.java中添加获取指定ID的数据的方法:

Address getAddressById(
	@Param("id") Integer id, 
	@Param("uid") Integer uid);

在AddressMapper.xml中配置映射:

<select id="getAddressById" 
	resultType="cn.tedu.store.bean.Address">
	SELECT 
		id, 
		uid,
		recv_person		recvPerson,
		recv_province	recvProvince, 
		recv_city		recvCity, 
		recv_area		recvArea, 
		recv_district	recvDistrict, 
		recv_addr		recvAddr, 
		recv_addr_code	recvAddrCode, 
		recv_phone		recvPhone, 
		recv_tel		recvTel, 
		recv_name		recvName, 
		is_default		isDefault   
	FROM 
		t_address 
	WHERE 
		id=#{id} AND uid=#{uid}
</select>

31.2 业务层

在AddressService接口中声明获取数据的方法:

Address getAddressById(Integer id, Integer uid);

在AddressServiceImpl类中实现以上方法。

31.3 控制器层

在AddressController中添加处理请求的方法:

@RequestMapping("/get_address.do")
@ResponseBody
public ResponseResult<Address> 
	getAddressById(Integer id, HttpSession session) {

}

31.4 测试

登录后,在浏览器的地址栏中输入地址进行测试。

32 在表单中显示指定ID的收货人信息

33 更新指定ID的收货人信息

33 更新指定ID的收货人信息

33.1 持久层

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

void updateAddressById(Address address);

在AddressMapper.xml中配置映射:

<update id="updateAddressById">
	UPDATE 
		t_address 
	SET 
		recv_person		=	#{address.recvPerson},
		recv_province	=	#{address.recvProvince},
		…… 除了uid和id以外的其它字段 …… 
	WHERE 
		id=#{id}  
		and uid=#{uid} 
</update>

33.2 业务层

在AddressService中添加新的抽象方法:

void updateAddressById(Address address);

在AddressServiceImpl中实现以上方法,依然通过在AddressServiceImpl中的全局变量addressMapper来完成!请注意:参考增加功能,此次编辑功能也需要拼接出省市区的名称并赋值给address对象的district属性!

33.3 控制器层

在AddressController中添加抽象方法:

@RequestMapping(
	method=RequestMethod.POST, 
	value="/handle_update_address.do")
@ResponseBody
public ResponseResult<Void> handleUpdateAddress(
	Address address, HttpSession session) {
	// 获取session中的uid
	// 并设置给address参数
}

33.4 前端页面

由于编辑操作的界面和新增操作的界面是完全一样的,甚至最后的提交按钮的点击响应也是一样的,为了判断当前的操作到底是新增还是编辑,先声明一个Javascript的全局变量(即:声明在<script>标签中,并不归属任何一个函数):

<script language="javascript" type="text/javascript">
	var actionId;
</script>

无论是新增操作,还是编辑操作,都会打开遮罩,即调用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原本编写左侧菜单代码的位置添加:

<c:import url="left_side_menu.jsp"></c:import>

还有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 id="setDefaultAddress">
	UPDATE 
		t_address 
	SET 
		is_default=1 
	WHERE 
		id=#{id} AND uid=#{uid}
</update>

<update id="cancelAllDefaultAddress">
	UPDATE 
		t_address 
	SET 
		is_default=0 
	WHERE 
		uid=#{uid}
</update>

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<Void> 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个对应的<select>节点都需要配置resultType

37.2 业务层

找到AddressServiceImpl中的delete()方法,在执行删除之前,添加相关的业务逻辑:

a) 如果当前登录用户的收货人信息只有1条,则直接删除

b) 如果这条将被删除的数据是默认收货人,则还需要将当前登录的用户的第1条(到底哪条设为默认可以自由设计,只要合理就是对的)收货人信息设为默认。

38 通过Interceptor管理是否登录

38.1 制定规则

如果已经登录,则所有请求都可以正常提交并处理

如果没有登录,则应该跳转(重定向)到/user/login.do

将把拦截器设置为拦截所有请求,判断是否放行,但是,有一些特殊的请求路径是不需要登录就直接放行的!目前,不被拦截的请求有:

/main/index.do
/user/register.do
/user/handleRegister.do
/user/login.do
/user/handleLogin.do
/user/checkUsername.do
/user/checkLoginUsername.do
/user/checkPhone.do
/user/checkEmail.do

38.2 编写拦截器类

创建cn.tedu.store.interceptor.LoginInterceptor,实现HandlerInterceptor接口,并实现接口中定义的3个抽象方法,然后编写preHandle()抽象方法:

// 日志:
System.out.println("LoginInterceptor");
System.out.println("\t" + request.getServletPath());
// 获取Session
HttpSession session = request.getSession();
// 判断是否登录
if (session.getAttribute("uid") != null) {
	// Session中的uid是存在的,表示已经登录,则放行
	// 日志:
	System.out.println("\t已经登录,放行!");
	return true;
} else {
	// 日志:
	System.out.println("\t没有登录,将拦截,并重定向到登录页!");

	// Session中没有uid,表示没有登录,或登录已经过期,则重定向
	String url=request.getContextPath() + "/user/login.do";
	response.sendRedirect(url);

	// 拦截
	return false;
}

38.3 配置拦截器

在spring-mvc.xml文件中,添加拦截器的配置:

<mvc:interceptors>
	<mvc:interceptors>
		<!-- 1. mapping:可以有多个该节点,拦截哪些请求路径:所有:/** -->
		<!-- 2. exclude:可以有多个该节点,例外,白名单 -->
		<bean class=""/>
	</mvc:interceptors>
</mvc:interceptors>

在以上配置中,mapping和exclude对应的节点都可以存在多个!

配置的exclude必须在mapping之后!

配置exclude要求根节点中mvc的命名空间中的xsd的版本至少是3.2!

如果需要匹配所有请求路径,必须使用/**,如果只是/*,则只能匹配例如/aaa.do/xxx.do这样的路径,却不能匹配到/user/aaa.do这样的路径!

39 测试获取商品分类表中的数据

39.1 实体类

创建cn.tedu.store.bean.GoodsCategory,对应t_goods_category数据表。

39.2 持久层

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

List<GoodsCategory> getGoodsCategoryList(Integer offset);

复制main\resources\mapper中的某个映射文件并粘贴,重命名GoodsCategoryMapper,然后开始编辑,根节点的namespace修改为以上接口的名称,然后添加<select>节点以配置读取数据。

39.3 业务层

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

List<GoodsCategory> 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<GoodsCategory> data;

	data = goodsCategoryService.getGoodsCategoryList(page);
	modelMap.addAttribute("data", data);

	return "goods_category_list";
}	

39.5 前端页面

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

<table border="1" cellspacing="0" cellpadding="5">
	<tr>
		<th>id</th>
		<th>parent_id</th>
		<th>name</th>
		<th>status</th>
		<th>sort_order</th>
		<th>is_parent</th>
	</tr>
	<c:forEach items="${data}" var="category">
	<tr>
		<td>${category.id}</td>
		<td>${category.parentId}</td>
		<td>${category.name}</td>
		<td>${category.status}</td>
		<td>${category.sortOrder}</td>
		<td>${category.isParent}</td>
	</tr>
	</c:forEach>
</talbe>

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<Goods> getGoodsByCategory(
	@Param("categoryId") Integer categoryId, 
	@Param("offset") Integer offset,
	@Param("pageCount") Integer pageCount
);

配置持久层的映射。

41.3 业务层

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

List<Goods> 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<GoodsCategory> 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<GoodsCategory> getGoodsCategoryListByParentId(
	Integer parentId, 
	Integer offset, 
	Integer pageCount
);

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

42.4 控制器层

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

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

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

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

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

List<List<GoodsCategory>> subCategories 
	= new ArrayList<GoodsCategory>;

然后,遍历之前获取的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进行处理:

<if test="offset != null">
	LIMIT #{offset}, #{pageCount}
</if>

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

43.2 业务层

可参考第42步

43.3 控制器层

可参考第42步

动态SQL

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

例如在MyBatis中的SQL映射中,使用<if>对某个条件进行判断,就可以动态的决定最终生成的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<Goods> getGoodsListByItemType(String itemType);

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

<select id="getGoodsById" resultType="xx.xx.xx.Goods">
	SELECT ?? FROM t_goods WHERE id=#{id} AND status=1 AND num>0
</select>

<select id="getGoodsListByItemType"
	resultType="xx.xx.xx.Goods">
	SELECT 
		id, title, num 
	FROM 
		t_goods
	WHERE 
		item_type=#{item_type} AND status=1
	ORDER BY 
		id DESC 
</select>

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 id="add" parameterType=".....Cart"
	useGenerateKey="true" keyProperty="id">
	INSERT INTO t_cart 
		(...)
	VALUES
		(...)
</insert>

47.4 业务层

(标准流程)

47.5 控制器层

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

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

@Resource
private CartService cartService;

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

47.6 前端页面

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

<!-- 数量-->
<form id="buyForm">
	<p class="accountChose">
		<s>数量:</s>
		<button class="numberMinus">-</button>
		<input type="text" name="num" value="1" class="number" id="buy-num">
		<button class="numberAdd">+</button>
		<!-- 以下是新增部分 -->
		<input type="hidden" name="goodsId" value="${goods.id }" />
		<input type="hidden" name="goodsTitle" value="${goods.title }" />
		<input type="hidden" name="goodsImage" value="${goods.image }" />
		<input type="hidden" name="goodsPrice" value="${goods.price }" />
	</p>
</form>

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

<a href="#" class="shop lf" id="addCart">
	<img src="../images/product_detail/product_detail_img7.png" alt="" />
	加入购物车
</a>

最后,添加Javascript部分代码:

<script type="text/javascript">
$("#addCart").click(function() {
	var url = "${pageContext.request.contextPath}/cart/add.do";
	var data = $("#buyForm").serialize();
	$.ajax({
		"url": url,
		"data": data,
		"type": "POST",
		"dataType": "json",
		"success": function(obj) {
			// ???
		}
	});
});
</script>

48 显示购物车

48.1 持久层

CartMapper.java接口中添加方法:

List<Cart> getCartList(Integer uid);

CartMapper.xml中配置映射

<select resultType="xx.xx.Cart" id="getCartList">
	SELECT 
		id,
		user_id		userId,
		goods_id	goodsId,
		goods_title	goodsTitle,
		goods_image	goodsImage,
		goods_price	goodsPrice,
		num 
	FROM 
		t_cart 
	WHERE 
		user_id=#{uid}
</select>

48.2 业务层

(标准流程)

48.3 控制器层

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

@RequestMapping("/list.do")
public String showCartList(
	HttpSession session, ModelMap modelMap) {
	// 获取当前登录的用户ID
	Integer uid = getUidFromSession(session);
	// 根据用户ID获取购物车列表
	List<Cart> 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中的隐藏域

设置<input>标签为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<OrderItem> 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<OrderItem> 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和注解驱动:

<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
	<!-- 以下ref引用到的是数据库连接池的Bean对象 -->
	<property name="dataSource" 
		ref="dataSource" />
</bean>

<!-- 以下配置的transaction-manager是以上Bean的id -->
<tx:annotation-driven 
	transaction-manager="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进行注解!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值