Fms-Java版开发实录:10 写的不怎么满意的用户资料页

Fms-Java版开发实录:10 写的不怎么满意的用户资料页

这次编写用户自己修改密码和电子邮件的功能

/account下边的路径现在还剩下profile和重置密码的没有写。我打算把展示用户信息和修改用户信息做在一个页面里。参考了这个页面的结构。

基础设计

由于上一次重构了模板继承关系,在/templates/account下边创建profile.html,直接采用基础模板如下:

<!doctype html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org"
      th:replace="~{/shared/standard::layout(~{::title}, ~{::style}, ~{::main}, ~{::script})}">
<head>
    <title></title>
    <style></style>
</head>

<body>
<main>

</main>
<script src=""></script>
</body>
</html>

模板样式

这个模板我采用了刚才页面的右侧部分来展示用户信息,将其排在页面的中央。然后把显示金额的地方换成了两个带图标的按钮,用来显示和隐藏两个表单,一个用来修改密码,一个用来修改邮箱。

之后使用了Collapse组件来达到点击两个按钮的时候显示和隐藏表单的效果。

页面的效果如下:
fms-profile1

点击按钮展开表单的效果如下:
fms-profile2

URL设计

我这里设计了一批URL如下:

  1. /account/profile,用来展示用户资料
  2. /account/profile/password,GET请求跳转到/account/profile,POST请求用来修改密码
  3. /account/profile/email,GET请求跳转到/account/profile,POST请求用来修改电子邮件

这里感觉设计的比较丑陋,不过能够达成效果就行。

控制器与表单

这里我们打算复用UserRegistraion这个VO,不打算编写新的与用户交互的数据对象,因此要在控制器和表单上做些手脚。

/account/profile控制器

这个地址用于返回用户页,由于必须登录才能够访问这个地址,因此控制器的核心功能就是取出用户的数据,然后渲染到页面上,如下:

@GetMapping("/profile")
public String profile(HttpServletRequest request, Model model) {
    UserData userData = userService.getUserDataByUsername(request.getUserPrincipal().getName());
    UserRegistration registration = new UserRegistration(userData.getUsername(), userData.getEmail(), "", "");
    model.addAttribute("userData", registration);
    return "/account/profile";
}

核心逻辑就是从请求中取出已经验证的用户名称,然后从数据库中加载对应的UserData,之后组装成UserRegistraion对象,很显然,这次我们要复用这个VO,不然还要为传入的部分字段再编写VO,没有必要。

这里如果仔细看一下我们的VOBO的关系就知道,二者之间没有任何联系,纯粹组装数据。不像BO内部其实包含着对PO的引用,因为二者的关系太密切。

将用户信息渲染到页面上很简单,然而针对两个表单,必须将一些信息以hiddeninput框预先埋进去。

两个表单的设计

对于修改电子邮件的表单,设置成不需要输入密码,因此我们只需要一个input框即可,埋入了一个用户名的input,和两个没有用的password1password2,以对应VO的四个字段,表单如下:

<form action="/account/profile/email" th:action="@{/account/profile/email}" method="post"
      th:object="${userData}">
    <input type="hidden" th:value="*{username}" th:field="*{username}">
    <input type="hidden" id="password1" name="password1" value="1">
    <input type="hidden" id="password2" name="password2" value="1">
    <div class="form-floating mb-3 justify-content-center">
        <input type="email" class="form-control" id="email" name="email" placeholder="电子邮件" required>
        <label>电子邮件</label>
        <div th:if="${#fields.hasErrors('email')}" class="alert alert-danger" role="alert"
             th:errors="*{email}"></div>
    </div>
    <div class="text-center mt-3 mb-2">
        <button class="btn btn-primary">提交</button>
    </div>
</form>

打算让用户看到表单中的的电子邮件框是空白,所以没有用th:field绑定,而是手工设置上了idname等属性。

对于修改密码的表单,我们的设计是输入原密码和新密码,这样两个密码正好能够复用VO,由于VO需要四个字段,因此埋了两个input

<form action="/account/profile/password" th:action="@{/account/profile/password}" method="post"
      th:object="${userData}">
    <input type="hidden" th:value="*{email}" th:field="*{email}">
    <input type="hidden" th:value="*{username}" th:field="*{username}">
    <div class="form-floating mb-3 justify-content-center">
        <input type="password" class="form-control" th:field="*{password1}" placeholder="原密码" required>
        <label>原密码</label>
        <div th:if="${#fields.hasErrors('password1')}" class="alert alert-danger" role="alert"
             th:errors="*{password1}"></div>
    </div>
    <div class="form-floating">
        <input type="password" class="form-control" th:field="*{password2}" placeholder="新密码" required>
        <label>新密码</label>
        <div th:if="${#fields.hasErrors('password2')}" class="alert alert-danger" role="alert"
             th:errors="*{password2}"></div>
    </div>
    <div class="text-center mt-3 mb-2">
        <button class="btn btn-primary">提交</button>
    </div>
