10天手敲一个SpringBoot网上商城项目(三)——个人资料修改功能及上传头像功能的实现

静态资源及sql文件分享
链接:https://pan.baidu.com/s/1X-yjmQcPD3PqS21x0HplNA?pwd=23gr
提取码:23gr

个人资料

其中用户名是不可修改的并且是在进个人资料页面时就从session获取值并将值加入到控件中

1.个人资料-持久层

1.1规划SQL语句
  • 获取用户信息的SQL语句

    update t_user set python=?,email=?,gender=?,modified_user=?,modified_time=? where uid=?
    
  • 根据uid查询用户数据

    select * from t_user where uid=?
    

    根据uid查询用户数据不需要再重复开发

1.2设计接口和抽象方法

更新用户的信息方法的定义

    /**
     * 参数为user的方法
     * @param user 用户的数据
     * @return 返回值为受影响的行数
     */
    Integer updateInfoByUid(User user);//也可以用三个String的形参接收电话,邮箱,性别,但不如直接写个user省事
1.3编写映射

在UserMapper.xml文件中进行映射编写

    <update id="updateInfoByUid">
        update t_user
        set
            <!--if是条件判断标签,属性test接受的是一个返回值为boolean类型的条件,
            如果test条件的结果为true则执行if标签内部的语句,注意逗号也要在标签内-->
            <if test="phone!=null">phone = #{phone},</if>
            <if test="email!=null">email = #{email},</if>
            <if test="gender!=null">gender = #{gender},</if>
            modified_user = #{modifiedUser},
            modified_time = #{modifiedTime}
        where uid=#{uid}
    </update>
1.4单元测试
@Test
public void updateInfoByUid() {
    User user = new User();
    user.setUid(11);
    user.setPhone("13333688");
    user.setEmail("1454@qq.com");
    user.setGender(1);
    userMapper.updateInfoByUid(user);
}

2.个人资料-业务层

设计两个功能:

1.当打开页面时显示当前登录的用户的信息

2.点击修改按钮时更新用户的信息

2.1规划异常
  • 点击个人资料页面时可能找不到用户的数据
  • 点击修改按钮时可能找不到用户数据,也可能修改时出现未知错误
2.2设计接口和抽象方法及实现

1.业务层有两个功能模块,对应的是两个抽象方法的设计,并且这两个功能都涉及到用户是否存在的查询操作,所以需要在业务层设计根据用户uid查询数据的方法(持久层已经设计过该方法,但是没有在业务层实现该方法的调用)

    /**
     * 根据用户的uid查询用户数据
     * @param uid 用户uid
     * @return 用户数据
     */
    User getByUid(Integer uid);

    /**
     * uid通过控制层在session中获取然后传递给业务层,并在业务层封装到User对象中
     * */
    void changeInfo(Integer uid,User user);

2.在实现类中实现当前的抽象方法

    @Override
    public User getByUid(Integer uid) {
        //查询用户是否存在
        User result = userMapper.findByUid(uid);
        if (result == null || result.getIsDelete() == 1) {
            throw new UsernameNotFoundException("用户数据不存在");
        }

        //可以直接返回result给控制层,但是太臃肿了
        User user = new User();
        user.setUsername(result.getUsername());
        user.setPhone(result.getPhone());
        user.setEmail(result.getEmail());
        user.setGender(result.getGender());

        return user;
    }

    /**
     *User对象中的数据只有phone,email,gender,username,因为springboot进行依赖
     * 注入的时候只注入表单中数据的值,所以需要手动将uid封装到user中
     */
    @Override
    public void changeInfo(Integer uid, User user) {
        User result = userMapper.findByUid(uid);
        if (result == null || result.getIsDelete() == 1) {
            throw new UsernameNotFoundException("用户数据不存在");
        }
        user.setUid(uid);
        user.setModifiedUser(user.getUsername());
        user.setModifiedTime(new Date());

        Integer rows = userMapper.updateInfoByUid(user);
        if (rows!=1) {
            throw new UpdateException("更新数据时产生异常");
        }
    }
