【苍穹外卖 | 篇②】功能实现(一)

在牛某网看见了牛肉哥的帖子之后,打算向牛肉大佬学习,故开始书写优快云博客,通过博客的方式来巩固自身知识学习。

因为之前有粗略的学习了Java Web的基础课程,所以博客内容主要是巩固之前学习当中的模糊点,以及一些自己认为重要的内容,用于自己进一步的掌握开发技能。

目录

课程内容

新增员工:

注意点:

注意点:

问题提出与解决:

用户已存在,未发出异常:

创建人的设置怎样变灵活:

ThreadLocal

员工分页查询:

注意点:

注意点:

注意点:

问题提出与解决:

时间的显示处理:

启用禁用员工账号:

注意点:

注意点:

注意点:

编辑员工:

导入分类模块功能代码:

根据类型查询:

注意点

课程内容

  • 新增员工

  • 员工分页查询

  • 启用禁用员工账号

  • 编辑员工

  • 导入分类模块功能代码

新增员工:

@PostMapping
    @ApiOperation("新增员工")
    public Result<String> save(@RequestBody EmployeeDTO employeeDTO) {
        log.info("新增员工:{}", employeeDTO);
        employeeService.save(employeeDTO);
        return Result.success();
    }

注意点:

  • 浏览器的请求数据如果是JSON格式的,要使用@RequestBody
  • DTO是数据传输对象 ,通常用于Controller层和浏览器,所以参数利用DTO型
    @Override
    public void save(EmployeeDTO employeeDTO) {
        Employee employee = new Employee();
        BeanUtils.copyProperties(employeeDTO, employee);
        employee.setStatus(StatusConstant.ENABLE);
        employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
        employee.setCreateTime(LocalDateTime.now());
        employee.setUpdateTime(LocalDateTime.now());
        employee.setCreateUser(10L);
        employee.setUpdateUser(10L);
        employeeMapper.insert(employee);
    }

注意点:

  • 传入传输是DTO型,为什么还要新建Employee呢:因为DTO是传输对象,更重要的是传输,尽量不做修改,同时EmployeeDTO的信息不完整,用Employee来set更多的信息
  • employee.setCreateUser(10L):暂时写死,后续解释

问题提出与解决:

用户已存在,未发出异常:

若不进行异常处理,当新增员工时就出现报错,如图所示,

所以在GlobalExceptionHandler.java添加方法

   @ExceptionHandler
    public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
        //Duplicate entry 'zhangsan' for key 'employee.idx_username'
        String message = ex.getMessage();
        if(message.contains("Duplicate entry")){
            String[] split = message.split(" ");
            String username = split[2];
            String msg = username + MessageConstant.ALREADY_EXISTS;
            return Result.error(msg);
        }else{
            return Result.error(MessageConstant.UNKNOWN_ERROR);
        }
    }
  • 定义SQLIntegrityConstraintViolationEXception:见报错中的提示
  • 先判断是否是因为重复了,即if的判断条件
  • 之后根据空格来分成数组元素,取出存放重复用户名的部分,进行拼装形成新的报错提示信息给用户

创建人的设置怎样变灵活:

在新增员工时,我们set创建者等是直接赋值写死的,怎么去灵活设置呢?

ThreadLocal

ThreadLocal 并不是一个Thread,而是Thread的局部变量。 ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。

常用方法:

  • public void set(T value) 设置当前线程的线程局部变量的值

  • public T get() 返回当前线程所对应的线程局部变量的值

  • public void remove() 移除当前线程的线程局部变量

客户端(浏览器):发出的每一次请求都是一个线程

所以从登录到新增一个员工的操作流程中都属于是一个线程

而只需要在登录时(令牌校验)将用户名放入线程局部变量中,在新增用户设置创建者的时候将其从线程局部变量中取出来就可以了
BaseContext包括了这些方法。

public class BaseContext {

    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id) {
        threadLocal.set(id);
    }

    public static Long getCurrentId() {
        return threadLocal.get();
    }

    public static void removeCurrentId() {
        threadLocal.remove();
    }

}

只需要做以下处理即可:

 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        
		//.............................
       
        //2、校验令牌
        try {
            //.................
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
            log.info("当前员工id:", empId);
            /////将用户id存储到ThreadLocal////////
            BaseContext.setCurrentId(empId);
            ////////////////////////////////////
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //......................
        }
    }
    public void save(EmployeeDTO employeeDTO) {
        //.............................

        //设置当前记录创建人id和修改人id
        employee.setCreateUser(BaseContext.getCurrentId());//目前写个假数据,后期修改
        employee.setUpdateUser(BaseContext.getCurrentId());

        employeeMapper.insert(employee);
    }

员工分页查询:

	/**
     * 员工分页查询
     * @param employeePageQueryDTO
     * @return
     */
    @GetMapping("/page")
    @ApiOperation("员工分页查询")
    public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO){
        log.info("员工分页查询,参数为:{}", employeePageQueryDTO);
        PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);//后续定义
        return Result.success(pageResult);
    }

注意点:

  • 为什么不用@RequestBody呢,因为此时是GetMapping,与GET请求的数据传输方式不一致,且请求数据格式不是Json格式
    public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
        // select * from employee limit 0,10
        //开始分页查询
        PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());

        Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);//后续定义

        long total = page.getTotal();
        List<Employee> records = page.getResult();

        return new PageResult(total, records);
    }
  • 注意点:

  • PageHelper是Mybatis提供的一个分页插件,它可以帮助我们在查询数据库时自动添加分页语句。
  • PageHelper 使用了AOP的思想。当你调用startPage方法后,它会在当前线程中设置一个分页参数。紧接着,当你执行下一次 MyBatis 查询(即调用 Mapper 接口的方法)时,PageHelper 会拦截这个查询,自动在你的 SQL 语句后面添加 LIMIT 和 OFFSET 子句,从而实现物理分页。
  • Page<Employee>: 这里的 Page 是 PageHelper 提供的一个类(通常是com.github.pagehelper.Page),表示一个列表,这个列表里的每一个元素都是一个 Employee 对象。
  • getTotal: 表示查询结果的总条数。
  • getResult(): 这是 Page 类提供的一个方法,用于获取分页查询的结果列表。
  •  虽然Page<Employee>和getResult()都返回一个列表,但是Page<Employee>更像是一个信息容器,他不只单纯是数据列表,而getResult()返回的是一个单纯的数据列表
<select id="pageQuery" resultType="com.sky.entity.Employee">
        select * from employee
        <where>
            <if test="name != null and name != ''">
                and name like concat('%',#{name},'%')
            </if>
        </where>
        order by create_time desc
    </select>

注意点:

  • 注意:此处的test=“name”是传入参数里的name,即在浏览器输入的内容
  • 模糊查找:concat('%',#{name},'%')在传入的参数 name 的前后都加上 % 通配符,从而实现对 name 的前后模糊匹配。

问题提出与解决:

时间的显示处理:

若不进行处理,时间只会显示为202511201518(LocalDateTime),而我们需要有格式的时间展示

在WebMvcConfiguration中扩展SpringMVC的消息转换器,统一对日期类型进行格式处理

/**
     * 扩展Spring MVC框架的消息转化器
     * @param converters
     */
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("扩展消息转换器...");
        //创建一个消息转换器对象
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        //需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
        converter.setObjectMapper(new JacksonObjectMapper());
        //将自己的消息转化器加入容器中
        converters.add(0,converter);
    }
  • 消息转换器:负责 HTTP 请求体(Request Body)和响应体(Response Body)与 Java 对象之间相互转换 的核心组件。解决数据格式转换的问题
  • Spring Boot 会自动配置一个默认的 MappingJackson2HttpMessageConverter,但它使用的是 Jackson 默认的 ObjectMapper 行为
  • 所以需要去新建一个 MappingJackson2HttpMessageConverter,且设置JacksonObjectMapper并且去调整它的调用优先级

成功原因:

JacksonObjectMapper,它的作用是定义一套全新的、自定义的 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_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
    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)))
                .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);
    }
}

解释:

1. 忽略未知属性:当 JSON 中存在 Java 对象没有的字段时,不抛出异常 

2. 反序列化时,忽略未知属性 

3. 创建一个 SimpleModule,用于注册自定义的序列化器和反序列化器 SimpleModule

