软件工程–书友网
1 项目概述
1.1 编写目的
提供一个可以发布对书籍的讨论,书友之间互相在帖子下讨论,关注书友结识书友的网站。
1.2 项目背景
在这次疫情中,我有幸得到了不少空余时间可以看看小说,充实了我对书本和文字的渴望,每当读到一本好的小说,或者读到一部分有趣的剧情的时候,就想将这本书或者这部分的剧情安利给自己朋友,而在书友网这里,你可以自由自在地发表自己对书籍人物,或者小说剧情的看法,可以随时随地发表自己有关的感受,同时可以结识书友,一起讨论有关内容。
1.3 开发环境
主体系统使用:SSM框架
开发语言:
- 前端(JSP+CSS+AJAX+ JAVASCRIPT)
- 后端(JAVA + MYSQL)传输协议:HTTP部署在TOMCAT服务器上
1.4 使用框架
- 前端采用JSP+CSS+JAVASCRIPT + jQuery
- 后端采用标准的MVC设计模型:
a. controller: 负责与前端进行数据交互, 并向业务层提供请求参数
b. DAO 对数据库的操作都封装于此
c. common 封装了一些公用方法比如邮箱验证码发送, 数据库连接等
d. service 负责业务逻辑的实现
e. filter 对后台管理权限的验证
f. manage 后台管理的所有操作
g. pojo 实体类
1. 5 小组成员
马赞 前端小组人员,设计实现界面和功能,前后端对接测试者
蔡恒杰 后端小组人员,接口文档设计者,接口功能实现者,后台管理, 本次项目的中流砥柱
周梓浩 后端小组人员,接口功能实现先行者,是本次项目中的主要技术负责人
屠闻哲 前端小组人员,页面和功能设计者,同时负责文档任务
项目github:https://github.com/m794525298/bookcom
2 需求文档
2.1 概要
2.1.1 用例图
2.1.2 层次图
2.1.3 数据流图
2.1.4 业务流程图
2.1.5 概要描述
从以上内容可以看出,书友交流平台的性质就像一个小型的贴吧/社区,在这里书友们互相结识,互相交流,通过管理员对发布帖子的审核来删除错误或不良信息。
2.2 功能需求
2.2.1主要实现功能模块
- 前端
- 主页系统
用户可以在主页看到经过系统用推荐算法算出的适合用户的帖子,作为精选内容。 - 个人中心系统
展示用户的个人信息,个人头像,粉丝数,发布的帖子,回复他人的帖子 - 账号管理系统
提供用户注册,及登录功能,并且可在会员中心,修改个人信息,和查看粉丝信息。 - 在线论坛系统
实现用户可以发布帖子,和回帖功能。
- 管理端
- 帖子管理
- 可以查看所有用户的帖子信息,同时可修改帖子,和提交发布帖子。
- 账号管理
可查看所有账号,及账号信息,可以直接修改相关账号信息。 - 在线论坛
实现论坛版块的添加,编辑,及帖子和回帖的修改删除功能 - 信息管理
展示型信息的编辑增减功能。
2.2.2 前端具体功能模块设计
1) 账号管理系统
用户输入登录信息,如用户名和密码,以系统承认角色身份进入本系统。
2) 主页
主页是一个网站的门面,在主页的布局上,我们希望设计简洁美观的页面来吸引用户,主页的功能有
3) 搜索与发布帖子
基本的登录注册不说,在搜索这里,用户需要输入想要的信息,例如你想讨论的书籍,或者你认识的书友,通过这样的搜索,我们就可以跳转到详尽的搜索页面,显示出用户想要的内容,搜索的内容不能过长,通过搜索发帖人或者搜索感兴趣的书籍就可以找到自己想要的内容。
4) 显示推荐用户
在主页的右侧,会提供一定的推荐用户,他们要么是关注数较多,要么是某方面内容的专家,通过提供推荐用户,让用户结识更多的书友,同时,关注优秀的书友可以关注到更多精品内容,通过关注的方式,可以提高我们网站用户粘性。
5) 显示推荐帖子
主页的大块版面就是推荐的帖子,他们要么讨论最多,要么热度最大,推荐的帖子显示的内容有 帖子的标题,帖子的发布者,帖子的部分简介内容。
6) 提供直观的分类
主页的大块版面就是推荐的帖子,他们要么讨论最多,要么热度最大,推荐的帖子显示的内容有 帖子的标题,帖子的发布者,帖子的部分简介内容。
7) 个人中心系统
个人中心系统应该是本次项目中最为复杂的实践内容,在这里,个人中心要分为两种情况,一种是自己观看自己的个人中心,要做到的功能是:能够更改自己的简介,查看自己的关注列表,在主页上显示有关的讨论帖子和与自己有关的讨论内容以及讨论的回复,方便使用者查看内容,同时在这里,使用者可以点击发布帖子,然后跳转到特定的服务于编辑帖子内容的帖子,暂订的编写方式是markdown,也就是说我们实现一个网页版的markdown供使用者使用,同时要探究如何传输这样的数据,目前还在学习之中。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ollzTYbT-1591271471447)(https://s1.ax1x.com/2020/06/04/t0dJJ0.png)] 当访问其他人的界面是,可以看到有关的个人信息页面内容,也可以看到他最近发布的帖子,通过关注书友,你可以很快了解到书友有关的动态。
当访问其他人的界面是,可以看到有关的个人信息页面内容,也可以看到他最近发布的帖子,通过关注书友,你可以很快了解到书友有关的动态。
8) 讨论贴系统
在讨论贴这里,我们可以看到讨论贴发布的内容,一般是图文并茂的,同时,在讨论帖的下方你可以看到,有关这些帖子的评论,你可以对感兴趣的帖子进行回复,其他人的回复信息你可以在帖子内看到,不仅如此,在个人中心中,其他人的回复内容也会显示在你的个人中心页面,便于查阅。
2.2.3 后台管理系统
在这里,主要负责审核内容和对不良信息的删除,这也是一个交流网站必备的功能。
2.3 非功能需求
2.3.1 用户界面需求
需求 | 详细要求 |
---|---|
界面颜色 | 使用白色蓝色为主色调,使用别的颜色进行搭配。 |
界面风格 | 简洁、大方、明朗界面 |
操作 | 简单易用、点击不超过3次,基本功能都可以在页面上快速找到位置。个人中心页面要求制作精美。 |
2.3.2 软硬件环境要求
需求 | 详细要求 |
---|---|
开发环境 | 服务器Tomcat 9,Java 1.8 ,IDEA |
浏览器配置 | 不得低于IE7浏览器,推荐使用Chrome, Firefox等主流浏览器。 |
云服务器配置 | 操作系统:centos 7; 数据库:MYSQL 8.0 |
2.3.3 安全性
安全要素 | 需求内容 |
---|---|
安全机制独立性 | 安全设计和实现应该具有独立性, 不能依赖当前主机的基础安全机制来确保自身和数据不受破环或拒绝服务. |
安全机制有效性 | 应防止用户绕过其安全控制机制直接尝试访问系统各项功能. |
访问权限 | 对不同用户的访问权限进行严格的访问控制, 特定权限的用户只能看到和使用特定的界面及相应的功能. |
输入限制 | 具备输入字符和输入数据的类型, 长度和范围检查功能. |
防止sql注入 | 系统应没有SQL注入情况. |
权限初始化 | 用户的权限应该符合最小权限原则. |
信息保护 | 用户个人信息保护 |
邮件安全 | 有关信息的邮件发送必须通过预先设定的邮件系统发送. |
安全系统 | 安全系统必须拦截非法的访问, 和对网站的恶意进攻包括 (XSS , SQL Injection, 非法盗链等, 非法字符输入等). |
2.3.4 易用性
要素 | 需求内容 |
---|---|
界面要求 | 要求各个界面风格简介,各个功能易于操作. |
导航栏 | 用户可以通过导航栏查询自己的位置信息,同时提供便利的操作. |
界面提示要求 | 对用户的各种合规以及不合规的操作进行提示. |
2.3.5 可靠性
采用面对对象的系统开发方法:
面向对象的开发方法,是软件工程中一门新的系统开发方法学,与传统的系统设计方法相比较,更便于修改和扩充。面向对象的开发方法是一种建立在对现实世界中的对象分析基础上的开发方法,它的整个开发过程都是围绕着对象展开的。面向对象的开发方法中的对象,是客观世界对象的直接映象,每个对象都是属性与操作的封装体,对象之间只能通过发送消息相互传递信息,因而当需求发生变化时,一般也只涉及个别对象或对象类的修改,不会影响整个系统的结构。当新的需求需要增加新的对象或对象类时,也可以方便地将这些对象添加到系统中去,从而实现系统增量式的连续演变。由于对象是构成系统的最基本的软件单元,每个对象都作为一个独立的单元被测试,易于保证其可靠性从而提高整个软件的可靠性。另外面向对象的开发方法对软件的可扩充性、可再用性、可修改性和兼容性得到了很好的保证。
- 采用结构化的程序设计方法:
“模块化方法”和“逐步求精法”是结构化程序设计的两种重要手法,这两种方法都是采用“处顶向下”的设计原则,分别采用分解和抽象的方法来分析解决复杂问题。结构化的程序设计方法把一个大而复杂的问题分解成若干个功能比较单纯的小问题,或是分步进行逐步求精,直到最后可编程。结构化程序中的三种基本结构,顾序结构、选择结构和循环结构都只有一个入口和一个出口,这种单入口和单出口的特点,对于程序的每一部分,只要考虑在其入口的条件下,在出口都可以获得正确的结果,这部分程序的正确就得到了保证,又由于结构化程序具线状性结构的特点,每一部分是正确的,就可保证整个程序的正确性。
2.3.6 性能
- 前端先对数据格式进行一系列验证,确实符合语法时候才向后端发送,减少不必要的请求。
- 后端对数据库进行合理设计,适当添加外键和索引,读写分离,尽量提高查询效率,降低响应时间的同时,还可以增强数据库的健壮性。
在这个范围内, 系统应该能够很好的工作.单次响应时间最大为3秒, 网页刷新响应在正常网速下要求低于80毫秒.
3 系统设计
3.1 概要设计
在定义需求阶段的结构化分析基础上进行结构化设计,主要采用层次图对系统进行概要设计和详细设计。同时通过接口文档定义下来的各个页面的功能和变量名方便前后端分离开发。
3.2 数据库设计
數据庫需要以下几張表:用戶表,帖子表,關注表,評論表,回覆表,書本类型表和驗證碼表。
要注意的是回覆表是評論與評論之間的關係。
user表:
CREATE TABLE `user` (
`USER_ID` BIGINT ( 20 ) NOT NULL AUTO_INCREMENT COMMENT 'user表主鍵',
`USER_NICKNAME` VARCHAR ( 50 ) NOT NULL COMMENT '昵称',
`USER_ACCOUNT` VARCHAR ( 16 ) NOT NULL COMMENT '帳號',
`USER_PASSWORD` VARCHAR ( 16 ) NOT NULL COMMENT '密碼',
`USER_ICON` VARCHAR ( 2083 ) DEFAULT NULL COMMENT '头像',
`USER_EMAIL` VARCHAR ( 30 ) NOT NULL COMMENT 'email',
`USER_IDENTITY` SMALLINT ( 2 ) NOT NULL DEFAULT '0' COMMENT '身份',
`USER_FOLLOWERSNUM` BIGINT ( 20 ) NOT NULL DEFAULT '0' COMMENT '被關注數',
`USER_FOLLOWINGNUM` BIGINT ( 20 ) NOT NULL DEFAULT '0' COMMENT '關注數',
`USER_MD5ID` VARCHAR ( 20 ) DEFAULT NULL COMMENT '加密后UserID',
PRIMARY KEY ( `USER_ID` ),
UNIQUE KEY `IDX_ACCOUNT` ( `USER_ACCOUNT` ) USING HASH,
UNIQUE KEY `IDX_MD5ID` ( `USER_MD5ID` )
) ENGINE = INNODB DEFAULT CHARSET = utf8;
user表中需要注意的USER_ICON字段和USER_MD5ID字段。
USER_ICON字段記錄的是頭像的存儲路徑。
USER_MD5ID字段記錄的是USER_ID字段通過MD5加密后的字段,而每次發往前端的ID都是MD5加密后的USER_ID。
post表:
CREATE TABLE `post` (
`POST_ID` BIGINT ( 20 ) NOT NULL,
`POST_TIME` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP,
`POST_COVER` VARCHAR ( 2083 ) NOT NULL,
`POST_CONTENT` VARCHAR ( 500 ) NOT NULL,
`POST_POSTTITLE` VARCHAR ( 64 ) NOT NULL,
`POST_BOOKTYPE` INT ( 10 ) NOT NULL,
`POST_BOOKTITLE` VARCHAR ( 32 ) NOT NULL,
`POST_BOOKAUTHOR` VARCHAR ( 20 ) NOT NULL,
`POST_PUBLISHERID` VARCHAR ( 20 ) NOT NULL,
`POST_COMMENTNUM` BIGINT ( 20 ) NOT NULL DEFAULT '0',
`POST_ISEXIST` TINYINT ( 2 ) NOT NULL DEFAULT '0',
PRIMARY KEY ( `POST_ID` ),
KEY `BOOK_PUBLISHERID` ( `POST_PUBLISHERID` ),
CONSTRAINT `BOOK_PUBLISHERID` FOREIGN KEY ( `POST_PUBLISHERID` ) REFERENCES `user` ( `USER_MD5ID` )
) ENGINE = INNODB DEFAULT CHARSET = utf8;
这里的POST_COVER字段也一样是存儲圖片路徑。
follow表:
CREATE TABLE `follow` (
`FOLLOW_ID` BIGINT ( 20 ) NOT NULL AUTO_INCREMENT,
`FOLLOW_FOLLOWER` VARCHAR ( 20 ) NOT NULL COMMENT '追随者',
`FOLLOW_FOLLOWING` VARCHAR ( 20 ) NOT NULL COMMENT '被关注ren',
PRIMARY KEY ( `FOLLOW_ID` ),
KEY `FOLLOW_FOLLOWER` ( `FOLLOW_FOLLOWER` ),
KEY `FOLLOW_FOLLOWING` ( `FOLLOW_FOLLOWING` ),
CONSTRAINT `FOLLOW_FOLLOWER` FOREIGN KEY ( `FOLLOW_FOLLOWER` ) REFERENCES `user` ( `USER_MD5ID` ),
CONSTRAINT `FOLLOW_FOLLOWING` FOREIGN KEY ( `FOLLOW_FOLLOWING` ) REFERENCES `user` ( `USER_MD5ID` )
) ENGINE = INNODB DEFAULT CHARSET = utf8;
comment表:
CREATE TABLE `comment` (
`COMMENT_ID` BIGINT ( 20 ) NOT NULL AUTO_INCREMENT,
`COMMENT_TIME` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`COMMENT_POSTID` BIGINT ( 20 ) NOT NULL,
`COMMENT_PARENTID` BIGINT ( 20 ) DEFAULT NULL,
`COMMENT_CONTENT` LONGTEXT NOT NULL,
`COMMENT_PUBLISHERID` VARCHAR ( 16 ) NOT NULL,
`COMMENT_ISEXIST` TINYINT ( 2 ) NOT NULL DEFAULT '0',
PRIMARY KEY ( `COMMENT_ID` ),
KEY `FOREIGN_POST_ID` ( `COMMENT_POSTID` )
) ENGINE = INNODB DEFAULT CHARSET = utf8;
reply表:
CREATE TABLE `reply` (
`REPLY_ID` BIGINT ( 20 ) NOT NULL AUTO_INCREMENT,
`COMMENT_ID` BIGINT ( 20 ) NOT NULL,
`COMMENT_PARENTID` BIGINT ( 20 ) NOT NULL,
`COMMENT_POSTPUBLISHERID` VARCHAR ( 20 ) NOT NULL,
`COMMENT_PARENTPUBLISHERID` VARCHAR ( 20 ) NOT NULL,
PRIMARY KEY ( `REPLY_ID` )
) ENGINE = INNODB DEFAULT CHARSET = utf8;
booktype表:
CREATE TABLE `booktype` (
`BOOKTYPE_ID` BIGINT ( 20 ) NOT NULL AUTO_INCREMENT,
`BOOKTYPE_TYPE` VARCHAR ( 20 ) NOT NULL,
PRIMARY KEY ( `BOOKTYPE_ID` ),
UNIQUE KEY `BOOK` ( `BOOKTYPE_TYPE` )
) ENGINE = INNODB AUTO_INCREMENT = 7 DEFAULT CHARSET = utf8;
因為書本类型不會有很大变化,所以基本上就是以下几种。
3.3 页面设计
3.3.1 首页
首先是导航栏,可以看到自己的登录状态,搜索栏,个人中心图标,发布帖子图标,同时,在首页,会显示最近比较有热度的帖子,和推荐的关注者。滑倒页面下方,会有分页按钮,查看下一页。
3.3.2 帖子页面
进入帖子的详细页面,可以看到发布者和帖子的有关信息,发布者的发布帖子的内容,加上一张帖子的封面,与此同时,页面右方会推荐值得关注的用户。
在帖子下,可以进行评论和回复,可以对特定的用户评论,也可以直接评论,不回复其它人,在这里,前端会对输入的内容进行一定的非法字符转义,从而保证页面的安全。后端也会负责对得到的内容进行处理,防止注入。
3.3.3 个人中心
查看他人个人中心时,可以看到他的各种信息,包括简介,关注列表,他最近的帖子,以及他发布的评论内容,通过这样的方式,关注拥有共同语言的书友,得到她/他最近的动态,进行交流。在查看自己的个人中心的时候也是类似,可以看到自己最近的动态和其他人与自己的交互,不同的是,功能上有所不同,在这里你可以更改自己的个人信息内容,同时在帖子管理和评论回复中可以直接看到自己帖子的最新动态,比如谁发了贴,同时,可以对自己的帖子进行管理编辑或删除,在评论回复中,可以直接看到最近有谁回复了自己,并且可以直接进行回复。
以下是查看自己的个人中心
1) 修改个人信息
可以对头像修改,账号和邮箱只是显示,并不能进行修改。
3.3.4 发布页面
在发布页面,我们要求发布者至少提供有关讨论的书籍信息,有关的帖子封面和帖子内容,这样发布后,就可以被其他书友看到并进行交流,这样,我们网站的一个用户流程就走完了。
3.4 项目开发目录结构
前端架构是依靠jsp实现功能内容的大体目录如下
css中储存各个页面的样式内容,img中储存相应页面和测试用图片 js文件夹储存了js方法以及测试使用的本地Json数据,方便前端功能测试,再就是目录下的jsp文件了。
后端架构
common 是通用工具包,如数据库连接和编码解码等。
controller 负责与前端进行对接,接收并回传数据。
dao 负责与数据库打交道,直接对数据库进行操作。
filter 过滤器对用户进行认证,非管理员用户不可进入后台页面。
interface 供controller调用的接口,service为其实现类。
manage 后台管理的所有操作都包含于此。
pojo 为对象实体类,方便装载数据并用于传参。
4 编码实现
项目github:https://github.com/m794525298/bookcom
4.1 前端部分
此处以代码中的一些典型例子给出项目的实现说明,详细代码可参考GitHub。
网络请求部分:前端通过Jquery封装好的Ajax中的post等方法请求后端数据,事实上
.
p
o
s
t
(
)
方
法
就
是
对
底
层
A
j
a
x
,
X
M
L
H
t
t
p
R
e
q
u
e
s
t
的
一
层
封
装
只
需
要
提
供
相
应
的
接
口
的
u
r
l
,
a
r
g
s
数
据
内
容
,
然
后
再
回
调
函
数
中
完
成
接
收
数
据
,
对
前
端
功
能
的
实
现
就
好
。
下
面
展
示
一
些
请
求
.post()方法就是对底层Ajax,XMLHttpRequest的一层封装只需要提供相应的接口的url,args数据内容,然后再回调函数中完成接收数据,对前端功能的实现就好。 下面展示一些请求
.post()方法就是对底层Ajax,XMLHttpRequest的一层封装只需要提供相应的接口的url,args数据内容,然后再回调函数中完成接收数据,对前端功能的实现就好。下面展示一些请求.post(url,args,function(){},“json”)内容
// An highlighted block
$(function(){
var url ="js/text.js";
/*var url ="js/text.js";*///这是主页得到精品讨论帖子的功能 对应主页功能1,此时请求的是本地json文件,方便本地测试用
var page=getUrlParam("page");
var args ={"time":new Date(),
"page":page};//args中储存着需要post给接口的数据内容
$.post(url,args,function(data){//执行回调函数,其中data代表的是返回的json文件对象
$(".all_channel").empty();//进行jquert的dom选择
var num=data.num;
for( i=0;i<num;i++){
var totalPage=data.totalPage;
var postID=data.posts[i].postID;
var title=data.posts[i].postTitle;
var content=data.posts[i].content;
var cover=data.posts[i].cover;
var commentNum=data.posts[i].commentNum;
var time=data.posts[i].time;
var author=data.posts[i].author;
var bookTitle=data.posts[i].bookTitle;
var publisher=data.posts[i].publisher;
var publisherName=data.posts[i].publisherName;
var post_href="./detail.jsp"+"?postID="+postID;
storage["totalPage"]=totalPage;
var publisher_href="properson.jsp?userID="+publisher;
$(".all_channel").append(//通过append()将内容展示在相应的标签位置
'<div class="channel">'+
'<div class="likes"><a class="like_num">'+commentNum+'</a><br><a>讨论</a></div>'+
'<div class="channel-item">'+
'<div class="bd">'+
"<a class='s_title'href="+post_href+">"+title+"</a>"+
'<div class="block">'+
'<div class="pic">'+
'<div class="pic-wrap">'+
'<a href='+post_href+'><img src="'+cover+'"></a>'+
'</div>'+
'</div>'+
'<div class="text_content">'+
'<a class="content" href='+post_href+'>'+content+'</a>'+
'</div>'+
'</div>'+
'<div class="source">'+
'<span class="from">来自<a class="publisher" href='+publisher_href+ '>'+publisherName+'</a></span>'+
'<span class="pubtime">'+time+'</span>'+
'<span >   书名'+bookTitle+'</span>'+
'<span >   作者'+author+'</span>'+
'</div>'+
'</div>'+
'</div>'+
'</div>');
}
},"json")
})
4.1.1 注册
在这里,我主要通过一个blur方法,来判断用户输入的数据是否符合规范,验证用户数据内容是否正确,比如在注册账号阶段,当用户输入完账号或者邮箱信息时,就会直接向后端请求判断这个账号或者邮箱是否被使用,来保证用户主页的账号以及注册用邮箱单一存在,同时,通过test()方法,来判断出用户输入的内容符合规范,比如邮箱的内容必须有邮箱的内容形式,而我们通过正则表达式限定了我们用户使用邮箱的形式,加以判断就能保证用户邮箱的正确性。
$("input").blur(function(){
var reg_test = /^\w+$/;
var reg_email_test = /([\w-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)/;
var p_name=this.name;
switch (p_name){
case "userAccount" :
var p_value=this.value;
if(p_value==""){
$(this).next().empty().append("不能为空");
}else{ if(!reg_test.test(p_value)){
$("input").eq(0).next().empty().append("非法字符");
}else{
if(!(p_value.length>5&&p_value.length<17) ){
$(this).next().empty().append("请限定长度在6—16"); }
else(
$(function(){
var url="CheckAccount";//这里是验证账号验证是否重复的内容,对应文档功能1
var args ={"time":new Date(),
"account":p_value};
$.post(url,args,function(data){
if(data.exist!="true"){
$("input").eq(0).next().empty();
}else{
$("input").eq(0).next().empty().append("此账号已存在");
}
},"json")
})
)}}
checkbutton();
break;
;
case "userPassWord" :
var p_value=this.value;
if(p_value==""){
$(this).next().empty().append("不能为空");
}else{
if(!reg_test.test(p_value)){
$("input").eq(2).next().empty().append("非法字符");
}else if(!(p_value.length>5&&p_value.length<17) ){
$(this).next().empty().append("请限定长度在6—16"); }
}
checkbutton();
break;
;
case "rePassWord" :
var p_value=this.value;
if(p_value==""){
$(this).next().empty().append("不能为空");
}else{
if(!reg_test.test(p_value)){
$("input").eq(2).next().empty().append("非法字符");
}else if(!(p_value.length>5&&p_value.length<17) ){
$(this).next().empty().append("请限定长度在6—16"); }else{
if(p_value!=$("input").eq(1).val()){
$(this).next().empty().append("请输入正确密码");
}
}
}
checkbutton();
break;
;
case "email" :
var p_value=this.value;
if(p_value==""){
$(this).next().empty().append("不能为空");
}else{ if(!reg_email_test.test(p_value)){
$("input").eq(3).next().empty().append("格式不符合邮箱");
}else(
$(function(){
var url="CheckEmail";//查找邮箱是否存在,验证邮箱唯一对应功能2
var args ={"time":new Date(),"email":p_value};
$.post(url,args,function(data){
if(data.exist!="true"){
$("input").eq(3).next().empty();
}else{
$("input").eq(3).next().empty().append("此email已存在");
}
},"json")
})
)}
checkbutton();
break;
case "code":
var captcha=this.value;
var url="VerifyCaptcha"; //这里是后台验证验证码是否正确 对应功能4
if(storage.getItem("email")==null){return;}
var email=storage.getItem("email");
args={"email":email,"captcha":captcha};
$.post(url,args,function(data){
if(data.correct=="true"){
$("input").eq(4).next().next().text("ok");
$.cookie("email",data.email);
checkbutton();
}
},"json");
checkbutton();
;
}
})
})
4.1.2 主页
主页的导航栏需要完成搜索内容,帖子和用户推荐,帖子和用户推荐无他,就是append()相应的内容到指定的标签,这里我们具体讲讲主页以及搜索页面怎么进行搜索内容。
当用户按下搜索之后,我们会将用户输入的关键词进行base64转码,然后将其保存的跳转的url中实现页面间值的传递,然后在search.jsp页面中,我们封装的getUrlParam()方法可以直接得到url中的数据内容,然后得到用户的关键词,将关键词和当前页数,以及搜索方式,搜索类型一共四种信息传递给后端,后端通过不同的信息组合方式给我们想要的内容,比如用户是在国内文学这一类型下进行的搜索,搜索内容为三体,那么后端就会根据国内文学以及三体这两个内容对我们的数据库内容进行搜索,然后传递相应的帖子内容给前端,前端再将这样的帖子内容展现给用户,具体代码如下。
function search(searchtype,url){
var url='Search';//搜索展示页面对应文档的唯一功能
var keywords=getUrlParam("keywords");//getUrlParam()方法,作用是得到url中的特定数据的内容
var page = getUrlParam("page");
var bookType = getUrlParam("bookType");
var searchType = getUrlParam("searchType");
var args ={
"keywords":keywords,
"page":page,
"bookType":bookType,
"searchType":searchType};//传递四种数据,后端根据传递内容提供不同搜索方式得到的数据
$.post(url,args,function(data){
$(".all_channel").empty();
var num=data.num;
for( i=0;i<num;i++){//得到有关帖子的内容例如 信息,发布者,封面等等
var totalPage=data.totalPage;
var postID=data.posts[i].postID;
var title=data.posts[i].postTitle;
var content=data.posts[i].content;
var cover=data.posts[i].cover;
var commentNum=data.posts[i].commentNum;
var time=data.posts[i].time;
var publisher=data.posts[i].publisher;
var author=data.posts[i].author;
var bookTitle=data.posts[i].bookTitle;
var publisherName=data.posts[i].publisherName;
var post_href="./detail.jsp"+"?postID="+postID;
storage["totalPage"]=totalPage;
var publisher_href="properson.jsp?userID="+publisher;
$(".all_channel").append(
'<div class="channel">'+
'<div class="likes"><a class="like_num">'+commentNum+'</a><br><a>讨论</a></div>'+
'<div class="channel-item">'+
'<div class="bd">'+
'<a class="s_title" href='+post_href+'>'+title+'</a>'+
'<div class="block">'+
'<div class="pic">'+
'<div class="pic-wrap">'+
'<a href='+post_href+'><img src="'+cover+'"></a>'+
'</div>'+
'</div>'+
'<div class="text_content">'+
'<a class="content">'+content+'</a>'+
'</div>'+
'</div>'+
'<div class="source">'+
'<span class="from">来自   <a class="publisher" href='+publisher_href+ '>'+publisherName+'   </a></span>'+
'<span class="pubtime">'+time+'</span>'+
'<span >   书名'+bookTitle+'</span>'+
'<span >   作者'+author+'</span>'+
'</div>'+
'</div>'+
'</div>'+
'</div>');
/* $(".s_title").eq(i).empty().append(title);
$(".s_title").eq(i).attr("href",post_href);
$(".content").eq(i).empty().append(content);
$('.pic-wrap img').eq(i).attr("src",cover);
$(".like").eq(i).empty().append(commentNum);
$(".pubtime").eq(i).empty().append(time);
$(".publisher").eq(i).empty().append(publisher);
/* /* $(".s_title")[0].empty().append(name);
$(".channel-item .bd .content")[0].empty().append(age); */
}
},"json")
}
4.1.3 帖子详情页面
帖子详情页面实现的功能较多,需要接收帖子的内容,需要展现出该帖子的有关评论,需要使用户进行评论,其中评论的方式包括回复某人或者单纯地在帖子下进行回复,不特定回复某人,具体的代码可以在github项目中查看,这里只展示部分内容和思路。这里我遇到的问题主要在于,Jquery中,append()方法添加的内容是无法执行js的function的,也就是说,如果我append()该帖子中的评论的时候,如果我给回复按钮绑定了一个function,作用是回复该人,得到被回复者信息并且储存在特定位置以便发布评论时,将数据传递给后端,如果仅仅依靠Jquery的click动作是不够的,我们需要用到的是jQuery的on()委托方法,二者在绑定静态控件时没有区别,但是如果面对动态产生的控件,只有 on() 能成功的绑定到动态控件中。
只有这样在我们的动态添加的控件中的方法就能绑定了。
$(function() {
$(".comment_all")//on方法把功能绑定到动态控件中
.on(
'click',
'[class="response_but"]',
function(event) {//回调函数的主要功能是获得这个被回复的ID以及其他内容
var parent = $(event.target).attr("commentID");
var parentPublisher = $(event.target).attr(
"publisher");
var parentPublisherName = $(event.target).attr(
"publisherName");
$(".response_to").empty().append(
parentPublisherName);
$(".response_to").attr("parent", parent);
$(".response_to").attr("parentPublisher",
parentPublisher);
$('html,body').animate({
scrollTop : $('#textarea').offset().top
}, 800);
})
})
4.1.4 个人中心
个人中心的实现较为简单,在这里只讲讲思路,在个人中心中展现的内容只需要请求特定的数据内容放到特定的控件之中就可以了。我们需要的数据是关注列表,与我有关的帖子,与我有关的评论回复,而个人信息我们可以通过登录时进行的cookie设置中得到。而在修改信息方面,用户只能修改用户名,头像,简介,以及修改密码信息。修改头像使用的方法我们在接下来的发布页面介绍。
发布页面
发布页面我们中我们发布的内容有书籍相关信息,用户的发布内容以及封面,图片上传方面,我们通过的FileReader将图片读取,转化为base64内容,然后将该信息发送给后端,后端再将图片的base64解码转化为图片从而实现图片的传输。在发帖的内容上,我们通过转义的方式防止用户特殊字符的注入影响,而为了能够按照特定的用户输入的格式显示,我们还将有关的空格和换行转换为 以及来保证输入的空格和换行有效显示。具体的代码内容可以在github中查看。
function run(get_data) {
/*input_file:文件按钮对象*/
/*get_data: 转换成功后执行的方法*/
if (typeof (FileReader) === 'undefined') {
alert("抱歉,你的浏览器不支持 FileReader,不能将图片转换为Base64,请使用现代浏览器操作!");
} else {
try {
/*图片转Base64 核心代码*/
var filemaxsize = 1024 * 2;
var file = $("#file")[0].files[0];
var fileSize = file.size;
var size = fileSize / 1024;
if (size > filemaxsize) {
alert("附件大小不能大于" + filemaxsize / 1024 + "M!");
return false;
}
if (size <= 0) {
alert("附件大小不能为0M!");
return false;
}
//这里我们判断下类型如果不是图片就返回 去掉就可以上传任意文件
if (!/image\/\w+/.test(file.type)) {
alert("请确保文件为图像类型");
return false;
}
var reader = new FileReader();
reader.onload = function() {
get_data(this.result);
}
reader.readAsDataURL(file);
} catch (e) {
alert('请不要传递空文件或非图片文件' + e.toString())
}
}
}
4.2 后端实現
后端代碼:https://github.com/m794525298/bookcom
后台中是以MVC結構实現的,分為主為三层:controller层,service层和dao层。
它們会建立各自的packet來存放,controller包主要存放servlet类,service包主要存放業务逻輯类,而dao包則存放對數据庫的操作类。
除此之外項目還有以下几个packet,存放工具类的common包,存放業务逻輯类接口的Interface包,存放簡單Java对象的pojo包,存放過濾器的filter包和專門存放管理員操作的manage包。
4.2.1 dao层
dao层中主要分為對用戶,對評論,對帖子和對關注的數据庫操作。
這里只用一个函數來細說,因為都是些對數据庫增刪改查的操作,詳見github。
以插入新用戶的數据庫操作為例子。
public static int regsist(String account,String username,String password,String email) {
Statement st = DataBaseConnector.getStatement();
try {
ResultSet rs = st.executeQuery("Select USER_ID from user where USER_ACCOUNT ='" + account + "' OR USER_EMAIL = '" + email +"';");
if (rs.next()) return 2;
rs.previous();
String sql="insert into user(USER_ACCOUNT,USER_PASSWORD,USER_EMAIL,USER_NICKNAME) values(?,?,?,?) ";//sql语句
st.close();
PreparedStatement pstmt= DataBaseConnector.getConnection().prepareStatement(sql);
pstmt.setString(1 , account);
pstmt.setString(2 , Coder.encrypted(password));
pstmt.setString(3 , email);
pstmt.setString(4, username);
int res = pstmt.executeUpdate();
pstmt.close();
if (res > 0) {
Statement st1 = DataBaseConnector.getStatement();
ResultSet rs1 = st1.executeQuery("Select USER_ID from user where USER_ACCOUNT ='" + account + "';");
while(rs1.next()) {
sql="update user set USER_MD5ID = '"+ Coder.encrypted(rs1.getString("USER_ID")) + "' where USER_ACCOUNT='"+account+"';";//sql语句
st1.executeUpdate(sql);
break;
}
st1.close();
}
return (res > 0)? 0 : 1 ;
} catch (SQLException e) {
System.out.println(e);
return 1;
}
}
首先函數開始時会通過common包的DataBaseConntector类中getStatement()來獲取數据庫連接的Statement。
因為注册前要判断用戶名或邮箱是否已經被注册,所以這里先查找用戶名或邮箱相同的數据,如果結果集不為空,則代表已經注册了。
之后通過prepareStatement來执行插入新用戶sql語句,如果插入成功,則獲取插入后的USER_ID。之后對USER_ID進行MD5加密后更新數据庫并返回是否成功。
4.2.2 service 层
service层中主要是实現業务逻輯,調用Dao层并返回數据到controller层。一般都是返回JsonObject类型數据或Boolean类型。
因為大多的业务函數都很簡單而且大多类同,所以這里只以比較重要的业务函數為例子,而其他沒說到的可在GitHub中查阅。
1) 發佈帖子
@Override
public JSONObject publishPost(PostBean post) {
// TODO Auto-generated method stub
JSONObject json = new JSONObject();
int postId;
try {
postId = PostDao.insertPost(post);
System.out.println(postId);
if(postId > 0) {
json.put("success", "true");
json.put("postID", String.valueOf(postId));
} else {
json.put("success", "false");
}
} catch (SQLException e) {
e.printStackTrace();
json.put("success", "false");
}
return json;
}
2) 查找功能
這里包括了三种查找方式,标題查找,标題加書本类型查找和用戶名查找。
getPostJson方法是用把从數据庫中獲取的結果集轉化為JsonObject类型。
@Override
public JSONObject searchPostByKeyword(String keyword, String bookType, String page) {
ResultSet rs = PostDao.searchPost(keyword, bookType);
return getPostJSON(rs,page, 10);
}
@Override
public JSONObject searchPostByKeyword(String keyword, String page) {
ResultSet rs = PostDao.searchPost(keyword);
return getPostJSON(rs,page, 10);
}
private JSONObject getPostJSON(ResultSet rs,String page, int count) {
JSONObject json = new JSONObject();
int totalPage,num;
try {
if (!rs.next()) {
json.put("totalPage",0+"");
json.put("num",0+"");
json.put("posts", "[]");
return json;
}
rs.last();
totalPage = rs.getRow()/count;
if (rs.getRow()%count != 0) {
totalPage++;
num = (totalPage != Integer.valueOf(page))?count:rs.getRow()%count;
}else{
num = count;
}
int i = 0;
json.put("totalPage",totalPage+"");
json.put("num",num+"");
rs.beforeFirst();
rs.relative((Integer.valueOf(page)-1)*count);
List<JSONObject> posts = new LinkedList<JSONObject>();
while(rs.next()) {
if (i == num) break;
JSONObject post = new JSONObject();
post.put("postID",rs.getString("POST_ID"));
post.put("postTitle",rs.getString("POST_POSTTITLE"));
post.put("content",rs.getString("POST_CONTENT"));
post.put("publisher",rs.getString("POST_PUBLISHERID"));
post.put("cover",rs.getString("POST_COVER"));
post.put("time",rs.getString("POST_TIME"));
post.put("bookType", rs.getString("POST_BOOKTYPE"));
post.put("bookTitle", rs.getString("POST_BOOKTITLE"));
post.put("author", rs.getString("POST_BOOKAUTHOR"));
ResultSet temp = UserDao.getUserName(rs.getString("POST_PUBLISHERID"));
String publisherName = "";
while(temp.next()) {
publisherName =temp.getString("USER_NICKNAME");
}
post.put("publisherName",publisherName);
post.put("commentNum",rs.getString("POST_COMMENTNUM"));
posts.add(post);
i++;
}
json.put("posts", posts);
return json;
} catch (SQLException e) {
System.out.println(e);
return null;
}
}
@Override
public JSONObject searchPostByUser(String keyword, String page) {
ResultSet rs = PostDao.searchPostByNickName(keyword);
return getPostJSON(rs,page, 10);
}
3) 獲取熱門帖子
实时按热门程度进行排序,获取指定页面下帖子集,比如说一个页面有10则帖子,那么第4页则是按热门程度排序,排在31~40的那些帖子。
@Override
public JSONObject getHotPost(String page) {
ResultSet rs = PostDao.getHotPost(page);
return getPostJSON(rs,page, 10);
}
4) 查看帖子詳細內容
返回该帖子的所有信息。
@Override
public JSONObject getPostDetail(String postId) {
try {
JSONObject rs = new JSONObject();
ResultSet postSet = PostDao.getPostDetail(postId);
String userId = "";
while(postSet.next()) {
userId = postSet.getString("POST_PUBLISHERID");
rs.put("postID",postSet.getString("POST_ID"));
rs.put("author",postSet.getString("POST_BOOKAUTHOR"));
rs.put("content",postSet.getString("POST_CONTENT"));
rs.put("postTitle",postSet.getString("POST_POSTTITLE"));
rs.put("bookTitle",postSet.getString("POST_BOOKTITLE"));
rs.put("publisher",postSet.getString("POST_PUBLISHERID"));
rs.put("cover",postSet.getString("POST_COVER"));
rs.put("time",postSet.getString("POST_TIME"));
}
ResultSet userSet = UserDao.getUser(userId);
while(userSet.next()) {
rs.put("icon",userSet.getString("USER_ICON"));
rs.put("publisherName",userSet.getString("USER_NICKNAME"));
}
System.out.println(rs.toJSONString());
return rs;
} catch (SQLException e) {
return null;
}
}
5) 在某帖子下发布評論
- 普通评论:判断帖子是否存在,发布用户是否存在,倘若都存在即可正常插入数据库,否则返回错误码。图中的0即为插入成功。
- 回复评论:除了验证用户和帖子是否存在之外,还要验证被回复评论的发布者是否存在,并且是否在该帖子下评论过。
@Override
public boolean publishComment(CommentBean comment) {
int res = CommentDao.insertComment(comment);
if(res == 0)
res = PostDao.addCommentNum(comment.getPostId());
return res == 0 ? true : false;
}
6) 查看帖子評論
按时间顺序排序,返回特定帖子下该页面下的10条评论。
private JSONObject getCommentJSON(ResultSet rs, int commentNum) {
JSONObject json = new JSONObject();
try {
if (rs == null || !rs.next() || commentNum == 0) {
json.put("totalPage","0");
json.put("num","0");
json.put("posts", "[]");
} else {
int num;
List<JSONObject> comments = new LinkedList<JSONObject>();
for(num = 0, rs.previous(); rs.next(); ++num) {
JSONObject comment = new JSONObject();
comment.put("commentID", rs.getString("COMMENT_ID"));
comment.put("publisher", rs.getString("COMMENT_PUBLISHERID"));
comment.put("time", rs.getString("COMMENT_TIME"));
comment.put("content", rs.getString("COMMENT_CONTENT"));
ResultSet publisher = UserDao.getUser(rs.getString("COMMENT_PUBLISHERID"));
if(publisher.next()) {
comment.put("icon", publisher.getString("USER_ICON"));
comment.put("publisherName", publisher.getString("USER_NICKNAME"));
}
if(rs.getString("COMMENT_PARENTID") != null) {
CommentBean commentParent = CommentDao.admin_selectByID(rs.getString("COMMENT_PARENTID"));
if(commentParent.getIsExist().equals("0")) {
comment.put("commentParentContent", commentParent.getContent());
comment.put("commentParentPublisher", commentParent.getPublisherId());
ResultSet parentPublisher = UserDao.getUserName(rs.getString("COMMENT_PUBLISHERID"));
if(parentPublisher.next())
comment.put("commentParentPublisherName", parentPublisher.getString(1));
} else {
comment.put("commentParentContent", "此评论不可见");
comment.put("commentParentPublisher", "此评论不可见");
comment.put("commentParentPublisherName", "此评论不可见");
}
} else {
comment.put("commentParentContent", "");
comment.put("commentParentPublisher", "");
comment.put("commentParentPublisherName", "");
}
comments.add(comment);
}
json.put("num", String.valueOf(num));
json.put("comments", comments);
int totalPage = (commentNum % countEachPage == 0) ? (commentNum / countEachPage) : (commentNum / countEachPage + 1);
json.put("totalPage", String.valueOf(totalPage));
}
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return json;
}
@Override
public JSONObject getPostComments(String postId, String page) {
int commentNum = CommentDao.getPostCommentNum(postId);
ResultSet rs = CommentDao.getPostComments(postId, page, countEachPage);
return getCommentJSON(rs, commentNum);
}
private JSONObject getReplyJSON(ResultSet rs, int commentNum, String userId, int limit) {
JSONObject json = new JSONObject();
try {
if (rs == null || !rs.next() || commentNum == 0) {
json.put("totalPage","0");
json.put("num","0");
json.put("posts", "[]");
} else {
int num;
List<JSONObject> comments = new LinkedList<JSONObject>();
for(num = 0, rs.previous(); rs.next() && num < limit;) {
JSONObject comment = new JSONObject();
CommentBean commentBean = CommentDao.admin_selectByID(rs.getString("COMMENT_ID"));
if(commentBean.getParentId() != null) {
CommentBean commentParent = CommentDao.admin_selectByID(commentBean.getParentId());
if(commentParent.getPublisherId().equals(userId)) {
continue;
}
if(commentParent.getIsExist().equals("0")) {
comment.put("commentParentContent", commentParent.getContent());
comment.put("commentParentPublisher", commentParent.getPublisherId());
ResultSet parentPublisher = UserDao.getUserName(commentParent.getPublisherId());
if(parentPublisher.next())
comment.put("commentParentPublisherName", parentPublisher.getString(1));
} else {
comment.put("commentParentContent", "此评论不可见");
comment.put("commentParentPublisher", "此评论不可见");
comment.put("commentParentPublisherName", "此评论不可见");
}
} else {
comment.put("commentParentContent", "");
comment.put("commentParentPublisher", "");
comment.put("commentParentPublisherName", "");
}
comment.put("commentID", commentBean.getId());
comment.put("publisher", commentBean.getPublisherId());
comment.put("time", commentBean.getTime());
comment.put("content", commentBean.getContent());
ResultSet post = PostDao.selectPostByID(commentBean.getPostId());
if(post.next())
comment.put("postID", post.getString("POST_ID"));
comment.put("cover", post.getString("POST_COVER"));
ResultSet publisher = UserDao.getUserName(commentBean.getPublisherId());
if(publisher.next())
comment.put("publisherName", publisher.getString(1));
++num;
comments.add(comment);
}
json.put("num", String.valueOf(num));
json.put("comments", comments);
int totalPage = (commentNum % countEachPage == 0) ? (commentNum / countEachPage) : (commentNum / countEachPage + 1);
json.put("totalPage", String.valueOf(totalPage));
}
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return json;
}
4.2.3 controller 层
controller层主要是接收前端參數,調用业务逻輯和返回Json字符串到前端。和service层一样,这里只詳説一个例子。
以查找功能的servlet类為例子。
1) SearchController.java
@WebServlet("/Search")
public class SearchController extends HttpServlet {
private static final long serialVersionUID = 1L;
private UserService userService;
private PostService postService;
public SearchController() {
super();
this.userService = new UserService();
this.postService = new PostService();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doPost(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=utf-8");
Map <String , String[]> map = request.getParameterMap();
String page = (map.containsKey("page") && !map.get("page")[0].equals("null") && !map.get("page")[0].equals(""))?map.get("page")[0]:"1";
String keyword =(map.containsKey("keywords"))?new String(Base64.getDecoder().decode(map.get("keywords")[0]),"UTF-8"):"";
String bookType = (map.containsKey("bookType") && !map.get("bookType")[0].equals(""))?map.get("bookType")[0]:"";
String searchType = "0";
if (map.containsKey("searchType")&& !map.get("searchType")[0].equals("null")) {
if (map.get("searchType")[0].equals("0") ||map.get("searchType")[0].equals("1")){
searchType =map.get("searchType")[0];
}
}
JSONObject rs = new JSONObject();
if (searchType.equals("0")) {
rs = (bookType.equals(""))?postService.searchPostByKeyword(keyword, page):postService.searchPostByKeyword(keyword, bookType, page);
}else {
rs = postService.searchPostByUser(keyword, page);
}
System.out.println(rs.toJSONString());
response.getWriter().write(rs.toJSONString());
}
}
這里的servlet类不再通過修改web.xml來設定接口映射,我們使用注釋的方法來設定接口映射。
因為查找功能可以通過帖子标题和用戶名两种方式進行查找,所以這里聲明了PostService类和UserService类。
當前端通過Restful方式訪問接口后,servlet通過request.getParameterMap()的方式獲取前端發送過來的參數。之后通過參數的不同來判斷使用哪种业务逻輯。最后使用response.getWriter().write(rs.toJSONString())返回json到前端。
當中需要注意的是如果某參數是可以為中文的話,后端是需要解碼的才可以獲得正確的參數。
4.2.4 common包
common包中有三个工具类,分別為coder.java,DataBaseConntector.java和EmailSender.java。
1) DataBaseConntector.java
這个类主要是方便獲取數据庫的連接。
public class DataBaseConnector {
private static Connection conn;
static{
try {
Class.forName("com.mysql.cj.jdbc.Driver");
setConnection();
System.setProperty("user.home", "D:/Program Files/eclipse/eclipse-workspace");
} catch (ClassNotFoundException e) {
System.out.println(e);
}
}
public static Statement getStatement(){
try {
return getConnection().createStatement();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
public static PreparedStatement getPreparedStatement(String sql) {
try {
return conn.prepareStatement(sql);
} catch (SQLException e) {
e.printStackTrace();
return null;
}
}
public static Connection getConnection() {
return conn;
}
public static Connection setConnection() {
try {
// conn =
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
}
當中setConnection()中的是獲取數据庫的連接,這里不顯示。
4.2.5 coder.java
這个类主要是對數据進行不同的轉換操作。
public class Coder {
public static String encrypted(String s){
try {
MessageDigest md = MessageDigest.getInstance("MD5");//获取MD5实例
md.update(s.getBytes());//此处传入要加密的byte类型值
byte[] digest = md.digest();//此处得到的是md5加密后的byte类型值
int i;
StringBuilder sb = new StringBuilder();
for (int offset = 0; offset < digest.length; offset++) {
i = digest[offset];
if (i < 0)
i += 256;
if (i < 16)
sb.append(0);
sb.append(Integer.toHexString(i));//通过Integer.toHexString方法把值变为16进制
}
return sb.toString().substring(0, 16);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
}
public static String textToBase64(String str, String charsetName)
throws UnsupportedEncodingException{
if (charsetName == null) throw new NullPointerException();
if(str == null)
return null;
String res = "";
try{
res = Base64.getEncoder().encodeToString(str.getBytes(charsetName));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return res;
}
public static String textToBase64(String str) throws UnsupportedEncodingException {
return Coder.textToBase64(str, "UTF-8");
}
public static boolean saveBase64Image(String Path,String imgStr) {
if (imgStr == null) //图像数据为空
return false;
Decoder decoder = Base64.getDecoder();
try
{
//Base64解码
byte[] b = decoder.decode(imgStr);
for(int i=0;i<b.length;++i)
{
if(b[i]<0)
{//调整异常数据
b[i]+=256;
}
}
File f = new File(Path);
if (!f.exists()) {
f.createNewFile();
}
OutputStream out = new FileOutputStream(f);
out.write(b);
out.flush();
out.close();
return true;
} catch (Exception e) {
System.out.println(e);
return false;
}
}
}
1) emailSender.java
這个类主要用于發送注册驗證碼。
public class EmailSender {
private static OutputStream out = null;
private static BufferedReader in = null;
/**
* send a captcha to the email
*
* @param receiver email of the receiver
* @param captcha a verification combine by six digits
*
* @throws IOException
*/
public static void send(String receiver, String captcha) throws IOException {
String buffer = null;
Socket socket = null;
RandomAccessFile accessFile = null;
try {
socket = new Socket("smtp.163.com", 25);
out = socket.getOutputStream();
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println(in.readLine());
outWrite("helo sis"); // say hello
System.out.println(in.readLine());
outWrite("auth login"); // prepare for login in
System.out.println(in.readLine());
outWrite(Coder.textToBase64("")); // 邮箱
System.out.println(in.readLine());
outWrite(Coder.textToBase64("")); // 邮箱的授权碼
System.out.println(in.readLine());
outWrite("mail from: <qwsa374293896@163.com>");
System.out.println(in.readLine());
outWrite("rcpt to: <" + receiver + ">");
System.out.println(in.readLine());
outWrite("data");
System.out.println(in.readLine());
outWrite("From: <qwsa374293896@163.com>");
outWrite("To: <" + receiver + ">");
outWrite("Subject: 书友网验证信息");
SimpleDateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss ZZZZ", Locale.ENGLISH);
Date day = new Date();
outWrite("Date: " + df.format(day));
outWrite("Mime-Version: 1.0");
outWrite("Content-Type: multipart/mixed; boundary=\"a\""); // the split sign
outWrite(""); // blank line (format)
outWrite("--a"); // split (to start writing content)
outWrite("Content-Type: text/plain; charset=\"gb18030\"");
outWrite("Content-Transfer-Encoding: base64");
outWrite(""); // blank line (format)
// content
String content = "欢迎您的到来, 您的验证码是: " + captcha + ", 请于5分钟内返回页面输入该验证码。";
outWrite(Coder.textToBase64(content, "gb18030")); // base64 of content
outWrite("");
outWrite("--a--");
outWrite("");
outWrite("."); // ending
System.out.println(in.readLine());
outWrite("quit");
} catch (Exception e) {
e.printStackTrace();
} finally {
in.close();
out.flush();
out.close();
}
}
/**
* output string to the socket
* @param str the output string
*/
private static void outWrite(String str) {
try {
out.write((str + "\r\n").getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
}
4.2.6 manage (后台管理)
User, Post, Follow 和 Comment的后台管理都差不多,都是对数据库额度增删改查,这里以User为例。
1) 添加用户
获取所有信息, 然后添加至数据库,这里没有判断是否为null,因为在数据库设计的时候已经设计好了,若有值为null,会返回错误,即插入失败。
如果删除失败会直接返回上一页,以免表格数据丢失,免去每次都要重新输入的麻烦。
2) 查询用户
查询用户分为两种,无关键字和有关键字,加入无关键字之间按id排序,返回特定页数下的用户即可;否则按用户昵称进行搜索,这里有说明一下,因为Get请求不接受中文字符,所以需要通过编码和解码进行还原。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nKbvjsTR-1591265491458)(https:/,/img-blog.csdnimg.cn/20200604181044874.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L01BX1pBTg==,size_16,color_FFFFFF,t_70)]
3) 删除用户
这里没什么特别需要说明的,就是从数据库中删除数据,如果不存在该用户或者有什么意外则返回删除失败。
4) 修改用户信息
这里,首先要先从数据库获取用户信息,并显示到页面上。管理员通过查阅用户信息,做出信息修正,最后把修正的信息回传给后端,由后端把数据写入数据库。
同时,这里会把保存原来所在的页数以及搜索关键字,再次调用查询servlect(DoUserSelect),把结果返回个前端并渲染出来。
5) 后台管理身份验证(filter)
凡是访问后台管理页面,都要确保这是管理员用户,通过验证session离你面的isAdmin判断,如果是管理员,可以正常访问,否则,直接跳转到管理员登录页面,要求用户登录。
我们后台管理页面的URI首部都是以/manage开头,而过滤器验证的方法也很简单,如果URI里面包括/manage,都必须经过过滤器的验证。不过这里也会排除两个文件,一个是登录界面,另一个是登录界面的css文件。
6) 后台管理员登录
后台登录跟前台登录其实差不多,区别在于身份只能是管理员,如果是普通用户会跳转到前台,要求用户再前台登录界面进行登录。
7) 后台管理员登出
后台登出和前台登出其实也差不多,就是从session里面移除对应内容,避免登出后仍然可以访问的情况。