2.3单元测试
    @Test
    public void getByUid() {
        //err是为了让输出信息为红色
        System.err.println(userService.getByUid(11).getUsername());
    }

    @Test
    public void changeInfo() {
        User user = new User();
        //这四个属性值在真实开发中都是在控制层就已经自动封装在User对象中了
        //而uid需要由控制层传给业务层并在业务层封装到user中
        user.setPhone("123456789");
        user.setEmail("123@qq.com");
        user.setUsername("mxy");
        user.setGender(0);
        userService.changeInfo(11,user);
    }

3.个人资料-控制层

3.1处理异常

没有新的异常,所以这里不需要有操作

3.2设计请求

1.设计一打开页面就发送当前用户数据的查询

  • /users/get_by_uid
  • GET
  • HttpSession session(用于获取uid)
  • JsonResult<User>

2.点击修改按钮发送用户的数据修改操作

  • users/change_info
  • POST
  • User user,HttpSession session(用于获取uid)
  • JsonResult<Void>
3.3处理请求

1.一打开页面就发送当前用户数据

@RequestMapping("get_by_uid")
public JsonResult<User> getByUid(HttpSession session) {
    User data = userService.getByUid(getUidFromSession(session));
    return new JsonResult<User>(OK,data);
}

启动服务,先登录账号然后在地址栏输入http://localhost:8080/users/get_by_uid看看状态码是否为200并且看data值是否不为null

2.点击修改按钮更改用户数据

@RequestMapping("change_info")
public JsonResult<Void> changeInfo(User user,HttpSession session) {
    //user对象中有四部分的数据:username,phone,email,gender
    //控制层给业务层传递uid,并在业务层通过user.setUid(uid);将uid封装到user中
    Integer uid = getUidFromSession(session);
    userService.changeInfo(uid,user);
    return new JsonResult<>(OK);
}

启动服务,先登录账号然后在地址栏输入http://localhost:8080/users/change_info?phone=175726&email=6695@qq.com&username=张9&gender=1观察状态码是否为200

4.个人资料-前端页面

1.在打开userdata.html(个人资料)页面自动发送ajax请求(get_by_uid),查询到的数据填充到这个页面

        <script>
            /**
             * 一旦检测到当前的页面被加载就会触发ready方法
             * $(document).ready(function(){
             *     //编写业务代码
             * });
             */
            //点击"个人资料"四个字加载userdata.html页面时$(document).ready(function(){});就会起作用发送ajax请求
            $(document).ready(function() {
                $.ajax({
                    url: "/users/get_by_uid",
                    type: "GET",
                    //data为null也可以,因为这里get是从数据库拉取数据,不需要data
                    data: $("#form-change-info").serialize(),
                    dataType: "JSON",
                    success: function (json) {
                        if (json.state == 200) {
                            //将查询到的数据设置到控件中
                            $("#username").val(json.data.username);
                            $("#phone").val(json.data.phone);
                            $("#email").val(json.data.email);
                            var radio = json.data.gender == 0 ?
                                $("#gender-female") : $("#gender-male");
                            //prop()表示给某个元素添加属性及属性的值
                            radio.prop("checked","checked");
                        } else {
                            alert("用户的数据不存在")
                        }
                    },
                    error: function (xhr) {
                        //xhr.message可以获取未知异常的信息
                        alert("查询用户信息时产生未知的异常!"+xhr.message);
                    }
                });
            });
        </script>

2.在检测到用户点击了修改按钮后发送一个ajax请求(change_info)

该ajax函数需要和上一个ajax同级

            $("#btn-change-info").click(function () {
                $.ajax({
                    url: "/users/change_info",
                    type: "POST",
                    data: $("#form-change-info").serialize(),
                    dataType: "JSON",
                    success: function (json) {
                        if (json.state == 200) {
                            alert("用户信息修改成功")
                            //修改成功后重新加载当前的页面
                            location.href = "userdata.html";
                        } else {
                            alert("用户信息修改失败")
                        }
                    },
                    error: function (xhr) {
                        //xhr.message可以获取未知异常的信息
                        alert("用户信息修改时产生未知的异常!"+xhr.message);
                    }
                });
            });

上传头像