注册 LocalDateTime 的反序列化器:将 "2023-10-27 14:30" 字符串转为 LocalDateTime 对象

注册 LocalDate 的反序列化器 

注册 LocalTime 的反序列化器 

注册 LocalDateTime 的序列化器:将 LocalDateTime 对象转为 "2023-10-27 14:30"

注册 LocalDate 的序列化器 

注册 LocalTime 的序列化器

将上面创建的模块注册到当前的 ObjectMapper 实例中  

启用禁用员工账号:

注意点:

	/**
     * 启用禁用员工账号
     * @param status
     * @param id
     * @return
     */
    @PostMapping("/status/{status}")
    @ApiOperation("启用禁用员工账号")
    public Result startOrStop(@PathVariable Integer status,Long id){
        log.info("启用禁用员工账号:{},{}",status,id);
        employeeService.startOrStop(status,id);//后绪步骤定义
        return Result.success();
    }
  • 为什么不用Result<>呢?----因为启用或禁用是不需要返回数据类型的,只需要Result里的code就行了,故使用Result即可

  • 为什么一个需要@PathVariable,另一个不需要呢?----因为接口文档中,一个是地址栏传入的id,只需要保证两边变量名一致即可,而另一个status是路径参数{},故需要@PathVariable

注意点:

	/**
     * 启用禁用员工账号
     *
     * @param status
     * @param id
     */
    public void startOrStop(Integer status, Long id) {
        Employee employee = Employee.builder()
                .status(status)
                .id(id)
                .build();

        employeeMapper.update(employee);
    }
  • 为什么要用Builder来代替new方式呢?-----增强update方法的通用性,带入Employee比带入status,id更通用

  • 使用 Builder 模式主要是为了提高代码的可读性、可维护性,并在一定程度上保证对象的不可变性 

  • 要保证该类添加了@Builder

注意点:

<update id="update" parameterType="Employee">
        update employee
        <set>
            <if test="name != null">name = #{name},</if>
            <if test="username != null">username = #{username},</if>
            <if test="password != null">password = #{password},</if>
            <if test="phone != null">phone = #{phone},</if>
            <if test="sex != null">sex = #{sex},</if>
            <if test="idNumber != null">id_Number = #{idNumber},</if>
            <if test="updateTime != null">update_Time = #{updateTime},</if>
            <if test="updateUser != null">update_User = #{updateUser},</if>
            <if test="status != null">status = #{status},</if>
        </set>
        where id = #{id}
    </update>
  • parameterType:指定传入参数的类型,帮助 MyBatis 正确地解析参数并绑定到 SQL 中。
  • <set>:动态生成 UPDATE 语句中的 SET 子句,自动处理逗号和多余的字段,避免语法错误。

编辑员工:

编辑员工功能涉及到两个接口:根据id查询员工信息,编辑员工信息

故需要实现两个功能

  • 根据ID获取信息
  • 通过修改来完成更新编辑

导入分类模块功能代码:

根据类型查询:

@GetMapping("/list")
@ApiOperation("根据Type查询分类")
public Result<List<Category>> findByType(Integer type) {
    log.info("根据Type查询:{}",type);
    Category catergory=categoryService.selectByType(type);
    return Result.success(catergory);
}
@GetMapping("/list")
@ApiOperation("根据类型查询分类")
public Result<List<Category>> list(Integer type){
    List<Category> list = categoryService.list(type);
    return Result.success(list);
}

注意点:

  • 选用下面的方式,原因:
  • 特性第一个方法 (findByType)第二个方法 (list)
    返回值类型Category (单个对象)List<Category> (对象列表)
    方法名暗示findByType 暗示查询一个匹配类型的分类list 暗示查询所有匹配类型的分类
    业务逻辑期望 type 是唯一的,只返回第一个匹配结果允许 type 不唯一,返回所有匹配结果

为什么没有查询按钮,点击修改也能有数据显示呢?

因为前端临时缓存了表格中的数据(比如表格加载时,后端返回的分页数据里包含了每个分类的namesort等字段)。当你点击 “修改” 时,前端直接从表格的当前行数据中提取信息,填充到弹窗输入框,不需要额外调用后端接口。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值