</form>

上边两个表单其实埋入用户名和电子邮件有些多余,因为我们在逻辑中依旧是要先获取用户数据,不过留着也好,以后也许会加上一些验证。

修改电子邮件地址的控制器

这个控制器对应/account/profile/email的POST请求,其核心逻辑是:

  1. 通过request获取BO
  2. 判断邮件地址是否通过验证,失败的话向视图传入VO和错误信息,返回/account/profile视图
  3. 邮件地址通过验证,则将新的电子邮件地址设置到BO
  4. 保存BO,由于设置了UserInfoUsercascade = CascadeType.ALL,所以保存User的时候也会一并保存UserInfo中更新的数据
  5. 返回成功信息,渲染/account/profile视图

这个控制器的逻辑比较简单,如下:

@PostMapping("/profile/email")
public String changeEmail(@ModelAttribute("userData") @Valid UserRegistration userRegistration, BindingResult rs, Model model, HttpServletRequest request) {
    UserData userData = userService.getUserDataByUsername(request.getUserPrincipal().getName());
    if (rs.hasErrors()) {
        model.addAttribute("emailError", "电子邮件格式不正确");
        model.addAttribute("userData", new UserRegistration(userData.getUsername(), userData.getEmail(), "", ""));
        return "/account/profile";
    }
    userData.setEmail(userRegistration.getEmail());
    userService.saveUserData(userData);
    model.addAttribute("emailSuccess", "成功修改电子邮件地址");
    model.addAttribute("userData", new UserRegistration(userData.getUsername(), userData.getEmail(), "", ""));
    return "/account/profile";
}

修改密码的控制器

这个控制器对应/account/profile/password的POST请求,逻辑要比修改邮件复杂一些,如下:

  1. 同样是先通过request获取BO
  2. 判断密码是否通过验证,失败的话向视图传入VO和错误信息,返回/account/profile视图
  3. 验证原密码是否正确,如果失败,设置错误信息,返回/account/profile视图
  4. 验证原密码成功,给UserData设置上passwordEncoder加密后的密码,然后保存BO
  5. 非常关键的一步,让session失效,以让用户重新登录
  6. 返回提示用户重新登录的页面(此时session已经失效)

为此,在/templates/account/下边创建一个passwordChanged.html,其内容很简单,就一段提示:

<main class="form-signin">
    <form action="/account/signout" th:action="@{/account/signout}" method="post">
        <h3 class="h3 mb-3 fw-normal">您已修改密码,请重新登录</h3>
        <a class="w-100 btn btn-lg btn-primary" th:href="@{/account/login}">登录</a>
    </form>
</main>

然后来编写控制器:

@PostMapping("/profile/password")
public String changePassword(@ModelAttribute("userData") @Valid UserRegistration userRegistration, BindingResult rs, Model model, HttpServletRequest request) {
    UserData userData = userService.getUserDataByUsername(request.getUserPrincipal().getName());
    if (rs.hasErrors()) {
        model.addAttribute("passwordError", "请检查输入的密码");
        model.addAttribute("userData", new UserRegistration(userData.getUsername(), userData.getEmail(), "", ""));
        return "/account/profile";
    }
    //判断原密码是否正确,如果正确,给UserData设置上新密码,然后保存进数据库,之后要刷新session强制登录
    if (passwordEncoder.matches(userRegistration.getPassword1(), userData.getPassword())) {
        userData.setPassword(passwordEncoder.encode(userRegistration.getPassword2()));
        userService.saveUserData(userData);
        //取消会话
        request.getSession().invalidate();
        return "/account/passwordChanged";
    } else {
        model.addAttribute("passwordError", "原密码验证失败");
        return "/account/profile";
    }
}

两个控制器要注意的地方,就是无论出错或者成功完成修改,都要弄一个只包含用户名和密码的VO传递给视图,以保证页面其他地方不出错。

这样就写好了用户修改资料的功能,不过我觉得我自己这里页面和URL设计的有些麻烦,尤其URL应该不用这么复杂,应该用一个页面让用户全部修改完毕,不过也算是折腾了一下各种用途。

接下来要编写用户重置密码的功能,这个我要仔细来考虑一下如何编写。

LICENSED UNDER CC BY-NC-SA 4.0
Comment