Java 项目实战: 外卖平台-修改分类与新增窗口复用

修改分类、Vue 数据绑定回显与 updateById 公共字段回填

纲要

  • 需求特点 :点击「修改」按钮弹出窗口并回显当前分类信息,改完点确定提交
  • 回显机制 :纯前端实现,靠 Vuev-model 双向绑定,编辑阶段不与后端交互
  • 请求特征PUT /category,请求体为 {"id":..., "name":..., "sort":...}JSON
  • 后端实现@PutMapping + @RequestBody Category + updateById(category),一行核心代码
  • 公共字段updateTimeupdateUserMyMetaObjecthandlerupdateFill 自动填充,无需手动 set
  • 窗口复用 :新增与修改共用同一个 el-dialog,靠 action 变量区分标题与提交逻辑
  • SQL 验证UPDATE category SET name=?, sort=?, update_time=?, update_user=? WHERE id=?

需求分析

分类管理列表每行都有一个「修改」按钮。点击后的行为分两个阶段:
后端服务 Vue 页面 用户 后端服务 Vue 页面 用户 #mermaid-svg-vCIHtatevIOciUkA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vCIHtatevIOciUkA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vCIHtatevIOciUkA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vCIHtatevIOciUkA .error-icon{fill:#552222;}#mermaid-svg-vCIHtatevIOciUkA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vCIHtatevIOciUkA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vCIHtatevIOciUkA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vCIHtatevIOciUkA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vCIHtatevIOciUkA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vCIHtatevIOciUkA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vCIHtatevIOciUkA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vCIHtatevIOciUkA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vCIHtatevIOciUkA .marker.cross{stroke:#333333;}#mermaid-svg-vCIHtatevIOciUkA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vCIHtatevIOciUkA p{margin:0;}#mermaid-svg-vCIHtatevIOciUkA .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vCIHtatevIOciUkA text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-vCIHtatevIOciUkA .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-vCIHtatevIOciUkA .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-vCIHtatevIOciUkA .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-vCIHtatevIOciUkA .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-vCIHtatevIOciUkA #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-vCIHtatevIOciUkA .sequenceNumber{fill:white;}#mermaid-svg-vCIHtatevIOciUkA #sequencenumber{fill:#333;}#mermaid-svg-vCIHtatevIOciUkA #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-vCIHtatevIOciUkA .messageText{fill:#333;stroke:none;}#mermaid-svg-vCIHtatevIOciUkA .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vCIHtatevIOciUkA .labelText,#mermaid-svg-vCIHtatevIOciUkA .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-vCIHtatevIOciUkA .loopText,#mermaid-svg-vCIHtatevIOciUkA .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-vCIHtatevIOciUkA .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-vCIHtatevIOciUkA .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-vCIHtatevIOciUkA .noteText,#mermaid-svg-vCIHtatevIOciUkA .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-vCIHtatevIOciUkA .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vCIHtatevIOciUkA .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vCIHtatevIOciUkA .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vCIHtatevIOciUkA .actorPopupMenu{position:absolute;}#mermaid-svg-vCIHtatevIOciUkA .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-vCIHtatevIOciUkA .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vCIHtatevIOciUkA .actor-man circle,#mermaid-svg-vCIHtatevIOciUkA line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-vCIHtatevIOciUkA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 此阶段无网络请求 点击「修改」按钮 editHandle(scope.row) 拿到整行数据 classData.title = '修改分类' action = 'edit' classData.name = row.name classData.sort = row.sort classData.id = row.id v-model 自动回显到输入框 修改内容,点击确定 submitForm() 校验 PUT /category {id, name, sort} {"code":1,"data":"修改分类信息成功"} 提示成功,关闭窗口,刷新列表

关键点 :回显阶段没有发请求。这是很多初学者会误解的地方------以为点修改时后端要提供「根据 id 查询」接口。实际上列表分页查询时已经把整行数据拉到前端了,editHandle(scope.row) 直接把这一行对象传给弹窗。

前端实现

修改按钮

html 复制代码
<el-button
  type="text"
  size="small"
  class="blueBug"
  @click="editHandle(scope.row)"
>
  修改
</el-button>

scope.rowElement UI 表格插槽提供的当前行数据对象,包含 idtypenamesort 等全部字段。

editHandle 方法

js 复制代码
editHandle(row) {
  this.classData.title = '修改分类'
  this.action = 'edit'
  this.classData.id = row.id
  this.classData.name = row.name
  this.classData.sort = row.sort
  this.dialogVisible = true
}

对比新增方法:

js 复制代码
addClass(st) {
  this.classData.title = '新增菜品分类'
  this.action = 'add'
  this.classData.name = ''
  this.classData.sort = ''
  this.dialogVisible = true
}

同一个窗口,两种用途

#mermaid-svg-YtOI7ll1PIXgwpnS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YtOI7ll1PIXgwpnS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YtOI7ll1PIXgwpnS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YtOI7ll1PIXgwpnS .error-icon{fill:#552222;}#mermaid-svg-YtOI7ll1PIXgwpnS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YtOI7ll1PIXgwpnS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YtOI7ll1PIXgwpnS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YtOI7ll1PIXgwpnS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YtOI7ll1PIXgwpnS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YtOI7ll1PIXgwpnS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YtOI7ll1PIXgwpnS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YtOI7ll1PIXgwpnS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YtOI7ll1PIXgwpnS .marker.cross{stroke:#333333;}#mermaid-svg-YtOI7ll1PIXgwpnS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YtOI7ll1PIXgwpnS p{margin:0;}#mermaid-svg-YtOI7ll1PIXgwpnS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-YtOI7ll1PIXgwpnS .cluster-label text{fill:#333;}#mermaid-svg-YtOI7ll1PIXgwpnS .cluster-label span{color:#333;}#mermaid-svg-YtOI7ll1PIXgwpnS .cluster-label span p{background-color:transparent;}#mermaid-svg-YtOI7ll1PIXgwpnS .label text,#mermaid-svg-YtOI7ll1PIXgwpnS span{fill:#333;color:#333;}#mermaid-svg-YtOI7ll1PIXgwpnS .node rect,#mermaid-svg-YtOI7ll1PIXgwpnS .node circle,#mermaid-svg-YtOI7ll1PIXgwpnS .node ellipse,#mermaid-svg-YtOI7ll1PIXgwpnS .node polygon,#mermaid-svg-YtOI7ll1PIXgwpnS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YtOI7ll1PIXgwpnS .rough-node .label text,#mermaid-svg-YtOI7ll1PIXgwpnS .node .label text,#mermaid-svg-YtOI7ll1PIXgwpnS .image-shape .label,#mermaid-svg-YtOI7ll1PIXgwpnS .icon-shape .label{text-anchor:middle;}#mermaid-svg-YtOI7ll1PIXgwpnS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YtOI7ll1PIXgwpnS .rough-node .label,#mermaid-svg-YtOI7ll1PIXgwpnS .node .label,#mermaid-svg-YtOI7ll1PIXgwpnS .image-shape .label,#mermaid-svg-YtOI7ll1PIXgwpnS .icon-shape .label{text-align:center;}#mermaid-svg-YtOI7ll1PIXgwpnS .node.clickable{cursor:pointer;}#mermaid-svg-YtOI7ll1PIXgwpnS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YtOI7ll1PIXgwpnS .arrowheadPath{fill:#333333;}#mermaid-svg-YtOI7ll1PIXgwpnS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YtOI7ll1PIXgwpnS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YtOI7ll1PIXgwpnS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YtOI7ll1PIXgwpnS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YtOI7ll1PIXgwpnS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YtOI7ll1PIXgwpnS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YtOI7ll1PIXgwpnS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YtOI7ll1PIXgwpnS .cluster text{fill:#333;}#mermaid-svg-YtOI7ll1PIXgwpnS .cluster span{color:#333;}#mermaid-svg-YtOI7ll1PIXgwpnS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YtOI7ll1PIXgwpnS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YtOI7ll1PIXgwpnS rect.text{fill:none;stroke-width:0;}#mermaid-svg-YtOI7ll1PIXgwpnS .icon-shape,#mermaid-svg-YtOI7ll1PIXgwpnS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YtOI7ll1PIXgwpnS .icon-shape p,#mermaid-svg-YtOI7ll1PIXgwpnS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YtOI7ll1PIXgwpnS .icon-shape .label rect,#mermaid-svg-YtOI7ll1PIXgwpnS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YtOI7ll1PIXgwpnS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YtOI7ll1PIXgwpnS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YtOI7ll1PIXgwpnS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