错误方法:把文件存到数据库中,需要图片时访问数据库,数据库将文件解析为字节流返回,最后写到本地的某一个文件.这种方法太耗费资源和时间了

正确方法:将对应的文件保存在操作系统上,然后再把这个文件路径记录下来,因为在记录路径的时候是非常便捷和方便的,将来如果要打开这个文件可以依据这个路径找到这个文件,所以说在数据库中保存该文件的路径即可.

稍微大一点的公司都会将所有的静态资源(图片,文件,其他资源文件)放到某台电脑上,再把这台电脑作为一台单独的服务器使用

1.上传头像-持久层

1.1SQL语句的规划

更新用户avatar字段的sql语句

update t_user set avatar=?,modified_user=?,modified_time=? where uid=?
1.2设计接口和抽象方法

在UserMapper接口中定义一个抽象方法用于修改用户的头像

    /**
     * 根据用户uid修改用户的头像
     * @param iddddd
     * @param avatar
     * @param modifiedUser
     * @param modifiedTime
     * @return
     */
    /**
     * 注解@Param("SQL映射文件中#{}占位符的变量名"),解决的问题:
     * 当SQL语句的占位符和映射的接口方法参数名不一致时,需要将某个参数强行注入到某个
     * 占位符变量上时,可以使用@Param这个注解来标注映射的关系
     * */
    Integer updateAvatarByUid(@Param("uid") Integer iddddd,//@Param("参数名")注解中的参数名需要和sql语句中
                              //的#{参数名}的参数名保持一致.该处表示iddddd中的变量值要注入到sql语句的uid中
                              String avatar,
                              String modifiedUser,
                              Date modifiedTime);
1.3编写映射

UserMapper.xml文件中编写映射的SQL语句

    <update id="updateAvatarByUid">
        update t_user
        set
            avatar = #{avatar},
            modified_user = #{modifiedUser},
            modified_time = #{modifiedTime}
        where
            uid = #{uid}
    </update>
1.4单元测试
@Test
public void updateAvatarByUid() {
    userMapper.updateAvatarByUid(
        11,
        "abc",
        "mxy",
        new Date());
}

2.上传头像-业务层

2.1规划异常
  • 用户数据不存在,找不到对应的用户数据
  • 更新的时候,出现未知异常

无需重复开发

2.2设计接口和抽象方法及实现

1.先分析一下业务层接口需要哪些参数:那就需要看持久层接口要的有什么参数:

uid,avatar,modifiedUser,modifiedTime,其中modifiedTime是在方法中创建的,uid和modifiedUser从session中获取,但是session对象是在控制层的并不会出现在业务层,所以业务层要保留这两个参数,以便控制层可以传递过来

    /**
     * 修改用户的头像
     * @param uid 用户uid
     * @param avatar 用户头像的路径
     * @param username 用户名称
     */
    void changeAvatar(Integer uid,
                      String avatar,
                      String username);//业务层一般叫username而不叫modifiedUser,因
                                        // 为业务层并没有直接和数据库关联

2.编写业务层的更新用户头像的方法

@Override
public void changeAvatar(Integer uid, String avatar, String username) {
    //查询当前的用户数据是否存在
    User result = userMapper.findByUid(uid);
    if (result == null || result.getIsDelete() == 1) {
        throw new UsernameNotFoundException("用户数据不存在");
    }
    Integer rows = userMapper.updateAvatarByUid(uid, avatar, username, new Date());
    if (rows!=1) {
        throw new UpdateException("更新用户头像时产生未知异常");
    }
}
2.3单元测试
@Test
public void changeAvatar() {
    userService.changeAvatar(11,"222","mmm");
}

3.上传头像-控制层

文件上传过程中产生的异常太多了,再比如文件类型不匹配或文件被损坏

3.1规划异常

客户端传递文件给服务器,服务器的控制端controller接收文件,接收时可能抛出异常,因为用户传过来的文件有可能超出了我们的大小限制

该异常能放在业务层抛出吗?没必要的,因为此时数据是从控制层往下传的,所以控制层产生的异常直接在这一层(控制层)抛就可以了

上传文件时的异常都是文件异常,所以可以先创建一个文件异常类的基类FileUploadException并使其继承RuntimeException

