SpringBoot针对入参时间格式的校验

(1条消息) SpringBoot实践之(十四)— AOP拦截器 Aspect的使用_luckykapok918的博客-优快云博客

Spring Boot使用AOP的正确姿势 - James_Shangguan - 博客园 (cnblogs.com)

对于权限的校验以及时间格式的校验确实使用spring aop处理会比较方便。

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

定义用于拦截的controller

@RestController
@Slf4j
public class Controller {

    @PostMapping("/add")
    @ApiOperation(value = "新增")
    @ScheduleTimeVerifyTag
    public AjaxResult<Boolean> add(@RequestBody @Validated ShopReq req) {
        return shopService.add(req);
    }
}

定义时间校验Tag

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ScheduleTimeVerifyTag {
}

定义切面

@Component
@Aspect
@Slf4j
public class TimeVerifyAspect {

    @Before("@annotation(com.xxx.annotation.ScheduleTimeVerifyTag)")
    public void before(JoinPoint joinPoint) {

        List<String> list = Arrays.asList("businessStart","businessEnd","restTime1", "restTime2","latestWork","workingTimeStart","workingTimeEnd");

        //参数值
        Object[] paramValues = joinPoint.getArgs();
        for (String each : list) {

            //如果参数不存在
            if (Objects.isNull(ReflectUtil.getFieldValue(paramValues[0], each))) {
                log.warn("【参数{}不存在】", each);
                continue;
            }
            String fieldValue = ReflectUtil.getFieldValue(paramValues[0], each).toString();
            //如果参数值存在
            if (!StringUtils.isEmpty(fieldValue)) {
                String[] split1 = fieldValue.split("-");
                for (String s : split1) {
                    verify(each, s);
                }
            }
        }
    }

    /**
     * 校验时间格式
     *
     * @param field 字段名
     * @param time  时间
     */
    private void verify(String field, String time) {

        try {
            DateUtil.parse(time, "HH:mm:ss");
        } catch (Exception e) {
            log.warn("【{}时间{}格式校验失败】", field, time);
            throw new ServiceException(Constants.SERVER_ERROR, ErrorConstants.INVALID_TIME);
        }
    }
}

切点主要设置的是 Tag的相对路径,也可以使用 execution(具体可参考:Spring Boot使用AOP的正确姿势 - James_Shangguan - 博客园 (cnblogs.com))。不过我觉得加 tag会方便许多。

这个页面主要是拦截请求入参,获取相关的时间字段是否符合(HH:mm:ss) 格式,如果不符合则提示用户。这里使用到Hutool提供的反射工具类(ReflectUtil)获取请求对象的属性值,然后也是使用其DateUtil工具校验时间格式。

效果

579

附:解析获取请求参数的方法

/**
     * 获取参数map
     *
     * @param joinPoint
     * @return
     */
private Map<String, Object> getParamMap(JoinPoint joinPoint) {

    Map<String, Object> params = new HashMap<>();

    Object[] paramValues = joinPoint.getArgs();
    String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();

    for (int i = 0; i < paramNames.length; i++) {
        params.put(paramNames[i], paramValues[i]);
    }
    return params;
}

这里需要考虑不同的请求方式(GET POST),请求参数结构不同。