el-dialog 弹窗
action === 'add' ?
标题:新增菜品分类 / 新增套餐分类
标题:修改分类
提交:POST /category
提交:PUT /category
清空表单
表单中携带 id

这种做法的好处是页面结构只维护一份 ,缺点是弹窗的 data 模型要同时容纳两种场景的字段。

模型数据与双向绑定

js 复制代码
data() {
  return {
    action: '',
    dialogVisible: false,
    classData: {
      title: '',
      id: '',
      name: '',
      sort: ''
    }
  }
}

模板中的输入框:

html 复制代码
<el-form-item label="分类名称:" prop="name">
  <el-input
    v-model="classData.name"
    placeholder="请输入分类名称"
    maxlength="14"
  />
</el-form-item>

<el-form-item label="排序:" prop="sort">
  <el-input
    v-model="classData.sort"
    placeholder="请输入排序"
    type="number"
  />
</el-form-item>

v-model="classData.name" 建立了双向绑定:

  • 数据 → 视图 :修改 classData.name 时,输入框内容自动更新(这就是回显的原理)
  • 视图 → 数据 :用户在输入框键入时,classData.name 自动同步

所以 editHandle 里给 classData 赋值,输入框就自动显示了,不需要任何 DOM 操作。

提交逻辑

js 复制代码
submitForm() {
  const classData = this.classData
  const valid = classData.name === '' || classData.sort === ''
  if (this.action === 'add') {
    if (!valid) {
      delete this.classData.id
      addCategory(this.classData).then(res => {
        if (res.code === 1) {
          this.$message.success('分类添加成功!')
          this.dialogVisible = false
          this.handleQuery()
        } else {
          this.$message.error(res.msg || '操作失败')
        }
      })
    }
  } else {
    if (!valid) {
      editCategory(this.classData).then(res => {
        if (res.code === 1) {
          this.$message.success('分类修改成功!')
          this.dialogVisible = false
          this.handleQuery()
        } else {
          this.$message.error(res.msg || '操作失败')
        }
      })
    }
  }
}

新增分支里 delete this.classData.id 是因为新增时不应携带 id(虽然服务端雪花算法会覆盖,但传了脏数据不优雅)。

接口封装

js 复制代码
// 修改分类
function editCategory(params) {
  return $axios({
    url: '/category',
    method: 'put',
    data: { ...params }
  })
}

data 会被 axios 序列化成 JSON 请求体,对应后端的 @RequestBody

后端实现

Controller 方法

java 复制代码
/**
 * 根据id修改分类信息
 * @param category
 * @return
 */
@PutMapping
public R<String> update(@RequestBody Category category){
    log.info("修改分类信息:{}",category);

    categoryService.updateById(category);

    return R.success("修改分类信息成功");
}

请求与响应

浏览器 Network

txt 复制代码
Request URL:    http://localhost:8080/category
Request Method: PUT
Content-Type:   application/json;charset=UTF-8

Request Payload:
{"id":"1397844303408570369","name":"123","sort":"11"}

Response:
{"code":1,"data":"修改分类信息成功","msg":null,"map":{}}

RESTful 语义对照

操作 方法 路径 请求体 / 参数
新增 POST /category {"name":"川菜","type":1,"sort":10}
修改 PUT /category {"id":"1397844303408570369","name":"123","sort":"11"}
删除 DELETE /category?id=xxx
查询 GET /category/page ?page=1&pageSize=10