文件异常基类的子类有:

  • FileEmptyException:文件为空的异常(没有选择上传的文件就提交了表单,或选择的文件是0字节的空文件)
  • FileSizeException:文件大小超出限制
  • FileTypeException:文件类型异常(上传的文件类型超出了限制)
  • FileUploadIOException:文件读写异常
  • FileStateException:文件状态异常(上穿文件时该文件正在打开状态)

在controller包下创子包ex,在ex包里面创建文件异常类的基类和上述五个文件异常类,创建的六个类都重写其父类的五个构造方法

3.2处理异常

在基类BaseController中进行编写和统一处理

else if (e instanceof FileEmptyException) {
    result.setState(6000);
} else if (e instanceof FileSizeException) {
    result.setState(6001);
} else if (e instanceof FileTypeException) {
    result.setState(6002);
} else if (e instanceof FileStateException) {
    result.setState(6003);
} else if (e instanceof FileUploadIOException) {
    result.setState(6004);
}

异常统一处理方法的修饰符@ExceptionHandler(ServiceException.class)表明我们现在创建的FileUploadException异常类不会被拦截到该方法中,点进@ExceptionHandler注解可以发现传参可以传数组类型,所以可以将异常统一处理方法上的注解改为:

@ExceptionHandler({ServiceException.class,FileUploadException.class})

3.3设计请求
  • /users/change_avatar
  • POST(GET请求提交数据只有2KB左右)
  • HttpSession session(获取uid和username),MultipartFile file
  • JsonResult<String>(不能是JsonResult<Void>:如果上传头像后浏览别的页面,然后再回到上传头像的页面就展示不出来了,所以图片一旦上传成功,就要保存该图片在服务器的哪个位置,这样的话一旦检测到进入上传头像的页面就可以通过保存的路径拿到图片,最后展示在页面上)
3.4处理请求
    @RequestMapping("change_avatar")
    public JsonResult<String> changeAvatar(HttpSession session,
                                           MultipartFile file) {
        /**
         * 1.参数名为什么必须用file:在upload.html页面的147行<input type=
         * "file" name="file">中的name="file",所以必须有一个方法的参数名
         * 为file用于接收前端传递的该文件.如果想要参数名和前端的name不一
         * 样:@RequestParam("file")MultipartFile ffff:把表单中name=
         * "file"的控件值传递到变量ffff上
         * 2.参数类型为什么必须是MultipartFile:这是springmvc中封装的一个
         * 包装接口,如果类型是MultipartFile并且参数名和前端上传文件的name
         * 相同,则会自动把整体的数据包传递给file
         */
        //判断文件是否为null
        if (file.isEmpty()) {
            throw new FileEmptyException("文件为空");
        }
        if (file.getSize()>AVATAR_MAX_SIZE) {
            throw new FileSizeException("文件超出限制");
        }
        //判断文件的类型是否是我们规定的后缀类型
        String contentType = file.getContentType();
        //如果集合包含某个元素则返回值为true
        if (!AVATAR_TYPE.contains(contentType)) {
            throw new FileTypeException("文件类型不支持");
        }

        //上传的文件路径:.../upload/文件名.png
        /**
         * session.getServletContext()获取当前Web应用程序的上下文
         * 对象(每次启动tomcat都会创建一个新的上下文对象)
         * getRealPath("/upload")的/代表当前web应用程序的根目录,通过该相
         * 对路径获取绝对路径,返回一个路径字符串,如果不能进行映射返回null,单
         * 斜杠可要可不要
         */
        String parent =
                session.getServletContext().getRealPath("/upload");
        System.out.println(parent);//调试用

        //File对象指向这个路径,通过判断File是否存在得到该路径是否存在
        File dir = new File(parent);
        if (!dir.exists()) {//检测目录是否存在
            dir.mkdirs();//创建当前目录
        }

        //获取这个文件名称(文件名+后缀,如avatar01.png,不包含父目录结构)用UUID
        // 工具生成一个新的字符串作为文件名(好处:避免了因文件名重复发生的覆盖)
        String originalFilename = file.getOriginalFilename();
        System.out.println("OriginalFilename="+originalFilename);
        int index = originalFilename.lastIndexOf(".");
        String suffix = originalFilename.substring(index);
        //filename形如SAFS1-56JHIOHI-HIUGHUI-5565TYRF.png
        String filename =
                UUID.randomUUID().toString().toUpperCase()+suffix;

        //在dir目录下创建filename文件(此时是空文件)
        File dest = new File(dir, filename);

        //java可以把一个文件的数据直接写到同类型的文件中,这里将参数file中的数据写入到空文件dest中
        try {
            file.transferTo(dest);//transferTo是一个封装的方法,用来将file文件中的数据写入到dest文件

            /**
             * 先捕获FileStateException再捕获IOException是
             * 因为后者包含前者,如果先捕获IOException那么
             * FileStateException就永远不可能会被捕获
             */
        } catch (FileStateException e) {
            throw new FileStateException("文件状态异常");
        } catch (IOException e) {
            //这里不用打印e,而是用自己写的FileUploadIOException类并
            // 抛出文件读写异常
            throw new FileUploadIOException("文件读写异常");
        }

        Integer uid = getUidFromSession(session);
        String username = getUsernameFromSession(session);
        String avatar = "/upload/"+filename;
        userService.changeAvatar(uid,avatar,username);
        //返回用户头像的路径给前端页面,将来用于头像展示使用
        return new JsonResult<>(OK,avatar);
    }

