好记性不如烂笔头,记录实现个人博客系统的学习过程
如有雷同,纯属巧合~~
1.个人博客系统页面展示
当前设计的个人博客系统主要有以下几个页面.
注册页,登录页,列表页,详情页,编辑页,添加页,个人中心页.
注册页
登录页
列表页
详情页
个人中心-我的文章
编辑页
个人中心-我的草稿
添加页
个人中心-我的发表评论
个人中心-修改头像
2.前置工作
①.环境及软件
- 抓包工具: Fiddler Classic
- 环境: jdk1.8 , spring-boot2.7.8
- 后端开发工具: IDEA 2021版 , MySQL5.7.x
- 前端开发工具: VScode (支持win7的版本最高为1.70.3)
- 部署环境: Linux(CentOS 7.6)
②.创建项目目录并添加依赖
- 创建一个项目.
- pom.xml里引入相关的依赖(spring-web,mybatis,mysql,lombok,redis…).
- 引入前端页面,静态网页资源,放在resources/static/文件夹下.
- 给项目设置分层(common,config,controller,entity,mapper,service…).
- application.yml里添加项目常用配置(配置数据库的连接,设置mybatis的xml保存路径,配置Redis的连接…).
③.设计数据库并创建表
目前博客系统中,涉及到三张表: 用户表,文章表,评论表.
因此创建的三张表如下:
- userinfo( id, username, password, photo, createtime, updatetime, state)
- articleinfo( id, title, content, createtime, updatetime, uid, rcount, state)
- commentinfo( id, articleid, uid, content, createtime, replyid, parentid)
设计好表之后,编写建库建表语句,保存为db1.sql,以备后续部署其他机器时候使用.
复制建库建表语句到MySQL里,真正的创建表.
④.创建实体类
创建实体类,实体类中的属性和表中的列对应.
针对用户表userinfo,创建实体类userinfo.
针对文章表articleinfo,创建实体类articleinfo.
针对评论表commentinfo,创建实体类commentinfo.
⑤.统一返回类型与异常处理
统一返回类型有利于后端统一标准规范,降低前后端的沟通成本。
首先实现一个统一返回格式的实体类,并添加一些方法.
再实现一个统一数据返回的保底类,在数据返回之前,对数据格式进行校验和封装.
检测数据的类型是否为统一对象,如果不是,封装成统一对象.
统一的异常处理,当程序出现异常时执行.
⑥.定义拦截器
个人博客系统所涉及到的拦截器,总共有三个.
- LoginInterceptor (实现用户登陆权限的统一校验)
- DedupInterceptor (处理短时间内的重复请求)
- WebsInterceptor (限制ip的接口访问次数)
Ⅰ.LoginInterceptor (实现用户登陆权限的统一校验)
基本思路:
用户登录状态通过判断是否有session对象,并且session对象里是否有用户信息.
如果用户未登录,session就拿不到.
之所以可以取到session,是因为登陆的时候,登录成功就会创建session,并存储用户信息.
因此,当用户登录时,就可正常访问,反之则重定向到登陆页面.
首先实现一个普通的拦截器.
实现HandlerInterceptor接口,重写preHeadler方法.(当登录成功写入session之后,拦截的页面可正常访问)
Ⅱ.DedupInterceptor (处理短时间内的重复请求)
基本思路:
短时间内,同一个用户访问同一个接口,带着同样的参数,就可判断是重复请求了.
至于同样的参数,就是把请求参数按照key做升序排序,将排序后拼成的字符串做MD5计算,以MD5结果作为参数标识.
String KEY = “dedup:U=”+userId + “M=” + method + “P=” + reqParamMD5;
首先实现一个方法级别的自定义注解,用于标记需要处理重复请求的方法
//处理重复请求
@Target(ElementType.METHOD) // 作用于方法上
@Retention(RetentionPolicy.RUNTIME) // 保留注解到运行时
public @interface DedupLimit {
long expireTime() default 3000L; //默认过期时间3秒
}
再实现一个普通的拦截器,用于处理请求去重的逻辑.
//请求去重处理
@Component
public class DedupInterceptor implements HandlerInterceptor {
@Resource
private PreHandleFalseResponse handleFalseResponse;
@Resource
private RedisUtils redisUtils;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 判断是否属于方法handler
if (handler instanceof HandlerMethod) {
// 获取判断是否含有注解
DedupLimit dedupLimit = ((HandlerMethod) handler).getMethodAnnotation(DedupLimit.class);
// 没有注解标记 直接返回允许通行
if (dedupLimit == null) {
return true;
}
// 获取参数
long expireTime = dedupLimit.expireTime();
// 获取方法名 这里实现对方法级别的限制访问
String methodName = ((HandlerMethod) handler).getMethod().getName();
String method = request.getRequestURI(); //获取web项目的相对路径
//获取request参数,需要排序,不然位置不同,生成的md5也不一样
Map<String, String[]> parameterMap = request.getParameterMap();
String parameter = "";
if(!parameterMap.isEmpty()){
List<Map.Entry<String,String[]>> lstEntry=new ArrayList<>(parameterMap.entrySet());
lstEntry.sort(((o1, o2) -> {
return o1.getKey().compareTo(o2.getKey());
}));
parameter = JSONUtil.toJsonStr(lstEntry);
}
Map<String, String> map = new HashMap<String, String>();
map.put("url", String.valueOf(request.getRequestURL()));
map.put("parameter",parameter);
ObjectMapper objectMapper = new ObjectMapper();
String paramTreeMapJSON = objectMapper.writeValueAsString(map);
String md5deDupParam = DigestUtils.md5DigestAsHex(paramTreeMapJSON.getBytes()); //进行MD5
//存储到redis
String userId = request.getRemoteAddr(); //用户ip
String KEY = "dedup:U=" + userId + "M=" + method + "P=" + md5deDupParam;
long expireAt = System.currentTimeMillis() + expireTime; //指定时间内的重复请求会认为重复
String val = "expireAt@" + expireAt;
//redis key还存在的话要就认为请求是重复的
Boolean firstSet = redisUtils.setIfAbsent(KEY,val,expireTime); //key还存在的话,就认为请求是重复的
if (firstSet != null && firstSet) {// 第一次访问
return true;
} else { // redis值已存在,认为是重复了
//是重复请求,设置response返回信息
handleFalseResponse.preHandleFalseResponseJsonBody(response,"重复请求!");
}
return false;
}
return true;
}
}
注意事项
值得注意的是:因为我的项目中,前端都是使用键值对传输数据,没有使用到json格式.
↓以下写法是获取不了json格式参数,也获取不了上传文件的信息.
所以如果想要获取json格式参数和上传图片的参数就需要改成如下:
当然了,显然你也注意到了,获取json格式是使用流对象.字节流读完就没了,不能重复获取.
想要重复获取流对象思路:
1.继承 HttpServletRequestWrapper 包装类,重写getInputStream()和getReader()方法,将请求参数body复制到自己requestWrapper中.
2.增加过滤器,将包装类加入过滤器链中.在这个过滤器里面判断,看request请求是不是json,如果是,就将里面的东西进行缓存,不是就直接放行.
3.注册过滤器.
Ⅲ.WebsInterceptor (限制ip的接口访问次数)
对接口限流,就是防止接口被恶意打击(短时间内大量请求).如果超过设置的访问次数,就直接拉黑此ip.
基本思路:
使用滑动窗口,来处理接口访问次数的限制.
用户每次访问接口时,记录当前用户访问的时间点(时间戳),并计算前x秒内用户访问该接口的总次数.
如果总次数大于限流次数,则将用户ip添加到黑名单里,不再允许用户访问.
这样就能保证在任意时刻用户的访问次数不会超过设定的访问次数.
关于滑动窗口使用的数据结构是zSet(有序集合),黑名单使用的数据结构是Set(集合).
Set(集合)是一个无序并且唯一的键值集合.
zSet(有序集合)相比于Set多了一个排序属性score(分值),存储的键值不能重复,但是分值可以重复.
因此,使用滑动窗口时,key则设置成用户ip和请求的方法路径.
将score设置成用户访问接口的时间戳,以便于后续通过score进行范围检查.
String KEY = “access:U=” + userId + “M=” + method;
首先实现一个方法级别的自定义注解,用于标记需要进行请求次数限制的方法.
//自定义注解
//接口配置,允许访问次数
@Target(ElementType.METHOD) // 作用于方法上
@Retention(RetentionPolicy.RUNTIME) // 保留注解到运行时
public @interface AccessLimit {
// 定义的两个注解参数
int maxCount(); //最大允许访问数量
int seconds(); //单位时间(秒)
}
再实现一个普通的拦截器,用于处理限制访问次数的逻辑.
//限制接口访问次数
@Component
public class WebsInterceptor implements HandlerInterceptor {
@Resource
private PreHandleFalseResponse handleFalseResponse; //拦截器false时返回的response信息
@Resource
private RedisUtils redisUtils;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//在黑名单里,直接拒绝访问
String blackList = "blackList";
String userId = request.getRemoteAddr(); //用户ip
Boolean isBlack = redisUtils.sIsMember(blackList,userId);//判断set集合里是否有此ip
if(Boolean.TRUE.equals(isBlack)){
handleFalseResponse.preHandleFalseResponseJsonBody(response,"拒绝访问!");
return false;
}
// 判断是否属于方法handler
if (handler instanceof HandlerMethod) {
// 获取判断是否含有注解
AccessLimit accessLimit = ((HandlerMethod) handler).getMethodAnnotation(AccessLimit.class);
// 没有注解标记 直接返回允许通行
if (accessLimit == null) {
return true;
}
// 获取 限流的参数
int maxCount = accessLimit.maxCount();
int seconds = accessLimit.seconds();
// 获取方法名 这里实现对方法级别的限制访问
String methodName = ((HandlerMethod) handler).getMethod().getName();
String method = request.getRequestURI(); //获取web项目的相对路径
// 获取当前时间戳,设置方法访问的 时间戳
long nowTime = System.currentTimeMillis();
//把用户IP和接口方法名拼接成 redis 的 key
String redisKey = "access:U=" + userId + "M=" + method;
//用于执行多个 Redis 命令
//将所有命令缓存在管道中,最终一次性发送给 Redis 服务器。这样可以减少与 Redis 服务器的通信次数,提高命令的执行效率和吞吐量。
List<Object> result = redisUtils.zExecutePipelined(redisKey,nowTime,seconds);
Long count = (Long) result.get(3);
// 如果超出访问限制 限流/拉黑
if (count > maxCount) {
Long addResult = redisUtils.sAdd(blackList,userId);//加入黑名单,使用set集合类型,无序并唯一,r=1
if(addResult==1){
//加入黑名单成功之后,删除此有序集合
redisUtils.del(redisKey);
}
handleFalseResponse.preHandleFalseResponseJsonBody(response,"拒绝访问!");
return false;
}
}
return true;
}
}
Ⅳ.设置拦截规则
实现了三个普通拦截器之后,将拦截器添加到配置文件中,并设置拦截规则.
其中关于登陆权限验证的拦截器,需要排除所有的静态资源,并且登录页面和注册页面不拦截,其他页面都拦截.
三个拦截器的拦截顺序: WebsInterceptor -> LoginInterceptor -> DedupInterceptor
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Autowired
private DedupInterceptor dedupInterceptor;
@Autowired
private WebsInterceptor websInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(websInterceptor) //接口访问次数限制
.order(0);
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/css/**") //不拦截所有静态资源
.excludePathPatterns("/editor.md/**")
.excludePathPatterns("/image/**")
.excludePathPatterns("/js/**")
.excludePathPatterns("/upload/**")
.excludePathPatterns("/login.html") //不拦截登陆和注册网页
.excludePathPatterns("/reg.html")
.excludePathPatterns("/user/login") //不拦截登陆和注册方法,获取验证码
.excludePathPatterns("/user/reg")
.excludePathPatterns("/user/getcaptcha")
.order(1);
registry.addInterceptor(dedupInterceptor) //请求处理去重
.order(2);
}
}
示范:
为防止几个拦截器功能混在一起,看不清.以下示范是单个拦截器的拦截功能.
DedupInterceptor(请求去重)
WebsInterceptor(接口访问次数的限制)
3.具体功能实现
①.注册页
Ⅰ.注册功能
用户输入用户名,密码,确认密码,点击提交就能够注册.
同一ip用户一小时内只允许注册1次.
约定前后端交互接口
前端请求:
POST /blog/user/reg
username=peach11&password=123456&captchatext=zA61S&uuid=4b37ada08d904aaeacc0b08ba3705862
后端响应:
HTTP /1.1 200 OK
{"code":-1,"msg":"'peach11'用户名已存在!","data":null}
fiddler抓包结果
实现后端代码
//注册,1h内只允许用户注册一次
@AccessLimit(maxCount = 500,seconds = 5) //接口访问限流
@DedupLimit(expireTime = 5000) //去除重复请求
@PostMapping("/reg")
public AjaxResult reg(HttpServletRequest request,Userinfo userinfo,String captchatext,String uuid){
//进行非空校验,长度校验,有效性校验
if (userinfo == null || !StringUtils.hasLength(userinfo.getUsername()) ||
!StringUtils.hasLength(userinfo.getPassword()) || !StringUtils.hasLength(captchatext) || !StringUtils.hasLength(uuid)){
return AjaxResult.fail(-1,"参数非法");
}
//判断用户1h是否重复注册
String ipUser = request.getRemoteAddr(); //用户ip
String method = request.getRequestURI(); //获取web项目的相对路径
String redisKey = "reg:U=" + ipUser + "M=" + method;
if(redisUtils.get(redisKey)!=null){
return AjaxResult.fail(-1,"重复注册!"); //重复注册
}
//判断验证码
//从redis上根据key获取code,并删除redis上的key
String contextPath = myContextPath.getContextPath(); //获取配置文件的项目路径
String key = "captcha:P=" + contextPath + "K=" + uuid;
String getCode = redisUtils.deleteKey(key); //防止3分钟内重复使用该key
//判断验证码,返回-1表示没有找到
if(getCode.equals("-1")){
return AjaxResult.fail(-1,"验证码过期!");
}
//验证码判断忽略大小写
if(captchatext.length()!=5 || !CaptchaUtils.verify(getCode,captchatext)){
return AjaxResult.fail(-1,"验证码错误!");
}
//用户名长度判断,密码长度判断
int usernameLength = userinfo.getUsername().length();
int passwordLentth = userinfo.getPassword().length();
if(usernameLength>16 || usernameLength<6 || passwordLentth>16 || passwordLentth<6){
return AjaxResult.fail(-1,"用户名或密码长度不符要求!");
}
//判断用户名是否已存在
if(userService.getUserByName(userinfo.getUsername())!=null){
return AjaxResult.fail(-1,"'"+userinfo.getUsername()+"'用户名已存在!");
}
//给密码加盐
userinfo.setPassword(PasswordUtils.encrypt(userinfo.getPassword()));
int result = userService.reg(userinfo); //注册用户
if(result==1){
//查询数据库
Userinfo user = userService.getUserByName(userinfo.getUsername());
//注册成功,生成session
HttpSession session = request.getSession();//没有,就创建
session.setAttribute(AppVariable.USER_SESSION_KEY,user);
//存储注册信息到redis,1h之内不允许重复注册
redisUtils.set(redisKey,"",Duration.ofHours(1));
}
return AjaxResult.success(result);
}
a.密码加盐
密码加盐,是为了提高系统安全性.
基本加密思路:
每次调用方法时,产生随机唯一的盐值(salt),
拼接盐值与密码并使用MD5对其加密得到加密密码saltPassword,
最后拼接盐值(salt)加分隔符加加盐后的密码(saltPassword),得到最终存储进数据库的密码(finalPassword).
实现代码
//密码加盐
public class PasswordUtils {
//加盐密码的生成
public static String encrypt(String password){
//产生盐值,32位
String salt = UUID.randomUUID().toString().replace("-","");
//生成加盐后的密码,32位
String saltPassword = DigestUtils.md5DigestAsHex((salt+password).getBytes());
//生成65位,最终密码
String finalPassword = salt+ "$" +saltPassword;
return finalPassword;
}
}
解密思路:
从数据库中取出最终密码得到盐值
将用户输入的明文密码,加上得到的盐值进行同样的加密操作步骤,得到和数据库存储的格式一样的最终密码,
对比生成的最终密码和数据库最终的密码是否相等,如果相等,用户名和密码就是对的,反之则是密码输入错误.
实现代码
//密码加盐
public class PasswordUtils {
//生成加盐的密码,重载
public static String encrypt(String password,String salt){
//生成加盐密码
String saltPassword = DigestUtils.md5DigestAsHex((salt+password).getBytes());
//生成最终密码
String finalPassword = salt+ "$" + saltPassword;
return finalPassword;
}
//验证两个加盐密码,用户输入的密码,数据库的最终密码
public static boolean check(String inputPassword,String finalPassword){
//判断密码合法性
if(finalPassword.length()==65 && StringUtils.hasLength(inputPassword) &&
StringUtils.hasLength(finalPassword)){
//得到盐值
String salt = finalPassword.split("\\$")[0];
//将明文加密
String confirmPassword = PasswordUtils.encrypt(inputPassword,salt);
//对比两个密码
return confirmPassword.equals(finalPassword);
}
return false;
}
}
②.登录页
Ⅰ.获取验证码
到达登录页面的时候,页面发送请求获取验证码.
基本思路:
用户进入页面,前端请求后端生成一张带验证码的图片路径.
后端生成新的验证码图片,并将验证码文本存到 Redis 中,key 用 uuid 生成, value 为验证码文本,有效时间3分钟,把生成的 uuid 和图片路径传给前端.
前端显示后端传来的验证码图片路径(path?res=uuid,在图片路径后加上?res=uuid也是防止浏览器图片缓存,没有及时感知到图片内容改变了).
后续用户点击登录时,将用户的输入的用户名,密码,验证码和 uuid 传给后端.
后端接受用户的输入的验证码和 uuid ,根据 key ,查出 value ,获取到正确的验证码,与用户的验证码比对.
根据key从redis获取完验证码之后,将此key删除(不管用户是否输入正确,这是防止三分钟内多次使用此uuid)
而关于生成的验证码图片,每天固定时间点进行扫描删除.
约定前后端交互接口
前端请求:
POST /blog/user/getcaptcha
后端响应:
HTTP /1.1 200 OK
{
"code":200,
"msg":"",
"data":"/blog/captcha/0039f3c4b5a543c4ba49fb582306af5e.png?res=0039f3c4b5a543c4ba49fb582306af5e"
}
Fiddler抓包结果
实现后端代码
//登陆之前获取验证码
@AccessLimit(maxCount = 500,seconds = 5) //接口访问限流
@PostMapping("/getcaptcha")
public AjaxResult getCaptcha(){
String codeuuid = UUID.randomUUID().toString().replace("-",""); // 32位
String code = CaptchaUtils.createCaptch(codeuuid); //生成验证码图片,获取验证码文本
String captchapath = "/blog/captcha/" + codeuuid + ".png"; //生成的图片路径
//存储到redis,设置过期时间3分钟
String contextPath = myContextPath.getContextPath(); //获取配置文件的项目路径
String key = "captcha:P=" + contextPath + "K=" + codeuuid;
redisUtils.set(key,code, Duration.ofMinutes(3));
//返回图片地址
captchapath = captchapath+"?res="+codeuuid;
return AjaxResult.success(captchapath);
}
验证码图片定时删除
每当用户点击验证码时,后端都会生成一张图片,图片的名字是随机的32位数字.这样的话指定文件夹下的图片会越来越多,因为每一张验证码的时效是3分钟.
因此,笔者选择的是,每天定时扫描指定文件夹,删除验证码时效已经过了的图片.
当然存放验证码图片的文件夹,与存放头像图片的文件夹不是同一个.
//定时删除验证码图片
@Component
@EnableScheduling //开启定时任务支持
@EnableAsync //开启对异步的支持
public class ImageScanner {
private static final String FORMAT_TYPE = "yyyy-MM-dd HH:mm:ss";
@Scheduled(cron = "0 0 1 * * ?") //每天凌晨1点执行一次
public void scanAndDeleteImages() throws IOException {
//获取当前时间
DateTimeFormatter format = DateTimeFormatter.ofPattern(FORMAT_TYPE);//指定格式
LocalDateTime time = LocalDateTime.now();//获取当前时间
LocalDateTime timeBefore = time.minusMinutes(10);//获取当前时间,并减去10分钟
File path = new File(ResourceUtils.getURL("classpath:").getPath());
if(!path.exists()) {
path = new File("");
}
File upload = new File(path.getAbsolutePath(),"static/captcha/");
if(!upload.exists()) {
upload.mkdirs();
}
String parent= upload.getPath();
File dir = new File(parent);
if (!dir.exists()) {
dir.mkdirs();
}
if (dir.exists()) {
File[] files = dir.listFiles();
for (File file : files) {
// 判断文件是否为图片格式,验证码
if (file.isFile() && file.getName().contains(".png") ) {
String timeStr = getFileLastModifiedTime(file); //获取时间
LocalDateTime localDate = LocalDateTime.parse(timeStr, format);//把String时间转换成LocalDateTime
if( timeBefore.isAfter(localDate)){
//删除图片,验证码图片明显过期了
file.delete();
}
}
}
}
}
}
值得注意的是,关于存放图片的路径,它是这样写的.↓
这是因为,当把项目打成jar包之后,里面的东西就不能修改了.
因此,需要在.yml里配置静态资源路径.
这样就会在项目的jar包的同级目录下生成一个static文件夹.
而用户新上传的头像,或是用户登录时生成的验证码图片就能存储在这个static文件夹下.
Ⅱ.登陆功能
用户输入用户名,密码,验证码,点击登陆就能登陆.
用户一小时内连续输错密码六次,则在接下来的一小时之内禁止再次操作.
约定前后端交互接口
前端请求:
POST /blog/user/login
username=peach11&password=123456789&captchatext=8tTVP&uuid=bbfae3d071354bf68c0bc9f522b3e190
后端响应:
HTTP /1.1 200 OK
{
"code":200,
"msg":"",
"data":{
"serializableId":1,
"id":1,
"username":"peach11",
"password":"",
"photo":"/blog/upload/8d13afb8d87248419b46841a257ba67b.jpeg",
"createtime":"2023-12-22T22:18:06",
"updatetime":"2023-12-22T22:18:06",
"state":1
}
}
Fiddler抓包结果
实现后端代码
//登陆
@AccessLimit(maxCount = 500,seconds = 5) //接口访问限流
@DedupLimit(expireTime = 5000) //去除重复请求,5秒之内
@PostMapping("/login")
public AjaxResult login(HttpServletRequest request, Userinfo userinfo,String captchatext,String uuid){
//参数校验,非空判断,用户名字,用户密码,用户验证码
if(!StringUtils.hasLength(userinfo.getUsername()) || !StringUtils.hasLength(userinfo.getPassword()) ||
!StringUtils.hasLength(captchatext) || !StringUtils.hasLength(uuid)){
return AjaxResult.fail(-1,"参数非法!");
}
//判断验证码
//从redis上根据key获取code,并删除redis上的key
String contextPath = myContextPath.getContextPath(); //获取配置文件的项目路径
String key = "captcha:P=" + contextPath + "K=" + uuid;
String getCode = redisUtils.deleteKey(key); //防止3分钟内重复使用该key
//判断验证码,返回-1表示没有找到
if(getCode.equals("-1")){
return AjaxResult.fail(-1,"验证码过期!");
}
//验证码判断忽略大小写
if(captchatext.length()!=5 || !CaptchaUtils.verify(getCode,captchatext)){
return AjaxResult.fail(-1,"验证码错误!");
}
//判断用户此次登录次数,1小时内超过6次直接返回
String userIp = request.getRemoteAddr(); //用户ip
String loignFalse = "loginFalse:U=" + userIp +"N="+ userinfo.getUsername() + "P=" + contextPath;
int count =0;
if(redisUtils.get(loignFalse)!=null){
count = Integer.parseInt(redisUtils.get(loignFalse));
if(count>=6){
String timeout = redisUtils.getExpire(loignFalse);
//用户登录次数超过6
return AjaxResult.fail(-1,"登录操作频繁! "+ timeout +" 时间后重试!");
}
}
//查询数据库
Userinfo user = userService.getUserByName(userinfo.getUsername());
if(user!=null && user.getId()>0){
//用户存在
//再判断密码是否正确
if(PasswordUtils.check(userinfo.getPassword(),user.getPassword())){
//登录成功
//存储session
HttpSession session = request.getSession();//没有,就创建
session.setAttribute(AppVariable.USER_SESSION_KEY,user);
user.setPassword("");//返回之前,将密码隐藏
//如果之前有记录用户登录次数,移除redis记录的用户登录次数
redisUtils.del(loignFalse);
return AjaxResult.success(user);
}
}
//用户不存在或用户存在但密码错误,redis记录次数增加+1
count = count+1;
redisUtils.set(loignFalse, String.valueOf(count), Duration.ofHours(1));//过期时间1h
return AjaxResult.fail(-1,"用户名或密码错误!");
}
Ⅲ.注销功能
用户点击注销,退出登录,返回到登录页面.
约定前后端交互接口
前端请求:
POST /blog/user/logout
后端响应:
HTTP /1.1 200 OK
{"code":200,"msg":"","data":1}
Fiddler抓包结果
实现后端代码
//注销
@AccessLimit(maxCount = 500,seconds = 5) //接口访问限流
@DedupLimit(expireTime = 5000) //去除重复请求,5秒之内
@PostMapping("/logout")
public AjaxResult logout(HttpSession session){
session.removeAttribute(AppVariable.USER_SESSION_KEY);
return AjaxResult.success(1);
}
③.列表页
Ⅰ.获取用户信息
左侧显示当前登录者的用户名,头像,文章数量
约定前后端交互接口
前端请求:
POST /blog/user/showData
后端响应:
HTTP /1.1 200 OK
{
"code":200,
"msg":"",
"data":{
"serializableId":1,
"id":1,
"username":"peach11",
"password":"",
"photo":"/blog/upload/8d13afb8d87248419b46841a257ba67b.jpeg",
"createtime":"2023-12-22T22:18:06",
"updatetime":"2023-12-22T22:18:06",
"state":1,
"artTotal":5
}
}
Fiddler抓包结果
实现后端代码
//获取用户文章数量
@AccessLimit(maxCount = 500,seconds = 5) //接口访问限流
@PostMapping("showData")
public AjaxResult showData(HttpServletRequest request){
UserinfoVO userinfoVO = new UserinfoVO();
//得到当前登录用户username,photo,从session中获得
Userinfo userinfo = UserSessionUtils.getUser(request);
if(userinfo == null){
return AjaxResult.fail(-1,"请求非法!");
}
//深克隆
BeanUtils.copyProperties(userinfo,userinfoVO);//userinfo克隆给userinfoVO
//得到用户发表文章总数
userinfoVO.setArtTotal(articleService.getArtCountByUid(userinfo.getId()));
return AjaxResult.success(userinfoVO);
}
Ⅱ.获取文章列表,分页展示
显示文章列表,但分页.
基本思路:
用户进入页面,前端记录列表的当前页码(pindex),每页显示条数最大6条(psize),并请求后端当前页面文章数据.
后端得到当前页码(pindex),每页最大显示条数(psize),计算最大显示页数(pcount)
pcount = 文章总条数 / psize.(结果向上取整).
而,当前页码的文章数据,使用limit查询出来.
查询当前页文章的详情,具体sql语句如下:
select * from articleinfo where state=1 order by updatetime desc limit psize offset (pindex-1)*psize;
之后,后端将最大显示页数,详细文章数据,一并返回给前端.
约定前后端交互接口
前端请求:
POST /blog/art/listbypage
pindex=1&psize=6
后端响应:
HTTP /1.1 200 OK
{
"code":200,
"msg":"",
"data":{"pcount":1,
"list":[
{"serializableId":1,
"id":24,
"title":"大都好物不坚牢,彩云易散琉璃脆。",
"content":"出自唐代白居易的《简简吟》\n解释:世间的好物大多不够坚固牢靠,就像那天边的彩云转瞬即逝,也如美丽的琉璃脆弱易碎。\n\n赏析:指世上美好的事物容易受损或消逝。",
"createtime":"2023-12-28 22:51:45",
"updatetime":"2023-12-28 22:51:45",
"uid":2,
"rcount":1,
"state":1},
...]
}
}
Fiddler抓包结果
实现后端代码
//查询所有文章
@AccessLimit(maxCount = 500,seconds = 5) //接口访问限流
@PostMapping("/listbypage")
public AjaxResult getListByPage(Integer pindex,Integer psize){
if(pindex==null || pindex<=1){
pindex=1;
}
if(psize==null || psize<=1){
psize=6;
}
//(当前页码-1)*每页显示条数
int offset = (pindex-1)*psize;
//文章所有数据
List<Articleinfo> list = articleService.getListByPage(psize,offset);
ArticleUtils.cutContent(list); //因为是列表,文章需要截取
//计算当前列表总共有多少页
//总条数/每页显示条数,向上取整
int totalcount = articleService.getCount();
double pcountdb = totalcount / (psize*1.0);
int pcount = (int) Math.ceil(pcountdb);
HashMap<String ,Object> result = new HashMap<>();
result.put("list",list); //文章
result.put("pcount",pcount); //最大显示页数
return AjaxResult.success(result);
}
④.添加页
Ⅰ.保存文章草稿
点击写博客,来到博客添加页,在里面输入文章标题,和正文后,点击保存,即可保存文章,页面不跳转.
基本思路:
用户进入添加页,输入标题和正文,点击保存后,前端请求保存文章.
新写的文章一般没有id,
因此后端一定会获得文章标题和正文,但不一定会有id.
没有id,则说明是新写的,直接新建文章,state状态设置为-1,表示当前文章是草稿,返回自增主键id
有id,说明,这篇草稿是之前已经保存过草稿的,已经创建过了,此时只需要修改文章即可.同时返回自增主键id,也就是文章的id.
约定前后端交互接口
前端请求:
POST /blog/art/draftadd
title=标题&content=正文
后端响应:
HTTP /1.1 200 OK
{"code":200,"msg":"","data":350}
Fiddler抓包结果
实现后端代码
//草稿保存
@AccessLimit(maxCount = 500,seconds = 5) //接口访问限流
@DedupLimit(expireTime = 5000) //去除重复请求,5秒之内
public AjaxResult draftAdd (HttpServletRequest request, Articleinfo articleinfo){
//非空判断
if(articleinfo==null || !StringUtils.hasLength(articleinfo.getTitle()) ||
!StringUtils.hasLength(articleinfo.getContent())){
return AjaxResult.fail(-1,"参数非法!");
}
//判断文章里是不是都是空格跟换行符
if(ArticleUtils.isEmpty(articleinfo)){
return AjaxResult.fail(-1,"文本为空!");//文本为空,不做任何操作
}
//判断文章标题长度
if(articleinfo.getTitle().length()>100){
return AjaxResult.fail(-1,"标题超过100字符!请减少标题字数!");
}
//得到当前用户
Userinfo userinfo = UserSessionUtils.getUser(request);
if(userinfo==null || userinfo.getId()<=0){
return AjaxResult.fail(-1,"用户无效!");
}
articleinfo.setUid(userinfo.getId());
//判断草稿是否是第一次创建
//id==null,说明是新创建的,没有id
if(articleinfo.getId()==null){
articleinfo.setState(-1);//说明是草稿
articleService.draftAdd(articleinfo);//新建文章,state状态为-1,返回自增主键id
}else{
//说明草稿箱文章,已经创建过,
//修改文章即可,title,content,updatetime,uid,id
articleinfo.setUpdatetime(LocalDateTime.now());
articleService.update(articleinfo);//修改文章
}
return AjaxResult.success(articleinfo.getId());//返回自增主键
}
Ⅱ.发布文章
点击写博客,来到博客添加页,在里面输入文章标题,和正文后,点击发布文章,即可发布成功.
约定前后端交互接口
前端请求:
POST /blog/art/add
title=标题&content=正文&id=350
后端响应:
HTTP /1.1 200 OK
{"code":200,"msg":"","data":1}
Fiddler抓包结果
实现后端代码
//新增一篇文章
@AccessLimit(maxCount = 500,seconds = 5) //接口访问限流
@DedupLimit(expireTime = 5000) //去除重复请求,5秒之内
@PostMapping("/add")
public AjaxResult add(HttpServletRequest request, Articleinfo articleinfo){
//非空判断,
if(articleinfo==null || !StringUtils.hasLength(articleinfo.getTitle()) ||
!StringUtils.hasLength(articleinfo.getContent())){
return AjaxResult.fail(-1,"参数非法!");
}
//判断文章里是不是都是空格跟换行符
if(ArticleUtils.isEmpty(articleinfo)){
return AjaxResult.fail(-1,"文本为空!"); //文本为空,不做任何操作
}
//判断文章标题长度
if(articleinfo.getTitle().length()>100){
return AjaxResult.fail(-1,"标题超过100字符!请减少标题字数!");
}
//数据库添加
//得到当前用户
Userinfo userinfo = UserSessionUtils.getUser(request);
if(userinfo==null || userinfo.getId()<=0){
return AjaxResult.fail(-1,"用户无效!");
}
articleinfo.setUid(userinfo.getId());
//判断是新写的文章,发布,还是草稿里的文章发布
if(articleinfo.getId()==null){
//新写的文章,没有id
return AjaxResult.success(articleService.add(articleinfo));
}else{
//草稿变成的文章,有id
//还需要修改文章的状态,添加更新时间
articleinfo.setUpdatetime(LocalDateTime.now());
articleinfo.setState(1);
articleService.draftupdate(articleinfo);//修改文章
return AjaxResult.success( articleService.draftupdate(articleinfo));
}
}
⑤.编辑页
Ⅰ.获取文章详情
在个人博客中心页,点击编辑已发表的文章,来到博客编辑页.
来到博客编辑页时,页面发送请求,获取要修改的文章详情.
此编辑页只有发布文章按钮,没有保存按钮.已发布的文章,点击编辑按钮,编辑文章后,只能继续发布文章.
约定前后端交互接口
前端请求:
POST /blog/art/detail
id=350
后端响应:
HTTP /1.1 200 OK
{
"code":200,
"msg":"",
"data":{
"serializableId":1,
"id":350,
"title":"定风波·莫听穿林打叶声","content":" 三月七日,沙湖道中遇雨。雨具先去,同行皆狼狈,余独不觉。已而遂晴,故作此词。\n\n莫听穿林打叶声,何妨吟啸且徐行。竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生。\n\n料峭春风吹酒醒,微冷,山头斜照却相迎。回首向来萧瑟处,归去,也无风雨也无晴。 ",
"createtime":"2024-02-16 01:30:22",
"updatetime":"2024-02-16 01:35:13",
"uid":1,
"rcount":1,
"state":1
}
}
Fiddler抓包结果
实现后端代码
//查看一篇文章
@AccessLimit(maxCount = 500,seconds = 5) //接口访问限流
@PostMapping("/detail")
public AjaxResult getDetail(Integer id){
if(id == null || id<=0){
return AjaxResult.fail(-1,"参数非法!");
}
return AjaxResult.success(articleService.getDetail(id));
}
Ⅱ.发布文章/修改文章
点击发布文章,即可修改文章并发布.
约定前后端交互接口
前端请求:
POST /blog/art/update
id=350&title=标题&content=正文
后端响应:
HTTP /1.1 200 OK
{"code":200,"msg":"","data":1}
Fiddler抓包结果
实现后端代码
//修改一篇文章
@AccessLimit(maxCount = 500,seconds = 5) //接口访问限流
@DedupLimit(expireTime = 5000) //去除重复请求,5秒之内
@PostMapping("/update")
public AjaxResult update(HttpServletRequest request,Articleinfo articleinfo){
if(articleinfo==null || !StringUtils.hasLength(articleinfo.getTitle()) ||
!StringUtils.hasLength(articleinfo.getContent()) || articleinfo.getId()==null){
return AjaxResult.fail(-1,"参数非法!");
}
//判断文章里是不是都是空格跟换行符
if(ArticleUtils.isEmpty(articleinfo)){
return AjaxResult.fail(-1,"文本为空!"); //文本为空,不做任何操作
}
//判断文章标题长度
if(articleinfo.getTitle().length()>100){
return AjaxResult.fail(-1,"标题超过100字符!请减少标题字数!");
}
//得到当前用户
Userinfo userinfo = UserSessionUtils.getUser(request);
if(userinfo == null || userinfo.getId() == null){
return AjaxResult.fail(-1,"用户无效!");
}
articleinfo.setUid(userinfo.getId());//修改的是当前的是当前登陆者的文章
articleinfo.setUpdatetime(LocalDateTime.now());//修改文章的修改时间
return AjaxResult.success(articleService.update(articleinfo));
}
⑥.博客详情页
点击博客列表页的查看全文按钮,即可来到博客详情页.
在个人中心页,点击自己的文章列表页的浏览按钮,也可来到博客详情页.
Ⅰ.获取文章详情(同博客编辑页的获取文章详情)(略)
Ⅱ.作者信息(类似博客列表页获取用户信息)(略)
Ⅲ.文章阅读量+1
来到详情页,或是刷新详情页时,阅读量就会+1.
约定前后端交互接口
前端请求:
POST /blog/art/incrcount
id=32
后端响应:
HTTP /1.1 200 OK
{"code":200,"msg":"","data":1}
Fiddler抓包结果
实现后端代码
//阅读量+1
@AccessLimit(maxCount = 500,seconds = 5) //接口访问限流
@PostMapping("/incrcount")
public AjaxResult incrRCount(Integer id){
if(id != null && id>0){
return AjaxResult.success(articleService.incrRCount(id));
}
return AjaxResult.fail(-1,"未知错误!");
}
Ⅲ.评论的获取
已发布的文章,点击浏览,就来到博客详情页.如果有评论,就会显示评论.
评论表设计的是两层型.
约定前后端交互接口
前端请求:
POST /blog/comment/list
articleid=32
后端响应:
HTTP /1.1 200 OK
{
"code":200,
"msg":"",
"data":[
{"serializableId":1,"id":20,"articleid":null,"uid":41,"content":"来了~~~~","createtime":"2024-01-05 22:31:14","replyid":null,"parentid":null,"username":"tenjutest","replyname":null,"userphoto":"/blog//upload/26fff453a06b41b5b38f909046f7f048.jpeg","articlename":null,"articleauthor":null,"showtime":null,"replies":[]},
{"serializableId":1,"id":14,"articleid":null,"uid":7,"content":"这个作者写的《黑天鹅》,我也强烈推荐!!!!","createtime":"2023-12-29 22:15:29","replyid":null,"parentid":null,"username":"mangopeach123","replyname":null,"userphoto":"/blog/upload/7bfed9abad2e4f61bf4987aa9f9da1c2.jpeg","articlename":null,"articleauthor":null,"showtime":null,"replies":[]},
{ "serializableId":1,
"id":13,
"articleid":null,
"uid":1,
"content":"欢迎大家一起来讨论~~~",
"createtime":"2023-12-29 22:08:16",
"replyid":null,
"parentid":null,
"username":"peach11",
"replyname":null,
"userphoto":"/blog/upload/8d13afb8d87248419b46841a257ba67b.jpeg",
"articlename":null,
"articleauthor":null,
"showtime":null,
"replies":[
{"serializableId":1,"id":21,"articleid":null,"uid":41,"content":"来了~~~","createtime":"2024-01-05 22:31:52","replyid":null,"parentid":13,"username":"tenjutest","replyname":"mangopeach123","userphoto":null,"articlename":null,"articleauthor":null,"showtime":null,"replies":[]},
{"serializableId":1,"id":15,"articleid":null,"uid":7,"content":"简单来说,反脆弱就是能够在突如其来的变化冲击下获益的能力。反脆弱可以帮助我们应对未知的事情,解决我们不了解的问题。","createtime":"2023-12-29 22:16:08","replyid":null,"parentid":13,"username":"mangopeach123","replyname":"peach11","userphoto":null,"articlename":null,"articleauthor":null,"showtime":null,"replies":[]}
]}
]
}
Fiddler抓包结果
实现后端代码
//获取指定文章的所有评论,联表查询
@AccessLimit(maxCount = 500,seconds = 5) //接口访问限流
@PostMapping("/list")
public AjaxResult getComVOByArticleid(Integer articleid){
if(articleid==null || articleid<=0){
return AjaxResult.fail(-1,"参数非法!");
}
//获取文章里的所有评论
List<CommentinfoVO> commentOne = commentService.getFirstCommentByArticle(articleid);//一级评论
List<CommentinfoVO> commentTwo = commentService.getSecondaryCommentByArticle(articleid);//二级评论
if(commentOne.size()==0){
return AjaxResult.success(null);//说明没有评论
}
//整合评论
for(CommentinfoVO comment : commentOne){
int value = comment.getId();//一级评论commentId
for(CommentinfoVO reply : commentTwo){
int parentId = reply.getParentid(); //找到父commentId
if(value == parentId){
comment.setReplies(reply);
}
}
}
return AjaxResult.success(commentOne);
}
Ⅳ.评论的添加
点击评论,或回复,即可添加评论.
基本思路:
因为评论表是两层型.因此前端发送请求会根据评论的层级,向后端传不同的参数.
点击评论按钮,是一级评论,只传,评论内容,博客id
点击回复按钮,是二级评论,只传,评论内容,博客id,被回复用户id,父评论id(指其一级评论的id).
后端根据接收到的参数不同,创建不同层级的评论,并返回给前端.
约定前后端交互接口
前端请求:
POST /blog/comment/add
articleid=32&content=评论内容&replyid=7&parentid=14
后端响应:
HTTP /1.1 200 OK
{
"code":200,
"msg":"",
"data":{
"serializableId":1,
"id":323,
"articleid":32,
"uid":1,
"content":"好耶!",
"createtime":"2024-02-16 02:13:11",
"replyid":7,
"parentid":14
}
}
Fiddler抓包结果
实现后端代码
//新增评论,返回自增主键id
@AccessLimit(maxCount = 500,seconds = 5) //接口访问限流
@DedupLimit(expireTime = 5000) //去除重复请求,5秒之内
@PostMapping("add")
public AjaxResult add(HttpServletRequest request, Commentinfo commentinfo){
Userinfo userinfo = UserSessionUtils.getUser(request); //新增的评论,一定是当前登陆者发的
if(commentinfo==null || commentinfo.getArticleid()==null || userinfo==null || userinfo.getId()==null){
return AjaxResult.fail(-1,"参数非法!");
}
//判断内容,文本为空,不做任何操作,判断当前内容是否全是空格与换行符
String content = commentinfo.getContent();
if(content != null){
content = content.replaceAll("\\s*|t|r|n","");
}
if("".equals(content) || content==null){
return AjaxResult.fail(-1,"文本为空!");
}
//判断文章状态,不是1说明不是已发布的文章
Articleinfo articleinfo = articleService.getDetail(commentinfo.getArticleid());
if(articleinfo.getState()!=1){
return AjaxResult.fail(-1,"文章状态异常!");
}
//一级评论,只传内容,博客id
//二级评论,传内容,博客id,回复id,父id
if(commentinfo.getParentid()==null){
commentinfo.setParentid(-1);//一级评论
}else{
//有父id,没有回复人id,或者回复id和当前登录者id一样
if(commentinfo.getReplyid()==null || commentinfo.getReplyid().equals(userinfo.getId())) {
return AjaxResult.fail(-1, "参数非法!");
}
}
commentinfo.setCreatetime(LocalDateTime.now());//时间
commentinfo.setUid(userinfo.getId());//设置uid进去
commentService.add(commentinfo);//返回自增主键id
return AjaxResult.success(commentinfo);
}
Ⅴ.评论的刷新(页面重新发送获取评论请求,此请求不刷新整个页面)(略)
⑦.博客个人中心页
Ⅰ.修改头像
点击头像可进行头像的修改.
约定前后端交互接口
前端请求:
POST /blog/user/uploadimg
formData
后端响应:
HTTP /1.1 200 OK
{
"code":200,
"msg":"",
"data":{图片路径}
}
Fiddler抓包结果
实现后端代码
//修改用户头像,linux
@AccessLimit(maxCount = 500,seconds = 5) //接口访问限流
@DedupLimit(expireTime = 5000) //去除重复请求,5秒之内
@PostMapping("/uploadimg")
@SneakyThrows
public AjaxResult uploadimg(@RequestPart("")MultipartFile file ,HttpServletRequest request){
//非空判断
if(file==null || file.getOriginalFilename()==null || file.getContentType()==null){
return AjaxResult.fail(-1,"参数非法!");
}
//判断类型
if(!file.getContentType().contains("image/")){
return AjaxResult.fail(-1,"参数非法!");
}
//判断文件大小,不允许超过2m
long size = file.getSize(); //单位b(字节)
double sizeKb = size/1024.0; //单位kb
if(sizeKb>(1024*2)){
return AjaxResult.fail(-1,"文件超过2MB!");
}else if(sizeKb<=0){
return AjaxResult.fail(-1,"文件为空!");
}
// 获取⽂件后缀名
String fileName = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));
//判断上传的是否是图片
if(!(fileName.equalsIgnoreCase(".PNG") || fileName.equalsIgnoreCase(".JPG")
|| fileName.equalsIgnoreCase(".JPEG")|| fileName.equalsIgnoreCase(".bmp")
|| fileName.equalsIgnoreCase(".GIF"))){
return AjaxResult.fail(-1,"文件非法!");
}
//判断图片内容
BufferedImage read = ImageIO.read(file.getInputStream());
if(read == null){
return AjaxResult.fail(-1,"图片非法!"); //虽然后缀名是图片标识,但内容不是图片
}
//判断用户是否登录
Userinfo userinfo = UserSessionUtils.getUser(request);
if(userinfo==null || userinfo.getId()==null){
return AjaxResult.fail(-1,"用户未登录!");
}
String name = UUID.randomUUID() + fileName;
String filename = name.replace("-","");
File path = new File(ResourceUtils.getURL("classpath:").getPath());
File upload = new File(path.getAbsolutePath(),"static/upload/");
String parent= upload.getPath();
File dir = new File(parent);
File dest = new File(dir, filename);
file.transferTo(dest); //保存图片
String avatar = "/upload/" +filename;// 路径
//删除原来的图片
if(!userinfo.getPhoto().equals("./image/00.png")){
//如果图片路径不是默认头像的路径,就删除原来用户图片
File oldimg = new File(dir,userinfo.getPhoto().split("upload/")[1]);
oldimg.delete();
}
//加入数据库
userinfo.setPhoto(avatar);
userService.updatePhoto(userinfo);
//更新session里的userinfo
UserSessionUtils.reflushUser(request,userinfo);
return AjaxResult.success(avatar);
}
Ⅱ.文章页
a.获取文章
获取已经发表的文章.
约定前后端交互接口
前端请求:
POST /blog/art/mylist
后端响应:
HTTP /1.1 200 OK
{
"code":200,
"msg":"",
"data":[
{"serializableId":1,"id":350,"title":"定风波·莫听穿林打叶声","content":" 三月七日,沙湖道中遇雨。雨具先去,同行皆狼狈,余独不觉。已而遂晴,故作此词。\n\n莫听穿林打叶声,何妨吟啸且徐行。竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生。\n\n料峭春风吹酒醒,微冷,山头斜照却相迎。回首向...","createtime":"2024-02-16 01:30:22","updatetime":"2024-02-16 01:47:02","uid":1,"rcount":1,"state":1},
{"serializableId":1,"id":348,"title":"他","content":"\n\n\n\n\n\n\n\n\n![](https://picx.zhimg.com/80/v2-bccf5a4c76390d3e9afb62d481bfc4c1_720w.webp?source=2c26e567...","createtime":"2024-02-11 21:30:03","updatetime":"2024-02-11 21:30:03","uid":1,"rcount":10,"state":1},
{"serializableId":1,"id":32,"title":"反脆弱","content":"《反脆弱》的作者是纳西姆·尼古拉斯·塔勒,《反脆弱》是一本著名的非小说类读物,由黎巴斯(Nassim Nicholas Taleb)所著。他是一名黎巴嫩裔美国数学家、哲学家、经济学家和风险分析学家。这...","createtime":"2023-12-29 22:07:49","updatetime":"2023-12-29 22:07:49","uid":1,"rcount":769,"state":1},{"serializableId":1,"id":5,"title":"vzvbzvzvz","content":"bxb xbbb","createtime":"2023-12-23 21:19:59","updatetime":"2023-12-29 22:02:52","uid":1,"rcount":7,"state":1},
{"serializableId":1,"id":2,"title":"FGWGWGW","content":"FSGSGSHBDNBDN","createtime":"2023-12-22 23:49:45","updatetime":"2023-12-22 23:49:45","uid":1,"rcount":6,"state":1},
{"serializableId":1,"id":1,"title":"1XXXCCFVAZFVAVA","content":"我去而为QDVZVZVZVZVZV","createtime":"2023-12-22 23:49:28","updatetime":"2023-12-22 23:49:38","uid":1,"rcount":5,"state":1}
]
}
Fiddler抓包结果
实现后端代码
@AccessLimit(maxCount = 500,seconds = 5) //接口访问限流
@PostMapping("mylist")
public AjaxResult getMyList(HttpServletRequest request){
Userinfo userinfo = UserSessionUtils.getUser(request);
if(userinfo == null){
return AjaxResult.fail(-1,"请求非法!");
}
List<Articleinfo> list = articleService.getMyList(userinfo.getId());
ArticleUtils.cutContent(list); //因为是列表,文章需要截取
return AjaxResult.success(list);
}
b.浏览文章/c.编辑文章(略)
点击浏览/编辑文章按钮,即可跳转相应的页面.
d.删除文章
点击删除文章按钮,即可删除.
约定前后端交互接口
前端请求:
POST /blog/art/delete
id=348
后端响应:
HTTP /1.1 200 OK
{"code":200,"msg":"","data":1}
Fiddler抓包结果
实现后端代码
//删除一篇文章,只需要一个id,Integer id
@AccessLimit(maxCount = 500,seconds = 5) //接口访问限流
@DedupLimit(expireTime = 5000) //去除重复请求,5秒之内
@PostMapping("/delete")
public AjaxResult delete(HttpServletRequest request,Articleinfo articleinfo ){
if(articleinfo.getId() == null || articleinfo.getId()<=0){
return AjaxResult.fail(-1,"参数异常!");
}
Userinfo userinfo = UserSessionUtils.getUser(request);
if(userinfo == null){
return AjaxResult.fail(-1,"用户未登陆!");
}
articleinfo.setUid(userinfo.getId());//里面有id,此时再设置用户id进去
//文章可能有评论,有外键依赖可能删不成功
//先判断是否是当前登录者的文章
int userid = articleService.getDetail(articleinfo.getId()).getUid();
if(userid == userinfo.getId()){
//是当前登陆者的文章,先删评论,评论有外键约束
commentService.deleteOfMineByArticle(articleinfo.getId());
}
return AjaxResult.success(articleService.delete(articleinfo));
}
Ⅲ.草稿页(同文章页)(略)
a.获取草稿(同文章页的获取文章)(略)
b.浏览文章(略)
c.编辑文章(略)
d.删除文章(同文章页的删除文章)(略)
Ⅳ.评论页
a.获取评论
约定前后端交互接口
前端请求:
POST /blog/comment/mycomment
后端响应:
HTTP /1.1 200 OK
{
"code":200,
"msg":"",
"data":[
{"serializableId":1,"id":323,"articleid":32,"uid":null,"content":"好耶!","createtime":null,"replyid":null,"parentid":null,"username":null,"replyname":null,"userphoto":null,"articlename":"反脆弱","articleauthor":"peach11","showtime":"2024-02-16","replies":[]},
{"serializableId":1,"id":13,"articleid":32,"uid":null,"content":"欢迎大家一起来讨论~~~","createtime":null,"replyid":null,"parentid":null,"username":null,"replyname":null,"userphoto":null,"articlename":"反脆弱","articleauthor":"peach11","showtime":"2023-12-29","replies":[]},
{"serializableId":1,"id":2,"articleid":1,"uid":null,"content":"12345","createtime":null,"replyid":null,"parentid":null,"username":null,"replyname":null,"userphoto":null,"articlename":"1XXXCCFVAZFVAVA","articleauthor":"peach11","showtime":"2023-12-23","replies":[]},
{"serializableId":1,"id":1,"articleid":2,"uid":null,"content":"FSGSGSGSGSGSG","createtime":null,"replyid":null,"parentid":null,"username":null,"replyname":null,"userphoto":null,"articlename":"FGWGWGW","articleauthor":"peach11","showtime":"2023-12-22","replies":[]}
]
}
Fiddler抓包结果
实现后端代码
//获取指定用户id的发表的所有评论,联表查询
@AccessLimit(maxCount = 500,seconds = 5) //接口访问限流
@PostMapping("mycomment")
public AjaxResult getCommentsByUid(HttpServletRequest request){
Userinfo userinfo = UserSessionUtils.getUser(request);
if(userinfo==null || userinfo.getId()==null){
return AjaxResult.fail(-1,"参数非法!");
}
//获取评论
List<CommentinfoVO> comments = commentService.getCommentsByUid(userinfo.getId()); // 获取登陆者发表的所有评论
if(comments.size()==0){
//说明没有评论
return AjaxResult.success(null);
}
//截取文章标题显示的长度
for(CommentinfoVO comment : comments){
if(comment.getArticlename().length() >= 23){
String title = comment.getArticlename();
title = title.substring(0,23) + "...";
comment.setArticlename(title);
}
}
return AjaxResult.success(comments);
}
b.删除评论
约定前后端交互接口
前端请求:
POST /blog/comment/delete
id=323
后端响应:
HTTP /1.1 200 OK
{"code":200,"msg":"","data":1}
Fiddler抓包结果
实现后端代码
//删除评论,自己的,Integer id
@AccessLimit(maxCount = 500,seconds = 5) //接口访问限流
@DedupLimit(expireTime = 5000) //去除重复请求,5秒之内
@PostMapping("/delete")
public AjaxResult deleteOfMineById(HttpServletRequest request, Integer id){
//只传入,评论自增主键id
Userinfo userinfo = UserSessionUtils.getUser(request);
if(id==null || userinfo==null ||userinfo.getId()==null){
return AjaxResult.fail(-1,"参数非法!");
}
//删除评论,一级评论或是二级评论
int result = commentService.deleteOfMineById(id, userinfo.getId()); //不为0,则删除评论成功
if(result!=0){
//删除二级评论,如果有的话
commentService.deleteSecondaryComment(id);//传入的是parentid
}
return AjaxResult.success(result);
}
4.自动化测试
暂时能力有限,此处实现的自动化测试脚本,只是使用黑盒测试方法去测试个人博客系统的大部分功能.
自动化测试
目前,对个人博客系统的测试就是对主要功能进行测试.
①.自动化测试用例编写
②.代码编写
根据博客系统测试用例编写自动化测试的代码.
Ⅰ.注册页
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)//人工控制执行顺序
public class RegTest extends InitAndEnd{
@Test
@BeforeEach
void init(){
//打开博客注册页
webDriver.get(WebVariable.BLOG_REG);
}
//注册成功
@Disabled
@Order(1)
@ParameterizedTest
@CsvSource(value = {"tenjutest,123456789,12345,12345,12345"})
void regSuccess(String username ,String password,String captcha) {
//输入账号密码,并点击提交按钮
webDriver.findElement(By.cssSelector("#username")).sendKeys(username);
webDriver.findElement(By.cssSelector("#password")).sendKeys(password);
webDriver.findElement(By.cssSelector("#confirmPassword")).sendKeys(password);
webDriver.findElement(By.cssSelector("#captchatext")).sendKeys(captcha);
webDriver.findElement(By.cssSelector("#submit")).click();
//等待,等待弹窗的出现
WebDriverWait wait = new WebDriverWait(webDriver, 10);
wait.until(ExpectedConditions.alertIsPresent());
//判断弹窗,提示框文本
String text = webDriver.switchTo().alert().getText();
Assertions.assertEquals("注册成功!",text);
//弹窗确认
webDriver.switchTo().alert().accept();
//跳转到博客个人中心页页,获取当前页面的url,如果url是/blog/blog-center.html,测试通过,否则不通过
String cur_url = webDriver.getCurrentUrl();
Assertions.assertEquals(WebVariable.BLOG_CENTER,cur_url);//断言
}
//注册失败
@Order(1)
@ParameterizedTest
@CsvFileSource(resources = "regFail.csv")
void regFail(String username ,String password,String confirmPassword,String captcha,String msg) throws IOException, InterruptedException {
//输入账号密码,并点击提交按钮
webDriver.findElement(By.cssSelector("#username")).sendKeys(username);
webDriver.findElement(By.cssSelector("#password")).sendKeys(password);
webDriver.findElement(By.cssSelector("#confirmPassword")).sendKeys(confirmPassword);
webDriver.findElement(By.cssSelector("#captchatext")).sendKeys(captcha);
webDriver.findElement(By.cssSelector("#submit")).click();
//等待,等待弹窗的出现
WebDriverWait wait = new WebDriverWait(webDriver, 30);
wait.until(ExpectedConditions.alertIsPresent());
//判断弹窗,提示框文本
String text = webDriver.switchTo().alert().getText();
Assertions.assertEquals(msg,text);
//弹窗确认
webDriver.switchTo().alert().accept();
}
//菜单栏显示
@Order(2)
@ParameterizedTest
@CsvSource(value = {"主页","写博客","登陆"})
void menuBar(String element ) {
MenuBar.menuBarUnlogin(element);
}
}
Ⅱ.登录页
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)//人工控制执行顺序
public class LoginTest extends InitAndEnd {
@Test
@BeforeEach
void init(){
//打开博客登录页
webDriver.get(WebVariable.BLOG_LOGIN);
}
//登陆失败
@Order(1)
@ParameterizedTest
@CsvFileSource(resources = "loginFail.csv")
void loginFail(String username ,String password,String captchatext,String msg){
LoginUtils.login(username,password,captchatext,msg);
}
//登陆成功
@Order(3)
@ParameterizedTest
@CsvSource(value = {"tenjutest,123456789,12345"})
void loginSuccess(String username,String password,String captchatext) throws InterruptedException, IOException {
//输入账号密码,并点击提交按钮
webDriver.findElement(By.cssSelector("#username")).sendKeys(username);
webDriver.findElement(By.cssSelector("#password")).sendKeys(password);
webDriver.findElement(By.cssSelector("#captchatext")).sendKeys(captchatext); //验证码
webDriver.findElement(By.cssSelector("#submit")).click();
//隐式等待,等待用户名的出现
WebDriverWait wait = new WebDriverWait(webDriver, 10);
wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("#username")));
WebDriverWait waitLink = new WebDriverWait(webDriver,10);
waitLink.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#artList > div:nth-child(1) > a")));
//验证用户名字
String curName = webDriver.findElement(By.cssSelector("#username")).getText();
Assertions.assertEquals(username,curName); // 校验username
//判断url
String cur_url = webDriver.getCurrentUrl();
Assertions.assertEquals(WebVariable.BLOG_LIST,cur_url);//断言,判断是否跳转到博客列表页
}
//菜单栏显示
@Order(2)
@ParameterizedTest
@CsvFileSource(resources = "loginMenuBar.csv")
void menuBar(String element ,String url) {
MenuBar.menuBarLogin(element,url);
}
}
Ⅲ.列表页
//登陆状态,测试列表页
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)//人工控制执行顺序
public class ListTest extends LoginInit {
@BeforeEach
void initget(){
webDriver.get(WebVariable.BLOG_LIST);
}
//判断列表是否显示
@Test
@Order(1)
void getList() {
//等待,等待标题的出现
WebDriverWait wait = new WebDriverWait(webDriver, 10);
wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#artList > div:nth-child(1) > a")));
//获取第一篇标题
String title = webDriver.findElement(By.cssSelector("#artList > div:nth-child(1) > div.title")).getText();
//Assertions.assertEquals("钱塘江上潮信来,今日方知我是我。",title);
Assertions.assertNotNull(title);
//获取时间
String time = webDriver.findElement(By.cssSelector("#artList > div:nth-child(1) > div.date")).getText();
//Assertions.assertEquals("2023-12-30 21:49:30",time);
Assertions.assertNotNull(time);
//获取链接
String link = webDriver.findElement(By.cssSelector("#artList > div:nth-child(1) > a")).getText();
String linkSrc = webDriver.findElement(By.cssSelector("#artList > div:nth-child(1) > a")).getAttribute("href");
Assertions.assertEquals("查看全文 >>",link);
Assertions.assertNotNull(linkSrc);
//文章数量
int number = webDriver.findElements(By.cssSelector(".title")).size();
Assertions.assertNotEquals(0,number); // 校验博客数量,博客数量>0则测试通过
}
//判断用户信息是否显示
@Test
@Order(2)
void getAuthor(){
String username = "tenjutest";
String element = "#artTotal";
GetAuthor.getAuthor(username,element);
}
//判断查看全文链接是否可以跳转
@Test
@Order(3)
void getLink(){
//等待,等待按钮可以被点击
WebDriverWait waitLogout = new WebDriverWait(webDriver, 8);
waitLogout.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#artList > div:nth-child(1) > a")));
//点击查看全文
String link = webDriver.findElement(By.cssSelector("#artList > div:nth-child(1) > a")).getAttribute("href");
webDriver.findElement(By.cssSelector("#artList > div:nth-child(1) > a")).click();
String cur_url = webDriver.getCurrentUrl();
Assertions.assertEquals(link,cur_url);
//浏览器回退,返回
//webDriver.navigate().back();
}
//判断分页功能
@ParameterizedTest
@CsvFileSource(resources = "listPaging.csv")
@Order(4)
void paging(String url,String element,String msg){
webDriver.get(url);
//等待,等待链接可以被点击
WebDriverWait wait = new WebDriverWait(webDriver, 10);
wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#artList > div:nth-child(1) > a")));
//获取页面所有文章列表
List<WebElement> webElements = webDriver.findElements(By.cssSelector(".blog"));
//获取第一篇文章
String title = webElements.get(0).findElement(By.cssSelector(".title")).getText();
Assertions.assertNotNull(title); //标题不为空
//滚动条操作,滑到最底端
((JavascriptExecutor)webDriver).executeScript("document.querySelector(\"body > div.container > div.container-right\").scrollTop=1476");
//翻页按钮,点击
webDriver.findElement(By.cssSelector(element)).click();
if(msg.equals("")){
//没有弹窗信息,就判断url
WebDriverWait waitPaging = new WebDriverWait(webDriver,10);
// waitPaging.until(ExpectedConditions.urlContains("l?pindex=")); //url包含预期字符
waitPaging.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#artList > div:nth-child(1) > a")));
//获取第一篇文章
List<WebElement> cur_webElements = webDriver.findElements(By.cssSelector(".blog"));
//获取第一篇文章的标题
String cur_title = cur_webElements.get(0).findElement(By.cssSelector(".title")).getText();
Assertions.assertNotNull(title); //标题不为空
Assertions.assertNotEquals(title,cur_title);//两次获取的标题不同
Assertions.assertNotEquals(url,webDriver.getCurrentUrl()); //两次url不一致
}else{
//判断弹窗内容
WebDriverWait waitAlert = new WebDriverWait(webDriver,10);
waitAlert.until(ExpectedConditions.alertIsPresent());
String alertmsg = webDriver.switchTo().alert().getText();
Assertions.assertEquals(msg,alertmsg);
webDriver.switchTo().alert().accept();//弹窗确认
}
}
//判断菜单栏
@Order(5)
@ParameterizedTest
@CsvFileSource(resources = "listMenuBar.csv")
void getMenu(String element,String url){
MenuBar.menuBarLogin(element,url);
}
}
Ⅳ.详情页
//博客详情页,正常登录状态
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)//人工控制执行顺序
public class DetailTest extends LoginInit {
//到指定详情页
@BeforeEach
void initget(){
String blogUrl = WebVariable.BLOG_DETAIL;
blogUrl = blogUrl + "?id=32";
webDriver.get(blogUrl);
WebDriverWait waitArticle = new WebDriverWait(webDriver,10);
waitArticle.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#ifDiv"))); //等待子页面可以被点击
}
//判断阅读量
@Test
@Order(1)
void readCount() throws InterruptedException {
int oldCount = Article.toArtDetail();
int newCount = Article.toArtDetail();
Assertions.assertEquals(oldCount+1,newCount);
}
//博客详情
@ParameterizedTest
@CsvSource(value = {"反脆弱,2023-12-29 22:07:49"})
@Order(2)
void getDetail(String title,String time) throws IOException, InterruptedException {
WebDriverWait waitTitle = new WebDriverWait(webDriver,10);
waitTitle.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("#title")));
waitTitle.until(ExpectedConditions.and(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("#content"))));
//校验标题
String cur_title = webDriver.findElement(By.cssSelector("#title")).getText();
Assertions.assertEquals(title,cur_title);
//校验时间
String cur_time = webDriver.findElement(By.cssSelector("#updatetime")).getText();
Assertions.assertEquals(time,cur_time);
//校验文章详情
String cur_content = webDriver.findElement(By.cssSelector("#content")).getText();
Assertions.assertNotNull(cur_content);
}
//判断博客作者信息
@ParameterizedTest
@Order(3)
@CsvSource(value = {"peach11"})
void getAuthor(String author){
WebDriverWait waitUser = new WebDriverWait(webDriver,10);
waitUser.until(ExpectedConditions.not(ExpectedConditions.textToBe(By.cssSelector("#art"),"-1")));
//判断作者名
String cur_author = webDriver.findElement(By.cssSelector("#user")).getText();
Assertions.assertEquals(author,cur_author);
//判断文章数量
String cur_artNumber = webDriver.findElement(By.cssSelector("#art")).getText();
//Assertions.assertEquals(artNumber,cur_artNumber);
Assertions.assertNotEquals("-1",cur_artNumber);
//判断头像
String img = webDriver.findElement(By.cssSelector("#headimg")).getAttribute("src");
String width = webDriver.findElement(By.cssSelector("#headimg")).getAttribute("width");
img = img.substring(img.lastIndexOf(".")+1);;
Assertions.assertEquals("jpeg",img);
Assertions.assertEquals("140",width);
}
//评论的显示
@Order(4)
//@Test
@ParameterizedTest
@CsvSource(value = {"3,2"})
// @CsvFileSource(resources = "commentShow.csv")
void getComment(int comments,int replies) throws InterruptedException {
WebDriverWait wait = new WebDriverWait(webDriver,10);
wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#ifDiv")));
//进入子页面
webDriver.switchTo().frame(webDriver.findElement(By.cssSelector("#ifDiv")));
WebDriverWait waitComment = new WebDriverWait(webDriver,10);
waitComment.until(ExpectedConditions.numberOfElementsToBeMoreThan(By.cssSelector(".comment-info"),0));
//判断一级评论的数量
int cur_comments = webDriver.findElements(By.cssSelector(".comment-info")).size();
//Assertions.assertEquals(comments,cur_comments);
Assertions.assertNotEquals(0,cur_comments);
//判断二级评论的数量
int cur_replies = webDriver.findElements(By.cssSelector(".reply")).size();
//Assertions.assertEquals(replies,cur_replies);
Assertions.assertNotEquals(0,cur_replies);
//此时还没有跳出子页面
}
//评论的添加,失败
//回复自己,回复为空,回复全是换行符空格
@Order(5)
@ParameterizedTest
@CsvSource(value = {"'',文本为空!","' ',文本为空!","换行符,文本为空!"})
// @CsvFileSource(resources = "addComment.csv")
void addCommentFail(String content,String msg){
WebDriverWait wait = new WebDriverWait(webDriver,10);
wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#ifDiv")));
//进入子页面
webDriver.switchTo().frame(webDriver.findElement(By.cssSelector("#ifDiv")));
WebDriverWait waitComment = new WebDriverWait(webDriver,10);
waitComment.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("#content1")));
//添加评论
if(content.equals("换行符")){
//键盘输入,换行符
webDriver.findElement(By.cssSelector("#content1")).sendKeys(Keys.ENTER);
}else {
webDriver.findElement(By.cssSelector("#content1")).sendKeys(content);
}
//webDriver.findElement(By.cssSelector("#content1")).sendKeys(content);
webDriver.findElement(By.cssSelector("#comment1")).click();
//等待弹窗出现
WebDriverWait waitAlert = new WebDriverWait(webDriver,10);
waitAlert.until(ExpectedConditions.alertIsPresent());
//判断弹窗提示内容
String alert = webDriver.switchTo().alert().getText();//获取提示
Assertions.assertEquals(msg,alert);
webDriver.switchTo().alert().accept();//点击确认,弹窗消失
}
//添加回复,失败
@ParameterizedTest
@Order(6)
@CsvSource(value = {"' ',文本为空!","'',文本为空!","换行符,文本为空!"})
void addReplyFail(String content,String msg){
WebDriverWait waitF = new WebDriverWait(webDriver,10);
waitF.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#ifDiv")));
//进入子页面
webDriver.switchTo().frame(webDriver.findElement(By.cssSelector("#ifDiv")));
WebDriverWait wait = new WebDriverWait(webDriver,10);
wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".reply-btn")));
//找到回复按钮
List<WebElement> webElements = webDriver.findElements(By.cssSelector(".comment-info"));
WebElement cur_webElement = webElements.get(2);
//点击回复
cur_webElement.findElement(By.cssSelector(".reply-btn")).click();
//添加回复
if(content.equals("换行符")){
WebDriverWait waitText = new WebDriverWait(webDriver,10);
waitText.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".replybox textarea")));
//键盘输入,换行符
cur_webElement.findElement(By.cssSelector(".mytextarea")).sendKeys(Keys.ENTER);
}else {
cur_webElement.findElement(By.cssSelector(".mytextarea")).sendKeys(content);
}
cur_webElement.findElement(By.cssSelector(".send")).click();
//等待弹窗出现
WebDriverWait waitAlert = new WebDriverWait(webDriver,5);
waitAlert.until(ExpectedConditions.alertIsPresent());
//判断弹窗提示内容
String alert = webDriver.switchTo().alert().getText();//获取提示
Assertions.assertEquals(msg,alert);
webDriver.switchTo().alert().accept();//点击确认,弹窗消失
}
//回复自己,失败
@ParameterizedTest
@CsvSource(value = {"body > div.container > div > div:nth-child(1) > div > div.comment-content-footer > div > div.col-md-2 > span.reply-btn","#\\31 3 > div:nth-child(1) > p > span.reply-list-btn"})
@Order(7)
void replyMyself(String element){
WebDriverWait wait = new WebDriverWait(webDriver,10);
wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#ifDiv")));
//进入子页面
webDriver.switchTo().frame(webDriver.findElement(By.cssSelector("#ifDiv")));
//回复自己
webDriver.findElement(By.cssSelector(element)).click();
//等待弹窗出现
WebDriverWait waitAlert = new WebDriverWait(webDriver,10);
waitAlert.until(ExpectedConditions.alertIsPresent());
//判断弹窗提示内容
String alert = webDriver.switchTo().alert().getText();//获取提示
Assertions.assertEquals("不能回复自己!",alert);
webDriver.switchTo().alert().accept();//点击确认,弹窗消失
}
//回复他人,成功
@ParameterizedTest
@CsvSource(value = {"在下名侦探小桃子"})
@Order(8)
void addReplySuccess(String content) throws InterruptedException {
WebDriverWait wait = new WebDriverWait(webDriver,10);
wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#ifDiv")));
//进入子页面
webDriver.switchTo().frame(webDriver.findElement(By.cssSelector("#ifDiv")));
WebDriverWait waitReply = new WebDriverWait(webDriver,10);
waitReply.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".reply-btn")));
//找到回复按钮
List<WebElement> webElements = webDriver.findElements(By.cssSelector(".comment-info"));
WebElement cur_webElement = webElements.get(2);
//点击回复
cur_webElement.findElement(By.cssSelector(".reply-btn")).click();
//添加回复
cur_webElement.findElement(By.cssSelector(".mytextarea")).sendKeys(content);
cur_webElement.findElement(By.cssSelector(".send")).click();//点击发送
//等待刷新
WebDriverWait waitRefresh = new WebDriverWait(webDriver,10);
int size = webDriver.findElements(By.cssSelector(".reply-list-btn")).size();
waitRefresh.until(ExpectedConditions.numberOfElementsToBeMoreThan(By.cssSelector(".reply-list-btn"),size)); //回复按钮,比之前多,说明刷新出来了
WebDriverWait waitRefreshC = new WebDriverWait(webDriver,10);
waitRefreshC.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector(".comment-info")));//
//元素刷新了,得重新找到
//找到回复按钮
List<WebElement> verify_webElements = webDriver.findElements(By.cssSelector(".comment-right"));
cur_webElement = verify_webElements.get(2);
//验证回复
//验证回复人名字
List<WebElement> reply_Elements = cur_webElement.findElements(By.cssSelector(".reply"));
WebElement reply_Element = reply_Elements.get(0); //回复列表的第一个回复
String cur_username = reply_Element.findElements(By.cssSelector("a")).get(0).getText();//回复人
Assertions.assertEquals("tenjutest",cur_username);
//验证被回复人名字
String beReply = cur_webElement.findElement(By.cssSelector("h3 a")).getText(); //被回复人,一级评论
String cur_beReply = reply_Element.findElements(By.cssSelector("a")).get(1).getText(); //被回复人,二级评论
beReply = "@"+beReply;
Assertions.assertEquals(beReply,cur_beReply);
//验证内容
String cur_content = reply_Element.findElement(By.cssSelector("div span")).getText();//回复内容
Assertions.assertEquals(content,cur_content);
//验证时间
String cur_time = reply_Element.findElements(By.cssSelector("p span")).get(0).getText();//回复时间
DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");//指定格式
LocalDateTime localDate = LocalDateTime.parse(cur_time, format);//把页面获取的String时间转换成LocalDateTime
LocalDateTime time = LocalDateTime.now();//获取当前时间
LocalDateTime timeAfter = time.plusSeconds(5);//获取当前时间,并增加5秒
LocalDateTime timeBefore = time.minusSeconds(5);//获取当前的时间,并减去5秒
String result = "";
if( timeBefore.isBefore(localDate) && timeAfter.isAfter(localDate)){
result = "时间测试通过";
System.out.println("时间测试通过");
}else{
result = "时间测试不通过";
}
Assertions.assertEquals("时间测试通过",result);
}
//添加文章评论,成功
@ParameterizedTest
@CsvSource(value = {"在下名侦探小桃子"})
@Order(9)
void addCommentSuccess(String content) {
WebDriverWait wait = new WebDriverWait(webDriver,10);
wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#ifDiv")));
//进入子页面
webDriver.switchTo().frame(webDriver.findElement(By.cssSelector("#ifDiv")));
//评论成功
//输入内容
webDriver.findElement(By.cssSelector("#content1")).sendKeys(content); //输入内容
//点击发送按钮
webDriver.findElement(By.cssSelector("#comment1")).click();//点击评论
//等待刷新
WebDriverWait waitRefresh = new WebDriverWait(webDriver,10);
int size = webDriver.findElements(By.cssSelector(".reply-btn")).size();
waitRefresh.until(ExpectedConditions.numberOfElementsToBeMoreThan(By.cssSelector(".reply-btn"),size)); //回复按钮,比之前多,说明刷新出来了
//验证回复
String cur_username = webDriver.findElement(By.cssSelector("body > div.container > div > div:nth-child(1) > div > h3 > a")).getText();//回复人
Assertions.assertEquals("tenjutest",cur_username);
//验证内容
String cur_content = webDriver.findElement(By.cssSelector("body > div.container > div > div:nth-child(1) > div > div.content")).getText();//回复内容
Assertions.assertEquals(content,cur_content);
//验证时间
String cur_time = webDriver.findElement(By.cssSelector("body > div.container > div > div:nth-child(1) > div > div.comment-content-footer > div > div.col-md-2 > span:nth-child(1)")).getText();//回复时间
DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");//指定格式
LocalDateTime localDate = LocalDateTime.parse(cur_time, format);//把页面获取的String时间转换成LocalDateTime
LocalDateTime time = LocalDateTime.now();//获取当前时间
LocalDateTime timeAfter = time.plusSeconds(5);//获取当前时间,并增加30秒
LocalDateTime timeBefore = time.minusSeconds(5);//获取当前的时间,并减去30秒
String result = "";
if( timeBefore.isBefore(localDate) && timeAfter.isAfter(localDate)){
result = "时间测试通过";
System.out.println("时间测试通过");
}else{
result = "时间测试不通过";
System.out.println("时间测试不通过");
}
Assertions.assertEquals("时间测试通过",result);
}
//判断菜单栏
@ParameterizedTest
@Order(10)
@CsvSource(value = {"主页,http://62.234.10.69/blog/blog-list.html","写博客,http://62.234.10.69/blog/blog-add.html","个人中心,http://62.234.10.69/blog/blog-center.html"})
void getMenuBar(String element,String url){
MenuBar.menuBarLogin(element, url);
}
}
Ⅴ.添加页
//编辑页,登录状态
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)//人工控制执行顺序
public class AddTest extends LoginInit {
@BeforeEach
void initget(){
webDriver.get(WebVariable.BLOG_ADD);
}
//判断用户信息,列表页也有
@Test
@Order(1)
void getAuthor() throws InterruptedException {
String username = "tenjutest";
String element = "#artTotal";
GetAuthor.getAuthor(username,element);
}
//保存新博客,异常填写
@ParameterizedTest
@Order(2)
@CsvFileSource(resources = "saveArt.csv")
void saveArtFail(String title,String content,String msg) throws InterruptedException {
String element = "body > div.container > div.container-right > div.title > button.mysave";
Article.artFail(title, content, msg, element);
}
//发布新博客,异常发布
@ParameterizedTest
@Order(3)
@CsvFileSource(resources = "addArt.csv")
void addArtFail(String title,String content,String msg) throws InterruptedException {
String element = "#submit";
Article.artFail(title, content, msg, element);
}
//保存文章,正常操作,
@ParameterizedTest
@Order(4)
@CsvSource(value = "今天天气很好呀-测试我的保存草稿,重游就地,保存草稿成功!")
void saveArtSuccess(String title,String content,String msg) throws InterruptedException {
//保存按钮可以被点击
WebDriverWait wait = new WebDriverWait(webDriver, 10);
wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector("body > div.container > div.container-right > div.title > button.mysave")));//等待保存按钮可以被点击
//填写标题
webDriver.findElement(By.cssSelector("#title")).sendKeys(title);
//填写正文
webDriver.findElement(By.cssSelector("#editor > div.editormd-toolbar > div > ul > li:nth-child(21) > a > i")).click();
//点击保存按钮
webDriver.findElement(By.cssSelector("body > div.container > div.container-right > div.title > button.mysave")).click();//点击保存
//获取文章数量
String oldnumber = webDriver.findElement(By.cssSelector("#artTotal")).getText();
//等待,等待弹窗的出现
WebDriverWait waitAlert = new WebDriverWait(webDriver, 10);
waitAlert.until(ExpectedConditions.alertIsPresent());
String text = webDriver.switchTo().alert().getText(); //判断弹窗提示信息
Assertions.assertEquals(msg,text);
webDriver.switchTo().alert().accept();//点击
//判断保存
webDriver.get(WebVariable.BLOG_CENTER);//到个人中心页
WebDriverWait waitDraft = new WebDriverWait(webDriver, 10);
waitDraft.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#ifDiv"))); //等待frame可以被点击
//判断文章数量
String newnumber = webDriver.findElement(By.cssSelector("#artTotal")).getText();
Assertions.assertEquals(newnumber,oldnumber); //保存文章草稿,文章数量不会变
webDriver.switchTo().frame(webDriver.findElement(By.cssSelector("#ifDiv")));//切换到子页面
webDriver.findElement(By.cssSelector("#mydraftlist")).click();//点击我的草稿
webDriver.switchTo().defaultContent();//跳出frame,默认是最外面默认页面
webDriver.switchTo().frame(webDriver.findElement(By.cssSelector("#ifDiv")));//切换到子页面
//等待,标题可以被点击
WebDriverWait waitTitle = new WebDriverWait(webDriver, 10);
waitTitle.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#artList > div:nth-child(1) > a")));
//判断第一篇,标题
String cur_title = webDriver.findElement(By.cssSelector("#artList > div:nth-child(1) > a")).getText(); //查找第一篇文章
Assertions.assertEquals(title,cur_title);
}
//发布文章,正常操作,
@ParameterizedTest
@Order(5)
@CsvSource(value = "今天天气很好呀-测试我的文章发布,重游就地,发布文章成功! 是否继续添加新文章?")
void addArtSuccess(String title,String content,String msg) throws InterruptedException {
Article.artSuccess(title,content,msg);
}
//判断菜单栏
@Order(6)
@ParameterizedTest
@CsvFileSource(resources = "listMenuBar.csv")
void getMenu(String element,String url) throws InterruptedException {
MenuBar.menuBarLogin(element,url);
}
}
Ⅵ.编辑页
//编辑页,正常登陆
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)//测试顺序
public class EditTest extends LoginInit {
//跳转到编辑页,无id,会跳转到添加页
@Test
@Order(1)
void getEdit() throws InterruptedException {
//没有id,会跳转到blog-add.html
webDriver.get(WebVariable.BLOG_EDIT); //才进行页面跳转
//等到按钮可以被点击
WebDriverWait wait = new WebDriverWait(webDriver,10);
wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector("body > div.container > div.container-right > div.title > button.mysave")));
String cur_url = webDriver.getCurrentUrl();
Assertions.assertEquals(WebVariable.BLOG_ADD,cur_url);
}
//跳转到个人中心页,点击第一篇发布的文章
@Test
@Order(2)
void getArtDetail() throws InterruptedException, IOException {
//到个人中心页
webDriver.get(WebVariable.BLOG_CENTER);
WebDriverWait waitFrame = new WebDriverWait(webDriver, 10);
waitFrame.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#ifDiv"))); //等待frame可以被点击
//跳转子页面
webDriver.switchTo().frame(webDriver.findElement(By.cssSelector("#ifDiv")));
//等待,标题可以被点击
WebDriverWait waitTitle = new WebDriverWait(webDriver, 10);
waitTitle.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#artList > div:nth-child(1) > a")));
//获取第一篇,标题
String cur_title = webDriver.findElement(By.cssSelector("#artList > div:nth-child(1) > a")).getText(); //查找第一篇文章
//点击第一篇文章,的编辑按钮
webDriver.findElement(By.cssSelector("#artList > div > div.articleButton > a:nth-child(5)")).click();
webDriver.switchTo().defaultContent();//跳出框架
//等待,提交按钮,可以被点击
WebDriverWait waitSubmit = new WebDriverWait(webDriver, 10);
waitSubmit.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("#title")));
WebDriverWait waitContent = new WebDriverWait(webDriver,10);
//等待第三方控件里的某个东西出现,不一定显示在页面上
waitContent.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("#editor .editormd-markdown-textarea")));
//获取标题
String js = "return jQuery(\"#title\")[0].value";
String cur_title_edit = (String) ((JavascriptExecutor)webDriver).executeScript(js);
Assertions.assertEquals(cur_title,cur_title_edit);
String editContent = "return editor.getValue()";
String cur_content_edit = (String) ((JavascriptExecutor)webDriver).executeScript(editContent);
Assertions.assertNotEquals("",cur_content_edit);//判断文章详情,不为空
}
//判断用户信息
@Order(3)
@Test
void getAuthor(){
String username = "tenjutest";
String element = "#artTotal";
GetAuthor.getAuthor(username,element);
}
//判断菜单栏
@ParameterizedTest
@Order(4)
@CsvSource(value = {"主页,http://62.234.10.69/blog/blog-list.html","写博客,http://62.234.10.69/blog/blog-add.html","个人中心,http://62.234.10.69/blog/blog-center.html"})
void getMenuBar(String element,String url){
MenuBar.menuBarLogin(element, url);
webDriver.navigate().back();//页面回退
}
//修改文章,异常
@Order(5)
@ParameterizedTest
@CsvFileSource(resources = "editArt.csv")
void EditFail(String title,String content,String msg) throws InterruptedException {
WebDriverWait waitButton = new WebDriverWait(webDriver,30);
waitButton.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#submit")));
//清空标题
webDriver.findElement(By.cssSelector("#title")).clear();
String element = "#submit";
Article.artFail(title, content, msg, element);
}
//修改文章,点击保存,点击确定,会跳转到个人中心页
@Order(6)
@ParameterizedTest
@CsvSource(value = {"江山代有才人出-各领风骚数百年,江山代有才人出-各领风骚数百年,修改文章成功!"})
void EditSuccess(String title ,String content,String msg) throws InterruptedException {
WebDriverWait wait = new WebDriverWait(webDriver,10);
wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#submit")));
//清空标题
webDriver.findElement(By.cssSelector("#title")).clear();
//此代码通用,修改文章,成功
String element = "#submit";
Article.artFail(title, content, msg, element);
//判断url
WebDriverWait waitFrame = new WebDriverWait(webDriver, 10);
waitFrame.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#ifDiv"))); //等待frame可以被点击
//跳转子页面
webDriver.switchTo().frame(webDriver.findElement(By.cssSelector("#ifDiv")));
//等待,标题可以被点击
WebDriverWait waitTitle = new WebDriverWait(webDriver, 10);
waitTitle.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#artList > div:nth-child(1) > a")));
//获取第一篇,标题
String cur_title = webDriver.findElement(By.cssSelector("#artList > div:nth-child(1) > a")).getText(); //查找第一篇文章
Assertions.assertEquals(title,cur_title);
//判断时间
String cur_time = webDriver.findElement(By.cssSelector("#artList > div > div.articleButton > span:nth-child(1)")).getText(); //查找第一篇文章
DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");//指定格式
LocalDateTime localDate = LocalDateTime.parse(cur_time, format);//把页面获取的String时间转换成LocalDateTime
LocalDateTime time = LocalDateTime.now();//获取当前时间
LocalDateTime timeAfter = time.plusSeconds(5);//获取当前时间,并增加30秒
LocalDateTime timeBefore = time.minusSeconds(5);//获取当前的时间,并减去30秒
String result = "";
if( timeBefore.isBefore(localDate) && timeAfter.isAfter(localDate)){
result = "时间测试通过";
System.out.println("时间测试通过");
}else{
result = "时间测试不通过";
System.out.println("时间测试不通过");
}
Assertions.assertEquals("时间测试通过",result);
//Assertions.assertEquals(time1,cur_time);
//跳出子页面
webDriver.switchTo().defaultContent();
//回退
webDriver.navigate().back();
}
}
Ⅶ.个人中心页
//个人中心页,正常登陆
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)//人工控制执行顺序
public class CenterTest extends LoginInit {
@BeforeEach
void initCenter(){
webDriver.get(WebVariable.BLOG_CENTER);
WebDriverWait wait = new WebDriverWait(webDriver,10);
wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("#username")));
}
//判断用户信息
@Order(1)
@Test
void getAuthor(){
String username = "tenjutest";
String element = "#artTotal";
GetAuthor.getAuthor(username,element);
}
//修改头像,失败
//直接点击上传,上传不是图片,上传后缀名是图片但不是图片
//@Test
@Order(3)
@ParameterizedTest
@CsvFileSource(resources = "uploadPhoto.csv")
void uploadPhotoFail(String path,String msg) throws IOException, InterruptedException {
webDriver.switchTo().defaultContent();
//点击头像
webDriver.findElement(By.cssSelector("#headimg")).click();
//到子页面
webDriver.switchTo().frame(webDriver.findElement(By.cssSelector("#ifDiv")));
//选择文件,上传
String namePhoto = System.getProperty("user.dir");
namePhoto = namePhoto+path;
System.out.println(namePhoto);
webDriver.findElement(By.cssSelector("#f1")).sendKeys(namePhoto);
//点击上传
webDriver.findElement(By.cssSelector("body > div > div > div.check > input:nth-child(2)")).click();
//等待弹窗
//等待弹窗出现
WebDriverWait waitAlert = new WebDriverWait(webDriver,30);
waitAlert.until(ExpectedConditions.alertIsPresent());
String alertText = webDriver.switchTo().alert().getText();//判断弹窗提示内容
Assertions.assertEquals(msg,alertText);
webDriver.switchTo().alert().accept(); //关闭弹窗
}
//获取我的文章
@Order(2)
@Test
void getArt() throws IOException {
//获取文章
//回到父页面
webDriver.switchTo().defaultContent();//回到父页面
//防止获取到原来的默认数字
WebDriverWait waitArtTotal = new WebDriverWait(webDriver,10);
//waitArtTotal.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#ifDiv")));
waitArtTotal.until(ExpectedConditions.not(ExpectedConditions.textToBe(By.cssSelector("#artTotal"),"-1")));
//父页面的文章数
int artNumbers = Integer.parseInt(webDriver.findElement(By.cssSelector("#artTotal")).getText());
//到子页面
webDriver.switchTo().frame(webDriver.findElement(By.cssSelector("#ifDiv")));
WebDriverWait waitDraftList = new WebDriverWait(webDriver,10);//等待获取文章列表
waitDraftList.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".articleList")));
//获取文章
//文章数量>0
int cur_number = webDriver.findElements(By.cssSelector(".articleList")).size();
Assertions.assertNotEquals(0,cur_number);//不为0
Assertions.assertEquals(artNumbers,cur_number);
}
//修改头像成功
@ParameterizedTest
@Order(11)
@CsvSource(value = {"/src/main/resources/photo/upload/正常图片.jpeg,修改头像成功!"})
void uploadPhotoSuccess(String path,String msg) throws InterruptedException {
//回到父页面
webDriver.switchTo().defaultContent();//回到父页面
//获取头像属性
String oldPhoto = webDriver.findElement(By.cssSelector("#headimg")).getAttribute("src");
//点击头像
webDriver.findElement(By.cssSelector("#headimg")).click();
//到子页面
webDriver.switchTo().frame(webDriver.findElement(By.cssSelector("#ifDiv")));
//选择文件,上传
String namePhoto = System.getProperty("user.dir");
namePhoto = namePhoto+path;
webDriver.findElement(By.cssSelector("#f1")).sendKeys(namePhoto);
//点击上传
webDriver.findElement(By.cssSelector("body > div > div > div.check > input:nth-child(2)")).click();
//判断上传
webDriver.switchTo().defaultContent();//回到父页面
//等待弹窗
//等待弹窗出现
WebDriverWait waitAlert = new WebDriverWait(webDriver,30);
waitAlert.until(ExpectedConditions.alertIsPresent());
String alertText = webDriver.switchTo().alert().getText();//判断弹窗提示内容
Assertions.assertEquals(msg,alertText);
webDriver.switchTo().alert().accept(); //关闭弹窗
//等待刷新
//防止获取到原来的图片路径,当src属性内容变了
WebDriverWait waitPhoto = new WebDriverWait(webDriver,10);
waitPhoto.until(ExpectedConditions.not(ExpectedConditions.attributeToBe(webDriver.findElement(By.cssSelector("#headimg")),"src",oldPhoto)));
String newPhoto = webDriver.findElement(By.cssSelector("#headimg")).getAttribute("src");
Assertions.assertNotEquals(newPhoto,oldPhoto);
}
//我的文章,功能,浏览
@Order(5)
@ParameterizedTest
@CsvSource(value = {"我的文章","我的草稿"})
void artDetail (String choice) throws InterruptedException {
//回到父页面
webDriver.switchTo().defaultContent();//回到父页面
//父页面的文章数
int artNumbers = Integer.parseInt(webDriver.findElement(By.cssSelector("#artTotal")).getText());
//到子页面
webDriver.switchTo().frame(webDriver.findElement(By.cssSelector("#ifDiv")));
if(choice.equals("我的草稿")){
WebDriverWait waitDraft = new WebDriverWait(webDriver,10);
waitDraft.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#mydraftlist")));
webDriver.findElement(By.cssSelector("#mydraftlist")).click(); //点击草稿
webDriver.switchTo().defaultContent();//跳出frame,默认是最外面默认页面
webDriver.switchTo().frame(webDriver.findElement(By.cssSelector("#ifDiv")));//切换到子页面
}
WebDriverWait waitDraftList = new WebDriverWait(webDriver,10);//等待获取文章列表
waitDraftList.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".articleList")));
//获取文章详情
//文章浏览
//获取文章详情
List<WebElement> webElements = webDriver.findElements(By.cssSelector(".articleList"));
WebElement webElement = webElements.get(0);
String title = webElement.findElement(By.cssSelector(".articleName")).getText();
String content = webElement.findElement(By.cssSelector(".articleContent1")).getText();//获取正文
String time = webElement.findElement(By.cssSelector(".articleContent")).getText();//获取时间
//判断详情不为空
Assertions.assertNotNull(title); //判断标题不为空
Assertions.assertNotNull(content);//判断正文不为空
Assertions.assertNotNull(time); //判断时间不为空
webElement.findElement(By.linkText("浏览")).click();//点击编辑按钮
//等待详情页刷新出来
WebDriverWait waitDetail = new WebDriverWait(webDriver,10);
waitDetail.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("#content")));
//获取文章详情
String cur_title = webDriver.findElement(By.cssSelector("#title")).getText();
Assertions.assertEquals(title,cur_title);//文章标题
String cur_content = webDriver.findElement(By.cssSelector("#content")).getText();
Assertions.assertNotNull(cur_content);//文章正文
String cur_time = webDriver.findElement(By.cssSelector("#updatetime")).getText();
Assertions.assertEquals(time,cur_time);//文章时间
//可以找得到评论
WebDriverWait waitComment = new WebDriverWait(webDriver,10);
if(choice.equals("我的草稿")){
waitComment.until(ExpectedConditions.invisibilityOfElementLocated(By.cssSelector("#ifDiv")));//找不到评论
}else {
waitComment.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("#ifDiv"))); //可以找到评论
}
}
//我的文章,编辑
@Order(6)
@ParameterizedTest
@CsvSource(value = {"我的文章","我的草稿"})
void artEdit (String choice) throws InterruptedException, IOException {
//回到父页面
webDriver.switchTo().defaultContent();//回到父页面
//父页面的文章数
int artNumbers = Integer.parseInt(webDriver.findElement(By.cssSelector("#artTotal")).getText());
//到子页面
webDriver.switchTo().frame(webDriver.findElement(By.cssSelector("#ifDiv")));
if(choice.equals("我的草稿")){
WebDriverWait waitDraft = new WebDriverWait(webDriver,10);
waitDraft.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#mydraftlist")));
webDriver.findElement(By.cssSelector("#mydraftlist")).click(); //点击草稿
webDriver.switchTo().defaultContent();//跳出frame,默认是最外面默认页面
webDriver.switchTo().frame(webDriver.findElement(By.cssSelector("#ifDiv")));//切换到子页面
}
WebDriverWait waitDraftList = new WebDriverWait(webDriver,10);//等待获取文章列表
waitDraftList.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".articleList")));
//文章浏览
//获取文章详情
List<WebElement> webElements = webDriver.findElements(By.cssSelector(".articleList"));
WebElement webElement = webElements.get(0);
String title = webElement.findElement(By.cssSelector(".articleName")).getText();
String content = webElement.findElement(By.cssSelector(".articleContent1")).getText();
webElement.findElement(By.linkText("编辑")).click();//点击编辑按钮
//判断详情不为空
Assertions.assertNotNull(title); //判断标题不为空
Assertions.assertNotNull(content);//判断正文不为空
//等待详情页刷新出来
//等待,提交按钮,可以被点击
WebDriverWait waitSubmit = new WebDriverWait(webDriver, 30);
//waitSubmit.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("#editor")));
//presenceOfElementLocated(By locator):判断某个元素是否被加到了dom树里,并不代表该元素一定可见;
waitSubmit.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("#editor .editormd-markdown-textarea")));
waitSubmit.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector(".CodeMirror-scroll")));
WebDriverWait waitTitle = new WebDriverWait(webDriver, 10);
waitTitle.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("#title")));
//点击获取正文
String editContent = "return editor.getValue()";
String cur_contnet = (String) ((JavascriptExecutor)webDriver).executeScript(editContent);
//获取文章详情
String cur_title = webDriver.findElement(By.cssSelector("#title")).getAttribute("value");
Assertions.assertEquals(title,cur_title);//文章标题
//Assertions.assertEquals(content, cur_contnet);
Assertions.assertNotEquals("",cur_contnet); //判断正文不为空
}
//我的文章,删除
@Order(7)
@ParameterizedTest
@CsvSource(value = {"我的文章,是否要删除文章: ","我的草稿"})
void artDelete(String choice) throws IOException {
//回到父页面
webDriver.switchTo().defaultContent();//回到父页面
//防止获取到原来的默认数字
WebDriverWait waitArtTotal = new WebDriverWait(webDriver,10);
//waitArtTotal.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#ifDiv")));
waitArtTotal.until(ExpectedConditions.not(ExpectedConditions.textToBe(By.cssSelector("#artTotal"),"-1")));
//父页面的文章数
int artNumbers = Integer.parseInt(webDriver.findElement(By.cssSelector("#artTotal")).getText());
//到子页面
webDriver.switchTo().frame(webDriver.findElement(By.cssSelector("#ifDiv")));
if(choice.equals("我的草稿")){
WebDriverWait waitDraft = new WebDriverWait(webDriver,10);
waitDraft.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#mydraftlist")));
webDriver.findElement(By.cssSelector("#mydraftlist")).click(); //点击草稿
webDriver.switchTo().defaultContent();//跳出frame,默认是最外面默认页面
webDriver.switchTo().frame(webDriver.findElement(By.cssSelector("#ifDiv")));//切换到子页面
}
WebDriverWait waitDraftList = new WebDriverWait(webDriver,10);//等待获取文章列表
waitDraftList.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".articleList")));
//文章删除
//获取文章详情
List<WebElement> webElements = webDriver.findElements(By.cssSelector(".articleList"));
int size = webElements.size();//控件数量
WebElement webElement = webElements.get(0); //选取第一篇文章
String title = webElement.findElement(By.cssSelector(".articleName")).getText();//文章标题
String time = webElement.findElement(By.cssSelector(".articleContent")).getText();//获取时间
Assertions.assertNotNull(title); //判断标题不为空
String msg = "是否要删除文章: " + title+" ?"; //弹窗提示信息
if(choice.equals("我的文章")){
//如果是文章页,父页面文章数,和当前列表控件数量一致
Assertions.assertEquals(artNumbers,size);
}
webElement.findElement(By.linkText("删除")).click();//点击删除按钮
//等待弹窗出现
WebDriverWait waitAlert = new WebDriverWait(webDriver,20);
waitAlert.until(ExpectedConditions.alertIsPresent());
//判断弹窗提示内容
String alert = webDriver.switchTo().alert().getText();//获取提示
Assertions.assertEquals(msg,alert); //判断弹窗提示信息
webDriver.switchTo().alert().accept();//点击确认,弹窗消失
//等待文章完成删除文章操作
//显式等待,文章数量少于之前的
WebDriverWait waitSubmit = new WebDriverWait(webDriver, 10);
waitSubmit.until(ExpectedConditions.numberOfElementsToBeLessThan(By.cssSelector(".articleList"),size));
//我的草稿,文章数量不会变
List<WebElement> newWebElements = webDriver.findElements(By.cssSelector(".articleList"));
int cur_size = newWebElements.size();//控件数量
Assertions.assertEquals(size-1,cur_size); //控件数量会减少一个
//删除文章之后,还有文章的话
if(cur_size!=0){
WebElement cur_ebElement = newWebElements.get(0); //选取第一篇文章
//文章标题可能一致,但时间不太可能一致
// String cur_title = cur_ebElement.findElement(By.cssSelector(".articleName")).getText();//文章标题
String cur_time = cur_ebElement.findElement(By.cssSelector(".articleContent")).getText();//获取时间
Assertions.assertNotNull(cur_time); //判断时间不为空
System.out.println(time);
System.out.println(cur_time);
Assertions.assertNotEquals(time,cur_time); //两次时间不会一致
}
//判断主页文章数量的变化
webDriver.switchTo().defaultContent();//回到父页面
int cur_artNumbers = Integer.parseInt(webDriver.findElement(By.cssSelector("#artTotal")).getText());
if(choice.equals("我的文章")){
//删除已发布的文章,文章数量会发生改变
int deleteAfter = artNumbers-1;
Assertions.assertEquals(deleteAfter,cur_artNumbers); //文章数量会减少1
}
}
//我的评论,显示
@Order(8)
@Test
void getComment(){
//回到父页面
webDriver.switchTo().defaultContent();//回到父页面
//点击评论
webDriver.findElement(By.linkText("评论")).click(); //点击评论
//到子页面
webDriver.switchTo().frame(webDriver.findElement(By.cssSelector("#ifDiv")));
WebDriverWait waitButton = new WebDriverWait(webDriver,10);
waitButton.until(ExpectedConditions.elementToBeClickable(By.linkText("删除")));
//获取评论
List<WebElement> webElements = webDriver.findElements(By.linkText("删除"));
int size = webElements.size();
Assertions.assertNotEquals(0,size); //评论数量不为0
}
//我的评论,删除
@Order(9)
//@Test
@ParameterizedTest
@CsvSource(value = {"1","2"})
void deleteComment(String element){
//回到父页面
webDriver.switchTo().defaultContent();//回到父页面
//点击评论
webDriver.findElement(By.linkText("评论")).click(); //点击评论
//到子页面
webDriver.switchTo().frame(webDriver.findElement(By.cssSelector("#ifDiv")));
//等待删除按钮可以被点击
WebDriverWait waitButton = new WebDriverWait(webDriver,10);
waitButton.until(ExpectedConditions.elementToBeClickable(By.linkText("删除")));
//获取评论数量
List<WebElement> webElements = webDriver.findElements(By.linkText("删除"));
int size = webElements.size();
Assertions.assertNotEquals(0,size); //评论数量不为0
//删除第一条评论
webDriver.findElement(By.cssSelector("body > div > div.container > div > div:nth-child(1) > div.daohang > a.delect")).click();
//等待弹窗出现
WebDriverWait waitAlert = new WebDriverWait(webDriver,10);
waitAlert.until(ExpectedConditions.alertIsPresent());
//判断弹窗提示内容
String alert = webDriver.switchTo().alert().getText();//获取提示
Assertions.assertEquals("是否要删除评论 ?",alert); //判断弹窗提示信息
webDriver.switchTo().alert().accept();//点击确认,弹窗消失
//等待完成删除操作
//显式等待,删除数量少于之前的
WebDriverWait waitSubmit = new WebDriverWait(webDriver, 10);
waitSubmit.until(ExpectedConditions.numberOfElementsToBeLessThan(By.linkText("删除"),size));
waitSubmit.until(ExpectedConditions.numberOfElementsToBeMoreThan(By.linkText("删除"),size-2));
//控件数量-1
List<WebElement> cur_WebElements = webDriver.findElements(By.linkText("删除"));
int cur_size = cur_WebElements.size();
Assertions.assertEquals(size-1,cur_size);
}
//判断菜单栏
@ParameterizedTest
@Order(10)
@CsvSource(value = {"主页,http://62.234.10.69/blog/blog-list.html","写博客,http://62.234.10.69/blog/blog-add.html"})
void getMenuBar(String element,String url){
MenuBar.menuBarLogin(element,url);
}
}
最后:
安排测试用例测试顺序.
//测试套件
@Suite
@SelectClasses({ListTest.class, AddTest.class, EditTest.class, DetailTest.class,CenterTest.class,UnLoginTest.class,LoginTest.class,RegTest.class})
public class runSuite {
}
③.测试环境
- 操作系统版本:Windows 7 旗舰版
- 操作系统处理器:Intel® Core™ i7-4710MQ CPU @ 2.5GHz 2.5GHz
- 浏览器:Google Chrome浏览器
- 浏览器版本: 109.0.5414.120(正式版本) (64 位)
- 浏览器驱动版本:109.0.5414.74
- 自动化测试工具:selenium3.141.59
④.测试结果
自动化测试测试了119个用例,总共耗时2分34秒.
⑤.关于等待
最后,关于个人博客系统的自动化测试用例代码的编写.
里面频繁使用到的就是等待.
关于等待,有三种等待方式.
- 强制等待
- 隐式等待
- 显式等待
三种等待的代码写法如下:
@Test
void test() throws InterruptedException {
WebDriver webDriver = new ChromeDriver();
//强制等待,休眠
Thread.sleep(3000); //强制等待,强制等待3秒
///隐式等待
webDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); //等待页面10秒
//显式等待,等待的是某一条件
WebDriverWait wait = new WebDriverWait(webDriver,10); //等待时间10秒
wait.until(ExpectedConditions.alertIsPresent()); //显式等待,等待弹窗出现
}
隐式等待:隐式等待针对的是一个全局的设置,设置隐式等待后,脚本中的所有页面元素操作都遵从这个类似全局性质的等待标准.但有时候页面元素加载完毕,不一定元素就能被点击,此时就需要显示等待来处理了.
显示等待:显示等待针对的是需要操作的单个元素,对单个元素生效.显示等待中有多种等待机制,可以等待元素出现直到被点击才算真正的等待完毕.
当页面加载很慢时,就可以使用显示等待(等到需要操作的那个元素加载成功之后就直接操作这个元素),不需要等待其他元素的加载.
使用ExpectedConditions类中自带方法,可以进行显试等待的判断.
ExpectedConditions常用方法如下:
Method | Description |
---|---|
alertIsPresent() | 判断页面上是否存在alert |
titleContains(String title) | 判断当前页面的title是否包含预期字符串 |
textToBe(By locator, String value) | 判断某个元素中的text是否包含了预期的字符串 |
elementToBeClickable(By locator) | 判断某个元素中是否可见并且是enable的 |
elementToBeSelected(By locator) | 判断某个元素是否被选中了,一般用在下拉列表 |
visibilityOfElementLocated(By locator) | 判断某个元素是否可见(元素的宽和高都不等于0) |
invisibilityOfElementLocated(By locator) | 判断某个元素中是否不存在于dom树或不可见 |
not(ExpectedCondition<?> condition) | 给定条件的逻辑相反条件的期望 |
presenceOfElementLocated(By locator) | 判断某个元素是否被加到了dom树里,并不代表该元素一定可见 |
attributeToBe(By locator, String attribute, String value) | 判断某个元素的特定属性具有预期的字符串 |
numberOfElementsToBeMoreThan(By locator, Integer number) | 判断某个元素的数量多于预期值 |