POSTPUT 路径相同但语义不同:POST 表示在集合中创建新资源,PUT 表示更新已有资源。区分依据是 HTTP 方法,而不是 URL

updateById 的行为

MyBatis-PlusupdateById(T entity) 有一个重要特性:只对非 null 字段生成 SET 子句

默认策略是 FieldStrategy.NOT_NULL,即:

java 复制代码
Category category = new Category();
category.setId(1397844303408570369L);
category.setName("川菜");
categoryService.updateById(category);

生成的 SQL

sql 复制代码
UPDATE category SET name='川菜' WHERE id=1397844303408570369

typesort 因为没有赋值(保持 null),不会出现在 SET 里。这避免了把未修改的字段覆盖成 null

如何显式更新为 null

如果确实需要把某个字段置空,默认策略下做不到。有三种办法:

方案 写法 影响范围
字段级注解 @TableField(updateStrategy = FieldStrategy.IGNORED) 仅该字段
全局配置 update-strategy: ignored 全部字段
UpdateWrapper new LambdaUpdateWrapper<Category>().set(Category::getName, null) 单次调用

推荐第三种,影响面最小:

java 复制代码
LambdaUpdateWrapper<Category> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(Category::getId, id).set(Category::getName, null);
categoryService.update(wrapper);

公共字段的自动回填

不写一行 set 的奥秘

修改分类时,updateTimeupdateUser 需要更新为当前时间与当前操作人。但 Controller 里没有 category.setUpdateTime(...),为什么生效?

答案在第 28 篇实现的 MyMetaObjecthandler

java 复制代码
package com.itheima.reggie.common;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

/**
 * 自定义元数据对象处理器
 */
@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler {

    /**
     * 插入操作时自动填充
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("公共字段自动填充[insert]...");
        log.info(metaObject.toString());

        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("createUser", BaseContext.getCurrentId());
        metaObject.setValue("updateUser", BaseContext.getCurrentId());
    }

    /**
     * 更新操作时自动填充
     * @param metaObject
     */
    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("公共字段自动填充[update]...");
        log.info(metaObject.toString());

        long id = Thread.currentThread().getId();
        log.info("线程id为:{}", id);

        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("updateUser", BaseContext.getCurrentId());
    }
}

触发条件

