员工的增删改查操作

本文详细描述了如何通过Java实现后端接口,包括使用DTO接收前端数据、处理员工信息(如MD5加密密码和使用ThreadLocal存储用户ID)、使用JWT验证并存储用户身份以及处理可能的异常,确保数据完整性并实现线程安全。
摘要由CSDN通过智能技术生成

新增员工:

1:新增员工的接口设计:

请求路径:/admin/employee

请求方式:POST

数据发送格式:json(新增员工肯定要新增员工的每个属性,就用一个json格式的数据来封装)

数据返回格式:Result

2:具体的代码实现:

接收前端传送过来的数据:

接收前端发送回来的数据:DTO。

接收数据,可以用我们封装的employee实体类,不过仔细观察前端传送过来的数据和这个实体类属性差别很大
(这个属性差别很大是什么意思呢,就是实体类中有很多属性,前端不会传过来,我们需要自己去设置)
这个时候可以考虑吧用employeeDTO来接收数据。

注意:当前端提交的数据和实体类中对应的属性差别比较大时,建议使用DTO来封装数据

Controller层代码:

这里提一下这个PostMapping的路径我已经写到类上了,所以这里不需要指定。

    /**
     * 新增员工操作
     * @param employeeDTO
     * @return
     */
    @PostMapping
    @ApiOperation("新增员工")
    public Result save(@RequestBody EmployeeDTO employeeDTO){
        log.info("新增员工:{}",employeeDTO);
        employeeService.insert(employeeDTO);
        return Result.success();
    }
Service层代码:
    /**
     * 新增员工操作
     * @param employeeDTO
     * @return
     */
    @Override
    public void insert(EmployeeDTO employeeDTO) {
        Employee employee = new Employee();

        //1:进行对象拷贝:BeanUtils
        BeanUtils.copyProperties(employeeDTO,employee);

        //2:设置账号的状态,这里的小技巧是引用常量类StatusConstant,这样可以避免把变量写死
        employee.setStatus(StatusConstant.ENABLE);

        //设置密码,密码这里同样用到了密码常量类,这里还需要进行MD5加密
        employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));

        //设置当前记录的创建时间和修改时间
        employee.setCreateTime(LocalDateTime.now());
        employee.setUpdateTime(LocalDateTime.now());

        //设置当前记录人id和修改人id 因为这个地方以后要进行优化,所以用一个TODO
        // TODO 后期需要优化当前登录用户的id
        employee.setCreateUser(10L);
        employee.setUpdateUser(10L);


        //调用持久层

        employeeMapper.insert(employee);
    }

这里用到的一些填充多余变量的一些技巧:

1:对对象进行拷贝:BeanUtils.copyProperties(employeeDTO,employee);

用到了BeanUtils的API,将employeeDTO传给employee

2:设置状态和默认密码,这里没有把这两个值写死,用了一个常量类。

并且对密码进行了MD5加密。

3:TODO,对后期需要优化的地方进行用TODO进行标记。

Mapper层代码:
    /*
    插入员工数据
     */

    @Insert("insert into sky_take_out.employee(name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user) " +
            "values " +
            "(#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{createTime},#{updateTime},#{createUser},#{updateUser})")
    void insert(Employee employee);

这里的注意点就是:()里面的变量名字是数据库中的字段名

#{}里面的是,Employee实体类中的变量名。

3:功能测试:

        功能测试方式:
            通过接口文档测试
            通过前后端联调测试

注意:由于开发阶段前端和后端是并行开发的,后端完成某个功能后,此时前端对应的功能可能还没有开发完成,
导致无法进行前后端联调测试。所以在开发阶段,后端测试主要以接口文档测试为主。

功能测试时遇到的问题,

如果我们直接在Swagger上进行测试:

如图:

系统调试会报401错误,原因就是,我们在代码中设置了拦截器interceptor,我们没有登录,没有获取到jwt令牌,我们无法新增员工。

解决办法:

现在登录接口获得一个jwt令牌:

然后在文档管理中的全局参数管理保存一个:

结果:

响应返回200

并且这里可以观察到那个请求头部,有一个红点,点进去就能看到我们刚刚设置的那个token。

数据库中的结果:

因为这个新增员工的操作写在查询员工之前,前端页面还展示不出来,所以这里没有进行前后端联调。

4:代码优化:

程序存在的问题:

1:录入的用户名已存在,抛出异常后没有处理 新增员工时,

如图当我们再次插入张三的时候:

就会报500,原因就是我们没有处理这个异常

解决方法:

在GlobalExceptionHandler中添加异常方法:

    @ExceptionHandler
    public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
        //Duplicate entry 'zhangsan' for key 'employee.idx_username
        String msg = ex.getMessage();
        if(msg.contains("Duplicate entry")){
            String[] s = msg.split(" ");
            String username = s[2];
            return Result.error(username+ MessageConstant.ALREAD_ERROR);
        }else{
            return Result.error(MessageConstant.UNKNOWN_ERROR);
        }
    }

这里的SQLIntegrityConstraintViolationException和

//Duplicate entry 'zhangsan' for key 'employee.idx_username

都是从控制台复制过来的。

具体的处理过程就如上面代码所示。

这里有一个点需要提一下就是这个返回的提示信息,我们进来不要自己去写,用常量类里面错误提示信息。

结果:

2:创建人id和修改人id设置为了固定值
        //设置当前记录人id和修改人id 因为这个地方以后要进行优化,所以用一个TODO
        // TODO 后期需要优化当前登录用户的id
        employee.setCreateUser(10L);
        employee.setUpdateUser(10L);

这一块代码这里的用户id是写死的,这在程序中一般不会出现,所以我们现在要想一个办法解决这个问题。

解决办法:

这里想要解决这个问题,我们需要引入一个新的知识点:ThreadLocal。

ThreadLocal:
我们首先要知道:

在Java中,每个请求不一定都是一个线程,但通常情况下,每个请求会被分配一个独立的线程来处理。而且,线程之间确实可以共享一些变量,但这也可能导致线程安全的问题。

这个是GPT的原话,虽然他说每个请求不一定对应一个线程,不过我们在这个项目中,可以先这么理解。

ThreadLocal概念:

ThreadLocal是Java中的一个类,它提供了线程局部变量的功能。每个ThreadLocal对象都可以维护一个独立的变量副本,每个线程都可以访问和修改自己的这个副本,而不会影响其他线程的变量副本。这样可以确保在多线程环境下,每个线程都拥有自己的变量副本,从而避免了线程间的数据共享和竞争条件。
线程安全指的是在多线程环境下,对共享资源的操作能够保证正确和一致的结果。使用ThreadLocal能够实现线程安全,因为每个线程都操作自己的变量副本,不存在线程间的竞争条件。

ThreadLocal变量副本:

这一段话差不多就是对ThreadLocal的解释,我想再解释一下这个:

每个ThreadLocal对象都可以维护一个独立的变量副本这一句话:

每个ThreadLocal实例确实只能维护一个变量副本,但这个变量可以是任意类型的对象,包括数组、集合等。因此,你可以通过ThreadLocal来存储任意类型的值,不仅仅是一个简单的值。例如,你可以将一个复杂的对象或者一个集合存储在ThreadLocal中,每个线程都可以独立地访问和修改自己的副本,而不会影响其他线程的副本。

ok,解释到这里差不多这个概念就解释的差不多了,然后我们再讨论一下如何解决上述获取用户id的方法:

通过JWT令牌:

校验这个JWT令牌的时候,我们可以设置一下这个ThreadLocal的值,就是这个变量副本,然后我们在save()方法中再取出来就好了。

登录接口具体代码:

    /**
     * 校验jwt
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }

        //1、从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getAdminTokenName());

        //2、校验令牌
        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
            log.info("当前员工id:", empId);
            //设置线程中的用户id
            BaseContext.setCurrentId(empId);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }

Service层代码:

        //设置当前记录人id和修改人id 因为这个地方以后要进行优化,所以用一个TODO
        //后期需要优化当前登录用户的id
        employee.setCreateUser(BaseContext.getCurrentId());
        employee.setUpdateUser(BaseContext.getCurrentId());

查询员工:

1:查询员工的接口设计:

查询员工

请求路径:/admin/employee/page

请求方式:GET

数据发送格式:Query

        name:员工姓名

        page:页码

        pagesize:每页记录数

数据返回格式:json

2:具体的代码实现:

接收前端传送过来的数据:

接收前端发送回来的数据:EmployeePageQueryDTO。

@Data
public class EmployeePageQueryDTO implements Serializable {

    //员工姓名
    private String name;

    //页码
    private int page;

    //每页显示记录数
    private int pageSize;

}

返回给前端的数据:Result(PageResult):

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {

    private long total; //总记录数

    private List records; //当前页数据集合

}

 Controller层代码:

    /**
     * 分页查询员工操作
     * @param employeePageQueryDTO
     * @return
     */
    @GetMapping("/page")
    @ApiOperation("分页查询员工")
    public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO){
        log.info("分页查询员工:{}",employeePageQueryDTO);
        PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);
        return Result.success(pageResult);
    }
这里提两个注意点把

        1:就是注意路径:需要在后面+/page

        2:注意返回的参数调用接口所传的参数(因为返回值有点套娃了)

 Service层:

    /**
     * 分页查询员工
     * @param employeePageQueryDTO
     * @return
     */
    @Override
    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插件:

pagehelper是mybatis的一个插件,其作用是更加方便地进行分页查询

PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize());

这一行主要是用来设置分页的页数和每页数量。相当于SQL语句中limit后面的两个参数。

注意:只有紧跟着PageHelper.startPage()的sql语句才被pagehelper起作用

Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);

使用了PageHelper返回值就得使用这个插件自己定义的返回值Page。

然后调用两个方法获取这个数量的总数和返回的数据。

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

Service层: 

    /**
     * 分页查询员工
     * @param employeePageQueryDTO
     * @return
     */
    @Override
    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);
    }

