TeduStore
将原来的项目中的applicationContext.xml复制到此次的项目中,改名为spring-mvc.xml,在这个配置文件中,配置注解驱动
配置CharacterEncodingFilter和DispatcherServlet。
注意:在配置DispatcherServlet时,初始化参数读取的配置文件需要修改为spring-*.xml。
创建cn.tedu.store.controller.MainController,使用@Controller和@RequestMapping("/main")进行注解,添加处理"/index.do"请求的方法public String showIndex(),使用@RequestMapping("/index.do")进行注解,该方法直接返回"index"。
打开spring-mvc.xml文件,配置组件扫描,扫描的包:cn.tedu.store.controller。
找到webapp/web/index.html文件,在该文件的最前部添加必要的JSP声明,将其扩展名改为.jsp。
将项目部署到Tomcat服务器。
打开浏览器,输入 http://SERVER:PORT/PROJECT/main/index.do 检查页面是否可以正常打开。
CREATE DATABASE tedu_store;
USE tedu_store;
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)
);
通过INSERT和SELECT语法进行测试
url=xxx
driver=xxx
user=xxx
password=xxx
initSize=xxx
maxSize=xxx
在resources下创建新的Spring配置文件:spring-dao.xml
... ...
在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();
}
创建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。
创建cn.tedu.store.mapper.UserMapper接口,并在接口中声明void createUser(User user)抽象方法
在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}
)
配置MapperScannerConfigurer和SqlSessionFactoryBean对应的节点,其中,MapperScannerConfigurer需要配置扫描映射的接口文件的包名,SqlSessionFactoryBean需要配置dataSource和mapperLocations。
通过Spring获取UserMapper对象,调用createUser(User user)方法,测试增加数据,并在Mysql控制台查询以检查增加是否成功。
User findUserByUsername(String username)
此次select节点中可以不配置参数类型。
由于数据表的字段名和实体类的属性名有4个不一致的名称,所以,这4个字段需要设置别名。
同上
创建cn.tedu.store.service.UserService接口,并添加抽象方法void register(User user)和User findUserByUsername(String username)
创建cn.tedu.store.service.UserServiceImpl类,实现以上接口,并使用@Service(“userService”)进行注解。
在类中声明private UserMapper userMapper;并使用@Resource进行注解。
先完成findUserByUsername()方法,再完成register()方法。
添加新的Spring配置文件spring-service.xml,在这个配置文件中,配置组件扫描cn.tedu.store.service
同上
在UserMapper接口中,添加Integer getRecordCountByEmail(@Param(“email”) String email)、Integer getRecordCountByPhone(@Param(“phone”) String phone)
在UserMapper.xml中,添加2个select节点对应以上2个方法,例如:
在UserService接口中,添加抽象方法boolean checkPhoneExists(String phone)和boolean checkEmailExists(String email)
在UserServiceImpl类中实现以上抽象方法
测试以上2个功能
1 使用Spring时,尽量不要使用5.?版本,凡是依赖的jar包名称中含有spring字样的,都应该使用相同的版本!
2 对于实体类,把属性声明完了以后,应该把Eclipse(或其它专业的开发工具)的Source菜单中的Generate系列方法全部执行一次!
3 通常在开发一个项目时,推荐先开发持久层的处理,即DAO,然后开发业务层,即Service,再开发控制器,即Controller,最后完成View。
在Spring的配置文件中配置DataSourceTransactionManager
请求路径:/user/checkUsername.do
请求类型:GET
请求参数:username=admin
响应结果类型:ResponseResult
响应结果:如果正确,则state=1,message=“当前用户名可以正常使用”;如果错误,则state=-1,message=“当前用户名已经被注册”。
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
}
创建cn.tedu.store.controller.UserController类,并使用@Controller进行注解。
为类添加@RequestMapping("/user")注解。
在类中添加private UserService userService变量,并使用@Resource注解。
添加public ResponseResult checkUsername(String username)方法,该方法使用@RequestMapping("/checkUsername.do")和@ResponseBody。
在方法内部,通过userService的findUserByUsername()方法判断用户名是否存在,然后返回匹配的ResponseResult。
在浏览器地址栏中输入 http://SERVER:PORT/PROJECT/user/checkUsername.do?username=admin
在webapp/web下找到register.html,在文件内部添加JSP声明的代码,然后将扩展名改为.jsp
在UserController中添加public String showRegister()方法,注解请求路径@RequestMapping(“register.do”),方法内不需要添加其它代码,直接返回"register"即可。
http://SERVER:PORT/PROJECT/user/register.do
在register.jsp文件中,大概在169行左右,找到“/**发起异步GET请求……”注释,然后将其后的ajax请求相关代码删除(包括注释的步骤的1、2、3、4和“//处理响应消息”)。
找到“/**发起异步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);
}
});
打开/user/register.do,输入用户名,观察右侧的提示
在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=“当前邮箱已经被注册”。
http://SERVER:PORT/PROJECT/user/checkPhone.do?phone=13800xxxx
http://SERVER:PORT/PROJECT/user/[email protected]
在register.jsp中,通过Ctrl+F查找“/**发起异步GET请求”,删除原有的AJAX请求代码,然后自行添加基于jQuery的AJAX请求,验证手机号和邮箱
打开http://SERVER:PORT/PROJECT/user/register.do页面,输入手机号和邮箱,观察提示是否正确。
在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);
}
在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);
// ...
}
在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);
}
}
});
通过前端页面注册新账号,如果注册成功,此时正确的响应应该是404错误页面,并且浏览器的地址栏的资源地址应该是login.do,通过mysql控制台可以看到新的账号已经添加到数据表记录中。
在UserService接口中定义登录的方法
/**
* @return 如果登录成功,返回登录成功的用户的信息,
* 如果登录失败,则返回null
*/
User login(String username, String password);
在UserServiceImpl实现类中,基于已有findUserByUsername()方法,实现以上login()方法
在UserController类中添加处理登录请求的方法
@RequestMapping("/handleLogin.do")
@ResponseBody
public ResponseResult handleLogin(
@RequestParam("lname") String username,
@RequestParam("lwd") String password,
HttpSession session) {
// ...
// TODO 处理Session
}
打开login.jsp,在约152行,找到登录的AJAX代码,删除,然后自行添加基于jQuery登录的AJAX代码。
如果登录成功,重定向到"…/main/index.do"。
1 如果某次判断的目标是得到一个值,并且只有2种可能,使用三元表达式(条件表达式)即可,无须使用if或switch语句。
2 java.lang.Void是一个占位符类,这个类本身没有实际的使用意义,仅用于表达“我不在乎这个数据类型”的含义。
3 在使用Spring MVC时,如果已经添加jackson相关依赖,在Controller中使用@ResponseBody注解的方法内返回某个对象,会自动转换为JSON格式的字符串。
在Controller中添加以下方法:
@RequestMapping("/checkLoginUsername.do")
@ResponseBody
public ResponseResult
checkLoginUsername(String username) {
// ...
}
该方法可以基于此前开发的checkUsername()方法进行改动!即复制粘贴后再调整,无须全部重新编写。
在login.jsp的约127行位置,添加ajax异步请求。
在login.jsp中,搜索“//记住用户名密码”,找到对应的Save()函数,将其改名为saveCookie()函数
在原有的代码中,登录的提交按钮中调用了“记住用户名和密码”功能的Save()函数:
应该将此处的onclick删除!
修改提交异步登录的代码,当登录成功后,在跳转到主页(window.location = “…/main/index.do”;)之前,调用saveCookie()函数。
在原Save()方法中,会判断checkbox是否选中,使用的代码是:
if ($("#ck_rmbUser").attr("checked")) {
此次调用的attr()函数很可能无法正确获取到checkbox的选中状态,需要将调用的函数调整为prop()函数,即:
if ($("#ck_rmbUser").prop("checked")) {
或者,也可以使用is()函数进行判断:
if ($("#ck_rmbUser").is(":checked")) {
注意:如果使用is()函数,在参数字符串中,checked字样的左侧需要添加冒号。
如果没有登录,顶部菜单保持与设计时的静态页面一致;如果已经登录,则在“我的收藏”左侧显示当前登录的用户的用户名,用户名链接到的资源是/user/user_center.do,并且最右侧不再显示“登录”,而是替换为“退出”,“退出”链接到的资源是/user/logout.do
在UserController中的handleLogin()方法中添加HttpSession session参数,然后在方法体中通过参数session添加数据:
session.setAttribute("uid", ???);
session.setAttribute("username", ???);
@RequestMapping("/logout.do")
public String logout(HttpSession session) {
session.invalidate();
return "redirect:../user/login";
}
检查Maven依赖中是否已经添加jstl,如果没有,则添加。
在index.jsp中,在最顶部添加引入标签库:
<% @taglib prefix="c" uri="...." %>
找到顶部菜单的html代码,在第1项之前判断是否登录并显示用户名:
${sessionScope.username}
|
并且在最后一项的“登录”处进行判断,仅当没有Session信息时显示“登录”,并调整此处链接到的地址:
登录
在“登录”之前或之后添加“退出”,仅当存在Session信息时显示:
退出
请求路径:/user/user_center.do
原静态页面:personage.html
打开现有的某个.jsp文件,复制最顶部的JSP声明代码,粘贴到personage.html文件的顶部,然后将personage.html文件的名称修改为user_center.jsp
在UserController中,添加public String showUserCenter()方法,并使用@RequestMapping("/user_center.do"),方法的返回值是"user_center"。
在现有的showUserCenter()方法中,添加ModelMap参数:
public String showUserCenter(ModelMap modelMap) {
在方法内部,通过userService对象的findUserByUsername()方法查询当前登录的用户的完整信息,其中,调用方法时所需的用户名是session.getAttribute(“username”).toString(),然后将找到的用户数据(User对象)添加到ModelMap中:
modelMap.addAttribute("user", user);
显示的语法:${user.username }
显示数据的位置:从约113行起
新建header.jsp文件,在该文件顶部添加正确的JSP声明,然后添加引入JSTL标签库。
打开index.jsp文件,复制“页面顶部”的代码(参考注释,约第20行起至第48行),粘贴到header.jsp中。
最终,在header.jsp中只会有JSP声明、引入JSTL标签库和“页面顶部”代码,不要保留html标签或body标签等其它代码。
打开index.jsp,删除文件中“页面顶部”的代码,添加使用jstl导入header.jsp
再打开user_center.jsp,处理方式同上。
在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}
在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中的用户名作为修改记录的值
// 邮箱也是同样的处理方式
}
请求路径: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中的方法,以执行更新数据库记录,然后返回结果。
在本次案例中,根据手机号查询用户是否存在,和根据邮箱查询用户是否存在,甚至根据用户名查询用户是否存在,可以使用极为相似的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个有效值时,可能查询结果就不是期望的结果!
1 当前用户的唯一标识,通常是用户的id
2 高频率使用的数据,例如多个页面都需要显示用户名的话,则把用户名也保存在Session中
3 在多次请求中都需要使用的数据,即使使用频率不高,但是也可以先使用Session存储,使用完毕后再清除
DAO -> Service -> Controller -> JSP
User u = userService.findUserByUsername(session.getAttribute("username").toString());
删除“头像”和“性别”相关的前端代码:打开user_center.jsp
文件,在源代码中查找关键字“我的头像”,即可找到相关代码,然后删除。
在user_center.jsp
中,按下Ctrl+O打开Outline,展开标签,点击任意的标签,然后找到引用的personage.css
,按下Ctrl键的同时鼠标单击源代码中该文件的名称,以打开personage.css
,然后在personage.css文件中,查找“.save”,为这套标签规则中添加“display: block;”
修改“保存更改”按钮的代码:
找到“用户名”、“绑定电话”和“绑定邮箱”的3个输入框(input标签),均指定id属性,以便于后续提交时获取输入框中的值。
在user_center.jsp
的末尾部分,添加新的标签,定义
doSave()
函数,在函数中,获取3个输入框中的值,后续将通过ajax方式提交表单。
在doSave()函数中,提交ajax请求
通过页面正常访问,测试完整的修改功能
在UserMapper.java
接口中声明修改密码的方法:
void updatePassword(
@Param("id") Integer uid,
@Param("password") String newPassword);
在UserMapper.xml
映射文件中添加修改密码的配置:
UPDATE
t_user
SET
password=#{password}
WHERE
id=#{id}
在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;
}
}
请求路径:update_password.do
请求类型:POST
请求参数:old_password=xx&new_password=xx
响应结果:ResponseResult
在UserController.java
中,添加处理修改密码请求的方法:
@RequestMapping(method=RequestMethod.POST,
value="update_password.do")
@ResponseBody
public ResponseResult handleUpdatePassword(
@RequestParam(name="old_password") String oldPassword,
@RequestParam(name="new_password") String newPassword,
HttpSession session) {
// 声明结果
ResponseResult rr = new ResponseResult();
// 从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;
}
先找到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请求,并实现。
通过前端页面测试,然后通过控制台查看数据库中的数据是否已经更改!
半透明:半透明的层是一个纯色的背景设置了半透明效果的,需要添加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层的各顶点作为参考决定自己的位置!
基本方式同遮罩层。
注意:内容层与遮罩层,从源代码来看,是没有父子级嵌套的两个标签!也就是说:千万不要把内容层做成遮罩层的子级标签!!!
内容层是浮于遮罩层的上方的!例如此前遮罩层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
});
先创建2个 在源文件末尾添加 将原有的 在地址列表的下方添加自定义按钮,用于“新建收货人信息”,并且,点击按钮时显示遮罩层( 调整内容层的布局,添加标题显示和取消按钮。 在自行调整界面时,保证界面易于操作,并且整齐即可。 进入MySQL控制台,应用项目的数据库( 导入完成后,可以通过 编写实体类: 实体类中的属性可参考对应的数据表。例如: 创建持久层接口: 配置持久层映射:将原 创建业务层接口: 创建业务层实现类: 创建控制器类: 在控制器类中声明属性: 在控制器类中声明3个处理查询数据请求的方法: 通过在浏览器地址栏中输入请求路径和参数来进行测试 打开某个已经完成的JSP页面,例如index.jsp,复制顶部的声明代码,然后打开addressAdmin.html,将复制的代码替换到html文件中,并且,从index.jsp中复制引用header.jsp的代码替换到html文件中,然后,将html文件改名为address.jsp 在UserController中,添加访问address.jsp页面的请求处理方法: 调整结果如下: 推荐“弹出遮罩时”运行以上函数,即在 通过页面操作进行测试 编写Javascript函数,用于根据省的code获取市的列表: 为省的下拉菜单的onchange事件绑定以上函数 或者 参考25.1 1 对于某个程序部分的功能而言,应该先检查数据库与数据表,然后开发持久层,再开发业务层,再开发控制器,最后处理前端页面! 2 对于某个模块而言,应该先完成增加数据的操作,再完成查询数据的操作,通常是查询数据的列表,然后再完成修改或删除。# 26 保存用户的收货地址信息 推荐在记事本中草拟创建数据表的SQL语句,然后在MySQL控制台创建数据表,并通过 在cn.tedu.store.mapper.DictMapper持久层添加以下方法及在resources/mappers/DictMapper.xml中配置映射 并在DictService业务层接口(cn.tedu.store.service.DictService)和实现类(cn.tedu.store.service.DictServiceImpl)中均添加同样的方法 创建实体类: 设计持久层接口及抽象方法: 处理持久层映射:resources/mappers/AddressMapper.xml 创建业务层接口及抽象方法: 创建业务层实现类并使用 在实现类声明处理持久层的对象: 注意:由Controller给出的Address对象中将不包含收货人的“省市区名称”,则在当前Service的方法中需要进行处理,大致如下: 所以,在当前Service中,还需要声明: 创建cn.tedu.store.controller.AddressController,使用 在控制器中声明对应的Service 添加处理“增加收货人信息”请求的方法: 以上功能暂不测试 在DictMapper.xml中新增的3个查询的 在AddressController中的handleAddAddress()方法中添加参数HttpSession session: 并且,在方法内部通过session对象获取当前登录的用户的uid,然后将uid设置到address参数中。 form必须设置id。 form下的表单标签都必须设置name属性,且name属性值必须与服务器端的Address类中的属性名保持一致。 在 为表单中的提交按钮绑定单击事件到该函数。 先序列化表单中的数据,然后提交ajax请求: 在AddressMapper.java接口中声明获取数据的方法: 配置AddressMapper.xml映射: 在AddressService接口中声明抽象方法: 在AddressServiceImpl实现类中实现以上方法。 在AddressController中添加处理“获取当前登录的用户的收货人列表”请求的方法: 然后实现以上方法。 直接在浏览器输入请求路径后观察返回的JSON是否正确。 注意:需要先登录! 1 在项目中,时间与空间是很难两全的,有时候需要牺牲空间,换取时间,以保证程序的运行效率。 2 在数据库的使用中,应该尽量避免出现数据冗余,但是,刻意的冗余存储可能会提升运行效率,也是一种牺牲空间,换取时间的作法。 3 做项目时,应该尽量多分析,再写代码。 分析原有静态页面中对应的HTML代码,调整为: 并在common.css中添加对应的样式(参考原有的样式): 分析原有页面的目的是读懂HTML部分的代码,保证后续的替换。 在原有的“ ”区域,在 并删除原有的模拟数据。 在第29.1步中的“设为默认”原本为: 调整为: 在原 仍然需要先登录,后测试。 当前代码中,如果弹出表单中填写过数据,后续再次弹出时,不会是空白表单,该问题下一步处理。 在AddressMapper.java中新增删除的方法: 在AddressMapper.xml中配置映射。 在AddressService接口中声明删除的方法: 并在AddressServiceImpl类中实现以上方法。 在AddressController中添加处理“删除收货人信息”请求的方法: 在Javascript,创建 当确认之后,通过ajax请求删除,删除完成后调用 在AddressMapper.java中添加获取指定ID的数据的方法: 在AddressMapper.xml中配置映射: 在AddressService接口中声明获取数据的方法: 在AddressServiceImpl类中实现以上方法。 在AddressController中添加处理请求的方法: 登录后,在浏览器的地址栏中输入地址进行测试。 在AddressMapper.java接口中声明抽象方法: 在AddressMapper.xml中配置映射: 在AddressService中添加新的抽象方法: 在AddressServiceImpl中实现以上方法,依然通过在AddressServiceImpl中的全局变量addressMapper来完成!请注意:参考增加功能,此次编辑功能也需要拼接出省市区的名称并赋值给address对象的district属性! 在AddressController中添加抽象方法: 由于编辑操作的界面和新增操作的界面是完全一样的,甚至最后的提交按钮的点击响应也是一样的,为了判断当前的操作到底是新增还是编辑,先声明一个Javascript的全局变量(即:声明在 无论是新增操作,还是编辑操作,都会打开遮罩,即调用 后续提交表单的 打开 新建 复制 在 还有 修改“地址管理”、“我的信息”和“安全管理”这3个超链接的地址。 建议使用绝对路径表示,并与 在AddressMapper.java接口中声明2个方法: 在AddressMapper.xml中配置以上2个方法的映射: 在AddressService接口中声明“设为默认”的抽象方法: 在AddressServiceImpl实现类实现以上方法: 在AddressController中声明处理“设为默认”请求的方法: 首先,定义提交请求的函数: 然后,修改HTML部分(在JS代码定义的模版中)的“设为默认”的点击响应: 正常测试 在正常情况下,通过HttpSession对象调用了setAttribute()后,在后续的Controller运行过程中都可以正确的获取到Session中的数据,如果获取不到,可能是因为: 浏览器不支持Cookie 访问超时(当前操作和前序操作间隔时间太长) 操作过程中曾经关闭浏览器 服务器曾经关闭或重启过 将从数据库中读到的List集合进行遍历,然后结合IO技术各某个扩展名为 关键在于目标Excel文件中各单元格的数据之间使用逗号进行分隔。 这种做法的缺陷在于最终的文件扩展名是.csv,且这种文件的内容是没有任何格式的!即不能设置字段样式,也不可以使用公式,或者设置数据类型等。 POI是Apache官方提供的用于访问Office文档的框架,可以访问Word、Excel、PPT…… 在AddressMapper.java接口中声明以下抽象方法: 在AddressMapper.xml中配置映射,配置时,2个对应的 找到AddressServiceImpl中的delete()方法,在执行删除之前,添加相关的业务逻辑: a) 如果当前登录用户的收货人信息只有1条,则直接删除 b) 如果这条将被删除的数据是默认收货人,则还需要将当前登录的用户的第1条(到底哪条设为默认可以自由设计,只要合理就是对的)收货人信息设为默认。 如果已经登录,则所有请求都可以正常提交并处理 如果没有登录,则应该跳转(重定向)到 将把拦截器设置为拦截所有请求,判断是否放行,但是,有一些特殊的请求路径是不需要登录就直接放行的!目前,不被拦截的请求有: 创建 在spring-mvc.xml文件中,添加拦截器的配置: 在以上配置中,mapping和exclude对应的节点都可以存在多个! 配置的exclude必须在mapping之后! 配置exclude要求根节点中mvc的命名空间中的xsd的版本至少是3.2! 如果需要匹配所有请求路径,必须使用 创建 创建 复制 创建 创建 创建 创建 1 不会写代码时,用中文写,写得差不多了,再翻译成程序代码。 2 在设计逻辑时,尽量的考虑各种可能,避免设计的逻辑不完整,后续可能出现BUG。 3 在设计逻辑时,一次性考虑得非常完整并不太现实,但是,至少考虑数据最极端的状况! 4 业务逻辑不一定有绝对的正确或错误,合理,就是好的。 在MySQL控制台中,输入 在MySQL控制台中,输入 找出值不是utf8的变量,然后通过以下指定重新设置: 将除了filesystem变量以外的其它变量的值都设置为utf8即可。 注意:以上修改的是运行时的变量,退出后再重新进入控制台则无效。 -------------- 草稿 -------------- 只显示笔记本电脑,只需要3条数据,数据根据优先级排列,优先级值越大表示优先级更高。 持久层处理当前业务的数据存取方法为: 配置持久层的映射。 声明业务层接口中的抽象方法: 在实现类中,通过goodsMapper完成以上方法。 在首页对应的MainController中,找到已有的showIndex()方法,在该方法中读取首页需要显示的数据,封装到ModelMap参数中,最后,将请求转发给index.jsp 通过EL表达式和JSTL显示数据 经查询,“电脑,办公”的是一级分类,id是161,归属该分类的有7个二级分类,只需要根据sort_order的顺序排列方式取出前3个,此次的查询结果是“电脑整机”,“电脑配件”,“外设产品”,对应的id分别是:162,171,186。 然后根据这些二级分类再查询对应的三级分类。 以上需要进行4次查询,查询结果使用4个List集合表示,然后全部转发给JSP页面。 在GoodsCategoryMapper.java接口中: 在GoodsCategoryMapper.xml中配置映射: 在GoodsCategoryService接口中添加: 在GoodsCategoryServiceImpl实现类中实现以上方法 在MainController的showIndex()方法中,先获取parent_id=161的3个分类信息: 然后根据以上得到的3个二级分类,获取对应的三级分类: 以上使用了3个List集合表示3个三级分类,当然,也可以只使用1个更大的List将这3个三级分类存储起来: 然后,遍历之前获取的3个二级分类,来确定所有三级分类的数据: 然后,将获取到的数据添加到ModelMap中以转发。 在index.jsp中显示数据。 将GoodsMapper.java接口中已有的getGoodsListByCategory()方法改名为getGoodsListCategoryId(),此次改动只是为了增加方法名称的识别度,没有功能上的调整。 注意:GoodsMapper.xml映射文件中的id也一并调整 将GoodsMapper.xml中的配置中,SQL语句最后的LIMIT部分使用动态SQL进行处理: 由于持久层的方法名称发生了变化,就会导致业务层的调用出错,所以,使用同样的方式对业务层、控制器层进行调整。 可参考第42步 可参考第42步 使用动态SQL可以使用“编程”的方式处理MyBatis中的SQL映射的XML文件,使得开发过程中,不用反复创建高度相似的SQL映射! 例如在MyBatis中的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属性用于:当图片无法加载时,显示alt属性的值,该值是一段文字 title属性用于:当鼠标悬停在图片上时,显示这段文字 因为这2个属性的值都只在特定情况下显示,如果没有出现这些情况,是看不到这些文字的! 但是,强烈推荐配置这2个属性,特别是动态数据的图片的这2个属性(也就是说例如素材图片就真的不用配置这个属性了,而商品等对于站点有价值的数据的图片都应该配置)。 之所以这样做,是因为在网页中源代码中体现了例如“联想笔记本”的字样后,更加利于站点被搜索引擎根据“联想笔记本”关键字搜索得到!!! 基于以上的思想,还需要另外注意一个问题:并不是所有的数据都应该通过ajax来获取,虽然通过ajax方式在有些场景中可以提高用户体验,并且看似很有格调,但是,通过ajax获取的数据都是变量,其具体的值不会体现在网页的源代码中,也就是说这样的数据是无利用SEO的。 更多详情请参见百度的SEO文档。 在GoodsMapper.java接口中声明: 在GoodsMapper.xml配置映射: (直接写) 在前端页面中显示分页信息 在GoodsMapper.java中创建新的方法,用于根据id获取商品信息: 另外,还要根据以上方法找到的商品信息中的 在GoodsMapper.xml配置以上2个方法对应的映射: (标准流程) 在 找到 将数据通过EL表达式替换到页面中。 该功能用于“商品详情”页的上方的路径导航,即“首页 > 电脑 -> 笔记本”这类导致 在 在 (标准流程) 在 然后,在现有的 在已经读取到商品信息后,根据商品信息中的 根据三级分类信息找到所属的二级分类: 再根据二级分类信息找到所属的一级分类: 最后,将以上3个分类的数据添加到 Goods CREATE DATABASE CREATE TABLE INSERT / UPDATE/ DELETE / SELECT 当数据操作满足特定的情况,会自动执行指定的另一套SQL指令。 例如存在部门信息表: 并且存在员工信息表: 当需要删除某条部门信息表中的数据时,员工信息表中的数据也应该有相关的处理,例如删除 使用触发器可以解决当某张表中的数据被执行了INSERT/DELETE/UPDATE操作时产生关联操作。 使用触发器的示例: 在使用触发器,原来的操作类型和BEFORE/AFTER会共同构成6种触发条件: 每张数据表中的触发器只能配置以上每种条件中各1种,即同一张数据表最多存在6种触发器。 在创建触发器时,可以在满足触发条件后执行多条SQL指令,即以上代码格式中的“操作”可以是多条指令,如果确实需要有多条指令,这些指令应该使用分号( 在MYSQL中,使用 一旦执行以上语句后,MYSQL只会将 需要注意的是:一旦改变了结束标识,在创建完触发器,应该还原为原来的结束标识: 针对以上列举的模拟数据和模拟需求,创建触发器应该是: 以上示例代码中,触发条件对应的“操作”中, 根据设计好的静态页面规划购物车的数据表: 规划完成后,创建数据表。 创建 创建 创建 (标准流程) 将接收到AJAX请求,所以,响应数据类型应该是ResponseResult>。 创建 在选择购买数量的部分的源代码调整为: 然后调整“加入购物车”的按钮ID: 最后,添加Javascript部分代码: 在 在 (标准流程) 在 将 在 设置 隐藏域的特点是在界面上完全不显示,但是,隐藏域的 通常会通过JSP来确定隐藏域中的值,或通过Javascript代码来控制其中的值。 使用隐藏域的典型需求就是编辑相关数据,通常编辑数据时会根据数据的id进行显示,当提交时也需要根据id最终执行数据库的update语句,由于显示编辑界面时,并没有哪个输入框或者相关控件表示数据的id,却又需要提交这个数据,所以,通常会在显示界面时,就把编辑的数据的id放在隐藏域中,最终该id值就可以随着form的所有数据一并提交。 规划订单的数据表: 以下是订单中的商品表: 综上,订单信息需要通过至少2张数据表才可以实现! 创建以上2张数据表对应的实体类: 创建 在 创建 创建 在Spring的XML配置文件中必须配置 应该保证在Service中存在与DAO/Mapper中对应的方法,即在DAO/Mapper中有什么方法,在Service中就存在直接调用它们的方法。 当某个方法需要以事务的方式执行,则可以使用 关于 最后,Spring也严重不推荐使用 在使用 该配置的属性取值为枚举值,推荐使用 如果没有配置 以上各 如果需要使用事务,先在Spring的XML文件中配置。 在编写Service类时,应该创建与DAO/Mapper中匹配所有方法,与其保持一致!然后,当需要以事务的方法执行时,另使用某个方法调用Service自身的方法来执行,同时,使用id=mask
和id=popup_content
,这2个标签推荐放在的末尾。
标签,自定义
showMask()
函数和hideMask()
函数。的所有代码(含
)剪切到
popup_content
中。id=mask
)和内容层(id=popup_content
)。23 开发省、市、区的数据查询
23.1 数据表
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
UserMapper.xml
复制一份,改名为DictMapper.xml
,删除原有的配置代码,然后配置根节点
的namespace
属性,再配置与接口对应的3个节点,例如:
23.3 业务层:Service
cn.tedu.store.service.DictService
,然后声明抽象方法:List
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
进行注解。@ResponseBody
@RequestMapping("/get_province_list.do")
public ResponseResult
> getProvinceList() {
}
@ResponseBody
@RequestMapping("/get_city_list.do")
public ResponseResult
> getCityList(
String provinceCode) {
}
@ResponseBody
@RequestMapping("/get_area_list.do")
public ResponseResult
> getAreaList(
String cityCode) {
}
23.5 测试
24 加载省列表
24.1 准备JSP页面
24.2 实现转发到address.jsp
@RequestMapping("/address.do")
public String showAddress() {
return "address";
}
24.3 调整选择省市区的html部分的代码
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 = "";
$("#province").append(op);
}
}
});
}
24.5 绑定事件到以上函数
showMask()
中调用以上函数!24.6 测试
25 实现省市区的联动
25.1 实现省和市的二级联动
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 = $("
$(function() {
$("#province").change(function() {
getCityList();
});
});
25.2 实现市和区的二级联动
25.3 测试
***** 经验总结 *****
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 创建数据表
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 增加字典数据的处理
String getProvinceNameByCode(String provinceCode);
String getCityNameByCode(String cityCode);
String getAreaNameByCode(String areaCode);
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);
}
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;
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);
@Resource
private DictService dictService;
26.5 控制器
@Controller
和@RequestMapping("/address")
注解:package cn.tedu.store.controller;
@Controller
@RequestMapping("/address")
public class AddressController {
}
@Resource
private AddressService addressService;
@RequestMapping(value="/handle_add_address.do",
method=RequestMethod.POST)
@ResponseBody
public ResponseResult
27 完成“增加收货人信息”功能
27.0 前序的问题
节点需要配置
resultType="java.lang.String"
。public ResponseResult
27.1 处理前端界面的表单
27.2 添加前端提交表单的函数
中定义提交表单的函数:
function postForm() {
}
27.3 通过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 持久层
List getAddressList(Integer uid);
28.2 业务层
List getAddressList(Integer uid);
28.3 控制器
@ResponseBody
@RequestMapping("/list.do")
public ResponseResult
>
getAddressList(HttpSession session) {
}
28.4 测试
***** 经验总结 *****
29 处理“显示收货人列表”的前端页面
29.1 分析
.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;
}
29.2.0 调整现有的代码
设为默认
设为默认
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 = '';
// 将当前数据的html代码添加到列表中
$("#address_list").append(htmlString);
}
}
});
}
29.3 增加数据后刷新列表
postForm()
函数中,去除测试使用的alert()
,新增调用以上showAddressList()
函数,以实现:增加收货人信息后列表自动刷新。29.4 测试
30 删除收货人信息
30.1 持久层
void delete(
@Param("id") Integer id,
@Param("uid") Integer uid);
30.2 业务层
void delete(Integer id, Integer uid);
30.3 控制器层
@RequestMapping("/delete.do")
@ResponseBody
public ResponseResult
30.4 JSP
deleteAddress(id)
函数,在函数的最初始位置,调用confirm()函数用于确认是否要删除数据,并获取此次确认的返回值,如果为false,则直接return不再向后执行,如果为true可以不处理,让程序自然向后执行。showAddressList()
函数刷新列表。31 获取指定ID的收货人信息
31.1 持久层
Address getAddressById(
@Param("id") Integer id,
@Param("uid") Integer uid);
31.2 业务层
Address getAddressById(Integer id, Integer uid);
31.3 控制器层
@RequestMapping("/get_address.do")
@ResponseBody
public ResponseResult
getAddressById(Integer id, HttpSession session) {
}
31.4 测试
32 在表单中显示指定ID的收货人信息
33 更新指定ID的收货人信息
33 更新指定ID的收货人信息
33.1 持久层
void updateAddressById(Address address);
33.2 业务层
void updateAddressById(Address address);
33.3 控制器层
@RequestMapping(
method=RequestMethod.POST,
value="/handle_update_address.do")
@ResponseBody
public ResponseResult
33.4 前端页面
标签中,并不归属任何一个函数):
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.jsp
和address.jsp
中,也删除原有代码,替换为上面的导入语句。3. 调用左侧菜单中各菜单项的超链接
UserController
中的配置保持一致。4.
35 完成“设为默认”的超链接点击及修改数据功能
35.1 持久层
void setDefaultAddress(
@Param("id") Integer id,
@Param("uid") Integer uid);
void cancelAllDefaultAddress(Integer uid);
35.2 业务层
void setDefaultAddress(Integer id, Integer uid);
@Override
public void setDefaultAddress(Integer id, Integer uid) {
addressMapper.cancelAllDefaultAddress(uid);
addressMapper.setDefaultAddress(id, uid);
}
36.3 控制器层
@RequestMapping("/set_default.do")
@ResponseBody
public ResponseResult
36.4 前端界面
function setDefaultAddress(id) {
var url = "........";
$.ajax({
"url": url,
"data": "id=" + id,
"type": "GET",
"dataType": "json",
"success": function(obj) {
// 刷新列表
}
});
}
onclick="setDefaultAddress(%id_val%)"
36.5 测试
***** 经验总结 *****
什么情况下会访问不到Session
通过Java读取数据库并将数据写入到Excel中
1 使用逗号分隔值文件
.csv
的文件中写入即可:id,username,password
1,chengheng,1234
2,liucs,5678
2 使用Apache POI
3 使用其它第三方框架
37 优化“删除收货人”的逻辑
37.1 持久层
/**
* 获取当前登录的用户的第1条记录的ID
* @param uid 当前登录的用户的uid
* @return 当前登录的用户的第1条记录的ID
*/
Integer getFirstRecordId(Integer uid);
/**
* 获取当前登录的用户的数据的数量
* @param uid 当前登录的用户的uid
* @return 当前登录的用户的数据的数量
*/
Integer getRecordCountByUid(Integer uid);
节点都需要配置
resultType
。37.2 业务层
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 配置拦截器
/**
,如果只是/*
,则只能匹配例如/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
main\resources\mapper
中的某个映射文件并粘贴,重命名GoodsCategoryMapper
,然后开始编辑,根节点的namespace
修改为以上接口的名称,然后添加节点以配置读取数据。
39.3 业务层
cn.tedu.store.service.GoodsCategoryService
接口,并在接口中声明获取数据的方法:List
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
39.5 前端页面
goods_category_list.jsp
文件,并在顶部添加配置以使用JTSL标签库,在中,创建一个表格:
id
parent_id
name
status
sort_order
is_parent
${category.id}
${category.parentId}
${category.name}
${category.status}
${category.sortOrder}
${category.isParent}
40 完成商品信息列表的测试
***** 经验总结 *****
怎么写设计逻辑
解决MySQL中可能存在的乱码问题
1 查看MySQL使用的编码
\s
查看状态,在显示端口号的上方会有4条编码信息,通常应该4条都是utf8,如果不是,则可能存在乱码的风险。2 查看哪些配置使用的其它编码
show variables like '%character%';
以查看哪些MySQL变量设置了哪种编码3 重新设置编码格式
set character_set_xx=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 要求
41.2 持久层
List
41.3 业务层
List
41.4 控制器层
41.5 前端页面
42 在首页显示“电脑,办公”的分类
42.1 分析
42.2 持久层
List
SELECT
XX
FROM
t_goods_category
WHERE
parent_id=#{parentId}
AND status=1
ORDER BY
sort_order ASC
LIMIT #{offset}, #{pageCount}
42.3 业务层
List
42.4 控制器层
List
List
List
> subCategories
= new ArrayList
int i = 0;
for (GoodsCategory goodsCategory : categories161) {
subCategories = goodsCategoryService.getGoodsCategoryListByParentId(goodsCategory.getId(), 0, 50);
i++;
}
42.5 前端界面
43 根据分类id显示商品列表
43.1 持久层
43.2 业务层
43.3 控制器层
动态SQL
对某个条件进行判断,就可以动态的决定最终生成的SQL语句是否包括SQL语句中的某个部分!在网页中标签中的alt和title属性
#{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 持久层
Integer getRecordCountByCategoryId(Integer categoryId);
SELECT COUNT(id) FROM t_goods
WHERE
category_id=#{categoryId}
AND status=1 AND num>0
2 业务层
3 控制器
4 前端页面
根据商品ID读取商品信息
1 持久层
Goods getGoodsById(Integer id);
item_type
去查找相同品类、具体参数略不同的相关商品:List
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
。通过商品分类的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());
ModelMap
中以转发,并在前端页面中显示。
[
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 常用操作
46.2 Trigger(触发器)
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
CREATE TRIGGER xx // 创建触发器并命名
AFTER // AFTER可以被替换为BEFORE
DELETE // 可以是INSERT/DELETE/UPDATE中的任何一种
ON table // 原来的操作是对哪张数据表的操作
FOR EACH ROW // 在MYSQL中是固定的
BEGIN // 准备开始执行另一张表的数据操作
操作 // 对另一张表的操作,可以是多条SQL语句
END // 结束
BEFORE INSERT
BEFORE UPDATE
BEFORE DELETE
AFTER INSERT
AFTER UPDATE
AFTER DELETE
;
)进行分隔,但是,当编写“操作”时,创建触发器的语句尚未结束,一旦执到“操作”中的分号时,MYSQL就会开始尝试创建触发器,就会导致创建失败!DELIMITER
指令可以改变结束标识,即默认情况下,MYSQL中把分号作为每条语句的结束,但是,可以更改!例如:DELIMITER $
$
作为结束标识,而不再使用分号!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
配置文件,并添加配置:47.4 业务层
47.5 控制器层
cn.tedu.store.controller.CartController
,添加必要的注解,然后添加处理请求的方法:@Resource
private CartService cartService;
@RequestMapping("add.do")
@ResponseBody
public ResponseResult
47.6 前端页面
加入购物车
48 显示购物车
48.1 持久层
CartMapper.java
接口中添加方法:List
CartMapper.xml
中配置映射
48.2 业务层
48.3 控制器层
CartController
中声明处理请求的方法:@RequestMapping("/list.do")
public String showCartList(
HttpSession session, ModelMap modelMap) {
// 获取当前登录的用户ID
Integer uid = getUidFromSession(session);
// 根据用户ID获取购物车列表
List
48.4 前端页面
cart.html
调整为cart_list.jsp
,源代码需要在顶部添加JSP声明和使用JSTL,并引用header.jsp
。cart_list.jsp
中找到显示购物车数据的代码部分,使用JSTL循环此前转发过来的carts
,并将数据填充到界面中。关于form中的隐藏域
标签为
type="hidden"
表示隐藏域。name
和value
可以随着整个form
的提交(submit)一起把这些数据提交到服务器。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)
);
49.2 实体类
Order
和OrderItem
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
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
基于SSM的事务
准备工作
TransactionManager
和注解驱动:使用事务
@Transactional
进行注解,则在这个方法执行的所有数据库的增删改操作都会以事务的方式执行,即一系列的操作将整体成功,或整体失败!@Transactional
注解,还可以用于注解整个类,则这个类中所有的方法都会以事务的方式执行,当然,这种做法是不推荐的!原因有:a)并不是所有方法都需要是以事务的方式执行的,例如单条增删改的操作,或者查询的操作;b)该注解还可以做其它配置,每个方法的配置可以不同!@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
的注释,或查阅资料。小结
@Transactional
进行注解!你可能感兴趣的:(电商项目)