4.上传头像-前端页面

1.在upload.html的上传头像的表单加上三个属性:

  • action=“/users/change_avatar”
  • method=“post”(get请求提交数据只有2KB左右)
  • enctype=“multipart/form-data”(如果直接使用表单进行文件的上传,需要给表单加该属性,这样不会将目标文件的数据结构做修改后再上传,这不同于字符串,字符串随意切割修改也能拼在一起,但文件不行)

2.确认<input type=“file” name=“file”>的type和name以及<input type=“submit” class=“btn btn-primary” value=“上传” />中的type

5.前端页面优化——修复bug

5.1更改默认的大小限制

springmvc默认为1MB文件可以进行上传,如果刚好是1024*1024=1048576 bytes则会报代码错误,自己在控制层设置的public static final int AVATAR_MAX_SIZE = 10*1024*1024;需要在不超过原有大小的情况下才会起作用,所以要手动修改springmvc默认上传文件的大小

方式1:直接在配置文件application.properties中进行配置:

  • spring.servlet.multipart.max-file-size=10MB(表示上传的文件最大是多大)
  • spring.servlet.multipart.max-request-size=15MB(整个文件是放在了request中发送给服务器的,请求当中还会有消息头等其他携带的信息,这里设置请求最大为15MB)

方式2:采用java代码的形式来设置文件的上传大小的限制:

1.该代码必须在主类中进行配置,因为主类是最早加载的,而配置文件必须是最早加载的

2.在主类中定义一个方法,方法名无所谓,但方法需要用@bean修饰,表示该方法返回值是一个bean对象,并且该bean对象被bean修饰,也就是这个方法返回了一个对象,然后把该对象交给bean管理,类似spring中的bean标签,含义是一样的,只是这里改为了注解

3.用@Configuration修饰主类使@bean注解生效,但其实@SpringBootApplication是@SpringBootConfiguration,@EnableAutoConfiguration,@ComponentScan三个注解的合并,所以可以不需要@Configuration

4.方法返回值是MultipartConfigElement类型,表示所要配置的目标的元素

@Bean
public MultipartConfigElement getMultipartConfigElement() {
    //1.创建一个配置的工厂类对象
    MultipartConfigFactory factory = new MultipartConfigFactory();

    //2.设置需要创建的对象的相关信息
    factory.setMaxFileSize(DataSize.of(10, DataUnit.MEGABYTES));
    factory.setMaxRequestSize(DataSize.of(15,DataUnit.MEGABYTES));

    //3.通过工厂类创建MultipartConfigElement对象
    return factory.createMultipartConfig();
}
5.2上传后显示头像

上传头像成功后不能显示头像.