自动填充生效需要两个条件同时满足
#mermaid-svg-mtNy4lOTdToJvTVn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mtNy4lOTdToJvTVn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mtNy4lOTdToJvTVn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mtNy4lOTdToJvTVn .error-icon{fill:#552222;}#mermaid-svg-mtNy4lOTdToJvTVn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mtNy4lOTdToJvTVn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mtNy4lOTdToJvTVn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mtNy4lOTdToJvTVn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mtNy4lOTdToJvTVn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mtNy4lOTdToJvTVn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mtNy4lOTdToJvTVn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mtNy4lOTdToJvTVn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mtNy4lOTdToJvTVn .marker.cross{stroke:#333333;}#mermaid-svg-mtNy4lOTdToJvTVn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mtNy4lOTdToJvTVn p{margin:0;}#mermaid-svg-mtNy4lOTdToJvTVn .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mtNy4lOTdToJvTVn .cluster-label text{fill:#333;}#mermaid-svg-mtNy4lOTdToJvTVn .cluster-label span{color:#333;}#mermaid-svg-mtNy4lOTdToJvTVn .cluster-label span p{background-color:transparent;}#mermaid-svg-mtNy4lOTdToJvTVn .label text,#mermaid-svg-mtNy4lOTdToJvTVn span{fill:#333;color:#333;}#mermaid-svg-mtNy4lOTdToJvTVn .node rect,#mermaid-svg-mtNy4lOTdToJvTVn .node circle,#mermaid-svg-mtNy4lOTdToJvTVn .node ellipse,#mermaid-svg-mtNy4lOTdToJvTVn .node polygon,#mermaid-svg-mtNy4lOTdToJvTVn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mtNy4lOTdToJvTVn .rough-node .label text,#mermaid-svg-mtNy4lOTdToJvTVn .node .label text,#mermaid-svg-mtNy4lOTdToJvTVn .image-shape .label,#mermaid-svg-mtNy4lOTdToJvTVn .icon-shape .label{text-anchor:middle;}#mermaid-svg-mtNy4lOTdToJvTVn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mtNy4lOTdToJvTVn .rough-node .label,#mermaid-svg-mtNy4lOTdToJvTVn .node .label,#mermaid-svg-mtNy4lOTdToJvTVn .image-shape .label,#mermaid-svg-mtNy4lOTdToJvTVn .icon-shape .label{text-align:center;}#mermaid-svg-mtNy4lOTdToJvTVn .node.clickable{cursor:pointer;}#mermaid-svg-mtNy4lOTdToJvTVn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mtNy4lOTdToJvTVn .arrowheadPath{fill:#333333;}#mermaid-svg-mtNy4lOTdToJvTVn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mtNy4lOTdToJvTVn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mtNy4lOTdToJvTVn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mtNy4lOTdToJvTVn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mtNy4lOTdToJvTVn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mtNy4lOTdToJvTVn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mtNy4lOTdToJvTVn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mtNy4lOTdToJvTVn .cluster text{fill:#333;}#mermaid-svg-mtNy4lOTdToJvTVn .cluster span{color:#333;}#mermaid-svg-mtNy4lOTdToJvTVn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-mtNy4lOTdToJvTVn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mtNy4lOTdToJvTVn rect.text{fill:none;stroke-width:0;}#mermaid-svg-mtNy4lOTdToJvTVn .icon-shape,#mermaid-svg-mtNy4lOTdToJvTVn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mtNy4lOTdToJvTVn .icon-shape p,#mermaid-svg-mtNy4lOTdToJvTVn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mtNy4lOTdToJvTVn .icon-shape .label rect,#mermaid-svg-mtNy4lOTdToJvTVn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mtNy4lOTdToJvTVn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mtNy4lOTdToJvTVn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mtNy4lOTdToJvTVn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 实体字段上有 @TableField(fill = ...)
自动填充生效
存在实现 MetaObjectHandler 的 Bean
调用的是 MyBatis-Plus 的 insert/update 方法

Category 实体中的声明:

java 复制代码
//更新时间
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;

//修改人
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;

FieldFill.INSERT_UPDATE 表示插入和更新时都填充。如果这里漏了注解,字段就会是 null,触发 update_time 的非空约束报错。

FieldFill 取值对照

取值 插入时填充 更新时填充
DEFAULT
INSERT
UPDATE
INSERT_UPDATE

PUT 请求不传 updateTime 也没关系

前端请求体只有 idnamesort,没有 updateTime。但因为 updateFill 是在服务端执行的,与前端传什么无关,所以字段依然被正确填充。这也是自动填充相对于「前端传时间戳」的明显优势------客户端时间不可信。

功能测试

修改流程

启动项目,进入分类管理,点击第一行的「修改」,把名称改成「123」,点确定。

控制台 SQL

txt 复制代码
==>  Preparing: UPDATE category SET name=?, sort=?, update_time=?, update_user=? WHERE id=?
==> Parameters: 123(String), 11(Integer), 2026-09-11T09:28:15(LocalDateTime), 1(Long), 1397844303408570369(Long)
<==    Updates: 1

注意 SET 子句中出现了 update_timeupdate_user,尽管 Controller 里一个字都没写。这两个值来自 updateFill

验证 updateUser 的来源

update_user1,是 BaseContext.getCurrentId() 的返回值。这个值由 LoginCheckFilter 在校验通过时写入:

java 复制代码
// 4、判断登录状态,如果已登录,则直接放行
if (request.getSession().getAttribute("employee") != null) {
    log.info("用户已登录,用户id为:{}", request.getSession().getAttribute("employee"));

    Long empId = (Long) request.getSession().getAttribute("employee");
    BaseContext.setCurrentId(empId);

    filterChain.doFilter(request, response);
    return;
}

