真实业务环境-需求分析思路(二)

用户管理模块优化

先聊一下写这次需求的感想,起初接下这个需求的时候,给我的感觉就是很简单,并且觉得代码三天不到就可以写完,即使是在业务不熟悉的情况下。然后就是经历了,第三方沟通需求、确定技术方案、熟悉用户管理涉及到的多个模块的业务细节、刷数SQL、优化代码避免出现超时......测试人员给代码提bug,优化、与第三方确定实现效果上线日期、确定刷数SQL是否可在上线中使用、上线确定等。大概就是这一套下来之后我才知道我有多天真!!! 你们应该能猜到我现在的心理变化,emmmm。不过,这次经历的好处是,我确实提升了自己的业务理解和代码能力,而且也验证了之前学到的知识是有用的,感觉还是挺不错的。

下面就写记录一下用户管理模块的实现思路~

需求分析

查询

(1)查询条件新增所属系统,可多选;选中多个时,数据中有任意一个则显示。

(2)列表增加所属系统,字段用 ',' 相隔。

添加

(1)所属系统:必填,多选。

(2)角色:可选角色根据所选系统动态渲染。

修改

(1)所属系统:必填,多选。

(2)角色:可选角色根据所选系统动态渲染。

(3)用户省份、地市、所属系统变更。

用户省份、地市、所属系统变更时校验用户是否为智联开发负责人(智联变成其他系统类型)、工单受理组人员、第一接单人、督办第一接单人、工单升级人员,根据用户身份弹窗提示。例:当前用户已配置对应区域工单受理组,请删除工单受理组配置后处理。

批量删除、删除

删除用户时校验用户是否为智联开发负责人、工单受理组人员、第一接单人、督办第一接单人、工单升级人员,根据用户身份弹窗提示。例:当前用户已配置对应区域工单受理组,请删除工单受理组配置后处理。

用户参与工单存在在途工单,不允许删除用户。提示:当前用户存在在途工单,不允许删除!

导出

导出字段加上所属系统

技术方案

在和三方沟通后确定了第一种技术方案

方案一:直接在t_user表中新增 “所属系统” 字段

优点:

简单易实现:直接在现有表中新增字段,不需要创建新的表或复杂的关联
查询方便:可以直接通过现有的查询条件获取信息
性能较高:在查询和更新时,性能较高,因为没有额外的表连接操作

缺点:

扩展性差: 如果将来需要增加更多的系统类型,字段长度限制可能会成为问题。
数据冗余: 如果一个用户属于多个系统,需要通过分隔符存储多个系统类型,这会导致数据冗余和维护困难。
数据一致性问题: 在更新和删除操作时,可能会因为字符串解析问题导致数据不一致。

方案二:新增t_user_systemType用户和所属系统字段中间表

优点:

扩展性强: 可以灵活地增加更多的系统类型,不受字段长度限制。
数据一致性高: 各个系统类型分开存储,避免了字符串解析问题,保证数据的一致性和完整性。
易于维护: 系统类型的增删改查操作更加灵活和简便。

缺点:

复杂性增加: 需要新增表和外键,增加了数据模型的复杂性。
性能开销: 查询时需要进行表连接操作,可能会影响性能。

实现思路

背景:在之前的版本中也是存在所属系统字段的,只不过这个所属系统字段是在角色表中存放,和角色绑定。之前的所属系统的赋值是通过用户获取角色列表,然后拿到角色列表中权限最大的角色,进而拿到最大角色对应的所属系统字段,这个逻辑明显不适用在这里。