在页面中通过ajax请求来提交文件,提交完成后返回了json串,解析出json串中的data数据设置到img标签的src属性上

1.删掉在upload.html的上传头像的表单中加的三个属性:action=“/users/change_avatar”,method=“post”,enctype=“multipart/form-data”.加上id属性:id=“form-change-avatar”

2.把153行的input标签里面的type="submit"改为type=“button”(因为submit按钮不能添加事件,所以要改为普通的按钮)并加上属性id=“btn-change-avatar”

1.serialize():可以将表单数据自动拼接成key=value的结构提交给服务器,一般提交的是普通的控件类型中的数据(type=text/password/radio/checkbox等等)

2.FormData类:将表单中数据保持原有的结构进行数据提交.文件类型的数据可以使用FormData对象进行存储

使用方法:new FormData($(“form”)[0]);

这行代码的含义是将id="form"的表单的第一个元素的整体值作为创建FormData对象的数据

3.虽然我们把文件的数据保护下来了,但是ajax默认处理数据时按照字符串的形式进行处理,以及默认会采用字符串的形式进行数据提交.手动关闭这两个功能:

  • processData: false,//处理数据的形式,关闭处理数据
  • contentType: false,//提交数据的形式,关闭默认提交数据的形式

下面给提交表单加上事件:

        <script>
            $("#btn-change-avatar").click(function () {
                $.ajax({
                    url: "/users/change_avatar",
                    type: "POST",
                    data: new FormData($("#form-change-avatar")[0]),
                    processData: false,//处理数据的形式,关闭处理数据
                    contentType: false,//提交数据的形式,关闭默认提交数据的形式
                    dataType: "JSON",
                    success: function (json) {
                        if (json.state == 200) {
                            alert("头像修改成功")
                            //将服务器端返回的头像地址设置到img标签的src属性上
                            //attr(属性,属性值)用来给某个属性设值
                            $("#img-avatar").attr("src",json.data);
                        } else {
                            alert("头像修改失败")
                        }
                    },
                    error: function (xhr) {
                        alert("修改头像时产生未知的异常!"+xhr.message);
                    }
                });
            });
        </script>
5.3登录后显示头像

将头像上传后会显示头像,但是关闭浏览器后再进入个人头像页面就不会显示头像了,因为只有点击"上传"才能发送ajax请求并显示头像.

可以在每次用户登录成功后将avatar保存在cookie中,登录的业务层返回给控制层user对象,该对象包含uid,username,avatar.所以要在登录页面login.html中将服务器返回的头像路径设置到cookie中,然后每次检测到用户打开上传头像页面,在这个页面中通过ready()方法来自动读取cookie中头像路径并设到src属性上

1.需要在login.html页面头部导入cookie.js文件

<script src="../bootstrap3/js/jquery.cookie.js" type="text/javascript" charset="utf-8"></script>

2.调用cookie方法保存路径

$.cookie(key,value,time);//time单位:天

在ajax请求原有的代码上加$.cookie(“avatar”,json.data.avatar,{expires: 7});

success: function (json) {
    if (json.state == 200) {
        location.href = "index.html";
        $.cookie("avatar",json.data.avatar,{expires: 7});
    } else {
        alert("登录失败")
    }
},

3.需要在upload.html获取cookie中的值,所以要在页面头部导入cookie.js文件

<script src="../bootstrap3/js/jquery.cookie.js" type="text/javascript" charset="utf-8"></script>

4.在upload.html的script标签中加ready()自动读取cookie数据

$(document).ready(function(){
    var avatar = $.cookie("avatar");
    console.log(avatar);//调试用
    $("#img-avatar").attr("src",avatar);
})
5.4显示最新头像

上传头像后不重新登录而是浏览其他页面,然后再进入个人头像页面时展示的头像是上次上传的,因为此时cookie中的值是上次上传的头像的路径,所以需要上传头像后使用同名覆盖更改cookie中路径

在ajax函数的success属性值的if语句加:

$.cookie("avatar",json.data,{expires: 7});

完善后重启服务测试,结果若和预测的不一样,则参考项目环境搭建->项目测试->测试静态资源能否正常加载里面的四种解决方法

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

认真生活的灰太狼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值