如果你用另一个员工账号登录再修改,update_user 会变成那个员工的 id

改回原值

测试完把「123」改回「香菜」,再次提交,控制台再发一条 UPDATE。数据恢复。

常见异常:没有 id

如果提交时 classData.id 为空(比如前端忘了在 editHandle 里赋值),updateById 会生成:

sql 复制代码
UPDATE category SET name=?, sort=?, update_time=?, update_user=?

没有 WHERE 条件------全表更新MyBatis-Plus 对这种情况会抛出 MybatisPlusException: 无法执行. 未找到更新的条件,但如果是手写 XML 就可能酿成事故。

防御措施:

java 复制代码
@PutMapping
public R<String> update(@RequestBody Category category){
    if (category.getId() == null) {
        return R.error("分类id不能为空");
    }
    categoryService.updateById(category);
    return R.success("修改分类信息成功");
}

完整可运行代码

目录结构

txt 复制代码
reggie_take_out/
└── src/
    ├── main/
    │   ├── java/com/itheima/reggie/
    │   │   ├── common/
    │   │   │   ├── R.java
    │   │   │   ├── BaseContext.java
    │   │   │   └── MyMetaObjecthandler.java         自动填充
    │   │   ├── entity/
    │   │   │   └── Category.java
    │   │   ├── mapper/
    │   │   │   └── CategoryMapper.java
    │   │   ├── service/
    │   │   │   ├── CategoryService.java
    │   │   │   └── impl/CategoryServiceImpl.java
    │   │   └── controller/
    │   │       └── CategoryController.java
    │   └── resources/
    │       └── backend/page/category/list.html       分类管理页面
    └── test/

CategoryController.java(含修改方法)

java 复制代码
package com.itheima.reggie.controller;

import com.itheima.reggie.common.R;
import com.itheima.reggie.entity.Category;
import com.itheima.reggie.service.CategoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * 分类管理
 */
@RestController
@RequestMapping("/category")
@Slf4j
public class CategoryController {
    @Autowired
    private CategoryService categoryService;

    /**
     * 根据id修改分类信息
     * @param category
     * @return
     */
    @PutMapping
    public R<String> update(@RequestBody Category category){
        log.info("修改分类信息:{}",category);

        categoryService.updateById(category);

        return R.success("修改分类信息成功");
    }
}

Category.java

java 复制代码
package com.itheima.reggie.entity;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * 分类
 */
@Data
public class Category implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    //类型 1 菜品分类 2 套餐分类
    private Integer type;

    //分类名称
    private String name;

    //顺序
    private Integer sort;

    //创建时间
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    //更新时间
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    //创建人
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;

    //修改人
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
}

MyMetaObjecthandler.java

java 复制代码
package com.itheima.reggie.common;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

/**
 * 自定义元数据对象处理器
 */
@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler {

    /**
     * 插入操作时自动填充
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("公共字段自动填充[insert]...");
        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("createUser", BaseContext.getCurrentId());
        metaObject.setValue("updateUser", BaseContext.getCurrentId());
    }

    /**
     * 更新操作时自动填充
     * @param metaObject
     */
    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("公共字段自动填充[update]...");
        long id = Thread.currentThread().getId();
        log.info("线程id为:{}", id);
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("updateUser", BaseContext.getCurrentId());
    }
}

BaseContext.java

java 复制代码
package com.itheima.reggie.common;

/**
 * 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
 */
public class BaseContext {
    private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    /**
     * 设置值
     * @param id
     */
    public static void setCurrentId(Long id){
        threadLocal.set(id);
    }

    /**
     * 获取值
     * @return
     */
    public static Long getCurrentId(){
        return threadLocal.get();
    }
}

前端 category/list.html 弹窗与提交

html 复制代码
<el-dialog
  :title="classData.title"
  :visible.sync="dialogVisible"
  width="30%"