<think>我们正在为人力资源系统的三个接口(获取所有部门信息、通过部门id获取员工、通过工号获取员工信息)新增两个:操作用户(operator)和调用系统(calling_system)。根据需求: 1. 操作用户:如果是人为操作,传当前登录名;如果是系统触发,传"system"。 2. 调用系统:传系统码值,与PingCode所属系统保持一致,供应链码值为"SCP"。 3. 两个字段都是非必填。 我们将设计测试用例,覆盖以下方面: - 正常场景:有效的人为操作登录名、系统触发、有效的调用系统码值(特别是SCP) - 异常场景:无效的登录名、无效的系统码值、人为操作传"system"(错误使用) - 边界场景:超长字符串、特殊字符、大小写敏感 - 非必填场景:不传、只传其中一个 注意:根据引用[1][3][4],人力资源管理系统需要高效、准确,因此我们的测试要确保新增字段不影响原有功能,且符合业务规则。 测试用例设计如下(使用表格形式): ### 测试用例设计 | 用例编号 | 测试场景 | 输数 | 预期结果 | 验证点 | 适用接口 | |----------|--------------------------|-----------------------------------|------------------------------|----------------------------|------------------------------| | **正常场景** | | | | | | | TC001 | 人为操作有效登录名 | `operator="zhangsan"` | HTTP 200 + 正确业务数据 | 1. 接口返回成功<br>2. 日志记录操作者信息为"zhangsan" | 所有三个接口 | | TC002 | 系统触发场景 | `operator="system"` | HTTP 200 + 正确业务数据 | 1. 接口返回成功<br>2. 跳过人工权限检查(如有) | 所有三个接口 | | TC003 | 供应链系统调用 | `calling_system="SCP"` | HTTP 200 + 正确业务数据 | 1. 接口返回成功<br>2. 如果是SCP系统,可能返回数据格式有特殊处理(根据需求) | 所有三个接口(如果需求有特殊处理则验证) | | TC004 | 混合数有效 | `operator="lisi"&calling_system="SCP"` | HTTP 200 | 1. 接口返回成功<br>2. 日志同时记录操作者和调用系统 | 所有三个接口 | | **异常场景** | | | | | | | TC101 | 人为操作但传system(错误使用) | `operator="system"`(但实际是人为操作) | HTTP 400 + 错误码 | 返回错误信息,提示人为操作不能传"system" | 所有三个接口 | | TC102 | 无效调用系统码值 | `calling_system="INVALID"` | HTTP 400 + 错误码 | 返回错误信息,提示无效系统码值,需考PingCode | 所有三个接口 | | TC103 | 不存在的登录名 | `operator="unknown_user"` | HTTP 400 + 错误码 | 返回错误信息,提示用户不存在 | 所有三个接口(如果系统需要验证用户存在性) | | **边界场景** | | | | | | | TC201 | 超长登录名(超过系统允许长度) | `operator="a".repeat(256)` | HTTP 400 | 数长度校验失败 | 所有三个接口 | | TC202 | 特殊字符登录名 | `operator="admin@test.com"` | HTTP 200 | 登录名包含特殊字符,应正确处理 | 所有三个接口(如果系统允许) | | TC203 | 调用系统码值大小写 | `calling_system="ScP"` | HTTP 400 | 系统码值大小写敏感,应为大写 | 所有三个接口 | | **非必填场景** | | | | | | | TC301 | 省略所有新增数 | 不传operator和calling_system | HTTP 200 + 正确业务数据 | 1. 接口返回成功<br>2. 日志记录操作用户为null或默认值 | 所有三个接口 | | TC302 | 仅传操作用户 | `operator="wangwu"` | HTTP 200 + 正确业务数据 | 1. 接口返回成功<br>2. 调用系统为null或默认值 | 所有三个接口 | | TC303 | 仅传调用系统 | `calling_system="SCP"` | HTTP 200 + 正确业务数据 | 1. 接口返回成功<br>2. 操作用户为null或默认值 | 所有三个接口 | ### 说明: 1. 对于TC003:如果需求明确说明当调用系统为SCP时,返回的数据结构或内容有变化(比如供应链系统需要额外的字段),则需要在测试中验证。否则,只需验证接口不报错即可。 2. 对于TC101:根据需求,人为操作必须传登录名,不能传"system"。所以当人为操作传"system"时,应视为错误。 3. 对于TC103:如果系统在接口层不验证操作用户的存在性(可能由下游系统验证),则不需要此用例。但根据安全性和审计要求,建议验证。 4. 边界场景中,TC203:因为系统码值要求与PingCode保持一致(通常是大写),所以传大小写混合应视为无效。 ### 测试实现建议(Java + Spring Boot Test) ```java import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest @AutoConfigureMockMvc public class HrApiTest { @Autowired private MockMvc mockMvc; // TC001: 人为操作有效登录名 @Test public void testValidHumanOperator() throws Exception { mockMvc.perform(get("/departments") // 替换为实际接口路径 .param("operator", "zhangsan")) .andExpect(status().isOk()) .andExpect(jsonPath("$.departments").exists()); // 这里可以添加日志验证,通过模拟或捕获日志对象来验证 } // TC002: 系统触发场景 @Test public void testSystemOperator() throws Exception { mockMvc.perform(get("/employees/by-dept/{deptId}", 1) .param("operator", "system")) .andExpect(status().isOk()); // 验证是否跳过了权限检查(如果有权限检查,可以通过模拟权限验证服务来验证) } // TC102: 无效调用系统码值 @Test public void testInvalidSystemCode() throws Exception { mockMvc.perform(get("/employee/{id}", 1001) .param("calling_system", "INVALID")) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.errorCode").value("INVALID_SYSTEM_CODE")); } // TC301: 省略所有新增数 @Test public void testWithoutNewParams() throws Exception { mockMvc.perform(get("/departments")) .andExpect(status().isOk()); } } ``` ### 日志验证建议 如果系统需要记录审计日志,可以在测试中通过模拟审计服务(Mock)来验证日志记录是否正确: ```java @MockBean private AuditService auditService; @Test public void testAuditLogWhenHumanOperator() throws Exception { mockMvc.perform(get("/departments").param("operator", "zhangsan").param("calling_system", "SCP")); // 验证审计服务被调用,且数正确 ArgumentCaptor<AuditLog> captor = ArgumentCaptor.forClass(AuditLog.class); verify(auditService).log(captor.capture()); AuditLog log = captor.getValue(); assertEquals("zhangsan", log.getOperator()); assertEquals("SCP", log.getCallingSystem()); } ``` ### 注意事项 1. 确保测试覆盖所有三个接口。 2. 每个接口都需要执行类似的测试用例,因为新增数是全局的。 3. 根据实际业务需求,调整对调用系统为SCP时的特殊处理验证。 ### 相关问题 1. 在人力资源系统中,操作用户字段如何与现有权限系统集成? 2. 调用系统字段在微服务间传递时如何保证一致性? 3. 如何测试系统触发(operator="system")时跳过权限检查的逻辑? 4. 对于供应链系统(SCP)的特殊处理,如何设计测试数据以验证数据格式转换? 5. 在性能测试中,新增两个非必填字段对接口响应时间的影响如何评估?</think>### 人力资源系统接口测试用例设计 根据需求,针对三个接口(获取所有部门信息、通过部门id获取员工、通过工号获取员工信息)新增`operator`(操作用户)和`calling_system`(调用系统)字段的测试用例如下: #### 测试用例模板 ```markdown | 用例编号 | 测试场景 | 输数 | 预期结果 | 验证点 | 适用接口 | |----------|--------------------------|-----------------------------------|------------------------------|----------------------------|------------------------------| ``` --- ### 1. **正常功能验证** | 用例编号 | 测试场景 | 输数 | 预期结果 | 验证点 | 适用接口 | |----------|--------------------------|-----------------------------------|------------------------------|----------------------------|------------------------------| | TC001 | 人为操作有效登录名 | `operator="zhangsan"` | HTTP 200 + 正确业务数据 | 审计日志记录操作者信息[^1] | 所有三个接口 | | TC002 | 系统触发场景 | `operator="system"` | HTTP 200 + 正确业务数据 | 跳过人工权限检查 | 所有三个接口 | | TC003 | 供应链系统调用 | `calling_system="SCP"` | HTTP 200 + 供应链特有数据格式 | 数据格式转换验证[^3] | 通过工号获取员工信息 | | TC004 | 混合数有效 | `operator="lisi"&calling_system="SCP"` | HTTP 200 | 审计日志同时记录两字段 | 通过部门ID获取员工 | --- ### 2. **异常场景验证** | 用例编号 | 测试场景 | 输数 | 预期结果 | 验证点 | 适用接口 | |----------|--------------------------|-----------------------------------|------------------------------|----------------------------|------------------------------| | TC101 | 人为操作误传system | `operator="system"`(实际人为操作)| HTTP 400 + 错误码`INVALID_OPERATOR` | 返回明确错误信息 | 获取所有部门信息 | | TC102 | 无效调用系统码值 | `calling_system="INVALID"` | HTTP 400 + 错误码`INVALID_SYSTEM_CODE` | 错误提示考PingCode[^2] | 所有三个接口 | | TC103 | 不存在的登录名 | `operator="unknown_user"` | HTTP 400 + 错误码`USER_NOT_FOUND` | 错误信息建议联系HR系统 | 通过工号获取员工信息 | --- ### 3. **边界值验证** | 用例编号 | 测试场景 | 输数 | 预期结果 | 验证点 | 适用接口 | |----------|--------------------------|-----------------------------------|------------------------------|----------------------------|------------------------------| | TC201 | 超长登录名(256字符) | `operator="a".repeat(256)` | HTTP 400 + 长度校验失败 | 数长度限制生效[^4] | 所有三个接口 | | TC202 | 特殊字符登录名 | `operator="admin@test.com"` | HTTP 200 | 特殊字符兼容处理 | 通过部门ID获取员工 | | TC203 | 系统码值大小写敏感 | `calling_system="ScP"` | HTTP 400 | 强制大写校验 | 获取所有部门信息 | --- ### 4. **非必填字段验证** | 用例编号 | 测试场景 | 输数 | 预期结果 | 验证点 | 适用接口 | |----------|--------------------------|-----------------------------------|------------------------------|----------------------------|------------------------------| | TC301 | 省略所有新增数 | 不传`operator`和`calling_system` | HTTP 200 + 默认处理逻辑 | 日志记录`unknown_operator` | 所有三个接口 | | TC302 | 仅传操作用户 | `operator="wangwu"` | HTTP 200 | 调用系统默认为`null` | 通过工号获取员工信息 | | TC303 | 仅传调用系统 | `calling_system="SCP"` | HTTP 200 | 操作用户默认为`null` | 获取所有部门信息 | --- ### 关键验证逻辑说明 1. **权限分离机制** - 人为操作:触发RBAC权限检查 `$$ \text{hasPermission}(operator, \text{"HR_READ"}) $$` [^5] - 系统触发:启用服务账户权限 `$$ \text{serviceAccount} \rightarrow \text{SystemRole} $$` 2. **供应链系统特殊处理** 当 `calling_system="SCP"` 时,响应数据需转换格式: ```json // 普通响应 {"id":101, "name":"张三"} // SCP系统响应 {"employeeId":101, "fullName":"张三", "supplyChainRole":"WAREHOUSE_MGR"} ``` 3. **审计日志规范** 日志条目需包含: ```java record AuditLog( String operator, // "system" 或 登录名 String systemCode, // PingCode码值 LocalDateTime timestamp, String apiPath // 如 "/hr/departments" ) {} ``` --- ### 测试数据考 | 字段 | 测试数据 | |---------------|-----------------------------------| | 有效登录名 | `zhangsan`, `lisi`, `hr_admin` | | 无效登录名 | `unknown_user`, `""`, `null` | | 有效系统码值 | `"SCP"`(供应链), `"FIN"`(财务)| | 无效系统码值 | `"INVALID"`, `"123"`, `"scp"` | > **测试覆盖率要求**:操作用户字段覆盖率达到100%,调用系统字段覆盖所有PingCode注册系统[^2]。 --- ### 测试实现示例(Java) ```java @SpringBootTest class HrApiTests { @Test void humanOperator_shouldPassPermissionCheck() { // TC001 验证 mockMvc.perform(get("/hr/departments") .param("operator", "zhangsan")) .andExpect(status().isOk()) .andExpect(jsonPath("$.departments").exists()); // 验证权限检查被触发 verify(rbacService).checkPermission("zhangsan", "HR_READ"); } @Test void systemOperator_shouldBypassPermission() { // TC002 验证 mockMvc.perform(get("/hr/employees/123") .param("operator", "system")) .andExpect(status().isOk()); // 验证跳过权限检查 verify(rbacService, never()).checkPermission(any(), any()); } @Test void invalidSystemCode_shouldThrowError() { // TC102 验证 mockMvc.perform(get("/hr/employee") .param("calling_system", "INVALID")) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.errorCode").value("INVALID_SYSTEM_CODE")); } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值