总体上思考一下需求,要完成这次需求我们要做什么,哪些技术点需要思考,哪些细节需要注意等等。

  • 查询:开发人员(我)和 三方沟通确定需求后,确认直接在t_user中添加所属系统字段。
    • 第一个点就是历史数据如何保证?原本存在的数据的所属系统的值如何赋值?(保证历史数据的准确性之后,其他的才有意义,否则没有实现的意义)
    • 前端后端实现细节,前端传的所属系统是String类型,且不同系统之间用逗号隔开,后端存储所属系统字段也是。那么查询就是多对多,这个如何解决?
    • 前端所属系统下拉框中的数据如何获取?直接从字典中拿所有的数据?
  • 新增:
    • 新增的对话框中所属系统和角色之间存在映射关系,不同的所属系统角色列表就不同
  • 修改:由于原本这块的逻辑有很大的问题,所以这里很多逻辑都要改
    • 所属系统和角色列表有映射关系
    • 点击edit弹出修改的对话框,此时对话框中的所属系统和角色列表数据获取都是有问题,那么应该如何获取?
    • 校验被修改用户是否是智联开发负责人(所属系统从智联到其他系统时校验)、第一接单人、督办第一接单人、工单升级人员、工单受理组。(这里涉及好多个模块,校验不只是简单的校验,还需要考虑关联性)
    • 在校验的时候需要调用不同组件服务,这里需要foreign
    • 这里涉及到的不同的模块,比如第一接单人,需要新增删除第一接单人接口等
    • 报错提示信息应该如何设计?
  • 删除:
    • 删除时校验被删除用户是否存在在途工单以及校验第一接单人、督办、升级人员
    • 报错信息应该如何设计?
  • 导出
    • 导出新增所属系统字段,这里加个注解就好
    • 导出时做一个转义

以上就是这次需求的简单构思,构思的时候需要结合代码结合业务去分析,不然只能是空想,并且也会漏掉很多点。

这里因为我是主后端手,在写前端的逻辑时相对较慢,所以就先把后端的接口给写了,先把后端初步思考的做了,之后思路打开之后前端就也完成了。要知道我在写这个需求的时候,业务也不能算很熟悉,所以要在规定的时间内提测一个测试版本,还是有一定难度,不过,好歹是拿下来了,hhh

实现过程

历史数据处理

备份和回退数据这里不多说,历史数据的处理需要注意一下

根据实现思路中的背景我们知道 所属系统字段在t_role中存在,并且和角色绑定,所以可以通过用户身上的角色去拿到所属系统数据。

比如: AA角色是A系统,BB角色是B系统,CC角色是C系统的

那么当张三用户有AA、BB角色,那么张三用户的所属系统字段就应该是 A,B系统

查询出 system_type 和 user_id

去重

更新t_user表中的所属系统字段

#内连接
update table1 join (table2) on ... set ...


# 更新字段数据
UPDATE t_user u
JOIN (
	SELECT
		u.USER_ID,
		GROUP_CONCAT( DISTINCT r.SYSTEM_TYPE ) AS SYSTEM_TYPE 
	FROM
		t_user u
		LEFT JOIN t_user_role t ON u.USER_ID = t.USER_ID
		LEFT JOIN t_role r ON r.ROLE_ID = t.ROLE_ID 
	GROUP BY
		u.USER_ID 
	) temp ON u.USER_ID = temp.USER_ID 
	SET u.SYSTEM_TYPE = temp.SYSTEM_TYPE;

上线和用户管理这块相关的所有sql

# 备份表数据
CREATE TABLE t_user_0815 LIKE t_user;
INSERT INTO t_user_0815 ( SELECT * FROM t_user );


# 新增字段
ALTER TABLE t_user ADD COLUMN SYSTEM_TYPE VARCHAR ( 255 ) DEFAULT NULL COMMENT '所属系统';
# 更新字段数据
UPDATE t_user u
JOIN (
	SELECT
		u.USER_ID,
		GROUP_CONCAT( DISTINCT r.SYSTEM_TYPE ) AS SYSTEM_TYPE 
	FROM
		t_user u
		LEFT JOIN t_user_role t ON u.USER_ID = t.USER_ID
		LEFT JOIN t_role r ON r.ROLE_ID = t.ROLE_ID 
	GROUP BY
		u.USER_ID 
	) temp ON u.USER_ID = temp.USER_ID 
	SET u.SYSTEM_TYPE = temp.SYSTEM_TYPE;

# 新增字段
ALTER TABLE t_user ADD COLUMN IS_VALID BIT(1);
# 更新字段数据
UPDATE t_user SET IS_VALID = 1;


#回退SQL
DROP TABLE t_user;
-- 重新创建 t_user 表并恢复数据
CREATE TABLE t_user LIKE t_user_0815;
INSERT INTO t_user (SELECT * FROM t_user_0815);

下面的具体功能实现,按照前后端,查询、新增、修改、删除、导出 这样的顺序去进行适当的记录


查询

新增查询条件 "所属系统" 、 列表数据显示增加 "所属系统" 字段

前端直接复用Vue和Element UI组件的页面代码,但是"所属系统" 字段是新增的,所以该字段的数据获取需要调后端接口

具体的语法含义可以去 element ui 官网查看 组件 | Element

# 查询条件中的所属系统
<el-select v-model="queryParams.systemType" clearable filterable multiple placeholder="所属系统" class="filter-item search-item">
  <el-option
    v-for="item in systemTypeList"
    :key="item.dictValue"
    :label="item.dictName"
    :value="item.dictValue"
  />
</el-select>

# 列表数据中的所属系统
<el-table-column label="所属系统" align="center" min-width="100px" show-overflow-tooltip>
    <template slot-scope="scope">
        <span>{{ translateSystemType(scope.row.systemType) }}</span>
    </template>
</el-table-column>

下面是所属系统数据的获取

在获取所属系统数据时,需要特别注意对后端返回的系统类型字段进行转换。后端返回的数据通常是一个逗号分隔的字符串,比如 "energy,zhuti,test",需要将其转换为对应的中文名称字符串。为实现这一转换,首先将字符串拆分为数组,再通过匹配系统类型列表,将每个英文代码转换为相应的中文名称,并最终将这些名称组合成一个完整的中文字符串。

// 请求参数对象,包括类型、用户名、地区、省市、时间范围、角色ID和所属系统字段
queryParams: {
  typename: '',  // 类型名称
  username: '',  // 用户名
  province: '',  // 省份
  city: '',      // 城市
  timeRange: '', // 时间范围
  roleId: [],    // 角色ID数组
  systemType: null // 所属系统字段,初始值为空
},
systemTypeList: [], // 初始化系统类型列表

    

// 初始化 systemTypeList 数据的方法
initSystemTypeList() {
  // 调用 getUserInfo 方法获取当前用户信息
  getUserInfo(this.currentUser.userId).then(res => {
    // 判断获取的用户系统类型数据是否为空
    if (res.data.data.systemType !== '') {
      // 如果不为空,将系统类型字符串转为数组
      const systemTypeArray = res.data.data.systemType.split(',');
      // 获取系统类型列表
      this.getSystemTypeList(systemTypeArray);
    } else {
      // 如果为空,显示错误信息
      this.$message.error("字典数据获取为空或者失败");
    }
  });
},

// 根据系统类型获取对应的系统类型列表 --- 比如 'energy' 变成 dictValue:'energy' 和 dictName:'能源'
getSystemTypeList(systemType) {
  // 遍历系统类型数组
  systemType.forEach(systemType => {
    // 如果系统类型为 'energy' 且未存在于 systemTypeList 中,则将其添加
    if (systemType === 'energy') {
      if (!this.systemTypeList.find(item => item.dictValue === 'energy')) {
        this.systemTypeList.push({ dictValue: systemType, dictName: '能源' });
      }
    }
    // 如果系统类型为 'yunguan' 且未存在于 systemTypeList 中,则将其添加
    if (systemType === 'yunguan') {
      if (!this.systemTypeList.find(item => item.dictValue === 'yunguan')) {
        this.systemTypeList.push({ dictValue: systemType, dictName: '行拓(智联)' });
      }
    }
    // 如果系统类型为 'yunyingshang' 且未存在于 systemTypeList 中,则将其添加
    if (systemType === 'yunyingshang') {
      if (!this.systemTypeList.find(item => item.dictValue === 'yunyingshang')) {
        this.systemTypeList.push({ dictValue: systemType, dictName: '主体' });
      }
    }
  });
},

// 将系统类型从英文代码翻译为对应的中文名称
translateSystemType(systemType) {
  if (!systemType) return ''; // 如果系统类型为空,返回空字符串
  // 将系统类型字符串按逗号分隔,并逐个查找对应的中文名称
  return systemType
    .split(',')
    .map(dictValue => {
      const item = this.systemTypeList.find(item => item.dictValue === dictValue);
      return item ? item.dictName : ''; // 找到对应的 dictCode 后返回 dictName,否则返回空字符串
    })
    .filter(name => name !== '') // 过滤掉空字符串
    .join(','); // 将结果连接成一个字符串
}

查询按钮,当我们点击查询时触发search方法

<el-button class="filter-item" type="primary" @click="search">
  {{ $t('table.search') }}
</el-button>

跳转search方法,在search方法中调用后端接口("/user/list")获取列表信息

// 请求参数对象,包括类型、用户名、地区、省市、时间范围、角色ID和所属系统字段
queryParams: {
  typename: '',  // 类型名称
  username: '',  // 用户名
  province: '',  // 省份
  city: '',      // 城市
  timeRange: '', // 时间范围
  roleId: [],    // 角色ID数组
  systemType: null // 所属系统字段,初始值为空
},

search() {
  this.fetch({
    ...this.queryParams,
    ...this.sort
  })
},

后端

后端User实体类中添加三个字段

为什么在实体类中添加systemType字段之后还需要添加systemTypeList、noSystemTypeList?

在上述技术方案中,选择在 t_user 表中新增一个字段来存储用户所属系统,而不是通过创建单独的表来维护用户与系统类型的关系。因此,在查询时需要处理多对多的关系。例如,查询条件为 "systemA,systemB",而数据库中的字段可能存储的是 "systemA,systemC,systemB"

两种解决方案(使用的第二种)

  1. 排列组合方案
    由于系统类型数量有限,并且未来扩展的可能性不大,可以将系统类型(如 "systemA""systemB")进行排列组合,生成所有可能的组合形式。例如,将三个系统类型映射为 0, 1, 2,然后生成组合如 01, 02, 12, 012,这些组合分别代表不同的系统类型组合。该方案的优点是可以实现精准查询,但随着系统类型的增加,组合数量和查询的复杂性也会增加。
  2. 包含与排除方案
    该方案使用 systemTypeListnoSystemTypeList 两个列表,分别表示需要包含和需要排除的系统类型。在查询时,通过检查数据库中所属系统字段是否包含 systemTypeList 中的类型,并且不包含 noSystemTypeList 中的类型,从而筛选出符合条件的数据。这种方式的优点是灵活性更高,能够适应更复杂的查询场景。
/**
 * 所属系统
 */
@TableField("SYSTEM_TYPE")
@ExcelField(value = "所属系统", required = true)
private String systemType;

/**
 * 查询条件:系统类型多选
 */
@TableField(exist = false)
private List<String> systemTypeList;

/**
 * 查询条件:当前登录用户不包含的系统类型列表
 */
@TableField(exist = false)
private List<String> noSystemTypeList;

SQL部分逻辑

<if test="user.systemTypeList != null and user.systemTypeList.size() > 0">
    AND (
        <foreach item="type" collection="user.systemTypeList" separator="or">
            FIND_IN_SET(#{type}, u.system_type)
        </foreach>
        )
</if>
<if test="user.noSystemTypeList != null and user.noSystemTypeList.size() > 0">
    AND
    <foreach item="type" collection="user.noSystemTypeList" separator="and">
        u.system_type not like CONCAT('%',#{type},'%')
    </foreach>
</if>

新增

前端优化直接参考修改处

修改

前端

角色列表数据根据所属系统和当前登录用户的最大角色权限动态获取展示

父组件中引入子组件对话框(父子组件之间的交互)

当我们点击修改按钮后,isVisible属性置为true,弹出修改对话框,并将用户数据赋值到子组件的属性中

<user-edit
  ref="edit"
  :dialog-visible="dialog.isVisible"
  :title="dialog.title"
  @success="editSuccess"
  @close="editClose"
/>

editClose() {
  # 关闭对话框
  this.dialog.isVisible = false
},
editSuccess() {
  #修改成功后,查询页面列表数据
  this.search()
}

在下面的edit方法中,赋值角色、部门信息,并且调用子组件中的setUser用户给属性赋值

edit(row) {
  let roleId = []
  if (row.roleId && typeof row.roleId === 'string') {
    roleId = row.roleId.split(',')
    #给row本行数据中的roleId赋值
    row.roleId = roleId
  }
  this.$get(`system/user/${row.userId}`).then((r) => {
    #部门ids赋值
    row.deptIds = r.data.data
    #用户信息赋值
    this.$refs.edit.setUser(row)
    this.$refs.edit.setSubFlag(false)
    this.dialog.title = this.$t('common.edit')
    this.dialog.isVisible = true
  })
},

子组件(修改对话框)

在子组件中要想在改变所属系统字段时,角色列表展示信息更新,只需要在设置一个@change="systemTypeChange"方法,该方法参数是systemType,然后调后端接口获取橘色列表信息

<el-select v-model="user.systemType" value="" multiple placeholder="" style="width:100%" @change="systemTypeChange">

后端

修改后端接口实现思路:

1.更改省份、地市时,需校验用户是否是第一接单人、督办第一接单人、工单升级人员,如果是则直接回显提示信息,如果不是则修改成功
2.禁用账号状态时,需校验用户是否是第一接单人、督办第一接单人、工单升级人员、工单受理组人员、用户在途工单(草稿,待评价,待处理等工单)
3.修改所属系统时,需校验所属系统是否是从智联 -> 其他系统,如果是则还需要校验智联开发负责人,如果不是则只需校验第一接单人、督办第一接单人、工单升级人员
4.删除用户时,需要校验用户是否是第一接单人、督办第一接单人、工单升级人员、工单受理组人员、用户在途工单(草稿,待评价,待处理等工单)

起初将这些校验逻辑写在了 update 接口中,但在本地运行时,接口经常会超时。下面我只展示最终的结果供大家参考。中间的过程就不详细赘述了,以免干扰大家

之前的文章中就说了,写需求首先就是要把思路理清,这样做才不会白费功夫

根据上面的校验内容可以把第一接单人、督办第一接单人、工单升级人员提在一个远程接口中复用,参数类型有userId,systemType,province,city

/**
 *  判断被修改用户是否是第一接单人、督办人、升级人员
 * @param userId
 * @param systemType
 * @param province
 * @param city
 * @return
 */
@GetMapping("/***/checkOtherBusiness")
public String checkOtherBusiness(@RequestParam("userId") String userId,
                          @RequestParam(value = "systemType",required = false) String systemType,
                          @RequestParam(value = "province",required = false) String province,
                          @RequestParam(value = "city",required = false) String city);

userId 参数是必填项,用于指定需要校验的用户,后面的参数可根据用户修改内容选择性填写。比如修改省份时,就传省份参数其他参数为空。

校验工单受理组和在途工单校验写了两个接口

/**
 * 远程调用 查询用户在途工单数量
 * @param userId
 * @return
 */
@GetMapping("/***/getJoinCount")
public Integer getJoinCount(@RequestParam(value = "userId",required = false) String userId);
/**
 * 根据userId在工单受理组中查询用户
 * @param userId
 * @return
 */
@GetMapping("/***/queryUserByAllProvince")
public List<ProEmployeeVo> queryUserByAllProvince(@RequestParam("userId") String userId);

接着就是主逻辑应该如何写

  1. 如果用户的修改是禁用账号,那么此时需要进行最全面的校验。若禁用状态的校验已经通过,则不再需要对省份、地市、所属系统等变更进行额外校验。因此,我设置了一个 boolean 值来控制这个逻辑。
  2. 省份或地市变更时,需要考虑省份变更是否影响地市的校验。由于铁塔总部在省份中,当省份变更为铁塔时,不需要进行额外校验。由于之前业务逻辑的漏洞,导致一个用户可以在不同省份拥有工单和身份。如果省份从北京变为上海,则只需要校验除上海以外的工单和身份。地市修改的逻辑相似,例如从北京朝阳区变为北京城区,则只需要校验除城区以外的身份和工单。
  3. 所属系统变更时,需要考虑系统是否从包含智联变为不包含智联,如果是这样,则需要校验用户在智联开发负责人角色中的身份。如果所属系统从智联、能源变为智联、主体,则需要校验用户在能源系统下是否仍有身份和工单。因为对于被修改的用户,新增系统通常不会有影响,而减少系统则可能带来影响,因此系统变更时,需要校验原本有但现在没有的系统。
  4. 校验信息的返回和打印,最初我使用了 StringBuilder,但考虑到省份、地市和所属系统都可能发生变更,调用的接口可能导致消息中出现重复信息。因此,我最终使用了 HashSet 来处理。
  5. 而且这里由于用户新增的涉及到的模块中的逻辑也需要更新

下面接口方法就是校验第一接单人、督办、升级,返回的String可能就是 "第一接单人、第一督办接单人、工单升级人员"

下面抽出来部分代码用于展示错误信息打印逻辑

// 定义返回结果的Msg
Set<String> errMsgSet = new HashSet<>();

String result = problemApiClient.checkOtherBusiness(String.valueOf(oldUser.getUserId()), null, null, null);
if (StringUtils.isNotBlank(result)) errMsgSet.addAll(Arrays.asList(result.split("、")));

// 检查用户是否有未处理的工单
joinCount = problemApiClient.getJoinCount(String.valueOf(user.getUserId()));
// 查询当前用户是否在工单受理组中
employeeVos = problemApiClient.queryUserByAllProvince(user.getUserId().toString());
if(employeeVos.size() > 0){
    errMsgSet.add("工单受理组人员");
}


StringBuilder resultErrMsg = new StringBuilder();
// 构建最终的错误信息并抛出异常
if (!errMsgSet.isEmpty() && joinCount > 0) {
  resultErrMsg.append("当前用户为").append(String.join("、", errMsgSet)).append(",并且存在").append(joinCount).append("个在途工单,无法进行修改操作。");
} else if (!errMsgSet.isEmpty()) {
  resultErrMsg.append("当前用户为").append(String.join("、", errMsgSet)).append(",无法进行修改操作。");
} else if (joinCount > 0) {
  resultErrMsg.append("当前用户存在 ").append(joinCount).append(" 个在途工单,无法进行修改操作。");
}
if(StringUtils.isNotBlank(resultErrMsg)) throw new FebsRuntimeException(resultErrMsg.toString().trim());

修改逻辑的总结,因为是需求写完之后写的笔记,所以其实在写的过程中一些比较好的点可能在写的时候没考虑到,所以只记录了部分逻辑

删除

这里可类比修改,调的接口都一样,只是错误打印的内容需要更改

导出

加注解,然后就是导出的时候做了转义

// 所属系统字段转义
List<SystemUser> users = this.baseMapper.exportUserDetail(user);
users.forEach(SystemUser::translateSystemType);
public void translateSystemType(){
    if(StringUtils.isNotEmpty(this.systemType)){
        this.systemType = Arrays.stream(this.systemType.split(","))
                .map(systemType  -> {
                    switch (systemType){
                        case "energy":
                            return "能源";
                        case "yunguan":
                            return "智联";
                        case "yunyingshang":
                            return "主体";
                        default:
                            return systemType;
                    }
                })
                .collect(Collectors.joining(","));
    }
}

需求测试

测试人员测试,提bug,然后开发人员修改代码

版本上线

和三方沟通确认功能,确认上线时间,上线版本的需求

知识总结

这里把接口调用写在JS文件中调用,不要直接写在代码中

好处:

首先,最大的好处就是代码复用,这点毋庸置疑。其次,分离业务逻辑代码和接口调用逻辑有助于解耦合,使代码结构更加清晰。再者,考虑到后期的维护性,如果接口调用需要修改,只需要在一个地方进行调整,而不必逐一修改所有调用该接口的代码。

http请求的严格区分

get、post、put、delete?_删除用get还是post-CSDN博客

因为再写删除第一接单人的时候用的是GET请求,这里是不好的

所有 http 请求,一律用 POST,在业务功能的实现是没有问题的.

post,get,put,delete 是标准, 大家都遵循这样的规则. 这样的api对于它人来说一目了然, get就是获取数据, post就是提交数据, put就是更新数据, delete就做删除操作. 如果一律使用post对一个项目组的内部人员来说是没有问题的, 但是对于对外公开的接口就让调用者摸不着头脑了。

以遵循 RFC-2616 所定义的协议的方式显式地使用 HTTP 方法,建立创建、检索、更新和删除(CRUD:Create, Retrieve, Update and Delete)操作与 HTTP 方法之间的一对一映射:

  • 若要在服务器上创建资源,应该使用 POST 方法;
  • 若要检索某个资源,应该使用 GET 方法;
  • 若要更改资源状态或对其进行更新,应该使用 PUT 方法;
  • 若要删除某个资源,应该使用 DELETE 方法。

@RequestParam和@PathVariable

请求参数和路径参数

相同点与区别

@RequestParam和@PathVariable都能够完成类似的功能——因为本质上,它们都是用户的输入,只不过输入的部分不同,一个在URL路径部分,另一个在参数部分。要访问一篇博客文章,这两种URL设计都是可以的:

  • 通过@PathVariable,例如/blogs/1
  • 通过@RequestParam,例如blogs?blogId=1

那么究竟应该选择哪一种呢?建议:

1、当URL指向的是某一具体业务资源(或资源列表),例如博客,用户时,使用@PathVariable

2、当URL需要对资源或者资源列表进行过滤,筛选时,用@RequestParam

例如我们会这样设计URL:

  • /blogs/
  • /blogs?state=publish而不是/blogs/state/publish来表示处于发布状态的博客文章

更多用法

一旦我们在方法中定义了@RequestParam变量,如果访问的URL中不带有相应的参数,就会抛出异常——这是显然的,Spring尝试帮我们进行绑定,然而没有成功。但有的时候,参数确实不一定永远都存在,这时我们可以通过定义required属性:

@RequestParam(value = "id", required = false)

当然,在参数不存在的情况下,可能希望变量有一个默认值:

@RequestParam(value = "id", required = false, defaultValue = "0")

前端父子组件之间的交互

这里父组件引入子组件

<user-edit
  ref="edit"
  :dialog-visible="dialog.isVisible"
  :title="dialog.title"
  @success="editSuccess"
  @close="editClose"
/>
<user-view
  ref="view"
  :dialog-visible="userViewVisible"
  @close="viewClose"
/>

import UserEdit from './Edit'
import UserView from './View'
ref="edit":ref 属性用于给组件实例一个引用名称。通过 this.$refs.edit 可以在父组件中访问这个 user-edit 组件实例。

:dialog-visible="dialog.isVisible":dialog-visible 是一个属性绑定,dialog.isVisible 是父组件中的一个布尔值,控制 user-edit 组件中的对话框是否可见。使用了 : 来动态绑定父组件中的 dialog.isVisible 数据。

:title="dialog.title":类似地,title 也是一个属性绑定,传递的是 dialog.title,用于设置 user-edit 组件中的标题。

@success="editSuccess":@ 是 Vue.js 事件绑定的简写形式。这个绑定表示,当 user-edit 组件触发 success 事件时,父组件的 editSuccess 方法会被调用。
@close="editClose":同样,当 user-edit 组件触发 close 事件时,父组件的 editClose 方法会被调用。

典型场景

  • 打开编辑对话框:用户在页面中点击“编辑”按钮,父组件将 dialog.isVisible 设置为 true,从而显示 <user-edit> 组件的对话框。
  • 成功保存:在 <user-edit> 组件中,用户成功保存信息时,组件会触发 success 事件,父组件的 editSuccess 方法被调用,可能会刷新用户列表或进行其他逻辑处理。
  • 关闭对话框:无论是 <user-edit> 还是 <user-view>,当用户关闭对话框时,组件会触发 close 事件,父组件会相应地将对话框的可见状态设置为 false

这里是如何获取row一行数据的

row 参数是通过 el-table 组件的 slot-scope 传递给插槽内的模板的

好吧什么是插槽?

对象展开语法

...val 表示将对象 val 的所有属性逐一展开到新的对象中。

{ ...val } 创建了一个新对象,这个新对象包含了 val 对象的所有属性和对应的值。

前端store的学习

vuex之store的基本使用

Arrays.asList返回的List无法add和remove

分析源码就知道了为什么了,这里我会写一篇新的

关于StringBuilder 出现null的问题

在写需求时出现了一个奇怪的报错,当problemApiClient调用checkOtherBusiness方法返回的String对象为空时,也就是null

然后调用roleErrMsg.append(result) 的时候会让这个StringBuilder对象中存储值 "null" ,导致不为空

我这里复现失败

https://blog.csdn.net/Mikeoperfect/article/details/106739567