>
  <el-form
    ref="classData"
    :model="classData"
    label-width="100px"
  >
    <el-form-item label="分类名称:" prop="name">
      <el-input
        v-model="classData.name"
        placeholder="请输入分类名称"
        maxlength="14"
      />
    </el-form-item>
    <el-form-item label="排序:" prop="sort">
      <el-input
        v-model="classData.sort"
        placeholder="请输入排序"
        type="number"
      />
    </el-form-item>
  </el-form>
  <span slot="footer" class="dialog-footer">
    <el-button @click="dialogVisible = false">取 消</el-button>
    <el-button type="primary" @click="submitForm()">确 定</el-button>
  </span>
</el-dialog>

前端 category.js

js 复制代码
// 修改分类
function editCategory(params) {
  return $axios({
    url: '/category',
    method: 'put',
    data: { ...params }
  })
}

API 速览

API 所属框架 作用
@PutMapping Spring MVC 限定 PUT 方法
@RequestBody Spring MVC JSON 请求体反序列化为对象
IService.updateById(T) MyBatis-Plus 根据主键更新,只更新非 null 字段
IService.update(Wrapper) MyBatis-Plus 按条件更新,可显式 setnull
MetaObjectHandler MyBatis-Plus 元对象处理器接口,实现自动填充
MetaObject.setValue(String, Object) MyBatis 为实体属性设值
FieldFill.INSERT_UPDATE MyBatis-Plus 插入与更新时均填充
@TableField(fill = ...) MyBatis-Plus 标记字段的填充时机
FieldStrategy.NOT_NULL MyBatis-Plus 默认更新策略,忽略 null 字段
R.success(String) 本项目 返回成功响应
v-model Vue 表单元素与数据的双向绑定
scope.row Element UI 表格插槽暴露的当前行数据

官方文档

总结

这篇文章主要讲了分类管理里「修改」功能的前后端实现,核心就三件事:

前端回显不靠后端 。点「修改」时,editHandle(scope.row) 直接把列表里已有的整行数据塞给弹窗,靠 v-model 双向绑定自动显示,全程没有发请求。很多人以为要写个「根据 id 查询」的接口,其实不需要。

后端一行代码搞定更新@PutMapping + @RequestBody 接住 JSON,updateById(category) 就完事了。这里有个坑要注意:updateById 默认只更新非 null 字段,所以前端没传的字段不会被覆盖成空值,这是好事。但如果真想把某个字段置空,默认策略做不到,得用 LambdaUpdateWrapper 显式 set

公共字段自动填充updateTimeupdateUser 不用手动 setMyMetaObjecthandlerupdateFill 会在更新时自动补上。前提是实体字段上标了 @TableField(fill = FieldFill.INSERT_UPDATE),两个条件缺一不可。

最后提醒一句:提交时一定要保证 id 有值,否则 updateById 没有 WHERE 条件,会全表更新,这是很危险的操作。

相关推荐
Web极客码2 小时前
抵御隐蔽的慢速连接攻击:我用 Claude 编写 nftables 规则,精准压制 HTTP Slo
服务器·copilot
fanxiaohui121382 小时前
2026年长效耐用磁吸充电宝推荐|南孚传应解锁安心出游
服务器·人工智能·手机·电池·南孚传应
Gauss松鼠会3 小时前
GaussDB 系统表与系统视图详解(内网运维版)
运维·服务器·网络·数据库·gaussdb·经验总结
小小de风呀3 小时前
de风——【从零开始学习Linux】(五):gcc编译器的基本使用
linux·运维·服务器
数智工坊3 小时前
RustDesk 自建服务器教程
运维·服务器
Julien20043 小时前
Docker 容器技术的本质
运维·服务器·ssh·学习方法
江屿风4 小时前
【Linux系统】【Linux 进程程序替换机制解析及自定义 Shell 核心逻辑实现 】流食般投喂
linux·运维·服务器·开发语言·笔记
好不玩呀4 小时前
一次PXE服务器上IPV6问题的记录
linux·运维·服务器
PiaoKe___6 小时前
云手机原理与 Python 自动化实战:ADB 批量控制、任务调度与落地建议
服务器·arm开发·python·自动化