Mapper层及动态SQL: 

Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
<mapper namespace="com.sky.mapper.EmployeeMapper">
    <select id="pageQuery" resultType="com.sky.entity.Employee">
        select * from sky_take_out.employee
        <where>
            <if test="name != null and name != ''">
                and name like concat('%',#{name},'%')
            </if>
        </where>
        order by create_time desc
    </select>
</mapper>

这里的的动态SQL:

需要回顾一下之前的Mybatis的动态SQL的知识:

 动态SQL:

随着用户的输入或外部条件的变化而变化的SQL语句,我们称为 动态SQL。

if标签:

<if>:用于判断条件是否成立。使用test属性进行条件判断,如果条件为true,则拼接SQL。

先来看第一个代码案例:


<select id="list" resultType="com.springbootcrud.Pojo.Emp">
        select *
        from mybatis.emp
        where
            <if test="name!=null">
                name like concat('%', #{name}, '%')
            </if>
            <if test="gender!=null">
                and gender = #{gender}
            </if>
            <if test="begin != null and end != null">
                and entrydate between #{begin} and #{end}
            </if>
        where
        order by update_time desc
    </select>

上面这段代码就可以解决查询的问题:比如你给我的测试条件是:
List<Emp> empList = empMapper.list("张", (short) 1, LocalDate.of(2010, 1, 1), LocalDate.of(2020, 1, 1));
每个属性都有,那肯定没什么问题。
List<Emp> empList = empMapper.list("张",null,null,null);
但如果是下面这种,三个条件中只有一个存在,如果没有用if判断语句的话就会报错。

where标签:

<where>:where 元素只会在子元素有内容的情况下才插入where子句。而且会自动去除子句的开头的AND 或OR

第二个案例:

基于上面的if标签,我们已经解决了一部分的问题:
不过还是不够全面,仔细看
    <if test="name!=null">
                name like concat('%', #{name}, '%')
            </if>
            <if test="gender!=null">
                and gender = #{gender}
            </if>
            <if test="begin != null and end != null">
                and entrydate between #{begin} and #{end}
            </if>
中间有很多的and,这样会导致什么情况呢:
List<Emp> empList = empMapper.list(null,(short)1,null,null);
因为第一个条件不存在,所以导致后面的(short)1这个条件根本查不到,所以,也会报错
所以我们引入<where>标签,作用就是能去除开头的and和or,还有一个作用,如果where条件中的所有if都不成立,那这个where标签就不会定义
比如List<Emp> empList = empMapper.list(null,null,null,null);这种情况就会查询出所有的记录。

3:功能测试:

        这里的功能测试还是主要用Swagger方式来测试

小插曲:

这个小插曲的原因和JWT令牌过期有关系:

代码没有问题的情况下报了401的响应码,原因就是因为JWT令牌的时间过期了。

所以这个时候我们需要重新获取一个JWT令牌。

查询结果:

4:代码优化:

程序存在的问题:

仔细看前端页面展示的结果-----最后操作时间那一栏

时间的格式不太对,下面就来优化这个问题。

解决方法:

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

扩展Spring MVC的消息转换器:

Spring MVC框架的消息转换器是用来处理请求和响应的数据格式转换的组件.
在SpringMVC中,java对象转JSON对象是通Jackson实现的,涉及到SpringMVC中的消息转换器MappingJackson2HttpMessageConverter.
当请求和响应的数据格式不匹配时,就需要对消息转换器功能进行拓展.

我们首先要在我们编写的 WebMvcConfiguration 类中继承 WebMvcConfigurationSupport(这个应该是写web相关程序都要继承的类,应该是SpringMVC那一块的东西)

    /**
     * 扩展Spring MVC的消息转化器
     * @param converters
     */
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("扩展消息转换器");
        //创建一个消息转换器对象
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        //需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象转换成json数据
        converter.setObjectMapper(new JacksonObjectMapper());
        //将自己的消息转化器加入到容器中,并设置优先级
        converters.add(0,converter);
    }
在WebMvcConfiguration 类中重写 extendMessageConverters 方法:        

     1:创建一个消息转换器对象

MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();

2:需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象转换成json数据
converter.setObjectMapper(new JacksonObjectMapper());

3:将自己的消息转化器加入到容器中,并设置优先级(0)
converters.add(0,converter);
对象转换器:

这个对象转换器是我们自己定义的:(比较固定的代码)

/**
 * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
 * 从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_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:启用禁用员工账号的接口设计:

请求路径:/admin/employee/status/{status}

请求方式:POST

数据请求参数:Headers

Query:id

数据返回格式:json

业务规则:

可以对状态为“启用” 的员工账号进行“禁用”操作

可以对状态为“禁用”的员工账号进行“启用”操作

状态为“禁用”的员工账号不能登录系统

2:具体的代码实现:

Controller层:

/**
     * 启用禁用员工账号
     */
    @PostMapping("/status/{status}")
    @ApiOperation("启用禁用员工账号")
    public Result StartOrStop(@RequestParam Long id,@PathVariable(value = "status") Integer status){
        log.info("启用禁用员工账号:{},{}",id,status);
        employeeService.StartOrStop(id,status);
        return Result.success();
    }

这里需要注意的就是两个参数的传递:

status是路径参数:用@PathVariable 来接收

id其实是前端的请求参数。我在这里加了@RequestParam,就是表明这个是前端请求参数)

这里其实也可以不用加,为什么呢?

因为前端传送过来的参数名字和id是一样的,然后我做了一个实验,将前端传过来的参数名称改为idd,结果不会报错,不过因为idd的值是null,所以不会在数据库中有什么体现。

 Service层代码:

    /**
     * 启用禁用员工账号
     */
    @Override
    public void StartOrStop(Long id, Integer status) {
        //update employee set status = ? where id = ?
        Employee employee = new Employee();
        employee.setId(id);
        employee.setStatus(status);

        //调用mapper接口
        employeeMapper.StartOrStop(employee);
    }

service层的代码就很中规中矩了。

有一个和之前不同的点是:在我第一次写crud的时候,如果碰到参数只有两个,我可能会直接传进去,不过听了老师的话,封装成一个集合传送。

Mapper层:

void StartOrStop(Employee employee);

注解层代码: 

<update id="StartOrStop" parameterType="Employee">
        update sky_take_out.employee
        <set>
            <if test="username != null">username = #{username},</if>
            <if test="name != null">name = #{name},</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 = #{isNumber},</if>
            <if test="updateTime != null">update_time = #{updateTime},</if>
            <if test="createUser != null">create_user = #{createUser},</if>
            <if test="updateUser != null">update_user = #{updateUser},</if>
            <if test="status != null">status = #{status},</if>
        </set>
        where id = #{id}
    </update>

注解层代码也够坑,因为employee中的变量名和mysql中的字段名不同导致改了半天,下次注意就好。

编辑员工操作 :

1:编辑员工的接口设计:

编辑员工功能涉及到两个接口:

(1)根据id查询员工信息

请求路径:/admin/employee/{id}

请求方式:GET

数据发送格式:路径参数

数据返回格式:Result

(2)编辑员工信息

请求路径:/admin/employee

请求方式:PUT

数据发送格式:json(包括要修改员工的基础属性值)

数据返回格式:Result

2:具体的代码实现:

Controller层代码:

        根据id查询员工:
    /**
     * 根据id查询员工
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    @ApiOperation("根据id查询员工")
    public Result<Employee> GetById(@PathVariable long id){
        log.info("根据id查询员工:{}",id);
        Employee emp = employeeService.GetById(id);
        return Result.success(emp);
    }

这里的注意点就是注意一下这个返回值需要将Employee对象封装到Result中。 

编辑员工信息:
    /**
     * 修改员工信息
     * @param employeeDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改员工信息")
    public Result UpdateEmp(@RequestBody EmployeeDTO employeeDTO){
        log.info("修改员工信息:{}",employeeDTO);
        employeeService.UpdateEmp(employeeDTO);
        return Result.success();
    }

Service层代码: 

根据id查询员工:
    /**
     * 根据id查询员工
     * @param id
     * @return
     */
    @Override
    public Employee GetById(long id) {
        Employee employee = employeeMapper.SelectById(id);
        employee.setPassword("********");//对密码进一步进行加密
        return employee;
    }

这里对这个密码加了一层回显,提高了安全性。 

编辑员工信息: 
    /**
     * 修改员工信息
     * @param employeeDTO
     * @return
     */
    @Override
    public void UpdateEmp(EmployeeDTO employeeDTO) {
        Employee employee = new Employee();
        //1:对对象进行拷贝
        BeanUtils.copyProperties(employeeDTO,employee);
        
        employee.setUpdateTime(LocalDateTime.now());
        employee.setUpdateUser(BaseContext.getCurrentId());

        employeeMapper.UpdateEmp(employee);
    }

Mapper层代码: 

根据id查询员工:
    /**
     * 根据id查询员工
     * @param id
     * @return
     */
    @Select("select * from sky_take_out.employee where id = #{id}")
    Employee SelectById(long id);
编辑员工信息:
    /**
     * 修改员工信息
     * @param
     * @return
     */
    void UpdateEmp(Employee employee);

    <update id="UpdateEmp" parameterType="Employee">
        update sky_take_out.employee
        <set>
            <if test="username != null">username = #{username},</if>
            <if test="name != null">name = #{name},</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>
  • 14
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值