【SSM】个人博客系统的功能实现和自动化测试

好记性不如烂笔头,记录实现个人博客系统的学习过程
如有雷同,纯属巧合~~

个人博客系统实现目录

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()); //显式等待,等待弹窗出现
        
    }

隐式等待:隐式等待针对的是一个全局的设置,设置隐式等待后,脚本中的所有页面元素操作都遵从这个类似全局性质的等待标准.但有时候页面元素加载完毕,不一定元素就能被点击,此时就需要显示等待来处理了.

显示等待:显示等待针对的是需要操作的单个元素,对单个元素生效.显示等待中有多种等待机制,可以等待元素出现直到被点击才算真正的等待完毕.

当页面加载很慢时,就可以使用显示等待(等到需要操作的那个元素加载成功之后就直接操作这个元素),不需要等待其他元素的加载.

selenium提供的显式等待

使用ExpectedConditions类中自带方法,可以进行显试等待的判断.
ExpectedConditions常用方法如下:

MethodDescription
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)判断某个元素的数量多于预期值
  • 18
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值