参数校验、时间传参
这篇解决什么
接口入参要在入库前拦住空值、格式和枚举范围;日期字段还要和前端 DatePicker 对上。读完能在 VO 上挂 jakarta.validation 注解、写自定义校验,以及按 Query / JSON 两种通道传时间。
默认端口 48080,管理端前缀 /admin-api。校验失败走全局异常,返回 code = 400,见 异常处理。VO 分层见 对象转换。
示意图:Controller 触发校验,失败由 GlobalExceptionHandler 收成 400。
失败响应形如:
{
"code": 400,
"data": null,
"msg": "请求参数不正确:密码不能为空"
}校验注解
依赖已在 Web Starter 里,一般不用再加:
| 名称 | 说明 | 仓库路径 |
|---|---|---|
spring-boot-starter-validation | Hibernate Validator,包名 jakarta.validation | ruoyi-office/yudao-framework/yudao-spring-boot-starter-web/pom.xml |
@Validated | 开方法级校验,打在 Controller / Service 类上 | 例如 AuthController |
@Valid | 级联校验 Bean 入参 | 方法参数上 |
不要再写 javax.validation,Spring Boot 3 只认 Jakarta。
常用注解
多数来自 jakarta.validation.constraints。@Length、@Range、@URL 在 org.hibernate.validator.constraints。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
@NotBlank | 字符串非 null,trim 后长度大于 0 | 标准库 |
@NotEmpty | 字符串 / 集合 / 数组非空 | AuthLoginReqVO.username |
@NotNull | 不能为 null | SealSaveReqVO.companyId |
@Pattern | 正则 | AuthLoginReqVO.username |
@Size / @Length | 长度或集合大小 | UserSaveReqVO.email、AuthLoginReqVO.password |
@Min / @Max / @Range | 数值范围 | TenantSaveReqVO.accountCount |
@Email | 邮箱 | UserSaveReqVO.email |
@AssertTrue / @AssertFalse | 布尔或组合校验方法 | AuthLoginReqVO.isSocialCodeValid |
不常用注解
数字精度、正负号、日期先后一般用下面这些,按字段语义选即可。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
@Null | 必须为 null | 标准库 |
@DecimalMin / @DecimalMax / @Digits | 小数范围与位数 | 标准库 |
@Positive / @PositiveOrZero | 正数、正数或 0 | 标准库 |
@Negative / @NegativeOrZero | 负数、负数或 0 | 标准库 |
@Future / @FutureOrPresent | 将来、将来或现在 | 标准库 |
@Past / @PastOrPresent | 过去、过去或现在 | 标准库 |
项目自定义注解
| 名称 | 说明 | 仓库路径 |
|---|---|---|
@Mobile | 手机号,空值默认通过 | yudao-framework/yudao-common/.../validation/Mobile.java |
@Telephone | 电话,空值默认通过 | 同包 Telephone.java |
@InEnum | 值必须落在 ArrayValuable 枚举 | 同包 InEnum.java |
@InDict | 值必须落在指定字典 type | yudao-spring-boot-starter-excel/.../dict/validation/InDict.java |
@Mobile 只判格式。必填时再叠 @NotEmpty,见 AuthSmsLoginReqVO。@InEnum 对 null 同样放行,必填再叠 @NotNull。
如何挂上校验
三步:依赖已引入、类上 @Validated、参数上 @Valid 或约束注解。
Bean 参数
AuthController 类上 @Validated,登录入参是 VO:
@RestController
@RequestMapping("/system/auth")
@Validated
public class AuthController {
@PostMapping("/login")
public CommonResult<AuthLoginRespVO> login(@RequestBody @Valid AuthLoginReqVO reqVO) {
return success(authService.login(reqVO));
}
}约束写在字段上。Lombok 很少手写 getter,注解不要只挂访问器:
public class AuthLoginReqVO extends CaptchaVerificationReqVO {
@NotEmpty(message = "登录账号不能为空")
@Length(min = 4, max = 30, message = "账号长度为 4-30 位")
@Pattern(regexp = "^[a-zA-Z0-9]{4,30}$", message = "账号格式为数字以及字母")
private String username;
@NotEmpty(message = "密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String password;
@InEnum(SocialTypeEnum.class)
private Integer socialType;
}路径:ruoyi-office/yudao-module-system/yudao-module-system-server/.../auth/。
简单类型
普通参数直接在形参上标约束。类上必须有 @Validated,否则不生效:
@PutMapping("/agree")
public CommonResult<Boolean> agreeFriendRequest(
@RequestParam("id") @NotNull(message = "申请编号不能为空") Long id) {
friendRequestService.agreeFriendRequest(getLoginUserId(), id);
return success(true);
}这段在 yudao-module-im-server 的 ImFriendRequestController。
Service 也要校验
Controller 拦得住 HTTP 入口。Service 还会被别的 Service 调用,实现类同样加 @Validated,方法参数加 @Valid 或约束。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
接口方法 @Valid | 登录 VO 再校验一遍 | AdminAuthService.login |
实现类 @Validated | 打开方法级校验代理 | 例如 PostServiceImpl |
| 手工校验 | 代码里立刻 validate | ValidationUtils.validate |
public interface AdminAuthService {
AuthLoginRespVO login(@Valid AuthLoginReqVO reqVO);
}
@Service
@Validated
public class PostServiceImpl implements PostService { }没有 @Validated 代理时,接口上的 @Valid 不会在内部调用里触发。
自定义注解
内置注解不够时,在 yudao-common 的 validation 包补一对「注解 + Validator」。以 @Mobile 为例。
@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 {};
}public class MobileValidator implements ConstraintValidator<Mobile, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (StrUtil.isEmpty(value)) {
return true;
}
return ValidationUtils.isMobile(value);
}
}正则在 ValidationUtils.PATTERN_MOBILE。短信登录叠必填:
public class AuthSmsLoginReqVO {
@NotEmpty(message = "手机号不能为空")
@Mobile
private String mobile;
}@InEnum 要求枚举实现 ArrayValuable。UserUpdateStatusReqVO.status 同时挂 @InEnum(CommonStatusEnum.class) 和 @InDict(type = DictTypeConstants.COMMON_STATUS)。
手工触发、分组校验用 ValidationUtils.validate(object, groups)。更完整的分组与 i18n 见 Hibernate Validator。
时间传参
yudao-server 打开了 spring.jackson.serialization.write-dates-as-timestamps: true。LocalDateTime 另有自定义毫秒序列化;LocalDate / LocalTime 没有,必须自己加 @JsonFormat。
常量在 cn.iocoder.yudao.framework.common.util.date.DateUtils:
| 名称 | 说明 | 仓库路径 |
|---|---|---|
FORMAT_YEAR_MONTH_DAY | yyyy-MM-dd | yudao-framework/yudao-common/.../date/DateUtils.java |
FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND | yyyy-MM-dd HH:mm:ss | 同上 |
示意图:Query 用字符串 + @DateTimeFormat;JSON 里 LocalDateTime 走毫秒,LocalDate 走 yyyy-MM-dd。
Query
GET 或 form-data 由 Spring MVC 绑字符串,字段加 @DateTimeFormat,不要用 @JsonFormat。
用户分页的创建时间:
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;访问日志的单个区间字段同样:
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] beginTime;印章台账的纯日期区间用 FORMAT_YEAR_MONTH_DAY:
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
private LocalDate[] purchaseDate;前端列表筛选用 RangePicker,默认格式必须和常量一致:
{
fieldName: 'createTime',
label: '创建时间',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
},
}getRangePickerDefaultProps() 在 ruoyi-office-vben/apps/web-antd/src/utils/rangePickerProps.ts,valueFormat 是 YYYY-MM-DD HH:mm:ss。纯日期区间改成 YYYY-MM-DD,对齐 SealPageReqVO。
Request Body
POST / PUT 的 JSON 用 @RequestBody。LocalDateTime 默认收毫秒,不要加 @JsonFormat。
TenantSaveReqVO.expireTime:
@NotNull(message = "过期时间不能为空")
private LocalDateTime expireTime;租户表单对应:
{
fieldName: 'expireTime',
label: '过期时间',
component: 'DatePicker',
componentProps: {
format: 'YYYY-MM-DD',
valueFormat: 'x',
placeholder: '请选择过期时间',
},
}valueFormat: 'x' 表示提交 epoch millis。需要带时分秒时再加 showTime: true。
LocalDate 在 SaveReqVO / RespVO 上都要 @JsonFormat:
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate purchaseDate;印章页 DatePicker 用 format / valueFormat = 'YYYY-MM-DD'。LocalTime 同理,pattern 为 HH:mm:ss。
Response
TenantRespVO.createTime、expireTime 无 @JsonFormat,JSON 里是数字毫秒。前端列表用 formatDate() 展示,不要标成 Date。
个别字段要直接出字符串时,再加 @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)。TimestampLocalDateTimeSerializer 发现字段上有该注解,就按 pattern 写字符串,否则写 toEpochMilli()。
注册入口是 YudaoJacksonAutoConfiguration:
.serializerByType(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE)
.deserializerByType(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE)
.serializerByType(LocalDate.class, LocalDateSerializer.INSTANCE)
.deserializerByType(LocalDate.class, LocalDateDeserializer.INSTANCE)路径:ruoyi-office/yudao-framework/yudao-spring-boot-starter-web/.../jackson/config/YudaoJacksonAutoConfiguration.java。
毫秒没有产品格式味道,展示交给前端。不要为了省事改全局 LocalDateTime 为字符串,会拆掉现有 valueFormat: 'x'。
前后端配对
| 名称 | 说明 | 仓库路径 |
|---|---|---|
LocalDate | JSON 必须 @JsonFormat(yyyy-MM-dd),前端 YYYY-MM-DD,TS string | SealSaveReqVO / sealinfo/data.ts |
LocalTime | 必须 @JsonFormat(HH:mm:ss),前端 TimePicker | 打卡等字段 |
LocalDateTime 默认 | 不加 @JsonFormat,毫秒;前端 valueFormat: 'x',TS number | TenantSaveReqVO.expireTime |
LocalDateTime 字符串 | 加 @JsonFormat,前端 YYYY-MM-DD HH:mm:ss,TS string | 导出或只读展示 |
| PageReqVO 日期区间 | @DateTimeFormat,前端 RangePicker 字符串 | UserPageReqVO、SealPageReqVO |
LocalDate 漏了 JsonFormat
write-dates-as-timestamps: true 时,无注解的 LocalDate 会序列化成 [2026, 3, 19]。DatePicker 解析失败,控制台常见 date.locale is not a function。RespVO 和 SaveReqVO 都要补 @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY)。
时间选择器显示 NaN
后端 LocalDateTime 返回毫秒,前端却写了 valueFormat: 'YYYY-MM-DD HH:mm:ss',选择器会出 NaN 或空白。无 @JsonFormat 时必须 valueFormat: 'x'。
DateTimeFormat 和 JsonFormat
@DateTimeFormat 只服务 Query / form 绑定。@JsonFormat 只服务 JSON。分页区间用前者,Body / 响应用后者(或默认毫秒)。不要互换。
配置与操作
校验注解写在 ReqVO,Controller 加 @Valid。分页区间用 @DateTimeFormat,JSON 体用 @JsonFormat 或默认毫秒。没有校验菜单。
开启见 框架层。
