某马瑞吉外卖单体架构项目完整开发文档,基于 Spring Boot 2.7.11 + JDK 11。预计 5 月 20 日前更新完成,有需要的胖友记得一键三连,关注主页 “瑞吉外卖” 专栏获取最新文章。
相关资料:https://pan.baidu.com/s/1rO1Vytcp67mcw-PDe_7uIg?pwd=x548
提取码:x548
文章目录
- 1.需求分析
- 2.代码开发
- 2.1 分析页面效果
- 2.2 分析执行过程
- 2.2.3 代码实现
- 3.功能测试
- 4.代码修复
1.需求分析
在员工管理列表页面,可以对某个员工账号进行启用或者禁用操作。账号禁用的员工不能登录系统,启用后的员工可以正常登录。需要注意的是,只有管理员(admin 用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用禁用按钮不显示。
下面是管理员登陆后的界面:
下面是普通员工登陆后的页面:
2.代码开发
2.1 分析页面效果
那么页面中是如何做到只有管理员(admin)才能够看到启用、禁用员工按钮的呢?我们可以追踪到 static/backend/page/member/list.html 中的如下位置:
可以看到,代码中会判断模型数据 user 的值是否为 admin,如果是则展示当前按钮,否则不展示。对应的 user 我们可以往上追踪到如下位置:
可以看到,前端会从 localStorage 获取当前登陆员工的 username,并赋值给模型数据 user。
2.2 分析执行过程
在代码开发之前,我们需要先梳理一下整个程序的执行过程:
- 客户端发送 ajax 请求,将参数(id,status)提交到服务端;
- 服务端 Controller 接收客户端提交的数据并调用 Service 更新数据;
- Service 调用 Mapper 操作数据库。
我们可以使用 admin 账户点击启用禁用按钮,然后查看浏览器调试工具对应的请求 URL 和携带的参数:
可以看出,当管理员点击禁用或启用按钮时,会发送一个 POST 请求,请求 URL 为 /employee,同时携带 json 数据为 id(员工 id)和 status(员工状态)。
2.2.3 代码实现
对应的 EmployeeController
中的处理方法如下:
@RestController
@RequestMapping("/employee")
public class EmployeeController {
/**
* 处理更新员工请求
* @param request 请求对象
* @param employee 员工对象
* @return 响应对象
*/
@PutMapping
public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
// 设置更新时间
employee.setUpdateTime(LocalDateTime.now());
// 获取当前登录的员工 id,作为更新人
Long empId = (Long) request.getSession().getAttribute("employee");
employee.setUpdateUser(empId);
// 更新员工信息
employeeService.updateById(employee);
// 返回更新成功结果
return R.success("更新员工成功");
}
}
实现思路比较简单,就是将客户端传送的 json 数据通过 @RequestBody
注解封装为 Employee 对象,在完成更新员工信息的同时也更新了对应的更新时间(update_time)和更新人(update_user)。
3.功能测试
下面我们直接进行功能测试,以 admin 账户登陆后点击【禁用】按钮:
可以看到,更新操作成功了,但是为什么状态仍旧是正常呢?我们不妨再查看数据库端的对应数据:
咦?这是为什么呢?我们接着来看看 IDEA 控制台的 SQL 日志:
执行结果是 Updates:0 ,也就意味着没有匹配到对应的记录,然而从 SQL 语句中不难看出,该语句是根据 id 进行匹配的。那我们重点关注传入的 id,值为 1658001441572294700,重点关注后三位 “700”。
然后我们再查看数据库中对应的 id:
发现端倪了吗?后三位居然与数据库的值不一样!我们再看看客户端请求中携带的 id 值:
可见当执行修改操作时客户端发送的 id 就是错的,我们在服务端通过该 id 值进行修改操作,自然是修改不成功的。那么是不是因为在进行分页操作的时候拿到的响应就是错的呢?我们对应找到分页完成客户端的响应数据:
很明显客户端返回的数据是没有问题的,该 id 与数据库中的一致。可是在我们点击【编辑】或【启用、禁用】按钮时获取到的 id 确实是错的,那么问题出来哪儿呢?
其实是因为服务端在进行分页查询时,响应给客户端的员工 id 是一个 long 类型的 19 位整数值,从上面的截图也能看出来了。但是问题就出现在,前端页面中是通过 js 去处理 long 类型的数值的,但是只能精确到 16 位,也就意味着后三位进行了四舍五入,因此最终通过 ajax 请求提交给服务端时 id 的后三位就由 “657”变成了 “700”,自然也就不可能更新成功了。
针对这个问题,为了避免 js 处理 long 类型数值时出现精度丢失,我们可以将原本响应给客户端的 long 类型的 id,将 long 型的数据进行处理转为 String 类型即可。
4.代码修复
具体实现步骤如下:
-
提供对象转换器 JacksonObjectMapper,基于 jackson 进行 Java 对象到 json 数据的转换;
资料位置:瑞吉外卖\瑞吉外卖项目\资料\对象映射器\JacksonObjectMapper.java
-
在 WebMvcConfig 配置类中扩展 Spring MVC 的消息转换器,在此消息转换器中使用提供的对象转换器进行 Java 对象到 jason 数据的转换。
我们直接将准备好的 JacksonObjectMapper.java 复制到项目的 common
包下即可,对应的完整代码如下:
package cn.javgo.reggie_take_out.common;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
/**
* 对象映射器:基于 jackson 将 Java 对象转为 json,或者将 json 转为 Java 对象
* 说明:
* 1.将 JSON 解析为 Java 对象的过程称为 [从 JSON 反序列化 Java 对象]
* 2.从 Java 对象生成 JSON 的过程称为 [序列化 Java 对象到 JSON]
*/
public class JacksonObjectMapper extends ObjectMapper {
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
public JacksonObjectMapper() {
// 调用父类的构造方法
super();
//收到未知属性时不报异常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
//反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
// 序列化时,日期的统一格式
SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
// 将 BigInteger、Long 类型的数据序列化为字符串类型
.addSerializer(BigInteger.class, ToStringSerializer.instance)
.addSerializer(Long.class, ToStringSerializer.instance)
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}
接下来新建 config
包用于存放各种配置类,然后新建一个 WebMvcConfig
类继承 WebMvcConfigurationSupport
类,并在中扩展 Spring MVC 的消息转换器即可,其实就是重写父类的 extendMessageConverters()
方法从而替换 Spring MVC 默认的消息转换器。
完整代码如下:
package cn.javgo.reggie_take_out.config;
import cn.javgo.reggie_take_out.common.JacksonObjectMapper;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import java.util.List;
/**
* @author: JavGo
* @description: TODO
* @date: 2023/5/16 12:49
*/
@SpringBootConfiguration
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 配置静态资源映射
* @param registry 静态资源注册器
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
// 配置静态资源映射(将所有的静态资源都映射到 classpath:/static/ 目录下)
registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
}
/**
* 扩展 Spring MVC 消息转换器
*
* @param converters 消息转换器列表
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// 创建 Jackson 消息转换器
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
// 设置具体的序列化和反序列化器(其实就是对象映射器)
messageConverter.setObjectMapper(new JacksonObjectMapper());
// 通过设置消息转换器的顺序,来改变 Spring MVC 消息转换器的优先级,从而实现自定义消息转换器的优先级高于默认的消息转换器
converters.add(0, messageConverter);
}
}
上述代码中笔者同时重写
addResourceHandlers()
方法进行了静态资源映射的处理,否则点击【编辑】按钮跳转静态资源页面时会显示 404。
在 Spring Boot 应用程序启动时会自动调用上述方法进行消息转换器的注册,我们可以在该方法上打上断点:
OK,现在以 Debug 方式重启应用再次进行测试,当执行完该方法后可以看到我们扩展的消息转换器被放到了第一个位置,具有最高优先级:
跳过调试,项目启动完成后再进行客户端功能测试,就能测试成功:
此时分页查询返回的 id 也加上了双引号作为字符串,并且与数据库端的 id 一致:
对应更新操作的客户端请求的 json 数据也获取到的正确的 id:
禁用后,我们将管理员账户退出登录,使用被禁用的 zhangsan 进行登录(默认密码为 12346)就会登陆失败: