@PostMapping("/create")
@Operation(summary = "新增用户")
@PreAuthorize("@ss.hasPermission('system:user:create')")
public CommonResult createUser(@Valid @RequestBody UserSaveReqVO reqVO) {
Long id = userService.createUser(reqVO);
return success(id);
}
@Operation
这是 Swagger 的注解,提供 API 文档的描述。
@PreAuthorize("@ss.hasPermission('system:user:create')")
Spring Security 的注解,用于权限控制。
public CommonResult
@Valid
:用于启用 JSR-303/JSR-380 的验证机制,表示请求体中的参数会根据 UserSaveReqVO
类中定义的校验规则进行校验。比如,@NotBlank
、@Size
等校验注解会生效,若校验失败会抛出 MethodArgumentNotValidException
异常,返回错误信息给客户端。
@Schema(description = "管理后台 - 用户创建/修改 Request VO")
@Data
public class UserSaveReqVO {
@Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao")
@NotBlank(message = "用户账号不能为空")
@Pattern(regexp = "^[a-zA-Z0-9]{4,30}$", message = "用户账号由 数字、字母 组成")
@Size(min = 4, max = 30, message = "用户账号长度为 4-30 个字符")
@DiffLogField(name = "用户账号")
private String username;
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String password;
@AssertTrue(message = "密码不能为空")
@JsonIgnore
public boolean isPasswordValid() {
return id != null // 修改时,不需要传递
|| (ObjectUtil.isAllNotEmpty(password)); // 新增时,必须都传递 password
}
}
上文@Valid注解,会根据 UserSaveReqVO
类中定义的校验规则进行校验;
@Schema
: Swagger/OpenAPI 用于描述模型类字段的注解,它通常用于生成 API 文档中字段的描述。requiredMode = Schema.RequiredMode.REQUIRED
用于标记该字段为必填字段,表示在生成的 API 文档中,该字段是必填的。
@NotBlank:确保 username不能为空,若为空则抛出自定义消息 "用户账号不能为空"。
@Pattern:限制用户名的格式,regexp属性指定了一个正则表达式,要求用户名只能由 字母和数字 组成,且长度在 4 到 30 个字符之间。
@Size:限制用户名的长度为 4 到 30 个字符。
@DiffLogField:这是一个自定义注解,通常用于日志记录。它标记该字段需要在日志中记录变更信息,并且 name参数用于设置字段的名称,以便日志更具可读性。
@AssertTrue:这个注解用于验证方法的返回值必须为 true,如果验证失败,将抛出错误信息 "密码不能为空"
@JsonIgnore:这个注解表示 isPasswordValid方法的返回值不会被序列化为 JSON 数据。它通常用于那些在传输时不需要的验证方法。
- 字段验证:如
@NotBlank
、@Size
、@Pattern
等注解用于验证字段输入是否合法。- 日志记录:使用
@DiffLogField
来记录字段的变化,确保在变更时能够记录相关的日志信息。- 密码验证:通过
isPasswordValid()
方法确保新增用户时必须提供密码,而更新时则不需要。
@Target({
ElementType.METHOD,
ElementType.FIELD,
ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR,
ElementType.PARAMETER,
ElementType.TYPE_USE
})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
validatedBy = MobileValidator.class
)
public @interface Mobile {
String message() default "手机号格式不正确";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
}
自定义注解 Mobile
,用于验证手机号的格式是否正确。这个注解本质上是一个约束(constraint),用来在 Java Bean Validation 框架中进行自定义验证。
@Target
注解定义了 @Mobile
注解可以应用的 Java 元素类型。这里表示它可以应用于以下元素:
ElementType.METHOD
:方法。ElementType.FIELD
:字段。ElementType.ANNOTATION_TYPE
:注解类型。ElementType.CONSTRUCTOR
:构造方法。ElementType.PARAMETER
:方法参数。ElementType.TYPE_USE
:类型使用(如泛型类型、局部变量等)。这意味着你可以在类的字段、方法参数、构造方法、注解类型等地方使用 @Mobile
注解。
@Retention
定义了该注解的生命周期。在这里,RUNTIME
表示该注解会在运行时被保留,因此可以在运行时通过反射获取到 @Mobile
注解的信息。@Mobile
注解在运行时可以用于执行验证。
@Documented
表示该注解会出现在 Javadoc 文档中。如果将 @Mobile
应用于某个类或字段,它会出现在该类或字段的 Javadoc 中。
message
属性是一个字符串,用来定义验证失败时的错误消息。默认值是 "手机号格式不正确"
。当手机号格式验证失败时,框架会返回这个错误消息。
groups
属性用于分组验证。通过指定不同的分组,可以在不同的场景下执行不同的验证。例如,在创建时验证某些字段,在更新时验证其他字段。
payload
属性用于携带额外的元数据。它可以用于传递一些信息给验证器,通常情况下,这个字段可以为空。它通常与集成第三方系统时使用,或者是用来附加附加的信息给验证器类。
public class MobileValidator implements ConstraintValidator {
@Override
public void initialize(Mobile annotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 如果手机号为空,默认不校验,即校验通过
if (StrUtil.isEmpty(value)) {
return true;
}
// 校验手机
return ValidationUtils.isMobile(value);
}
}
Mobile
:是该校验器适用于的注解类型。String
:表示该校验器适用于字符串类型的字段(即手机号字段是字符串类型)。initialize(Mobile annotation)
:这是 ConstraintValidator
接口中的方法。它在校验器创建时被调用,可以在这里进行初始化操作。对于 @Mobile
注解,不需要做任何初始化工作,因此该方法为空。isValid(String value, ConstraintValidatorContext context)
:这是 ConstraintValidator
接口中的核心方法,实际执行验证的地方。value
:是被校验的字段值,这里就是传入的手机号(一个字符串)。context
:是校验的上下文,包含了校验相关的附加信息,通常用于生成错误信息或报告校验失败等。private static final Pattern PATTERN_MOBILE =
Pattern.compile("^(?:(?:\\+|00)86)?1(?:(?:3[\\d])
|(?:4[0,1,4-9])
|(?:5[0-3,5-9])
|(?:6[2,5-7])
|(?:7[0-8])
|(?:8[\\d])
|(?:9[0-3,5-9]))\\d{8}$");
@Override
@Transactional(rollbackFor = Exception.class)
@LogRecord(type = SYSTEM_USER_TYPE, subType = SYSTEM_USER_CREATE_SUB_TYPE, bizNo = "{{#user.id}}",
success = SYSTEM_USER_CREATE_SUCCESS)
public Long createUser(UserSaveReqVO createReqVO) {
// 1.1 校验账户配合
tenantService.handleTenantInfo(tenant -> {
long count = userMapper.selectCount();
if (count >= tenant.getAccountCount()) {
throw exception(USER_COUNT_MAX, tenant.getAccountCount());
}
});
// 1.2 校验正确性
validateUserForCreateOrUpdate(null, createReqVO.getUsername(),
createReqVO.getMobile(), createReqVO.getEmail(), createReqVO.getDeptId(), createReqVO.getPostIds());
// 2.1 插入用户
AdminUserDO user = BeanUtils.toBean(createReqVO, AdminUserDO.class);
user.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 默认开启
user.setPassword(encodePassword(createReqVO.getPassword())); // 加密密码
userMapper.insert(user);
// 2.2 插入关联岗位
if (CollectionUtil.isNotEmpty(user.getPostIds())) {
userPostMapper.insertBatch(convertList(user.getPostIds(),
postId -> new UserPostDO().setUserId(user.getId()).setPostId(postId)));
}
// 3. 记录操作日志上下文
LogRecordContext.putVariable("user", user);
return user.getId();
}
@LogRecord
:自定义的注解,用于记录操作日志。日志记录的内容包括:
type
:操作的类型(如 SYSTEM_USER_TYPE
)。subType
:子类型(如 SYSTEM_USER_CREATE_SUB_TYPE
)。bizNo
:业务编号,这里通过 {{#user.id}}
动态获取用户的 id
值。success
:操作是否成功,这里是常量 SYSTEM_USER_CREATE_SUCCESS
。tenantService.handleTenantInfo
:处理租户信息,这里是通过 tenant
来获取租户的账户信息。userMapper.selectCount()
:查询当前用户表中已有用户的数量。if (count >= tenant.getAccountCount())
:如果当前已有用户数量超过租户允许的最大账户数,则抛出异常,阻止创建新用户。private AdminUserDO validateUserForCreateOrUpdate(Long id, String username, String mobile, String email,
Long deptId, Set postIds) {
// 关闭数据权限,避免因为没有数据权限,查询不到数据,进而导致唯一校验不正确
return DataPermissionUtils.executeIgnore(() -> {
// 校验用户存在
AdminUserDO user = validateUserExists(id);
// 校验用户名唯一
validateUsernameUnique(id, username);
// 校验手机号唯一
validateMobileUnique(id, mobile);
// 校验邮箱唯一
validateEmailUnique(id, email);
// 校验部门处于开启状态
deptService.validateDeptList(CollectionUtils.singleton(deptId));
// 校验岗位处于开启状态
postService.validatePostList(postIds);
return user;
});
}
DataPermissionUtils.executeIgnore
是一个工具类,调用该方法的目的是在执行校验时忽略数据权限的限制。通常,数据权限的校验可能导致某些数据查询不到,这里显式关闭数据权限,以确保校验操作能够顺利进行,避免因数据权限问题导致的校验失败。
通过后则数据都正确,可以进行插入;
BeanUtils.toBean(createReqVO, AdminUserDO.class)
:使用 BeanUtils
工具将请求对象(createReqVO
)转换为数据库实体类对象(AdminUserDO
)。user.setStatus(CommonStatusEnum.ENABLE.getStatus())
:设置用户的状态为启用,假设 CommonStatusEnum.ENABLE.getStatus()
返回的是用户启用的状态值。user.setPassword(encodePassword(createReqVO.getPassword()))
:对传入的密码进行加密,保证安全性。userMapper.insert(user)
:将用户信息插入到数据库中。user.getPostIds()
:获取用户关联的岗位 ID 列表。if (CollectionUtil.isNotEmpty(user.getPostIds()))
:判断岗位 ID 列表是否为空,只有当该列表非空时,才会执行插入操作。userPostMapper.insertBatch(...)
:批量插入用户与岗位的关联关系,使用 convertList
方法将岗位 ID 列表转换为 UserPostDO
对象,然后插入到 user_post
表中。LogRecordContext.putVariable("user", user)
:将创建的 user
对象放入日志记录上下文中,方便后续的日志记录使用。
- 校验租户的账户配额,防止超过最大账户数。
- 校验用户的基本信息(如用户名、手机号、邮箱等)。
- 插入用户信息并加密密码。
- 如果用户关联了岗位,插入用户与岗位的关联数据。
- 记录操作日志。
- 返回新创建用户的 ID。