- 书城项目第一阶段
- 书城项目第二阶段
- 第一节 不带数据库的登录注册
- 第二节 三层架构
- 第四节 持久化层
- 第五节 完成带数据库的登录注册
- 1、密码加密
- ①加密方式介绍
- ②加密算法:HASH
- ③执行加密的工具方法
- #[2]实现UserService接口">#[2]实现UserService接口
- [3]修改RegisterServlet
- [2]LoginServlet的doPost()方法
- 1、密码加密
- 书城项目第三阶段
- 书城项目第四阶段
- 书城项目第五阶段
- 书城项目第六阶段
书城项目第一阶段
第一节 事件驱动补充
1、取消控件的默认行为
①控件默认行为
- 点超链接会跳转页面
- 点表单提交按钮会提交表单
本来控件的默认行为是天经地义就该如此的,但是如果我们希望点击之后根据我们判断的结果再看是否要跳转,此时默认行为无脑跳转的做法就不符合我们的预期了。
②取消方式
[1]超链接举例
HTML代码:
<a id="anchor" href="http://www.baidu.com">超链接</a>
JavaScript代码:
document.getElementById("anchor").onclick = function() {
console.log("我点击了一个超链接");
event.preventDefault();
}
[2]表单提交按钮举例
HTML代码:
<form action="http://www.baidu.com" method="post">
<button id="submitBtn" type="submit">提交表单</button>
</form>
JavaScript代码:
document.getElementById("submitBtn").onclick = function() {
console.log("我点击了一个表单提交按钮");
event.preventDefault();
}
2、阻止事件冒泡
图中的两个div,他们的HTML标签是:
<div id="outterDiv">
<div id="innerDiv"></div>
</div>
点击里面的div同时也等于点击了外层的div,此时如果两个div上都绑定了单击响应函数那么就都会被触发:
document.getElementById("outterDiv").onclick = function() {
console.log("外层div的事件触发了");
}
document.getElementById("innerDiv").onclick = function() {
console.log("内层div的事件触发了");
}
所以事件冒泡就是一个事件会不断向父元素传递,直到window对象。
如果这不是我们想要的效果那么可以使用事件对象的stopPropagation()函数阻止。
document.getElementById("innerDiv").onclick = function() {
console.log("内层div的事件触发了");
event.stopPropagation();
}
3、Vue事件修饰符
对于事件修饰符,Vue官网的描述是:
在事件处理程序中调用 event.preventDefault() 或 event.stopPropagation() 是非常常见的需求。尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。
①取消控件的默认行为
控件的默认行为指的是:
- 点击超链接跳转页面
- 点击表单提交按钮提交表单
实现这个需求使用的Vue事件修饰符是:.prevent
<a href="http://www.baidu.com" @click.prevent="clickAnchor">超链接</a>
<form action="http://www.baidu.com" method="post">
<button type="submit" @click.prevent="clickSubmitBtn">提交表单</button>
</form>
②取消事件冒泡
实现这个需求使用的Vue事件修饰符是:.stop
<div id="outterDiv" @click="clickOutterDiv">
<div id="innerDiv" @click.stop="clickInnerDiv"></div>
</div>
第二节 正则表达式
1、从凤姐的择偶标准说起
本人对伴侣要求如下:
- 第一、必须为北京大学或清华大学硕士毕业生。必须本科硕士连读,中途无跳级,不留级,不转校。在外参加工作后再回校读书者免。
- 第二、必须为经济学专业毕业。非经济学专业毕业则必须精通经济学。或对经济学有浓厚的兴趣。
- 第三、必须具备国际视野,但是无长期定居国外甚至移民的打算。
- 第四、身高176—183左右。长得越帅越好。
- 第五、无生育史。过往所有女友均无因自身而致的堕胎史。
- 第六、东部户籍,即江、浙、沪三地户籍或黑龙江、广东、天津、山东、北京、吉林、辽宁等。
- 东北三省和内蒙古等地户籍,西南地区即重庆、贵州、云南、西藏和湖南、湖北等地籍贯者不予考虑。
- 第七、年龄25—28岁左右。即06届,07届,08届,09届毕业生。有一至两年的工作经验,06级毕业生需年龄在28岁左右,09级毕业生则需聪明过人。且具备丰富的社会实践经验。就职于国家机关,国有企事业单位者不愿考虑。但就职于中石油,中石化等世界顶尖型企业或银行者又比较喜欢。现自主创业者要商榷一番了。
2、标准在手,世界我有
①模式验证
使用标准衡量一位具体的男士,返回一个布尔值,从而知道这位男士是否满足自己的标准——相当于我们使用正则表达式验证一个字符串是否满足规则。比如验证一个字符串是否是一个身份证号。②匹配读取
对全中国的男士应用这个标准,返回一个数组,遍历这个数组,可以得到所有符合标准的男士——相当于我们使用正则表达式获取一段文本中匹配的子字符串。比如将一篇文章中的电子邮件地址读取出来。③匹配替换
对全中国的男士应用这个标准,把其中已婚的变成未婚,这样凤姐就有机会了——相当于我们使用正则表达式替换所有匹配的部分。比如将一段文字中的”HelloWorld”替换为”HelloJava”。花絮: 记者:封老师您好!由于您的名字『封捷』和『凤姐』谐音,同学们总是以此来调侃您,说您是尚硅谷『凤姐』,对此您有什么想说的吗? 封老师:太过分了!我咋能和人家比! 记者:呃……太意外了,您的意思是? 封老师:虽然过气了,但人家好歹也是网红呀!
3、正则表达式的概念
使用一段字符串定义的一个规则,用以检测某个字符串是否满足这个规则,或将目标字符串中满足规则的部分读取出来,又或者将目标字符串中满足标准的部分替换为其他字符串。所以正则表达式有三个主要用途:
- 模式验证
- 匹配读取
- 匹配替换
4、正则表达式零起步
①创建正则表达式对象
[1]使用两个斜杠
// 类似创建数组时可以使用[]、创建对象可以使用{}
var reg = /a/;
[2]使用new关键字创建RegExp类型的对象
// 类似创建数组可以new Array()、创建对象可以使用new Object()
var reg = new RegExp("a");
②正则表达式的组成
正则表达式本身也是一个字符串,它由两种字符组成:
- 普通字符,例如大、小写英文字母;数字等。
- 元字符:被系统赋予特殊含义的字符。例如:^表示以某个字符串开始,$表示以某个字符串结束。
③正则表达式初体验
[1]模式验证
注意:这里是使用正则表达式对象来调用方法。 ```javascript // 创建一个最简单的正则表达式对象 var reg = /o/;
// 创建一个字符串对象作为目标字符串 var str = ‘Hello World!’;
// 调用正则表达式对象的test()方法验证目标字符串是否满足我们指定的这个模式,返回结果true console.log(“/o/.test(‘Hello World!’)=”+reg.test(str));
<a name="bOwl9"></a>
##### [2]匹配读取
**注意**:这里是使用**字符串对象**来**调用**方法。
```javascript
// 在目标字符串中查找匹配的字符,返回匹配结果组成的数组
var resultArr = str.match(reg);
// 数组长度为1
console.log("resultArr.length="+resultArr.length);
// 数组内容是o
console.log("resultArr[0]="+resultArr[0]);
[3]替换
注意:这里是使用字符串对象来调用方法。
var newStr = str.replace(reg,'@');
// 只有第一个o被替换了,说明我们这个正则表达式只能匹配第一个满足的字符串
console.log("str.replace(reg)="+newStr);//Hell@ World!
// 原字符串并没有变化,只是返回了一个新字符串
console.log("str="+str);//str=Hello World!
④匹配方式
[1]全文查找
如果不使用g对正则表达式对象进行修饰,则使用正则表达式进行查找时,仅返回第一个匹配;使用g后,返回所有匹配。
// 目标字符串
var targetStr = 'Hello World!';
// 没有使用全局匹配的正则表达式
var reg = /[A-Z]/;
// 获取全部匹配
var resultArr = targetStr.match(reg);
// 数组长度为1
console.log("resultArr.length="+resultArr.length);
// 遍历数组,发现只能得到'H'
for(var i = 0; i < resultArr.length; i++){
console.log("resultArr["+i+"]="+resultArr[i]);
}
对比代码:
// 目标字符串
var targetStr = 'Hello World!';
// 使用了全局匹配的正则表达式
var reg = /[A-Z]/g;
// 获取全部匹配
var resultArr = targetStr.match(reg);
// 数组长度为2
console.log("resultArr.length="+resultArr.length);
// 遍历数组,发现可以获取到“H”和“W”
for(var i = 0; i < resultArr.length; i++){
console.log("resultArr["+i+"]="+resultArr[i]);
}
[2]忽略大小写
//目标字符串
var targetStr = 'Hello WORLD!';
//没有使用忽略大小写的正则表达式
var reg = /o/g;
//获取全部匹配
var resultArr = targetStr.match(reg);
//数组长度为1
console.log("resultArr.length="+resultArr.length);
//遍历数组,仅得到'o'
for(var i = 0; i < resultArr.length; i++){
console.log("resultArr["+i+"]="+resultArr[i]);
}
对比代码:
//目标字符串
var targetStr = 'Hello WORLD!';
//使用了忽略大小写的正则表达式
var reg = /o/gi;
//获取全部匹配
var resultArr = targetStr.match(reg);
//数组长度为2
console.log("resultArr.length="+resultArr.length);
//遍历数组,得到'o'和'O'
for(var i = 0; i < resultArr.length; i++){
console.log("resultArr["+i+"]="+resultArr[i]);
}
[3]多行查找
不使用多行查找模式,目标字符串中不管有没有换行符都会被当作一行。
//目标字符串1
var targetStr01 = 'Hello\nWorld!';
//目标字符串2
var targetStr02 = 'Hello';
//匹配以'Hello'结尾的正则表达式,没有使用多行匹配
var reg = /Hello$/;
console.log(reg.test(targetStr01));//false
console.log(reg.test(targetStr02));//true
对比代码:
//目标字符串1
var targetStr01 = 'Hello\nWorld!';
//目标字符串2
var targetStr02 = 'Hello';
//匹配以'Hello'结尾的正则表达式,使用了多行匹配
var reg = /Hello$/m;
console.log(reg.test(targetStr01));//true
console.log(reg.test(targetStr02));//true
5、元字符
①概念
在正则表达式中被赋予特殊含义的字符,不能被直接当做普通字符使用。如果要匹配元字符本身,需要对元字符进行转义,转义的方式是在元字符前面加上“\”,例如:^
②常用元字符
代码 | 说明 |
---|---|
. | 匹配除换行字符以外的任意字符。 |
\w | 匹配字母或数字或下划线等价于[a-zA-Z0-9_] |
\W | 匹配任何非单词字符。等价于[^A-Za-z0-9_] |
\s | 匹配任意的空白符,包括空格、制表符、换页符等等。等价于[\f\n\r\t\v]。 |
\S | 匹配任何非空白字符。等价于[^\f\n\r\t\v]。 |
\d | 匹配数字。等价于[0-9]。 |
\D | 匹配一个非数字字符。等价于[^0-9] |
\b | 匹配单词的开始或结束 |
^ | 匹配字符串的开始,但在[]中使用表示取反 |
$ | 匹配字符串的结束 |
[1]例1
var str = 'one two three four';
// 匹配全部空格
var reg = /\s/g;
// 将空格替换为@
var newStr = str.replace(reg,'@'); // one@two@three@four
console.log("newStr="+newStr);
[2]例2
var str = '今年是2014年';
// 匹配至少一个数字
var reg = /\d+/g;
str = str.replace(reg,'abcd');
console.log('str='+str); // 今年是abcd年
[3]例3
var str01 = 'I love Java';
var str02 = 'Java love me';
// 匹配以Java开头
var reg = /^Java/g;
console.log('reg.test(str01)='+reg.test(str01)); // flase
console.log("<br />");
console.log('reg.test(str02)='+reg.test(str02)); // true
[4]例4
var str01 = 'I love Java';
var str02 = 'Java love me';
// 匹配以Java结尾
var reg = /Java$/g;
console.log('reg.test(str01)='+reg.test(str01)); // true
console.log("<br />");
console.log('reg.test(str02)='+reg.test(str02)); // flase
6、字符集合
语法格式 | 示例 | 说明 |
---|---|---|
[字符列表] | 正则表达式:[abc] 含义:目标字符串包含abc中的任何一个字符 目标字符串:plain 是否匹配:是 原因:plain中的“a”在列表“abc”中 |
目标字符串中任何一个字符出现在字符列表中就算匹配。 |
[^字符列表] | [^abc] 含义:目标字符串包含abc以外的任何一个字符 目标字符串:plain 是否匹配:是 原因:plain中包含“p”、“l”、“i”、“n” |
匹配字符列表中未包含的任意字符。 |
[字符范围] | 正则表达式:[a-z] 含义:所有小写英文字符组成的字符列表 正则表达式:[A-Z] 含义:所有大写英文字符组成的字符列表 |
匹配指定范围内的任意字符。 |
var str01 = 'Hello World';
var str02 = 'I am Tom';
//匹配abc中的任何一个
var reg = /[abc]/g;
console.log('reg.test(str01)='+reg.test(str01));//flase
console.log('reg.test(str02)='+reg.test(str02));//true
7、重复
代码 | 说明 |
---|---|
* | 重复零次或多次 |
+ | 重复一次或多次 |
? | 重复零次或一次 |
{n} | 重复n次 |
{n,} | 重复n次或多次 |
{n,m} | 重复n到m次 |
console.log("/[a]{3}/.test('aa')="+/[a]{3}/g.test('aa')); // flase
console.log("/[a]{3}/.test('aaa')="+/[a]{3}/g.test('aaa')); // true
console.log("/[a]{3}/.test('aaaa')="+/[a]{3}/g.test('aaaa')); // true
8、在正则表达式中表达『或者』
使用符号:|
// 目标字符串
var str01 = 'Hello World!';
var str02 = 'I love Java';
// 匹配'World'或'Java'
var reg = /World|Java/g;
console.log("str01.match(reg)[0]="+str01.match(reg)[0]);//World
console.log("str02.match(reg)[0]="+str02.match(reg)[0]);//Java
9、常用正则表达式
需求 | 正则表达式 |
---|---|
用户名 | /^[a-zA-Z][a-zA-Z\-0-9]{5,9}$/ |
密码 | /^[a-zA-Z0-9_\-@\#\&\*]{6,12}$/ |
前后空格 | /^\s+|\s+$/g |
电子邮箱 | /^[a-zA-Z0-9_\.-]+@([a-zA-Z0-9-]+[\.]{1})+[a-zA-Z]+$/ |
第三节 第一阶段要实现的功能
0、准备工作
创建目录后,把一整套现成的前端页面复制到新建的目录下,然后用HBuilderX打开这个目录。然后把vue.js文件复制到script目录下。
1、登录页面的表单验证
①规则设定
- 用户名非空
-
②在login.html页面中加入Vue的环境
</body>
<script src="/bookstoreV01/static/script/vue.js" type="text/javascript" charset="utf-8"></script>
</html>
③思路
④代码实现
[1]HTML代码
<form id="loginForm" action="login_success.html">
<label>用户名称:</label>
<input class="itxt" type="text" v-model:value="username" placeholder="请输入用户名" autocomplete="off" tabindex="1" name="username" id="username" />
<br />
<br />
<label>用户密码:</label>
<input class="itxt" type="password" v-model:value="password" placeholder="请输入密码" autocomplete="off" tabindex="1" name="password" id="password" />
<br />
<br />
<button type="submit" id="sub_btn" @click="loginCheck">登录</button>
</form>
[2]Vue代码
new Vue({
"el":"#loginForm",
"data":{
"username":"",
"password":""
},
"methods":{
"loginCheck":function(){
// 判断用户名或密码是否为空
if(this.username == "" || this.password == "") {
// 如果不满足验证条件,那么阻止表单提交
event.preventDefault();
}
}
}
});
2、注册页面的表单验证
①HTML代码
<form id="registerForm" action="regist_success.html">
<div class="form-item">
<div>
<label>用户名称:</label>
<input v-model:value="username" type="text" placeholder="请输入用户名" />
<span></span>
</div>
<span>{{usernameCheckMessage}}</span>
</div>
<div class="form-item">
<div>
<label>用户密码:</label>
<input v-model:value="password" type="password" placeholder="请输入密码" />
</div>
<span class="errMess">密码的长度至少为8位</span>
</div>
<div class="form-item">
<div>
<label>确认密码:</label>
<input v-model:value="passwordConfirm" type="password" placeholder="请输入确认密码" />
</div>
<span class="errMess">密码两次输入不一致</span>
</div>
<div class="form-item">
<div>
<label>用户邮箱:</label>
<input v-model:value="email" type="text" placeholder="请输入邮箱" />
</div>
<span class="errMess">请输入正确的邮箱格式</span>
</div>
<div class="form-item">
<div>
<label>验证码:</label>
<div class="verify">
<input v-model:value="code" type="text" placeholder="" />
<img src="../../static/img/code.bmp" alt="" />
</div>
</div>
<span class="errMess">请输入正确的验证码</span>
</div>
<button type="submit" @click="registerCheck" class="btn">注册</button>
</form>
②Vue代码
new Vue({
"el":"#registerForm",
"data":{
"username":"",
"password":"",
"passwordConfirm":"",
"email":"",
"code":"",
"usernameCheckMessage":""
},
"watch":{
"username":function(inputValue){
var usernameRegExp = /^[A-Z,a-z,0-9,_]{5,8}$/;
if (usernameRegExp.test(this.username)) {
this.usernameCheckMessage = "";
}else{
this.usernameCheckMessage = "用户名不符合规则";
}
}
},
"methods":{
"registerCheck":function(){
// 1.检查用户名
var usernameRegExp = /^[A-Z,a-z,0-9,_]{5,8}$/;
if (!usernameRegExp.test(this.username)) {
// 如果不满足条件,则阻止表单提交
event.preventDefault();
// 有任何一个条件不满足,后面就没必要检查了,所以函数可以停止执行
return ;
}
// 2.检查密码
var passwordRegExp = /^[A-Z,a-z,0-9,_]{5,8}$/;
if (!passwordRegExp.test(this.password)) {
// 如果不满足条件,则阻止表单提交
event.preventDefault();
// 有任何一个条件不满足,后面就没必要检查了,所以函数可以停止执行
return ;
}
// 3.检查确认密码是否和密码一致
if (this.password != this.passwordConfirm) {
// 如果不满足条件,则阻止表单提交
event.preventDefault();
// 有任何一个条件不满足,后面就没必要检查了,所以函数可以停止执行
return ;
}
// 4.检查电子邮件
var emailRegExp = /^[a-zA-Z0-9_\.-]+@([a-zA-Z0-9-]+[\.]{1})+[a-zA-Z]+$/;
if (!emailRegExp.test(this.email)) {
// 如果不满足条件,则阻止表单提交
event.preventDefault();
// 有任何一个条件不满足,后面就没必要检查了,所以函数可以停止执行
return ;
}
// 5.检查验证码
var codeRegExp = /[A-Z,a-z,0-9]{5}/;
if(!codeRegExp.test(this.code)) {
// 如果不满足条件,则阻止表单提交
event.preventDefault();
// 有任何一个条件不满足,后面就没必要检查了,所以函数可以停止执行
return ;
}
}
}
});
书城项目第二阶段
第一节 不带数据库的登录注册
1、创建动态Web工程
2、把V1中的页面粘贴过来
3、使用base标签统一设置路径基准
①base标签的语法规则
base标签要写在head标签内
- base标签必须写在所有其他有路径的标签的前面
- base标签使用href属性设置路径的基准
- base标签生效的机制是:最终的访问地址=base标签href属性设置的基准+具体标签内的路径
- 对于想要参考base标签的具体标签,如果路径是以斜杠开头,那么它将不参考base标签
②base标签使用举例
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>书城首页</title>
<base href="/bookstore/"/>
<link rel="stylesheet" href="static/css/minireset.css"/>
<link rel="stylesheet" href="static/css/common.css"/>
<link rel="stylesheet" href="static/css/iconfont.css"/>
<link rel="stylesheet" href="static/css/index.css"/>
<link rel="stylesheet" href="static/css/swiper.min.css"/>
</head>
4、基于base标签调整整个页面的路径
①base标签的代码
在需要的页面把下面的base标签代码粘贴到head标签内、需要路径的标签前即可<base href="/bookstore/"/>
②对需要统一调整的路径执行替换
Ctrl+r调出替换操作窗口
具体操作时请参考页面的实际情况进行替换。5、基本假设
为了实现『不带数据库』的登录注册,我们需要假设:系统中目前已有用户:
用户名 | 密码 |
---|---|
tom | 123456 |
6、登录功能
①明确目标
在服务器端检查用户通过表单提交的用户名、密码是否正确。
- 成功:跳转到login_success.html页面
- 失败:返回错误消息
②思路
友情提示:分析业务功能,捋清思路最好的办法就是画流程图③代码
[1]创建LoginServlet
创建Packages时的规范: 公司或组织域名倒序.项目名.模块名.具体包名 或 公司或组织域名倒序.项目名.具体包名
下面是完整的Servlet配置信息:
<servlet>
<servlet-name>LoginServlet</servlet-name>
<servlet-class>com.atguigu.bookstore.servlet.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>LoginServlet</servlet-name>
<url-pattern>/LoginServlet</url-pattern>
</servlet-mapping>
[2]完成doPost()方法中的代码
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.声明两个变量,用于存储假设的用户名、密码
String usernameSystem = "tom";
String passwordSystem = "123456";
// 2.获取请求参数中的用户名、密码
String usernameForm = request.getParameter("username");
String passwordForm = request.getParameter("password");
// 3.执行判断
if(usernameSystem.equals(usernameForm) && passwordSystem.equals(passwordForm)) {
// 4.登录成功:重定向到登录成功页面
response.sendRedirect(request.getContextPath() + "/pages/user/login_success.html");
}else{
// 5.登录失败
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write("抱歉!用户名或密码不正确,请重新输入!");
}
}
[3]HTML页面设置表单提交地址
<form id="loginForm" action="LoginServlet" method="post">
<label>用户名称:</label>
<input class="itxt" type="text" v-model:value="username" placeholder="请输入用户名" autocomplete="off" tabindex="1" name="username" id="username" />
<br />
<br />
<label>用户密码:</label>
<input class="itxt" type="password" v-model:value="password" placeholder="请输入密码" autocomplete="off" tabindex="1" name="password" id="password" />
<br />
<br />
<button type="submit" id="sub_btn" @click="loginCheck">登录</button>
</form>
④提示消息改进探索
以下代码仅供参考:
// 5.登录失败
// 返回提示消息方案一:过于简陋
// response.setContentType("text/html;charset=UTF-8");
// response.getWriter().write("抱歉!用户名或密码不正确,请重新输入!");
// 返回提示消息方案二:没有提示消息,让用户非常困惑
// request.getRequestDispatcher("/pages/user/login.html").forward(request, response);
// 返回提示消息方案三:确实能在登录页面显示提示消息,但是实现的方式让我想骂人
response.setContentType("text/html;charset=UTF-8");
PrintWriter writer = response.getWriter();
writer.write("<!DOCTYPE html>");
writer.write("<html>");
writer.write(" <head>");
writer.write(" <base href='/bookstore/' />");
7、注册功能
①明确目标
用户提交注册表单后,检查用户名是否被占用
- 没有被占用:注册成功
-
②思路
③代码
[1]创建RegisterServlet
完整配置信息:<servlet>
<servlet-name>RegisterServlet</servlet-name>
<servlet-class>com.atguigu.bookstore.servlet.RegisterServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>RegisterServlet</servlet-name>
<url-pattern>/RegisterServlet</url-pattern>
</servlet-mapping>
[2]完成doPost()方法中的代码
```java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.声明变量保存系统内置的用户名 String usernameSystem = “jerry”;
// 2.获取请求参数 String usernameForm = request.getParameter(“username”);
// 3.比较 if (usernameSystem.equals(usernameForm)) {
// 4.说明用户名已经被占用,需要提示错误消息
response.setContentType("text/html;charset=UTF-8");
PrintWriter writer = response.getWriter();
writer.write("抱歉!用户名已经被占用,请重新输入!");
}else{
// 5.说明用户名可用,跳转到注册成功页面
response.sendRedirect(request.getContextPath() + "/pages/user/regist_success.html");
}
}
<a name="Uqlc4"></a>
##### [3]HTML页面调整表单中的设置
```html
<form id="registerForm" action="RegisterServlet" method="post">
<div class="form-item">
<div>
<label>用户名称:</label>
<input v-model:value="username" type="text" name="username" placeholder="请输入用户名" />
<span></span>
</div>
<span>{{usernameCheckMessage}}</span>
</div>
<div class="form-item">
<div>
<label>用户密码:</label>
<input v-model:value="password" type="password" name="password" placeholder="请输入密码" />
</div>
<span class="errMess">密码的长度至少为8位</span>
</div>
<div class="form-item">
<div>
<label>确认密码:</label>
<input v-model:value="passwordConfirm" type="password" placeholder="请输入确认密码" />
</div>
<span class="errMess">密码两次输入不一致</span>
</div>
<div class="form-item">
<div>
<label>用户邮箱:</label>
<input v-model:value="email" type="text" name="email" placeholder="请输入邮箱" />
</div>
<span class="errMess">请输入正确的邮箱格式</span>
</div>
<div class="form-item">
<div>
<label>验证码:</label>
<div class="verify">
<input v-model:value="code" type="text" name="code" placeholder="" />
<img src="static/img/code.bmp" alt="" />
</div>
</div>
<span class="errMess">请输入正确的验证码</span>
</div>
<button type="submit" @click="registerCheck" class="btn">注册</button>
</form>
第二节 三层架构
1、三层架构划分
- 表述层:负责处理浏览器请求、返回响应、页面调度
- 业务逻辑层:负责处理业务逻辑,根据业务逻辑把持久化层从数据库查询出来的数据进行运算、组装,封装好后返回给表述层,也可以根据业务功能的需要调用持久化层把数据保存到数据库、修改数据库中的数据、删除数据库中的数据
- 持久化层:根据上一层的调用对数据库中的数据执行增删改查的操作
2、三层架构好处
如果不做三层架构形式的拆分:
所有和当前业务功能需求相关的代码全部耦合在一起,如果其中有任何一个部分出现了问题,牵一发而动全身,导致其他无关代码也要进行相应的修改。这样的话代码会非常难以维护。
所以为了提高开发效率,需要对代码进行模块化的拆分。整个项目模块化、组件化程度越高,越容易管理和维护,出现问题更容易排查。3、三层架构和模型的关系
模型对整个项目中三层架构的每一层都提供支持,具体体现是使用模型对象封装业务功能数据。Java实体类有很多不同名称:
- POJO:Plain old Java Object,传统的普通的Java对象
- entity:实体类
- bean或Java bean
- domain:领域模型
4、模型开发的要求
①ORM
ORM:Object Relative Mapping对象关系映射
对象:Java对象
关系:关系型数据库
映射:Java对象和数据库表之间的对应关系
Java类 | 数据库表 |
---|---|
类 | 表 |
属性 | 字段/列 |
对象 | 记录/行 |
属性按照驼峰式命名 | 字段名各个单词之间用下划线分开 |
②Java实体类的要求
- 必须有一个无参构造器将来使用框架后,大量的对象都是框架通过反射来创建的。Class
clazz = Class.forName(“全类名”);clazz.newInstance(); 通过getXxx()、setXxx()方法定义属性:getXxx()或setXxx()方法去掉get或set后,Xxx把首字母小写,得到的xxx就是属性名。 ```java public class User {
private String safeUserName;
public String getUserName(){
return this.safeUserName;
}
public void setUserName(String userName){
this.safeUserName = userName;
}
}
在上面例子中,getXxx()、setXxx()方法定义的属性是userName,不是safeUserName。
<a name="o7ovt"></a>
## 第三节 建模
<a name="NR3kL"></a>
### 1、创建数据库
```sql
CREATE DATABASE bookstore210107 CHARACTER SET utf8;
USE `bookstore210107`;
2、创建数据库表
物理建模的思路参考这里。
CREATE TABLE t_user(
user_id INT PRIMARY KEY AUTO_INCREMENT,
user_name CHAR(100),
user_pwd CHAR(100),
email CHAR(100)
);
3、创建Java实体类
public class User {
private Integer userId;// user_id
private String userName;// user_name
private String userPwd;// user_pwd
private String email;// email
……
第四节 持久化层
1、加入所需jar包
2、创建连接数据库的工具类
3、创建外部属性文件
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://192.168.198.100:3306/bookstore210107
username=root
password=atguigu
initialSize=10
maxActive=20
maxWait=10000
4、在JDBCUtils类中创建数据源对象
private static DataSource dataSource;
static {
// 1.创建一个用于存储外部属性文件信息的Properties对象
Properties properties = new Properties();
// 2.使用当前类的类加载器加载外部属性文件:jdbc.properties
InputStream inputStream = JDBCUtils.class.getClassLoader().getResourceAsStream("jdbc.properties");
try {
// 3.将外部属性文件jdbc.properties中的数据加载到properties对象中
properties.load(inputStream);
// 4.创建数据源对象
dataSource = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
}
}
5、声明工具方法操作数据库连接
/**
* 从数据源中获取数据库连接
* @return 数据库连接对象
*/
public static Connection getConnection() {
Connection connection = null;
try {
connection = dataSource.getConnection();
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
return connection;
}
/**
* 释放数据库连接
* @param connection 要执行释放操作的连接对象
*/
public static void releaseConnection(Connection connection) {
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
测试代码如下:
public class BookstoreTest {
@Test
public void testConnection() {
Connection connection = JDBCUtils.getConnection();
System.out.println("connection = " + connection);
}
}
6、创建BaseDao
①DAO概念
DAO:Data Access Object数据访问对象
DAL:Data Access Layer数据访问层
②创建Java类
③编写通用方法
/**
* 各个具体Dao类的基类,泛型T对应具体实体类类型
* @param <T>
*/
public class BaseDao<T> {
private QueryRunner queryRunner = new QueryRunner();
/**
* 通用的增删改方法
* @param sql 要执行的SQL语句
* @param param 为SQL语句准备好的参数
* @return 受影响的行数
*/
public int update(String sql, Object ... param) {
int updatedRowCount = 0;
Connection connection = JDBCUtils.getConnection();
try {
updatedRowCount = queryRunner.update(connection, sql, param);
}
// 为了让上层方法调用方便,将编译时异常捕获
catch (SQLException e) {
e.printStackTrace();
// 为了不掩盖问题,将编译时异常封装为运行时异常抛出
throw new RuntimeException(e);
} finally {
// 关闭数据库连接
JDBCUtils.releaseConnection(connection);
}
return updatedRowCount;
}
/**
* 查询单个对象
* @param clazz 单个对象所对应的实体类类型
* @param sql 查询单个对象所需要的SQL语句
* @param param SQL语句的参数
* @return 查询到的单个对象
*/
public T getBean(Class<T> clazz, String sql, Object ... param) {
Connection connection = JDBCUtils.getConnection();
T t = null;
try {
t = queryRunner.query(connection, sql, new BeanHandler<>(clazz), param);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
} finally {
// 关闭数据库连接
JDBCUtils.releaseConnection(connection);
}
return t;
}
/**
* 查询集合对象
* @param clazz 集合中单个对象所对应的实体类类型
* @param sql 查询集合所需要的SQL语句
* @param param SQL语句的参数
* @return 查询到的集合对象
*/
public List<T> getBeanList(Class<T> clazz, String sql, Object ... param) {
Connection connection = JDBCUtils.getConnection();
List<T> list = null;
try {
list = queryRunner.query(connection, sql, new BeanListHandler<>(clazz), param);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
} finally {
// 关闭数据库连接
JDBCUtils.releaseConnection(connection);
}
return list;
}
}
测试方法:
@Test
public void testUpdate() {
BaseDao<Object> baseDao = new BaseDao<>();
String sql = "insert into t_user(user_name,user_pwd) values(?,?)";
int count = baseDao.update(sql, "罗志祥", "789456");
System.out.println("count = " + count);
}
@Test
public void testGetBean() {
BaseDao<User> baseDao = new BaseDao<>();
// user_id userId
// user_name userName
// user_pwd userPwd
String sql = "select user_id userId,user_name userName,user_pwd userPwd from t_user where user_id=?";
User user = baseDao.getBean(User.class, sql, 2);
System.out.println("user = " + user);
}
@Test
public void testGetBeanList() {
BaseDao<User> baseDao = new BaseDao<>();
String sql = "select user_id userId,user_name userName,user_pwd userPwd from t_user";
List<User> userList = baseDao.getBeanList(User.class, sql);
for (User user : userList) {
System.out.println("user = " + user);
}
}
7、创建UserDao
①用户登录注册功能中的组件关系图
②声明UserDao接口
public interface UserDao {
/**
* 根据用户名查询User对象
* @param username 用户名
* @return 查询到的User对象
*/
User selectUserByName(String username);
/**
* 将User对象保存到数据库
* @param user 要保存的User对象
* @return 受影响的行数
*/
int insertUser(User user);
}
③声明UserDaoImpl实现类
public class UserDaoImpl extends BaseDao<User> implements UserDao {
@Override
public User selectUserByName(String username) {
String sql = "select user_id userId,user_name userName,user_pwd userPwd,email from t_user where user_name=?";
return super.getBean(User.class, sql, username);
}
@Override
public int insertUser(User user) {
String sql = "insert into t_user(user_name,user_pwd,email) values(?,?,?)";
return super.update(sql, user.getUserName(), user.getUserPwd(), user.getEmail());
}
}
④测试方法
@Test
public void testUserDaoGetUserByName() {
UserDao userDao = new UserDaoImpl();
User user = userDao.selectUserByName("陈冠希");
System.out.println("user = " + user);
}
@Test
public void testUserDaoSaveUser() {
UserDao userDao = new UserDaoImpl();
User user = new User(null, "陈冠希", "666666", "aaa@qq.com");
int count = userDao.insertUser(user);
System.out.println("count = " + count);
}
第五节 完成带数据库的登录注册
1、密码加密
①加密方式介绍
- 对称加密:在知道密文和加密算法的情况下,能够反推回明文
非对称加密:
特点1:不可逆
- 特点2:加密后,密文长度固定
- 特点3:输入数据不变,输出数据也保证不变;输入数据变化,输出数据一定变化
常见的HASH算法举例:
- MD5
- SHA1
- SHA512
-
③执行加密的工具方法
```java public class MD5Util {
/**
- 针对明文字符串执行MD5加密
- @param source
@return */ public static String encode(String source) {
// 1.判断明文字符串是否有效 if (source == null || “”.equals(source)) {
throw new RuntimeException("用于加密的明文不可为空");
}
// 2.声明算法名称 String algorithm = “md5”;
// 3.获取MessageDigest对象 MessageDigest messageDigest = null; try {
messageDigest = MessageDigest.getInstance(algorithm);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
// 4.获取明文字符串对应的字节数组 byte[] input = source.getBytes();
// 5.执行加密 byte[] output = messageDigest.digest(input);
// 6.创建BigInteger对象 int signum = 1; BigInteger bigInteger = new BigInteger(signum, output);
// 7.按照16进制将bigInteger的值转换为字符串 int radix = 16; String encoded = bigInteger.toString(radix).toUpperCase();
return encoded; }
}
<a name="KGVII"></a>
### 2、注册功能
<a name="HIaT5"></a>
#### ①目标
检查用户名是否可用,如果用户名可用则保存User对象
<a name="w2IoO"></a>
#### ②思路

<a name="F8n9k"></a>
#### ③代码
<a name="hEx1n"></a>
##### [1]创建UserService

```java
public interface UserService {
void doRegister(User userForm);
User doLogin(User userForm);
}
开发中,接口设计和接口中方式定义的理念:
- 方法的返回值应该对应这个方法本身的业务功能
- 写操作:没有返回值
- 读操作:有返回值,返回值就是查询的结果
- 方法执行是否成功
- 成功:不抛异常
- 失败:抛异常
启发:
上层方法向下层方法布置任务:方法名、方法的参数
下层方法向上层方法反馈结果:返回值、是否抛异常
#[2]实现UserService接口
(1)UserDao声明为成员变量
说明:将来在Servlet中使用Service的时候,也是同样声明为成员变量,那么从Servlet、Service到Dao就都是『单实例,多线程』方式运行。
public class UserServiceImpl implements UserService {
private UserDao userDao = new UserDaoImpl();
@Override
public void doRegister(User userForm) {
}
@Override
public User doLogin(User userForm) {
return null;
}
}
理由:
- 创建对象的操作只执行一次
-
(2)实现注册功能
@Override
public void doRegister(User userForm) {
// 1.从userForm对象中获取用户通过表单提交的用户名
String userName = userForm.getUserName();
// 2.根据用户名调用UserDao方法查询对应的User对象
User userDB = userDao.selectUserByName(userName);
// 3.检查User对象是否为空
if (userDB != null) {
// 4.如果User对象不为空,则抛出异常,通知上层调用方法:用户名已经被占用
throw new RuntimeException("用户名已经被占用");
}
// 5.对表单提交的密码执行MD5加密
// ①取出表单的密码
String userPwd = userForm.getUserPwd();
// ②执行加密
String encode = MD5Util.encode(userPwd);
// ③将加密得到的密文字符串设置回userForm对象
userForm.setUserPwd(encode);
// 6.调用UserDao方法将userForm对象保存到数据库
userDao.insertUser(userForm);
}
[3]修改RegisterServlet
```java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.从请求参数中获取数据封装为User对象 String username = request.getParameter(“username”); String password = request.getParameter(“password”); String email = request.getParameter(“email”);
User userForm = new User(null, username, password, email);
// 2.调用UserService的方法执行注册 try {
userService.doRegister(userForm);
// 3.如果没有抛出异常那么就跳转到注册成功的页面
// 选择重定向的原因:跳转到regist_success.html页面后,用户刷新浏览器不会重复提交注册表单
response.sendRedirect(request.getContextPath()+"/pages/user/regist_success.html");
} catch (Exception e) {
e.printStackTrace();
// 4.如果抛出了异常
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write("注册失败:" + e.getMessage());
}
}
<a name="is5SF"></a>
### 3、登录功能
<a name="Sxudq"></a>
#### ①关键点提示
对用户密码进行验证时,无法将密文解密为明文,只能将明文再次加密为密文,**『比较密文是否一致』**。
<a name="zCBq9"></a>
#### ②思路

<a name="VX6Hl"></a>
#### ③代码
<a name="BYgkZ"></a>
##### [1]UserService接口的doLogin()
```java
@Override
public User doLogin(User userForm) {
// 1.获取表单提交的用户名
String userName = userForm.getUserName();
// 2.根据用户名调用UserDao方法查询User对象
User userDB = userDao.selectUserByName(userName);
// 3.检查数据库查询的User对象是否为null
if (userDB == null) {
// 4.如果数据库查询的User对象为null,说明用户名不正确,抛出异常:登录失败
throw new RuntimeException("用户名或密码不正确");
}
// 5.密码验证
// ①获取表单提交的密码
String userPwdForm = userForm.getUserPwd();
// ②对表单提交的密码进行加密
String encode = MD5Util.encode(userPwdForm);
// ③获取数据库查询到的密码
String userPwdDB = userDB.getUserPwd();
// ④比较表单密码和数据库密码
if (Objects.equals(encode, userPwdDB)) {
// 6.如果密码验证成功,则将从数据库查询出来的User对象返回
return userDB;
}else{
// 7.如果密码验证失败,说明密码不正确,抛出异常:登录失败
throw new RuntimeException("用户名或密码不正确");
}
}
[2]LoginServlet的doPost()方法
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.从请求参数中获取用户名和密码
String username = request.getParameter("username");
String password = request.getParameter("password");
// 2.封装为User对象
User userForm = new User(null, username, password, null);
// 3.调用UserService的方法执行登录验证
try {
User userDB = userService.doLogin(userForm);
// 4.登录成功后跳转到登录成功页面
response.sendRedirect(request.getContextPath() + "/pages/user/login_success.html");
} catch (Exception e) {
e.printStackTrace();
// 5.登录失败则显示提示消息
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write("登录失败:" + e.getMessage());
}
}
书城项目第三阶段
第一节 创建module,迁移代码
1、创建module
2、加入jar包
attoparser-2.0.5.RELEASE.jar commons-dbutils-1.6.jar druid-1.1.9.jar hamcrest-core-1.3.jar javassist-3.20.0-GA.jar junit-4.12.jar log4j-1.2.15.jar mysql-connector-java-5.1.37-bin.jar ognl-3.1.26.jar slf4j-api-1.7.25.jar slf4j-log4j12-1.7.25.jar thymeleaf-3.0.12.RELEASE.jar unbescape-1.1.6.RELEASE.jar
3、从V02迁移代码
①src目录下的Java源代码
- 在V02的module中的src目录上点右键
- show in explorer在操作系统的窗口内打开
- 在操作系统的窗口内复制package目录和jdbc.properties
- 在操作系统的窗口内粘贴到V03的module中的src目录下
- 在V03的src目录下,找到上一个版本的Servlet,全部删除
- 创建两个子包
- 存放Servlet基类:com.atguigu.bookstore.servlet.base
- 存放Servlet子类:com.atguigu.bookstore.servlet.model
从view-demo中,将两个基类迁移过来
将V02中的pages目录整体复制到V03 module的WEB-INF目录下
- 将V02中的static目录整体复制到V03 module的web目录下
将V02中的index.html复制到V03 module的WEB-INF/pages目录下,将来通过Servlet访问
4、显示首页
①配置web.xml
<!-- 在上下文参数中配置视图前缀和视图后缀 -->
<context-param>
<param-name>view-prefix</param-name>
<param-value>/WEB-INF/pages/</param-value>
</context-param>
<context-param>
<param-name>view-suffix</param-name>
<param-value>.html</param-value>
</context-param>
注意:这里需要将WEB-INF下的view改成pages,和当前项目环境的目录结构一致。
②创建PortalServlet
注意:这个PortalServlet映射的地址是/index.html,这样才能保证访问首页时访问它。
<servlet>
<servlet-name>PortalServlet</servlet-name>
<servlet-class>com.atguigu.bookstore.servlet.model.PortalServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>PortalServlet</servlet-name>
<url-pattern>/index.html</url-pattern>
</servlet-mapping>
注意:PortalServlet服务于首页的显示,为了降低用户访问首页的门槛,不能附加任何请求参数,所以不能继承ModelBaseServlet,只能继承ViewBaseServlet。 ```java public class PortalServlet extends ViewBaseServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 将两种请求方式的处理逻辑合并到一个方法
doPost(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 声明视图名称
String viewName = "index";
// 解析模板视图
processTemplate(viewName, request, response);
}
}
<a name="IMYVu"></a>
#### ③调整index.html
- 加入Thymeleaf名称空间
```html
<html lang="en" xmlns:th="http://www.thymeleaf.org">
修改base标签
<base th:href="@{/}" href="/bookstore/"/>
第二节 完成用户模块
1、重构登录功能
①思路
②实现:创建并组装组件
[1]创建UserServlet
web.xml中的配置:
<servlet>
<servlet-name>UserServlet</servlet-name>
<servlet-class>com.atguigu.bookstore.servlet.model.UserServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>UserServlet</servlet-name>
<url-pattern>/UserServlet</url-pattern>
</servlet-mapping>
Java代码:
public class UserServlet extends ModelBaseServlet {
protected void doLogin(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
protected void toLoginPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
protected void toLoginSuccessPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
}
注意:记得修改UserServlet继承的类ModelBaseServlet。
[2]把UserService组件组装到UserServlet中
public class UserServlet extends ModelBaseServlet {
private UserService userService = new UserServiceImpl();
③实现:前往登录页面
[1]修改首页中登录超链接
<a href="UserServlet?method=toLoginPage" class="login">登录</a>
[2]完成UserServlet.toLoginPage()方法
protected void toLoginPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String viewName = "user/login";
processTemplate(viewName, request, response);
}
[3]调整登录页面代码
加入Thymeleaf名称空间
<html lang="en" xmlns:th="http://www.thymeleaf.org">
修改base标签
<base th:href="@{/}" href="/bookstore/" />
修改form标签action属性
<form id="loginForm" action="UserServlet" method="post">
增加method请求参数的表单隐藏域
<input type="hidden" name="method" value="doLogin" />
根据条件显示登录失败消息
<p style="color: red;font-weight: bold;" th:if="${not #strings.isEmpty(message)}" th:text="${message}">这里根据条件显示登录失败消息</p>
[4]回显表单中的用户名
遇到问题:使用th:value=”${param.username}”确实实现了服务器端渲染,但是实际打开页面并没有看到。原因是页面渲染顺序:
服务器端渲染
- 服务器端将渲染结果作为响应数据返回给浏览器
- 浏览器加载HTML文档
- 读取到Vue代码后,执行Vue代码
- Vue又进行了一次浏览器端渲染,覆盖了服务器端渲染的值
解决办法:将服务器端渲染的结果设置到Vue对象的data属性中。
new Vue({
"el":"#loginForm",
"data":{
"username":"[[${param.username}]]",
"password":""
},
④实现:前往登录成功页面
UserServlet.toLoginSuccessPage()
protected void toLoginSuccessPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String viewName = "user/login_success";
processTemplate(viewName, request, response);
}
login_success.html
<html lang="en" xmlns:th="http://www.thymeleaf.org">
……
<base th:href="@{/}" href="/bookstore/"/>
⑤实现:完成登录操作
protected void doLogin(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.从请求参数中获取用户名和密码
String username = request.getParameter("username");
String password = request.getParameter("password");
// 2.封装为User对象
User userForm = new User(null, username, password, null);
// 3.调用UserService的方法执行登录验证
try {
User userDB = userService.doLogin(userForm);
// 4.登录成功后跳转到登录成功页面
response.sendRedirect(request.getContextPath() + "/UserServlet?method=toLoginSuccessPage");
} catch (Exception e) {
e.printStackTrace();
// 5.登录失败则显示提示消息
// ①将登录失败的提示消息存入请求域
request.setAttribute("message", e.getMessage());
// ②执行登录页面的模板渲染
String viewName = "user/login";
processTemplate(viewName, request, response);
}
}
2、重构注册功能
①思路
②实现:前往注册页面
[1]修改首页中注册超链接
<a href="UserServlet?method=toRegisterPage" class="register">注册</a>
[2]完成UserServlet.toRegisterPage()方法
protected void toRegisterPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String viewName = "user/regist";
processTemplate(viewName, request, response);
}
[3]调整注册页面代码
<html lang="en" xmlns:th="http://www.thymeleaf.org">
……
<base th:href="@{/}" href="/bookstore/"/>
……
<form id="registerForm" action="UserServlet" method="post">
<input type="hidden" name="method" value="doRegister" />
……
<p style="color: red;font-weight: bold;" th:if="${not #strings.isEmpty(message)}" th:text="${message}">这里根据条件显示注册失败消息</p>
new Vue({
"el":"#registerForm",
"data":{
"username":"[[${param.username}]]",
"password":"",
"passwordConfirm":"",
"email":"[[${param.email}]]",
"code":"",
"usernameCheckMessage":""
}
③实现:前往注册成功页面
UserServlet:
protected void toRegisterSuccessPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String viewName = "user/regist_success";
processTemplate(viewName, request, response);
}
regist_success.html:
<html lang="en" xmlns:th="http://www.thymeleaf.org">
……
<base th:href="@{/}" href="/bookstore/"/>
④实现:完成注册操作
protected void doRegister(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.从请求参数中获取数据封装为User对象
String username = request.getParameter("username");
String password = request.getParameter("password");
String email = request.getParameter("email");
User userForm = new User(null, username, password, email);
// 2.调用UserService的方法执行注册
try {
userService.doRegister(userForm);
// 3.如果没有抛出异常那么就跳转到注册成功的页面
// 选择重定向的原因:跳转到regist_success.html页面后,用户刷新浏览器不会重复提交注册表单
response.sendRedirect(request.getContextPath()+"/UserServlet?method=toRegisterSuccessPage");
} catch (Exception e) {
e.printStackTrace();
// 4.如果抛出了异常
request.setAttribute("message", e.getMessage());
String viewName = "user/regist";
processTemplate(viewName, request, response);
}
}
第三节 进入后台开发
1、概念辨析
2、访问后台首页
①思路
首页→后台系统超链接→AdminServlet.toPortalPage()→manager.html
②实现:创建AdminServlet
web.xml
<servlet>
<servlet-name>AdminServlet</servlet-name>
<servlet-class>com.atguigu.bookstore.servlet.model.AdminServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>AdminServlet</servlet-name>
<url-pattern>/AdminServlet</url-pattern>
</servlet-mapping>
Java代码:
public class AdminServlet extends ModelBaseServlet {
protected void toPortalPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String viewName = "manager/manager";
processTemplate(viewName, request, response);
}
}
③实现:调整manager.html
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<base th:href="@{/}" href="/bookstore/"/>
④实现:抽取页面公共部分
[1]公共部分内容
三个超链接:
<a href="./book_manager.html" class="order">图书管理</a>
<a href="./order_manager.html" class="destory">订单管理</a>
<a href="../../index.html" class="gohome">返回商城</a>
[2]抽取它们的理由
[3]创建包含代码片段的页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!-- 使用th:fragment属性给代码片段命名 -->
<div th:fragment="navigator">
<a href="book_manager.html" class="order">图书管理</a>
<a href="order_manager.html" class="destory">订单管理</a>
<a href="index.html" class="gohome">返回商城</a>
</div>
</body>
</html>
[4]在有需要的页面引入片段
<div th:include="segment/admin-navigator :: navigator"></div>
第四节 后台图书CRUD
C:Create 增
R:Retrieve 查
U:Update 改
D:Delete 删
1、建模
①物理建模
点击这里查看SQL文件
注:上面链接建议点右键→目标另存为,直接打开会显示乱码
②逻辑建模
package com.atguigu.bookstore.entity;
public class Book {
private Integer bookId;
private String bookName;
private String author;
private Double price;
private Integer sales;
private Integer stock;
private String imgPath;
2、创建并组装组件
①创建Servlet
注意:由于项目分成『前台』和『后台』,所以Servlet也分成两个:
- 前台:BookPortalServlet
-
②创建BookService
接口:BookService
-
③创建BookDao
接口:BookDao
-
④组装
给BookManagerServlet组装BookService
-
3、图书列表显示功能
①思路
manager.html→图书管理超链接→BookManagerServlet→showBookList()→book_manager.html
②实现:修改图书管理超链接
超链接所在文件位置:
WEB-INF/pages/segment/admin-navigator.html<a href="BookManagerServlet?method=showBookList" class="order">图书管理</a>
③实现:BookManagerServlet.showBookList()
```java protected void showBookList(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.调用Service方法查询图书列表 List
bookList = bookService.getBookList(); // 2.将图书列表数据存入请求域 request.setAttribute(“bookList”, bookList);
// 3.执行视图模板渲染 String viewName = “manager/book_manager”;
processTemplate(viewName, request, response);
}
<a name="mOnGV"></a>
#### ④实现:BookService.getBookList()
```java
@Override
public List<Book> getBookList() {
return bookDao.selectBookList();
}
⑤实现:BookDao.selectBookList()
@Override
public List<Book> selectBookList() {
String sql = "select book_id bookId, book_name bookName, author, price, sales, stock, img_path imgPath from t_book order by book_Id desc";
return getBeanList(Book.class, sql);
}
⑥实现:调整book_manager.html
- Thymeleaf名称空间
- base标签
- 路径中的../和./
-
⑦实现:在book_manager.html中迭代显示图书列表
<tbody th:if="${#lists.isEmpty(bookList)}">
<tr>
<td colspan="7">抱歉,没有查询到您要的数据!</td>
</tr>
</tbody>
<tbody th:if="${not #lists.isEmpty(bookList)}">
<tr th:each="book : ${bookList}">
<td>
<img th:src="${book.imgPath}" src="static/uploads/huozhe.jpg" alt=""/>
</td>
<td th:text="${book.bookName}">活着</td>
<td th:text="${book.price}">100.00</td>
<td th:text="${book.author}">余华</td>
<td th:text="${book.sales}">200</td>
<td th:text="${book.stock}">400</td>
<td>
<a href="book_edit.html">修改</a><a href="" class="del">删除</a>
</td>
</tr>
</tbody>
4、图书删除功能
①思路
book_manager.html→删除超链接→BookManagerServlet.removeBook()→重定向显示列表功能
②实现:删除超链接
<a th:href="@{/BookManagerServlet(method=removeBook,bookId=${book.bookId})}" class="del">删除</a>
③实现:BookManagerServlet.removeBook()
```java protected void removeBook(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.从请求参数中获取bookId String bookId = request.getParameter(“bookId”);
// 2.调用Service方法执行删除 bookService.removeBook(bookId);
// 3.重定向到显示列表功能 response.sendRedirect(request.getContextPath() + “/BookManagerServlet?method=showBookList”);
}
<a name="DUCaB"></a>
#### ④实现:BookService.removeBook()
```java
@Override
public void removeBook(String bookId) {
bookDao.deleteBook(bookId);
}
⑤实现:BookDao.deleteBook()
@Override
public void deleteBook(String bookId) {
String sql = "delete from t_book where book_id=?";
update(sql, bookId);
}
5、新增图书功能
①思路
book_manager.html→添加图书超链接→BookManagerServlet.toAddPage()→book_add.html
book_add.html→提交表单→BookManagerServlet.saveBook()→重定向显示列表功能
②实现:添加图书超链接
<a href="BookManagerServlet?method=toAddPage">添加图书</a>
③实现:BookManagerServlet.toAddPage()
protected void toAddPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String viewName = "book_add";
processTemplate(viewName, request, response);
}
④实现:book_add.html
由book_edit.html复制出来,然后调整表单标签:
<form action="BookManagerServlet" method="post">
<input type="hidden" name="method" value="saveBook" />
<input type="text" name="bookName" placeholder="请输入名称"/>
<input type="number" name="price" placeholder="请输入价格"/>
<input type="text" name="author" placeholder="请输入作者"/>
<input type="number" name="sales" placeholder="请输入销量"/>
<input type="number" name="stock" placeholder="请输入库存"/>
⑤实现:BookManagerServlet.saveBook()
protected void saveBook(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.获取请求参数
String bookName = request.getParameter("bookName");
String price = request.getParameter("price");
String author = request.getParameter("author");
String sales = request.getParameter("sales");
String stock = request.getParameter("stock");
// 2.封装对象
// ※imgPath按说应该通过文件上传的方式提供,但是现在这个技术还没学
// 所以暂时使用一个固定值
String imgPath = "static/uploads/mi.jpg";
Book book = new Book(null, bookName, author, Double.parseDouble(price), Integer.parseInt(sales), Integer.parseInt(stock), imgPath);
// 3.调用Service方法执行保存
bookService.saveBook(book);
// 4.重定向到显示列表页面
response.sendRedirect(request.getContextPath() + "/BookManagerServlet?method=showBookList");
}
⑥实现:BookService.saveBook()
@Override
public void saveBook(Book book) {
bookDao.insertBook(book);
}
⑦实现:BookDao.insertBook()
@Override
public void insertBook(Book book) {
String sql = "insert into `t_book`(`book_name`,`author`,`price`,`sales`,`stock`,`img_path`) values (?,?,?,?,?,?)";
update(sql, book.getBookName(), book.getAuthor(), book.getPrice(), book.getSales(), book.getStock(), book.getImgPath());
}
6、修改图书功能
①思路
book_manager.html→修改图书超链接→BookManagerServlet.toEditPage()→book_edit.html(表单回显)
book_edit.html→提交表单→BookManagerServlet.updateBook()→重定向显示列表功能
②实现:修改图书超链接
<a th:href="@{/BookManagerServlet(method=toEditPage,bookId=${book.bookId})}">删除</a>
③实现:BookManagerServlet.toEditPage()
protected void toEditPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.从请求参数中获取bookId
String bookId = request.getParameter("bookId");
// 2.根据bookId查询对应的Book对象
Book book = bookService.getBookById(bookId);
// 3.把Book对象存入请求域
request.setAttribute("book", book);
// 4.渲染视图
String viewName = "manager/book_edit";
processTemplate(viewName, request, response);
}
④实现:BookService.getBookById()
@Override
public Book getBookById(String bookId) {
return bookDao.selectBookByPrimaryKey(bookId);
}
⑤实现:BookDao.selectBookByPrimaryKey()
@Override
public Book selectBookByPrimaryKey(String bookId) {
String sql = "select book_id bookId, book_name bookName, author, price, sales, stock, img_path imgPath from t_book where book_id=?";
return getBean(Book.class, sql, bookId);
}
⑥实现:book_edit.html(表单回显)
<form action="BookManagerServlet" method="post">
<input type="hidden" name="method" value="updateBook" />
<input type="text" name="bookName" th:value="${book.bookName}" placeholder="请输入名称"/>
<input type="number" name="price" th:value="${book.price}" placeholder="请输入价格"/>
<input type="text" name="author" th:value="${book.author}" placeholder="请输入作者"/>
<input type="number" name="sales" th:value="${book.sales}" placeholder="请输入销量"/>
<input type="number" name="stock" th:value="${book.stock}" placeholder="请输入库存"/>
<button class="btn">更新</button>
⑦实现:BookManagerServlet.updateBook()
protected void updateBook(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.获取请求参数
String bookId = request.getParameter("bookId");
String bookName = request.getParameter("bookName");
String price = request.getParameter("price");
String author = request.getParameter("author");
String sales = request.getParameter("sales");
String stock = request.getParameter("stock");
// 2.封装对象
Book book = new Book(Integer.parseInt(bookId), bookName, author, Double.parseDouble(price), Integer.parseInt(sales), Integer.parseInt(stock), null);
// 3.调用Service方法执行更新
bookService.updateBook(book);
// 4.渲染视图
response.sendRedirect(request.getContextPath() + "/BookManagerServlet?method=showBookList");
}
⑧实现:BookService.updateBook()
@Override
public void updateBook(Book book) {
bookDao.updateBook(book);
}
⑨实现:BookDao.updateBook()
注意:这里不修改imgPath字段
@Override
public void updateBook(Book book) {
String sql = "update t_book set book_name=?, author=?, price=?, sales=?, stock=? where book_id=?";
update(sql, book.getBookName(), book.getAuthor(), book.getPrice(), book.getSales(), book.getStock(), book.getBookId());
}
第五节 前台图书展示
1、思路
index.html→PortalServlet.doPost()→把图书列表数据查询出来→渲染视图→页面迭代显示图书数据
2、实现:PortalServlet.doPost()
public class PortalServlet extends ViewBaseServlet {
private BookService bookService = new BookServiceImpl();
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 将两种请求方式的处理逻辑合并到一个方法
doPost(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 查询图书列表数据
List<Book> bookList = bookService.getBookList();
// 将图书列表数据存入请求域
request.setAttribute("bookList", bookList);
// 声明视图名称
String viewName = "index";
// 解析模板视图
processTemplate(viewName, request, response);
}
}
3、实现:页面迭代显示图书数据
页面文件:index.html
<div class="list-content" th:if="${#lists.isEmpty(bookList)}">
抱歉,本商城现在没有上架任何商品
</div>
<div class="list-content" th:if="${not #lists.isEmpty(bookList)}">
<div class="list-item" th:each="book : ${bookList}">
<img th:src="${book.imgPath}" src="static/uploads/huozhe.jpg" alt="">
<p>书名:<span th:text="${book.bookName}">活着</span></p>
<p>作者:<span th:text="${book.author}">余华</span></p>
<p>价格:¥<span th:text="${book.price}">66.6</span></p>
<p>销量:<span th:text="${book.sales}">230</span></p>
<p>库存:<span th:text="${book.stock}">1000</span></p>
<button>加入购物车</button>
</div>
</div>
书城项目第四阶段
第一节 保持登录状态
1、创建module
- 迁移src目录下的Java代码
- 迁移web目录下的static目录
- 迁移web/WEB-INF目录下的lib目录和pages目录
- 将lib目录下的jar包添加到运行时环境
将旧的web.xml中的配置复制到新module的web.xml中
2、将登录成功的User对象存入会话域
```java protected void doLogin(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.从请求参数中获取用户名和密码 String username = request.getParameter(“username”); String password = request.getParameter(“password”);
// 2.封装为User对象 User userForm = new User(null, username, password, null);
// 3.调用UserService的方法执行登录验证 try {
User userDB = userService.doLogin(userForm);
// ※保持登录状态,将User对象存入会话域
HttpSession session = request.getSession();
// 注意:不要放错,这里要使用从数据库查询得到的User对象
session.setAttribute("user", userDB);
// 4.登录成功后跳转到登录成功页面
response.sendRedirect(request.getContextPath() + "/UserServlet?method=toLoginSuccessPage");
} catch (Exception e) {
e.printStackTrace();
// 5.登录失败则显示提示消息
// ①将登录失败的提示消息存入请求域
request.setAttribute("message", e.getMessage());
// ②执行登录页面的模板渲染
String viewName = "user/login";
processTemplate(viewName, request, response);
}
}
<a name="sVFb3"></a>
### 3、修改欢迎信息
<a name="N8RRo"></a>
#### ①登录成功页面
```html
<span>欢迎<span class="um_span" th:text="${session.user.username}">张总</span>光临尚硅谷书城</span>
②首页
<div class="topbar-right" th:if="${session.user == null}">
<a href="UserServlet?method=toLoginPage" class="login">登录</a>
<a href="UserServlet?method=toRegisterPage" class="register">注册</a>
<a href="pages/cart/cart.html" class="cart iconfont icon-gouwuche">
购物车
<div class="cart-num">3</div>
</a>
<a href="AdminServlet?method=toPortalPage" class="admin">后台管理</a>
</div>
<!-- 登录后风格-->
<div class="topbar-right" th:if="${session.user != null}">
<span>欢迎你<b th:text="${session.user.userName}">张总</b></span>
<a href="#" class="register">注销</a>
<a href="pages/cart/cart.jsp" class="cart iconfont icon-gouwuche">
购物车
<div class="cart-num">3</div>
</a>
<a href="pages/manager/book_manager.html" class="admin">后台管理</a>
</div>
4、退出登录功能
①页面超链接
<a href="UserServlet?method=logout">注销</a>
②UserServlet.logout()
protected void logout(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession();
// 退出登录方案一:从会话域移除User对象
// session.removeAttribute("user");
// 退出登录方案二:强制Session对象失效
session.invalidate();
processTemplate("index", request, response);
}
第二节 验证码
1、目标
通过让用户填写验证码并在服务器端检查,防止浏览器端使用程序恶意访问。
2、思路
3、操作
①导入jar包
②配置KaptchaServlet
jar包中已经写好了Servlet的Java类,我们只需要在web.xml中配置这个Servlet即可。
<servlet>
<servlet-name>KaptchaServlet</servlet-name>
<servlet-class>com.google.code.kaptcha.servlet.KaptchaServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>KaptchaServlet</servlet-name>
<url-pattern>/KaptchaServlet</url-pattern>
</servlet-mapping>
③通过页面访问测试
http://localhost:8080/bookstore/KaptchaServlet
④在注册页面显示验证码图片
<img src="KaptchaServlet" alt="" />
⑤调整验证码图片的显示效果
[1]去掉边框
KaptchaServlet会在初始化时读取init-param,而它能够识别的init-param在下面类中:
com.google.code.kaptcha.util.Config
web.xml中具体配置如下:
<servlet>
<servlet-name>KaptchaServlet</servlet-name>
<servlet-class>com.google.code.kaptcha.servlet.KaptchaServlet</servlet-class>
<!-- 通过配置初始化参数影响KaptchaServlet的工作方式 -->
<!-- 可以使用的配置项参考com.google.code.kaptcha.util.Config类 -->
<!-- 配置kaptcha.border的值为false取消图片边框 -->
<init-param>
<param-name>kaptcha.border</param-name>
<param-value>no</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>KaptchaServlet</servlet-name>
<url-pattern>/KaptchaServlet</url-pattern>
</servlet-mapping>
开发过程中的工程化细节: no、false、none等等单词从含义上来说都表示『没有边框』这个意思,但是这里必须使用no。 参考的依据是下面的源码:
public boolean getBoolean(String paramName, String paramValue, boolean defaultValue) {
boolean booleanValue;
if (!"yes".equals(paramValue) && !"".equals(paramValue) && paramValue != null) {
if (!"no".equals(paramValue)) {
throw new ConfigException(paramName, paramValue, "Value must be either yes or no.");
}
booleanValue = false;
} else {
booleanValue = defaultValue;
}
return booleanValue;
}
[2]设置图片大小
<img style="width: 150px; height: 40px;" src="KaptchaServlet" alt="" />
⑥点击图片刷新
[1]目的
验证码图片都是经过刻意扭曲、添加了干扰、角度偏转,故意增加了识别的难度。所以必须允许用户在看不出来的时候点击图片刷新,生成新的图片重新辨认。
[2]实现的代码
修改图片的img标签:
<img @click="refreshCodeImage" style="width: 150px; height: 40px;" src="KaptchaServlet" alt="" />
Vue代码:将refreshCodeImage()单击响应函数声明到注册表单验证功能的Vue对象的methods属性中
,
"refreshCodeImage":function () {
// 通过event事件对象的target属性获取当前正在点击的img标签
var imgEle = event.target;
// 设置img标签的src属性
imgEle.src = "KaptchaServlet?random=" + Math.random();
}
⑦执行注册前检查验证码
[1]确认KaptchaServlet将验证码存入Session域时使用的属性名
通过查看源码,找到验证码存入Session域时使用的属性名是:
KAPTCHA_SESSION_KEY
[2]在执行注册的方法中添加新的代码
protected void doRegister(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// ※检查验证码
// 1.从请求参数中获取用户提交的验证码
String codeForm = request.getParameter("code");
// 2.获取Session域中保存的验证码
HttpSession session = request.getSession();
String codeSystem = (String) session.getAttribute("KAPTCHA_SESSION_KEY");
// 3.将表单验证码和系统验证码进行比较
if (Objects.equals(codeForm, codeSystem)) {
// 4.如果比较后发现二者一致,则将用过的验证码从Session域移除
session.removeAttribute("KAPTCHA_SESSION_KEY");
}else{
// 5.如果比较后发现二者不一致,则返回注册的表单页面显示提示信息
request.setAttribute("message", "验证码不正确,请重新填写");
String viewName = "user/regist";
processTemplate(viewName, request, response);
// 6.停止执行当前方法
return ;
}
// 后续是原来的代码……
第三节 购物车
1、功能清单
- 添加购物车
- 显示购物车信息
- 修改购物车中具体商品的数量
- 删除购物车中某个具体商品
-
2、创建购物车模型
①购物车详情类:CartItem
public class CartItem {
private String bookId;
private String bookName;
private String imgPath;
private Double price;
private Integer count;
private Double amount;
// 获取金额时需要计算得到
public Double getAmount() {
return this.count * this.price;
}
②购物车类:Cart
public class Cart {
private Map<String, CartItem> cartItemMap = new HashMap<>();
// 添加购物车
public void addCartItem(Book book) {
// 1.获取当前Book对象的id值
String bookId = book.getBookId() + "";
// 2.根据id值到Map中检查是否已存在
if (cartItemMap.containsKey(bookId)) {
// 3.如果存在,则给原有的CartItem增加数量
CartItem cartItem = cartItemMap.get(bookId);
int newCount = cartItem.getCount() + 1;
cartItem.setCount(newCount);
} else {
// 4.如果不存在,则创建新的CartItem对象
CartItem cartItem = new CartItem();
cartItem.setBookId(bookId);
cartItem.setBookName(book.getBookName());
cartItem.setCount(1);
cartItem.setImgPath(book.getImgPath());
cartItem.setPrice(book.getPrice());
// 5.将新的CartItem对象存入cartItemMap
cartItemMap.put(bookId, cartItem);
}
}
// 从购物车中删除CartItem
public void removeCartItem(String bookId) {
cartItemMap.remove(bookId);
}
// 把某个CartItem的数量修改为指定值
public void updateItemCount(String bookId, Integer newCount) {
// 根据bookId从Map中获取对应的CartItem对象
CartItem cartItem = cartItemMap.get(bookId);
// 设置新的数量值
cartItem.setCount(newCount);
}
// 把某个CartItem的数量+1
public void itemCountIncrease(String bookId) {
CartItem cartItem = cartItemMap.get(bookId);
cartItem.setCount(cartItem.getCount() + 1);
}
// 把某个CartItem的数量-1
public void itemCountDecrease(String bookId) {
CartItem cartItem = cartItemMap.get(bookId);
cartItem.setCount(cartItem.getCount() - 1);
if (cartItem.getCount() == 0) {
removeCartItem(bookId);
}
}
public Map<String, CartItem> getCartItemMap() {
return cartItemMap;
}
// 计算总数量
public Integer getTotalCount() {
// 1.声明一个变量用于存储累加结果
Integer sum = 0;
// 2.遍历Map集合
Set<String> keySet = cartItemMap.keySet();
for (String key : keySet) {
CartItem cartItem = cartItemMap.get(key);
Integer count = cartItem.getCount();
sum = sum + count;
}
// 3.返回累加结果
return sum;
}
// 计算总金额
public Double getTotalAmount() {
// 1.声明一个变量用于存储累加结果
Double sum = 0.0;
// 2.遍历Map集合
Set<String> keySet = cartItemMap.keySet();
for (String key : keySet) {
CartItem cartItem = cartItemMap.get(key);
Double amount = cartItem.getAmount();
sum = sum + amount;
}
// 3.返回累加结果
return sum;
}
}
3、添加购物车功能
①目标
②思路
首页→加入购物车→CartServlet.addCart()→执行添加操作→回到首页
③代码实现
[1]创建CartServlet
```java public class CartServlet extends ModelBaseServlet {
private BookService bookService = new BookServiceImpl();
protected void addCart(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.从请求参数中获取bookId
String bookId = request.getParameter("bookId");
// 2.根据bookId查询图书数据
Book book = bookService.getBookById(bookId);
// 3.获取Session对象
HttpSession session = request.getSession();
// 4.尝试从Session域获取购物车对象
Cart cart = (Cart) session.getAttribute("cart");
// 5.判断Cart对象是否存在
if (cart == null) {
// 6.如果不存在,则创建新的Cart对象
cart = new Cart();
// 7.将新创建的Cart对象存入Session域
session.setAttribute("cart", cart);
}
// 8.添加购物车
cart.addCartItem(book);
// 9.回首页
response.sendRedirect(request.getContextPath() + "/index.html");
}
}
<a name="GOucd"></a>
##### [2]index.html页面
购物车数量显示:
```html
<!-- 登录后风格-->
<div class="topbar-right" th:if="${session.user != null}">
<span>欢迎你<b th:text="${session.user.userName}">张总</b></span>
<a href="#" class="register">注销</a>
<a href="pages/cart/cart.jsp" class="cart iconfont icon-gouwuche">
购物车
<div class="cart-num" th:if="${session.cart != null}" th:text="${session.cart.totalCount}">3</div>
</a>
<a href="pages/manager/book_manager.html" class="admin">后台管理</a>
</div>
加入购物车:
<div class="list-content" th:if="${not #lists.isEmpty(bookList)}">
<div class="list-item" th:each="book : ${bookList}">
<img th:src="${book.imgPath}" src="static/uploads/huozhe.jpg" alt="">
<p>书名:<span th:text="${book.bookName}">活着</span></p>
<p>作者:<span th:text="${book.author}">余华</span></p>
<p>价格:¥<span th:text="${book.price}">66.6</span></p>
<p>销量:<span th:text="${book.sales}">230</span></p>
<p>库存:<span th:text="${book.stock}">1000</span></p>
<!--<button>加入购物车</button>-->
<a th:href="@{/CartServlet(method=addCart,bookId=${book.bookId})}">加入购物车</a>
</div>
</div>
④CSS样式
.books-list .list-content .list-item a {
display: block;
width: 80px;
height: 30px;
border: none;
line-height: 30px;
background-color: #39987c;
margin-top: 5px;
outline: none;
color: #fff;
cursor:pointer;
font-size:12px;
text-align:center;
}
如果修改完成后页面效果没有改变,可以使用Ctrl+F5强制刷新。
4、显示购物车页面
①目标
②思路
首页→购物车超链接→CartServlet.showCart()→cart.html
③代码实现
[1]购物车超链接
登录状态和未登录状态
<a href="CartServlet?method=showCart" class="cart iconfont icon-gouwuche">购物车</a>
[2]CartServlet
protected void showCart(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String viewName = "cart/cart";
processTemplate(viewName, request, response);
}
[3]cart.html
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<base th:href="@{/}"/>
……
<div class="header-right" th:include="segment/welcome :: welcome-page"></div>
……
<tbody th:if="${session.cart == null || session.cart.cartItemMap.size() == 0}">
<tr>
<td colspan="6">购物车还是空空的,赶紧去添加吧!</td>
</tr>
</tbody>
<tbody th:if="${session.cart != null && session.cart.cartItemMap.size() > 0}">
<tr th:each="cartItemEntry : ${session.cart.cartItemMap}">
<td>
<img th:src="${cartItemEntry.value.imgPath}" src="static/uploads/huozhe.jpg" alt=""/>
</td>
<td th:text="${cartItemEntry.value.bookName}">活着</td>
<td>
<span class="count">-</span>
<input class="count-num" type="text" th:value="${cartItemEntry.value.count}" value="1"/>
<span class="count">+</span>
</td>
<td th:text="${cartItemEntry.value.price}">36.8</td>
<td th:text="${cartItemEntry.value.amount}">36.8</td>
<td><a href="">删除</a></td>
</tr>
</tbody>
</table>
<div class="footer" th:if="${session.cart != null && session.cart.cartItemMap.size() > 0}">
<div class="footer-left">
<a href="#" class="clear-cart">清空购物车</a>
<a href="index.html">继续购物</a>
</div>
<div class="footer-right">
<div>共<span th:text="${session.cart.totalCount}">3</span>件商品</div>
<div class="total-price">总金额<span th:text="${session.cart.totalAmount}">99.9</span>元</div>
<a class="pay" href="checkout.html">去结账</a>
</div>
</div>
5、清空购物车
①目标
当用户确定点击清空购物车,将Session域中的Cart对象移除。
②思路
cart.html→清空购物车超链接→绑定单击响应函数→confirm()确认→确定→CartServlet.clearCart()→从Session域移除Cart对象
③代码实现
[1]清空购物车超链接
<a href="CartServlet?method=clearCart" class="clear-cart">清空购物车</a>
[2]绑定单击响应函数
(1)引入Vue
<script src="static/script/vue.js" type="text/javascript" charset="utf-8"></script>
(2)编写Vue代码
new Vue({
"el":"#appCart",
"methods":{
"clearCart":function () {
// 1.使用confirm()弹框向用户确认是否要清空
var confirmResult = confirm("您真的要清空购物车吗?");
// 2.如果用户点击了取消
if (!confirmResult) {
// 3.调用事件对象的方法阻止超链接跳转页面
event.preventDefault();
}
}
}
});
[3]CartServlet.clearCart()
protected void clearCart(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.通过request对象获取Session对象
HttpSession session = request.getSession();
// 2.将Session域的Cart对象移除
session.removeAttribute("cart");
// 3.渲染视图
String viewName = "cart/cart";
processTemplate(viewName, request, response);
}
6、减号
①目标
- 在大于1的数值基础上-1:执行-1的逻辑
-
②思路
减号span绑定单击响应函数→获取文本框中的数据:当前数量→检查数量是否大于1
数量大于1:发送请求访问CartServlet.decrease()方法→回到cart.html
数量等于1:confirm确认→发送请求访问CartServlet.removeItem()方法→回到cart.html
③后端代码
CartServlet.decrease() ```java protected void decrease(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.通过请求参数获取bookId String bookId = request.getParameter(“bookId”);
// 2.通过request对象获取Session对象 HttpSession session = request.getSession();
// 3.从Session域获取购物车对象 Cart cart = (Cart) session.getAttribute(“cart”);
// ※考虑到如果Session超时会导致购物车数据丢失,所以先判断购物车数据是否为空 if (cart != null) {
// 4.从购物车对象中根据bookId执行-1操作
cart.itemCountDecrease(bookId);
}
// 5.回到购物车页面 String viewName = “cart/cart”;
processTemplate(viewName, request, response);
}
<a name="O80p6"></a>
#### ④前端代码
HTML代码:
```html
<td>
<input type="hidden" name="bookId" th:value="${cartItemEntry.value.bookId}" />
<span @click="itemDecrease" class="count">-</span>
……
</td>
Vue代码:
,
"itemDecrease":function () {
// 0.不管执行两种逻辑中的哪一种,都需要将当前item的bookId发送到Servlet
// event:事件对象
// event.target:当前事件操作的控件(HTML元素)
// previousSibling:上一个兄弟节点。当前span标签的上一个兄弟节点是空格,所以要再找上一个兄弟
var bookId = event.target.previousSibling.previousSibling.value;
console.log("bookId="+bookId);
// 1.获取文本框中的value属性值:当前item的数量
// event:事件对象
// event.target:当前事件操作的控件(HTML元素)
// nextSibling:下一个兄弟节点。当前span标签的下一个兄弟节点是空格,所以要再找下一个兄弟
// value:获取input标签的value属性值
var currentCount = event.target.nextSibling.nextSibling.value;
console.log("currentCount="+currentCount);
// 2.判断currentCount是否大于1
if (currentCount > 1) {
// 3.应用-1逻辑
console.log("currentCount > 1");
window.location.href = "CartServlet?method=decrease&bookId="+bookId;
} else {
// 通过DOM树形结构获取图书名称数据
// event:事件对象
// event.target:当前事件操作的控件(HTML元素)
// parentNode:访问父节点
// parentNode.getElementsByTagName():在父节点范围内根据标签名称查找元素,返回数组
// innerHTML:返回标签内部HTML代码
var bookName = event.target.parentNode.parentNode.getElementsByTagName("td")[1].innerHTML;
// 4.应用删除item的逻辑
var confirmResult = confirm("你真的要从购物车中删除『"+bookName+"』这条记录吗?");
if (confirmResult) {
window.location.href = "CartServlet?method=removeItem&bookId="+bookId;
}
}
}
7、删除
①目标
②思路
cart.html→删除超链接→CartServlet.removeItem()→回到cart.html
③代码实现
[1]后端代码
CartServlet.removeItem()
protected void removeItem(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.从请求参数获取bookId
String bookId = request.getParameter("bookId");
// 2.通过request对象获取Session对象
HttpSession session = request.getSession();
// 3.从Session域获取购物车对象
Cart cart = (Cart) session.getAttribute("cart");
// ※考虑到如果Session超时会导致购物车数据丢失,所以先判断购物车数据是否为空
if (cart != null) {
// 4.执行删除item操作
cart.removeCartItem(bookId);
}
// 5.回到购物车页面
String viewName = "cart/cart";
processTemplate(viewName, request, response);
}
[2]前端代码
HTML代码:
<a @click="removeConfirm" th:href="@{/CartServlet(method=removeItem,bookId=${cartItemEntry.value.bookId})}">删除</a>
Vue代码:
,
"removeConfirm":function () {
// 通过DOM树形结构获取图书名称数据
// event:事件对象
// event.target:当前事件操作的控件(HTML元素)
// parentNode:访问父节点
// parentNode.getElementsByTagName():在父节点范围内根据标签名称查找元素,返回数组
// innerHTML:返回标签内部HTML代码
var bookName = event.target.parentNode.parentNode.getElementsByTagName("td")[1].innerHTML;
console.log("bookName=" + bookName);
if(!confirm("你真的要从购物车中删除『"+bookName+"』这条记录吗?")) {
event.preventDefault();
}
}
8、文本框修改
①目标
用户在文本框输入新数据后,根据用户输入在Session中的Cart中修改CartItem中的count
②思路
cart.html→前端数据校验→CartServlet.changeCount()→回到cart.html
③代码实现
[1]后端代码
CartServlet.changeCount()
protected void changeCount(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.从请求参数获取bookId和newCount
String bookId = request.getParameter("bookId");
Integer newCount = Integer.parseInt(request.getParameter("newCount"));
// 2.通过request对象获取Session对象
HttpSession session = request.getSession();
// 3.从Session域获取购物车对象
Cart cart = (Cart) session.getAttribute("cart");
// ※考虑到如果Session超时会导致购物车数据丢失,所以先判断购物车数据是否为空
if (cart != null) {
// 4.执行修改数量操作
cart.updateItemCount(bookId, newCount);
}
// 5.回到购物车页面
String viewName = "cart/cart";
processTemplate(viewName, request, response);
}
[2]前端代码
HTML代码:
<td>
<input type="hidden" name="bookId" th:value="${cartItemEntry.value.bookId}" />
……
<input @change="itemCountChange" class="count-num" type="text" th:value="${cartItemEntry.value.count}" value="1"/>
……
</td>
Vue代码:
,
"itemCountChange":function () {
// 1.获取文本框中输入的数据
var newCount = event.target.value;
// 6.排除小数或零或负数的情况
var regExp = /^[0-9]+$/;
if (!regExp.test(newCount) || newCount == 0) {
// 7.如果用户输入的数据不是数字,给出提示
alert("请输入正整数!");
// 8.将文本框恢复为默认值
event.target.value = event.target.defaultValue;
// 9.当前函数停止执行
return ;
}
// 10.获取bookId值
var bookId = event.target.parentNode.getElementsByTagName("input")[0].value;
console.log("bookId="+bookId);
// 11.发送请求执行修改
window.location.href = "CartServlet?method=changeCount&bookId="+bookId+"&newCount="+newCount;
}
9、加号
①目标
告诉Servlet将Session域中Cart对象里面对应的CartItem执行count+1操作
②思路
加号span绑定单击响应函数→获取文本框中的数据:当前数量→CartServlet.increase()→回到cart.html
③代码实现
[1]后端代码
CartServlet.increase()
protected void increase(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.通过请求参数获取bookId
String bookId = request.getParameter("bookId");
// 2.通过request对象获取Session对象
HttpSession session = request.getSession();
// 3.从Session域获取购物车对象
Cart cart = (Cart) session.getAttribute("cart");
// ※考虑到如果Session超时会导致购物车数据丢失,所以先判断购物车数据是否为空
if (cart != null) {
// 4.从购物车对象中根据bookId执行-1操作
cart.itemCountIncrease(bookId);
}
// 5.回到购物车页面
String viewName = "cart/cart";
processTemplate(viewName, request, response);
}
[2]前端代码
HTML代码:
<td>
<input type="hidden" name="bookId" th:value="${cartItemEntry.value.bookId}" />
……
<span @click="itemIncrease" class="count">+</span>
</td>
Vue代码:
,
"itemIncrease":function () {
// 1.获取bookId值
var bookId = event.target.parentNode.getElementsByTagName("input")[0].value;
// 2.发送请求执行+1
window.location.href = "CartServlet?method=increase&bookId="+bookId;
}
10、Double数据运算过程中精度调整
①问题现象
②解决方案
- 使用BigDecimal类型来进行Double类型数据运算
- 创建BigDecimal类型对象时将Double类型的数据转换为字符串
Cart类:
// 计算总金额
public Double getTotalAmount() {
// 1.声明一个变量用于存储累加结果
BigDecimal sum = new BigDecimal("0.0");
// 2.遍历Map集合
Set<String> keySet = cartItemMap.keySet();
for (String key : keySet) {
CartItem cartItem = cartItemMap.get(key);
Double amount = cartItem.getAmount();
sum = sum.add(new BigDecimal(amount + ""));
}
// 3.返回累加结果
return sum.doubleValue();
}
CartItem类:
// 获取金额时需要计算得到
public Double getAmount() {
BigDecimal bigDecimalCount = new BigDecimal(this.count + "");
BigDecimal bigDecimalPrice = new BigDecimal(this.price + "");
BigDecimal multiReuslt = bigDecimalCount.multiply(bigDecimalPrice);
return multiReuslt.doubleValue();
}
书城项目第五阶段
第一节 登录检查
1、目标
把项目中需要保护的功能保护起来,没有登录不允许访问。但是我们不考虑后台登录检查,仅完成前台登录检查。
- 购物车功能
-
2、思路
3、代码实现
①拦截受保护资源的请求
购物车资源地址:/protected/CartServlet
订单资源地址:/protected/OrderServlet
Filter拦截的地址:/protected/*②对访问购物车资源的地址进行修改
[1]首页加入购物车
<a th:href="@{/protected/CartServlet(method=addCart,bookId=${book.bookId})}">加入购物车</a>
[2]首页显示购物车
<a href="protected/CartServlet?method=showCart" class="cart iconfont icon-gouwuche">购物车</a>
[3]cart.html清空购物车
<a @click="clearCart" href="protected/CartServlet?method=clearCart" class="clear-cart">清空购物车</a>
[4]cart.html删除超链接
<a @click="removeConfirm" th:href="@{/protected/CartServlet(method=removeItem,bookId=${cartItemEntry.value.bookId})}">删除</a>
[5]cart.html中Vue代码
凡是涉及到window.location.href都需要修改:
window.location.href = "protected/CartServlet?method=decrease&bookId="+bookId;
……
③web.xml中修改CartServlet的url-pattern
<servlet>
<servlet-name>CartServlet</servlet-name>
<servlet-class>com.atguigu.bookstore.servlet.model.CartServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>CartServlet</servlet-name>
<url-pattern>/protected/CartServlet</url-pattern>
</servlet-mapping>
④创建执行登录检查的Filter
[1]Filter类
```java /**
- 判断当前请求是否已登录的Filter
- 由于在web.xml的配置中,这个Filter只拦截/protected开头的路径,
所以,凡是被这个Filter拦截的请求都要求必须登录 */ public class LoginFilter implements Filter { public void destroy() {} public void init(FilterConfig config) throws ServletException {}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
// 1.将request对象的类型转换为HttpServletRequest
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 2.通过httpRequest对象获取HttpSession对象
HttpSession session = httpRequest.getSession();
// 3.尝试从Session域获取User对象
User user = (User) session.getAttribute("user");
// 4.判断User对象是否存在
if (user == null) {
// 5.转发到登录页面,提示用户登录后再操作
request.setAttribute("message", "请登录后再操作");
request.getRequestDispatcher("/UserServlet?method=toLoginPage").forward(request, response);
}else{
// 6.如果User对象存在,说明用户已登录,直接放行
chain.doFilter(request, response);
}
}
}
<a name="eC1p9"></a>
##### [2]注册Filter类
```xml
<filter>
<filter-name>LoginFilter</filter-name>
<filter-class>com.atguigu.bookstore.filter.LoginFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>LoginFilter</filter-name>
<url-pattern>/protected/*</url-pattern>
</filter-mapping>
第二节 结账
1、创建订单模型
①物理建模
[1]t_order表
CREATE TABLE t_order(
order_id INT PRIMARY KEY AUTO_INCREMENT,
order_sequence VARCHAR(200),
create_time VARCHAR(100),
total_count INT,
total_amount DOUBLE,
order_status INT,
user_id INT
);
字段名 | 字段作用 |
---|---|
order_id | 主键 |
order_sequence | 订单号 |
create_time | 订单创建时间 |
total_count | 订单的总数量 |
total_amount | 订单的总金额 |
order_status | 订单的状态 |
user_id | 下单的用户的id |
- 虽然order_sequence也是一个不重复的数值,但是不使用它作为主键。数据库表的主键要使用没有业务功能的字段来担任。
- 订单的状态
- 待支付(书城项目中暂不考虑)
- 已支付,待发货:0
- 已发货:1
- 确认收货:2
- 发起退款或退货(书城项目中暂不考虑)
- 用户id
- 从逻辑和表结构的角度来说,这其实是一个外键。
- 但是开发过程中建议先不要加外键约束:因为开发过程中数据尚不完整,加了外键约束开发过程中使用测试数据非常不方便,建议项目预发布时添加外键约束测试。
[2]t_order_item表
| 字段名称 | 字段作用 | | —- | —- | | item_id | 主键 | | book_name | 书名 | | price | 单价 | | item_count | 当前订单项的数量 | | item_amount | 当前订单项的金额 | | order_id | 当前订单项关联的订单表的主键 |CREATE TABLE t_order_item(
item_id INT PRIMARY KEY AUTO_INCREMENT,
book_name VARCHAR(20),
price DOUBLE,
img_path VARCHAR(50),
item_count INT,
item_amount DOUBLE,
order_id VARCHAR(20)
);
说明:book_name、author、price这三个字段其实属于t_book表,我们把它们加入到t_order_item表中,其实并不符合数据库设计三大范式。这里做不符合规范的操作的原因是:将这几个字段加入当前表就不必在显示数据时和t_book表做关联查询,提高查询的效率,这是一种变通的做法。
②逻辑模型
[1]Order类
public class Order {
private Integer orderId;
private String orderSequence;
private String createTime;
private Integer totalCount;
private Double totalAmount;
private Integer orderStatus = 0;
private Integer userId;
[2]OrdrItem类
public class OrderItem {
private Integer itemId;
private String bookName;
private Double price;
private String imgPath;
private Integer itemCount;
private Double itemAmount;
private Integer orderId;
2、创建组件
①持久化层
②业务逻辑层
③表述层
3、结账功能
①具体操作清单
- 创建订单对象
- 给订单对象填充数据
- 生成订单号
- 生成订单的时间
- 从购物车迁移总数量和总金额
- 从已登录的User对象中获取userId并设置到订单对象中
- 将订单对象保存到数据库中
- 获取订单对象在数据库中自增主键的值
- 根据购物车中的CartItem集合逐个创建OrderItem对象
- 每个OrderItem对象对应的orderId属性都使用前面获取的订单数据的自增主键的值
- 把OrderItem对象的集合保存到数据库
- 每一个item对应的图书增加销量
- 每一个item对应的图书减少库存
-
②思路
③代码实现
[1]购物车页面结账超链接
cart.html
<a class="pay" href="protected/OrderClientServlet?method=checkout">去结账</a>
[2]OrderClientServlet.checkout()
protected void checkout(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.获取HttpSession对象
HttpSession session = request.getSession();
// 2.获取购物车对象
Cart cart = (Cart) session.getAttribute("cart");
if (cart == null) {
String viewName = "cart/cart";
processTemplate(viewName, request, response);
return ;
}
// 3.获取已登录的用户对象
User user = (User) session.getAttribute("user");
// 4.调用Service方法执行结账的业务逻辑
String orderSequence = orderService.checkout(cart, user);
// 5.清空购物车
session.removeAttribute("cart");
// 6.将订单号存入请求域
request.setAttribute("orderSequence", orderSequence);
// 7.将页面跳转到下单成功页面
String viewName = "cart/checkout";
processTemplate(viewName, request, response);
}
[3]OrderService.checkout()
@Override
public String checkout(Cart cart, User user) {
// 从User对象中获取userId
Integer userId = user.getUserId();
// 创建订单对象
Order order = new Order();
// 给订单对象填充数据
// 生成订单号=系统时间戳
String orderSequence = System.currentTimeMillis() + "_" + userId;
// 生成订单的时间
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
String createTime = simpleDateFormat.format(new Date());
// 从购物车迁移总数量和总金额
Integer totalCount = cart.getTotalCount();
Double totalAmount = cart.getTotalAmount();
order.setOrderSequence(orderSequence);
order.setCreateTime(createTime);
order.setTotalCount(totalCount);
order.setTotalAmount(totalAmount);
// 将订单对象保存到数据库中
// ※说明:这里对insertOrder()方法的要求是获取自增的主键并将自增主键的值设置到Order对象的orderId属性中
orderDao.insertOrder(order);
// 获取订单对象在数据库中自增主键的值
Integer orderId = order.getOrderId();
// 根据购物车中的CartItem集合逐个创建OrderItem对象
Map<String, CartItem> cartItemMap = cart.getCartItemMap();
Collection<CartItem> cartItems = cartItemMap.values();
List<CartItem> cartItemList = new ArrayList<>(cartItems);
// 为了便于批量保存OrderItem,创建Object[][]
// 二维数组第一维:SQL语句的数量
// 二维数组第二维:SQL语句中参数的数量
Object[][] saveOrderItemParamArr = new Object[cartItems.size()][6];
// 为了便于批量更新Book,创建Object[][]
Object[][] updateBookParamArr = new Object[cartItems.size()][3];
for (int i = 0;i < cartItemList.size(); i++) {
CartItem cartItem = cartItemList.get(i);
// 为保存OrderItem创建Object[]
Object[] orderItemParam = new Object[6];
// book_name,price,img_path,item_count,item_amount,order_id
orderItemParam[0] = cartItem.getBookName();
orderItemParam[1] = cartItem.getPrice();
orderItemParam[2] = cartItem.getImgPath();
orderItemParam[3] = cartItem.getCount();
orderItemParam[4] = cartItem.getAmount();
orderItemParam[5] = orderId;
// 将一维数组存入二维数组中
saveOrderItemParamArr[i] = orderItemParam;
// 创建数组用于保存更新Book数据的信息
String[] bookUpdateInfoArr = new String[3];
// 增加的销量
bookUpdateInfoArr[0] = cartItem.getCount() + "";
// 减少的库存
bookUpdateInfoArr[1] = cartItem.getCount() + "";
// bookId
bookUpdateInfoArr[2] = cartItem.getBookId();
// 将数组存入List集合
updateBookParamArr[i] = bookUpdateInfoArr;
}
// 把OrderItem对象的集合保存到数据库:批量操作
orderItemDao.insertOrderItemArr(saveOrderItemParamArr);
// 使用bookUpdateInfoList对图书数据的表执行批量更新操作
bookDao.updateBookByParamArr(updateBookParamArr);
// 返回订单号
return orderSequence;
}
[4]orderDao.insertOrder(order)
```java @Override public void insertOrder(Order order) {
// ※DBUtils没有封装获取自增主键的方法,需要我们使用原生的JDBC来完成 // 1.获取数据库连接 Connection connection = JDBCUtils.getConnection();
// 2.创建PreparedStatement对象 String sql = “INSERT INTO t_order(order_sequence,create_time,total_count,total_amount,order_status,user_id) VALUES(?,?,?,?,?,?)”;
try {
// ①创建PreparedStatement对象,指明需要自增的主键
PreparedStatement preparedStatement = connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
// ②给PreparedStatement对象设置SQL语句的参数
preparedStatement.setString(1, order.getOrderSequence());
preparedStatement.setString(2, order.getCreateTime());
preparedStatement.setInt(3, order.getTotalCount());
preparedStatement.setDouble(4, order.getTotalAmount());
preparedStatement.setInt(5, order.getOrderStatus());
preparedStatement.setInt(6, order.getUserId());
// ③执行更新
preparedStatement.executeUpdate();
// ④获取封装了自增主键的结果集
ResultSet generatedKeysResultSet = preparedStatement.getGeneratedKeys();
// ⑤解析结果集
if (generatedKeysResultSet.next()) {
int orderId = generatedKeysResultSet.getInt(1);
order.setOrderId(orderId);
}
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
} finally {
JDBCUtils.releaseConnection(connection);
}
}
<a name="tLslB"></a>
##### [5]BaseDao.batchUpdate()
```java
/**
* 通用的批量增删改方法
* @param sql
* @param params 执行批量操作的二维数组
* 每一条SQL语句的参数是一维数组
* 多条SQL语句的参数就是二维数组
* @return 每一条SQL语句返回的受影响的行数
*/
public int[] batchUpdate(String sql, Object[][] params) {
Connection connection = JDBCUtils.getConnection();
int[] rowCountArr = null;
try {
rowCountArr = queryRunner.batch(connection, sql, params);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
} finally {
JDBCUtils.releaseConnection(connection);
}
return rowCountArr;
}
[6]orderItemDao.insertOrderItemArr(saveOrderItemParamArr)
@Override
public void insertOrderItemArr(Object[][] saveOrderItemParamArr) {
String sql = "INSERT INTO t_order_item(book_name,price,img_path,item_count,item_amount,order_id) VALUES(?,?,?,?,?,?)";
super.batchUpdate(sql, saveOrderItemParamArr);
}
[7]bookDao.updateBookByParamArr(updateBookParamArr)
@Override
public void updateBookByParamArr(Object[][] updateBookParamArr) {
String sql = "update t_book set sales=sales+?,stock=stock-? where book_id=?";
super.batchUpdate(sql, updateBookParamArr);
}
第三节 结账过程中使用事务(重要)
1、事务回顾
①ACID属性
- A:原子性 事务中包含的数据库操作缺一不可,整个事务是不可再分的。
- C:一致性 事务执行之前,数据库中的数据整体是正确的;事务执行之后,数据库中的数据整体仍然是正确的。
- 事务执行成功:提交(commit)
- 事务执行失败:回滚(rollback)
- I:隔离性 数据库系统同时执行很多事务时,各个事务之间基于不同隔离级别能够在一定程度上做到互不干扰。简单说就是:事务在并发执行过程中彼此隔离。
- D:持久性 事务一旦提交,就永久保存到数据库中,不可撤销。
②隔离级别
[1]并发问题
| 并发问题 | 问题描述 | | —- | —- | | 脏读 | 当前事务读取了其他事务尚未提交的修改
如果那个事务回滚,那么当前事务读取到的修改就是错误的数据 | | 不可重复读 | 当前事务读取同一个数据,第一次和第二次不一致 | | 幻读 | 当前事务在执行过程中,数据库表增减或减少了一些记录,感觉像是出现了幻觉 |
[2]隔离级别
隔离级别 | 描述 | 能解决的并发问题 |
---|---|---|
读未提交 | 允许当前事务读取其他事务尚未提交的修改 | 啥问题也解决不了 |
读已提交 | 允许当前事务读取其他事务已经提交的修改 | 脏读 |
可重复读 | 当前事务执行时锁定当前记录,不允许其他事务操作 | 脏读、不可重复读 |
串行化 | 当前事务执行时锁定当前表,不允许其他事务操作 | 脏读、不可重复读、幻读 |
2、JDBC事务控制
①同一个数据库连接
②关闭事务的自动提交
connection.setAutoCommit(false);
③提交事务
connection.commit();
④回滚事务
connection.rollBack();
⑤事务整体的代码块
try{
// 关闭事务的自动提交
connection.setAutoCommit(false);
// 事务中包含的所有数据库操作
// 提交事务
connection.commit();
} catch(Excetion e) {
// 回滚事务
connection.rollBack();
} finally {
// 释放数据库连接
connection.close();
}
3、将事务对接到书城项目中
①三层架构中事务要对接的位置
从逻辑上来说,一个事务对应一个业务方法(Service层的一个方法)。
②假想
每一个Service方法内部,都套用了事务操作所需要的try…catch…finally块。
③假想代码的缺陷
- 会出现大量的冗余代码:我们希望能够抽取出来,只写一次
- 对核心业务功能是一种干扰:我们希望能够在编写业务逻辑代码时专注于业务本身,而不必为辅助性质的套路代码分心
将持久化层对数据库的操作写入业务逻辑层,是对业务逻辑层的一种污染,导致持久化层和业务逻辑层耦合在一起
④事务代码抽取
只要是Filter拦截到的请求都会从doFilter()方法经过
- chain.doFilter(req, resp);可以包裹住将来要执行的所有方法
事务操作的try…catch…finally块只要把chain.doFilter(req, resp)包住,就能够包住将来要执行的所有方法 ```java public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
try{
// 关闭事务的自动提交
connection.setAutoCommit(false);
// 『事务中包含的所有数据库操作』就在chain.doFilter(req, resp);将来要调用的方法中
// 所以用事务的try...catch...finally块包住chain.doFilter(req, resp);
// 就能让所有事务方法都『享受』到事务功能的『服务』。
// 所谓框架其实就是把常用的『套路代码』抽取出来,为大家服务,我们享受框架服务提高开发效率。
chain.doFilter(req, resp);
// 提交事务
connection.commit();
}catch(Excetion e){
// 回滚事务
connection.rollBack();
}finally{
// 释放数据库连接
connection.close();
}
}
<a name="Q6uie"></a>
#### ⑤在Filter中获取数据库连接
```java
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
try{
// 获取数据库连接
Connection connection = JDBCUtils.getConnection();
// 关闭事务的自动提交
connection.setAutoCommit(false);
// 『事务中包含的所有数据库操作』就在chain.doFilter(req, resp);将来要调用的方法中
// 所以用事务的try...catch...finally块包住chain.doFilter(req, resp);
// 就能让所有事务方法都『享受』到事务功能的『服务』。
// 所谓框架其实就是把常用的『套路代码』抽取出来,为大家服务,我们享受框架服务提高开发效率。
chain.doFilter(req, resp);
// 提交事务
connection.commit();
}catch(Excetion e){
// 回滚事务
connection.rollBack();
}finally{
// 释放数据库连接
connection.close();
}
}
⑥保证所有数据库操作使用同一个连接
『重要发现』:在书城项目中所有执行SQL语句的代码都是通过JDBCUtils.getConnection()方法获取数据库连接。所以我们可以通过重构JDBCUtils.getConnection()方法实现:所有数据库操作使用同一个连接。
[1]从数据源中只拿出一个
为了保证各个需要Connection对象的地方使用的都是同一个对象,我们从数据源中只获取一个Connection。不是说整个项目只用一个Connection,而是说调用JDBCUtils.getConnection()方法时,只使用一个。所以落实到代码上就是:每次调用getConnection()方法时先检查是否已经拿过了,拿过就给旧的,没拿过给新的。
[2]公共区域
为了保证各个方法中需要Connection对象时都能拿到同一个对象,需要做到:将唯一的对象存入一个大家都能接触到的地方。
结论:使用线程本地化技术实现Connection对象从上到下传递。
⑦线程本地化
[1]确认同一个线程
在从Filter、Servlet、Service一直到Dao运行的过程中,我们始终都没有做类似new Thread().start()这样开启新线程的操作,所以整个过程在同一个线程中。
[2]一条小河
[3]一个线程
[4]代码
java.lang.ThreadLocal的set()方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
java.lang.TheadLocal的get()方法:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
所以TheadLocal的基本原理是:它在内部维护了一个Map,需要存入数据时,就以this为键,要存入的数据为值,存入Map。需要取出数据时,就以this为键,从Map中取出数据。
[5]结论
如果我们需要将数据在整个项目中按照从上到下的方式传递,但是又没法通过方法的参数来实现,这时使用线程本地化技术是一个非常好的选择。
⑧异常向上抛出的线路
上图中标记颜色的位置都是有try…catch块的代码,需要逐个检查一下,catch块捕获的异常是否转换为运行时异常又再次抛出。
如果没有抛出,异常就不会传递到Filter中,TransactionFilter就会认为代码执行过程中没有发生问题,从而提交事务,但是实际上应该回滚。下面是一个例子:
/**
* 通用的批量增删改方法
* @param sql
* @param params 执行批量操作的二维数组
* 每一条SQL语句的参数是一维数组
* 多条SQL语句的参数就是二维数组
* @return 每一条SQL语句返回的受影响的行数
*/
public int[] batchUpdate(String sql, Object[][] params) {
Connection connection = JDBCUtils.getConnection();
int[] rowCountArr = null;
try {
rowCountArr = queryRunner.batch(connection, sql, params);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
} finally {
JDBCUtils.releaseConnection(connection);
}
return rowCountArr;
}
4、代码实现
①重构JDBCUtils类
- 要点1:将ThreadLocal对象声明为静态成员变量
- 要点2:重构获取数据库连接的方法
要点3:重构释放数据库连接的方法
/**
* 功能1:创建数据源对象
* 功能2:获取数据库连接并绑定到当前线程上
* 功能3:释放数据库连接并从当前线程移除
*/
public class JDBCUtils {
// 将数据源对象设置为静态属性,保证大对象的单一实例
private static DataSource dataSource;
// 将ThreadLocal对象设置为静态成员变量,保证以此为键时从Map中取值能够取到同一个值
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
static {
// 1.创建一个用于存储外部属性文件信息的Properties对象
Properties properties = new Properties();
// 2.使用当前类的类加载器加载外部属性文件:jdbc.properties
InputStream inputStream = JDBCUtils.class.getClassLoader().getResourceAsStream("jdbc.properties");
try {
// 3.将外部属性文件jdbc.properties中的数据加载到properties对象中
properties.load(inputStream);
// 4.创建数据源对象
dataSource = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 从数据源中获取数据库连接
* @return 数据库连接对象
*/
public static Connection getConnection() {
// 1.尝试从当前线程获取Connection对象
Connection connection = threadLocal.get();
if (connection == null) {
try {
// 2.如果从当前线程上没有获取到Connection对象那么从数据源获取
connection = dataSource.getConnection();
// 3.将Connection对象绑定到当前线程
threadLocal.set(connection);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
// 4.返回Connection对象
return connection;
}
/**
* 释放数据库连接
* @param connection 要执行释放操作的连接对象
*/
public static void releaseConnection(Connection connection) {
if (connection != null) {
try {
connection.close();
// 将Connection对象从当前线程移除
threadLocal.remove();
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
}
②重构BaseDao
要点:去除释放数据库连接的操作(转移到TransactionFilter中) ```java /**
- 各个具体Dao类的基类,泛型T对应具体实体类类型
@param
*/ public class BaseDao { private QueryRunner queryRunner = new QueryRunner();
/**
- 通用的批量增删改方法
- @param sql
- @param params 执行批量操作的二维数组
- 每一条SQL语句的参数是一维数组
- 多条SQL语句的参数就是二维数组
@return 每一条SQL语句返回的受影响的行数 */ public int[] batchUpdate(String sql, Object[][] params) {
Connection connection = JDBCUtils.getConnection();
int[] rowCountArr = null;
try {
rowCountArr = queryRunner.batch(connection, sql, params);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}/* finally {
JDBCUtils.releaseConnection(connection);
}*/
return rowCountArr;
}
/**
- 通用的增删改方法
- @param sql 要执行的SQL语句
- @param param 为SQL语句准备好的参数
@return 受影响的行数 */ public int update(String sql, Object … param) {
int updatedRowCount = 0;
Connection connection = JDBCUtils.getConnection();
try {
updatedRowCount = queryRunner.update(connection, sql, param);
} // 为了让上层方法调用方便,将编译时异常捕获 catch (SQLException e) {
e.printStackTrace();
// 为了不掩盖问题,将编译时异常封装为运行时异常抛出
throw new RuntimeException(e);
}/* finally {
// 关闭数据库连接
JDBCUtils.releaseConnection(connection);
}*/
return updatedRowCount;
}
/**
- 查询单个对象
- @param clazz 单个对象所对应的实体类类型
- @param sql 查询单个对象所需要的SQL语句
- @param param SQL语句的参数
@return 查询到的单个对象 */ public T getBean(Class
clazz, String sql, Object … param) { Connection connection = JDBCUtils.getConnection();
T t = null;
try {
t = queryRunner.query(connection, sql, new BeanHandler<>(clazz), param);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}/* finally {
// 关闭数据库连接
JDBCUtils.releaseConnection(connection);
}*/
return t; }
/**
- 查询集合对象
- @param clazz 集合中单个对象所对应的实体类类型
- @param sql 查询集合所需要的SQL语句
- @param param SQL语句的参数
@return 查询到的集合对象 */ public List
getBeanList(Class clazz, String sql, Object … param) { Connection connection = JDBCUtils.getConnection();
List
list = null; try {
list = queryRunner.query(connection, sql, new BeanListHandler<>(clazz), param);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}/* finally {
// 关闭数据库连接
JDBCUtils.releaseConnection(connection);
}*/
return list; }
}
**注意**:OrderDao中insertOrder()方法也要去掉关闭数据库连接的操作。
```java
@Override
public void insertOrder(Order order) {
// ※DBUtils没有封装获取自增主键的方法,需要我们使用原生的JDBC来完成
// 1.获取数据库连接
Connection connection = JDBCUtils.getConnection();
// 2.创建PreparedStatement对象
String sql = "INS222ERT INTO t_order(order_sequence,create_time,total_count,total_amount,order_status,user_id) VALUES(?,?,?,?,?,?)";
try {
// ①创建PreparedStatement对象,指明需要自增的主键
PreparedStatement preparedStatement = connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
// ②给PreparedStatement对象设置SQL语句的参数
preparedStatement.setString(1, order.getOrderSequence());
preparedStatement.setString(2, order.getCreateTime());
preparedStatement.setInt(3, order.getTotalCount());
preparedStatement.setDouble(4, order.getTotalAmount());
preparedStatement.setInt(5, order.getOrderStatus());
preparedStatement.setInt(6, order.getUserId());
// ③执行更新
preparedStatement.executeUpdate();
// ④获取封装了自增主键的结果集
ResultSet generatedKeysResultSet = preparedStatement.getGeneratedKeys();
// ⑤解析结果集
if (generatedKeysResultSet.next()) {
int orderId = generatedKeysResultSet.getInt(1);
order.setOrderId(orderId);
}
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
} /*finally {
JDBCUtils.releaseConnection(connection);
}*/
}
③创建一个用于显示通用错误信息的页面
[1]创建页面
[2]创建Servlet跳转到页面
protected void showSystemError(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String viewName = "error";
processTemplate(viewName, request, response);
}
④创建TransactionFilter
<filter>
<filter-name>TransactionFilter</filter-name>
<filter-class>com.atguigu.bookstore.filter.TransactionFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>TransactionFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Java代码如下:
public class TransactionFilter implements Filter {
private static final Set<String> PUBLIC_STATIC_RESOURCE_EXT_NAME_SET = new HashSet<>();
static {
PUBLIC_STATIC_RESOURCE_EXT_NAME_SET.add(".png");
PUBLIC_STATIC_RESOURCE_EXT_NAME_SET.add(".css");
PUBLIC_STATIC_RESOURCE_EXT_NAME_SET.add(".js");
PUBLIC_STATIC_RESOURCE_EXT_NAME_SET.add(".jpg");
PUBLIC_STATIC_RESOURCE_EXT_NAME_SET.add(".gif");
}
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
// 排除掉静态资源,它们和数据库操作没有关系
// 1.给请求和响应对象转换类型
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
// 2.获取当前请求的ServletPath
String servletPath = request.getServletPath();
// 3.检查servletPath中是否包含“.”
if (servletPath.contains(".")) {
int index = servletPath.lastIndexOf(".");
String extensionName = servletPath.substring(index);
if (PUBLIC_STATIC_RESOURCE_EXT_NAME_SET.contains(extensionName)) {
chain.doFilter(request, response);
return ;
}
}
// 执行事务操作
// 1.获取数据库连接
Connection connection = JDBCUtils.getConnection();
// 2.使用try...catch...finally块管理事务
try{
// 3.关闭事务的自动提交
connection.setAutoCommit(false);
// 4.尝试执行目标代码
chain.doFilter(request, response);
// 5.如果上面的操作没有抛出异常
connection.commit();
}catch (Exception e){
// 6.如果上面的操作抛出了异常
try {
connection.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
// 7.捕获到异常后,跳转到专门的页面显示提示消息
String message = e.getMessage();
request.setAttribute("error", message);
request.getRequestDispatcher("/ErrorServlet?method=showSystemError").forward(request, response);
}finally {
// 8.不管前面操作是成功还是失败,到这里都要释放数据库连接
JDBCUtils.releaseConnection(connection);
}
}
public void init(FilterConfig config) throws ServletException {}
public void destroy() {}
}
第四节 后台查看订单列表&发货
第五节 前台查看订单列表&确认收货
书城项目第六阶段
第一节 注册页面用户名唯一性检查优化
1、准备工作
- 创建module
-
2、加入Ajax开发环境
①前端所需axios库
②后端所需Gson库
3、封装AjaxCommonsResult
①模型的作用
在整个项目中,凡是涉及到给Ajax请求返回响应,我们都封装到AjaxCommonsResult类型中。
②模型的代码
public class AjaxCommonResult<T> {
public static final String SUCCESS = "SUCCESS";
public static final String FAILED = "FAILED";
private String result;
private String message;
private T data;
各个属性的含义:
属性名 | 含义 |
---|---|
SUCCESS | 代表服务器端处理请求成功 |
FAILED | 代表服务器端处理请求失败 |
result | 服务器端处理请求的结果,取值在SUCCESS和FAILED二者中选择一个 |
message | 失败消息 |
data | 针对查询操作返回的数据 |
③模型的好处
- 作为整个团队开发过程中,前后端交互时使用的统一的数据格式
-
4、功能实现
①定位功能的位置
②思路
[1]给用户名输入框绑定的事件类型
结论:不能在针对username设定的watch中发送Ajax请求。
原因:服务器端响应的速度跟不上用户输入的速度,而且服务器端异步返回响应数据,无法保证和用户输入的顺序完全一致。此时有下面几点问题: 给服务器增加不必要的压力
- 用户输入的数据在输入过程中是不断发生变化的
- 响应数据和输入顺序不对应,会发生错乱
[2]流程图
③代码实现
[0]在当前页面引入axios库文件
<script src="static/script/axios.js" type="text/javascript" charset="utf-8"></script>
[1]给用户名输入框绑定值改变事件
<input v-model:value="username" @change="usernameUniqueCheck" type="text" name="username" placeholder="请输入用户名" />
[2]JavaScript代码
var registerApp = new Vue({
"el":"#registerForm",
"data":{
"username":"[[${param.username}]]",
"password":"",
"passwordConfirm":"",
"email":"[[${param.email}]]",
"code":"",
"usernameCheckMessage":""
},
"watch":{……},
"methods":{
……,
……,
"usernameUniqueCheck":function () {
// 获取用户在文本框中输入的数据
var username = this.username;
// 发送Ajax请求执行检查
axios({
"method":"post",
"url":"UserServlet",
"params":{
"method":"checkUsernameUnique",
"username":username
}
}).then(function (response) {
// 1.从响应数据中获取请求处理结果
var result = response.data.result;
// 2.判断result的值
if (result == "SUCCESS") {
// 3.用户名可用
// 注意:现在我们在then()的回调函数中,this已经不再指向Vue对象了
// 所以,我们通过Vue对象的变量名来访问Vue对象
registerApp.usernameCheckMessage = "用户名可用";
} else {
// 4.用户名不可用
registerApp.usernameCheckMessage = response.data.message;
}
}).catch(function (error) {
console.log(error);
});
}
}
});
[3]UserServlet
protected void checkUsernameUnique(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
AjaxCommonResult<String> ajaxResult = null;
// 1.从请求参数中获取用户名
String username = request.getParameter("username");
try {
// 2.调用Service方法检查用户名是否被占用
userService.checkUsernameUnique(username);
// 3.按照检测成功的结果创建AjaxCommonResult对象
ajaxResult = new AjaxCommonResult<>(AjaxCommonResult.SUCCESS, null, null);
} catch (Exception e) {
e.printStackTrace();
// 4.按照检测失败的结果创建AjaxCommonResult对象
ajaxResult = new AjaxCommonResult<>(AjaxCommonResult.FAILED, e.getMessage(), null);
}
// 5.根据ajaxResult对象返回响应数据
// ①创建Gson对象
Gson gson = new Gson();
// ②执行JSON数据转换
String json = gson.toJson(ajaxResult);
// ③设置响应体内容类型
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(json);
}
[4]UserService
@Override
public void checkUsernameUnique(String username) {
User user = userDao.selectUserByName(username);
if (user != null) {
throw new RuntimeException("用户名已经被占用");
}
}
第二节 加入购物车
1、思路
2、代码实现
①加入layer弹层组件
<script type="text/javascript" src="static/script/jquery-1.7.2.js"></script>
<script type="text/javascript" src="static/layer/layer.js"></script>
②顶层bar绑定Vue对象
Thymeleaf在服务器端渲染的过程中将购物车总数量计算得到,通过表达式设置写入JavaScript代码,作为Vue对象的初始值。然后由Vue对象通过v-show判断是否显示数量标签。
[1]在HTML标签上标记id
由于要考虑是否登录的情况,所以id加到了两种情况外层的div
<div id="topBarApp" class="w">
<div class="topbar-left">
<i>送至:</i>
<i>北京</i>
<i class="iconfont icon-ai-arrow-down"></i>
</div>
<div class="topbar-right" th:if="${session.user == null}">
<a href="UserServlet?method=toLoginPage" class="login">登录</a>
<a href="UserServlet?method=toRegisterPage" class="register">注册</a>
<a href="protected/CartServlet?method=showCart" class="cart iconfont icon-gouwuche">购物车</a>
<a href="AdminServlet?method=toPortalPage" class="admin">后台管理</a>
</div>
<!-- 登录后风格-->
<div class="topbar-right" th:if="${session.user != null}">
<span>欢迎你<b th:text="${session.user.userName}">张总</b></span>
<a href="#" class="register">注销</a>
<a href="protected/CartServlet?method=showCart" class="cart iconfont icon-gouwuche">
购物车
<div class="cart-num" v-show="totalCount > 0">{{totalCount}}</div>
</a>
<a href="pages/manager/book_manager.html" class="admin">后台管理</a>
</div>
</div>
[2]创建Vue对象
// topBarApp对象的totalCount属性的初始值是Thymeleaf在服务器端运算出来用表达式设置的
var topBarApp = new Vue({
"el": "#topBarApp",
"data": {
"totalCount": [[${(session.cart == null)?"0":session.cart.totalCount}]]
}
});
③图书列表div绑定Vue对象
[1]在HTML标签上标记id
目的是为了便于创建Vue对象
<div id="bookListApp" class="list-content" th:if="${not #lists.isEmpty(bookList)}">
<div class="list-item" th:each="book : ${bookList}">
<img th:src="${book.imgPath}" src="static/uploads/huozhe.jpg" alt="">
<p>书名:<span th:text="${book.bookName}">活着</span></p>
<p>作者:<span th:text="${book.author}">余华</span></p>
<p>价格:¥<span th:text="${book.price}">66.6</span></p>
<p>销量:<span th:text="${book.sales}">230</span></p>
<p>库存:<span th:text="${book.stock}">1000</span></p>
<!--<button>加入购物车</button>-->
<a th:href="@{/protected/CartServlet(method=addCart,bookId=${book.bookId})}">加入购物车</a>
</div>
</div>
[2]在首页引入Vue和axios库文件
<script src="static/script/vue.js" type="text/javascript" charset="utf-8"></script>
<script src="static/script/axios.js" type="text/javascript" charset="utf-8"></script>
[3]创建Vue对象
<script type="text/javascript">
new Vue({
"el":"#bookListApp"
});
</script>
[4]绑定单击响应函数
给加入购物车按钮绑定单击响应函数
<button @click="addToCart">加入购物车</button>
Vue代码:
new Vue({
"el":"#bookListApp",
"methods":{
"addToCart":function () {
}
}
});
[5]将bookId设置到按钮中
为了便于在按钮的单击响应函数中得到bookId的值
<button th:id="${book.bookId}" @click="addToCart">加入购物车</button>
[6]在单击响应函数中发送Ajax请求
new Vue({
"el":"#bookListApp",
"methods":{
"addToCart":function () {
// event:事件对象
// event.target:当前事件操作的对象
// event.target.id:前事件操作的对象的id属性的值
var bookId = event.target.id;
axios({
"method":"post",
"url":"protected/CartServlet",
"params":{
"method":"addCart",
"bookId":bookId
}
}).then(function (response) {
var result = response.data.result;
if (result == "SUCCESS") {
// 给出提示:加入购物车成功
layer.msg("加入购物车成功");
// 从响应数据中获取购物车总数量
// response.data其实就是AjaxCommonResult对象的JSON格式
// response.data.data就是访问AjaxCommonResult对象的data属性
var totalCount = response.data.data;
// 修改页头位置购物车的总数量
topBarApp.totalCount = totalCount;
}else {
// 给出提示:response.data.message
layer.msg(response.data.message);
}
}).catch(function (error) {
console.log(error);
});
}
}
});
④后端代码
CartServlet
protected void addCart(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.从请求参数中获取bookId
String bookId = request.getParameter("bookId");
// 2.根据bookId查询图书数据
Book book = bookService.getBookById(bookId);
// 3.获取Session对象
HttpSession session = request.getSession();
// 4.尝试从Session域获取购物车对象
Cart cart = (Cart) session.getAttribute("cart");
// 5.判断Cart对象是否存在
if (cart == null) {
// 6.如果不存在,则创建新的Cart对象
cart = new Cart();
// 7.将新创建的Cart对象存入Session域
session.setAttribute("cart", cart);
}
// 8.添加购物车
cart.addCartItem(book);
// 9.给Ajax返回JSON格式响应
// ①创建AjaxCommonResult对象
AjaxCommonResult<Integer> result = new AjaxCommonResult<>(AjaxCommonResult.SUCCESS, null, cart.getTotalCount());
// ②创建Gson对象
Gson gson = new Gson();
// ③将AjaxCommonResult对象转换为JSON字符串
String json = gson.toJson(result);
// ④设置响应体的内容类型
response.setContentType("application/json;charset=UTF-8");
// ⑤返回响应
response.getWriter().write(json);
}
第三节 显示购物车数据
1、思路
2、代码实现
①CartServlet增加getCartJSON()方法
[1]Cart模型的局限性
目前的Cart对象转换为JSON后,没有totalCount、totalAmount这样的属性,Map结构也不如LIst遍历方便。
[2]调整方式
[3]方法代码
protected void getCartJSON(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
AjaxCommonResult<Map<String, Object>> result = null;
// 1.获取Session对象
HttpSession session = request.getSession();
// 2.尝试获取购物车对象
Cart cart = (Cart) session.getAttribute("cart");
// 3.检查cart对象是否为空
if (cart == null) {
result = new AjaxCommonResult<>(AjaxCommonResult.FAILED, null, null);
} else {
Map<String, Object> cartJSONMap = new HashMap<>();
cartJSONMap.put("totalCount", cart.getTotalCount());
cartJSONMap.put("totalAmount", cart.getTotalAmount());
cartJSONMap.put("cartItemList", cart.getCartItemMap().values());
result = new AjaxCommonResult<Map<String, Object>>(AjaxCommonResult.SUCCESS, null, cartJSONMap);
}
// 4.将AjaxCommonResult对象转换为JSON作为响应返回
Gson gson = new Gson();
String json = gson.toJson(result);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(json);
}
②前端代码
[1]去除Thymeleaf痕迹
将cart.html页面中,由Thymeleaf渲染数据的部分去掉。
[2]使用Vue对象初步接管页面渲染
new Vue({
"el":"#appCart",
"data":{
"cart":"empty"
},
HTML标签:
<tbody v-if="cart == 'empty'">
<tr>
<td colspan="6">购物车还是空空的,赶紧去添加吧!</td>
</tr>
</tbody>
[3]在mounted生命周期环境发Ajax请求
记得加入axios库:
<script src="static/script/axios.js" type="text/javascript" charset="utf-8"></script>
var cartApp = new Vue({
"el":"#appCart",
"data":{
"cart":"empty"
},
"mounted":function () {
axios({
"method":"post",
"url":"protected/CartServlet",
"params":{
"method":"getCartJSON"
}
}).then(function (response) {
// 1.从响应数据中获取请求处理结果
var result = response.data.result;
// 2.检查结果是成功还是失败
if (result == "SUCCESS") {
// 3.获取购物车数据并赋值给Vue对象
cartApp.cart = response.data.data;
console.log(cartApp.cart);
}
}).catch(function (error) {
console.log(error);
});
},
……
[4]完成Vue页面渲染
<div id="appCart" class="w">
<table>
<thead>
<tr>
<th>图片</th>
<th>商品名称</th>
<th>数量</th>
<th>单价</th>
<th>金额</th>
<th>操作</th>
</tr>
</thead>
<tbody v-if="cart == 'empty'">
<tr>
<td colspan="6">购物车还是空空的,赶紧去添加吧!</td>
</tr>
</tbody>
<tbody v-if="cart != 'empty'">
<tr v-for="cartItem in cart.cartItemList">
<td>
<img :src="cartItem.imgPath" alt=""/>
</td>
<td>{{cartItem.bookName}}</td>
<td>
<input type="hidden" name="bookId" :value="cartItem.bookId" />
<span @click="itemDecrease" class="count">-</span>
<input @change="itemCountChange" class="count-num" type="text" :value="cartItem.count"/>
<span @click="itemIncrease" class="count">+</span>
</td>
<td>{{cartItem.price}}</td>
<td>{{cartItem.amount}}</td>
<td><a @click="removeConfirm" href="protected/CartServlet">删除</a></td>
</tr>
</tbody>
</table>
<div class="footer">
<div class="footer-left">
<a @click="clearCart" href="protected/CartServlet?method=clearCart" class="clear-cart">清空购物车</a>
<a href="index.html">继续购物</a>
</div>
<div class="footer-right">
<div>共<span>{{cart.totalCount}}</span>件商品</div>
<div class="total-price">总金额{{cart.totalAmount}}元</div>
<a class="pay" href="protected/OrderClientServlet?method=checkout">去结账</a>
</div>
</div>
</div>