Files
water/docs/superpowers/plans/2026-07-16-app-controller-review-remediation.md
yuhaiming 70e2356f22 fix(app): 修复 AppController 安全与查询问题
- 加强异常处理、类型安全和图片上传校验
- 优化设备相关查询,避免重复访问数据源
- 补充并记录并发测试与审查修复实施计划
2026-07-17 08:20:44 +08:00

26 KiB

AppController Review Remediation Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Fix the approved exception-safety, type-safety, N+1-query, and image-upload findings while preserving every existing /app/v1/** route.

Architecture: Keep AppController in place for this phase, but move database batching behind existing service interfaces. Route all non-degraded exceptions through GlobalExceptionHandler, validate image bytes before OSS, and cover behavior through focused unit and standalone MVC tests.

Tech Stack: Java 17, Spring Boot 3.5, Spring MVC, MyBatis Plus, Hutool, JUnit 5, Mockito, AssertJ, Maven.

Global Constraints

  • Do not split AppController in this phase.
  • Preserve existing route paths, request field names, successful response payloads, MQTT topics, and database schema.
  • Unexpected system exceptions must not expose their original messages to clients.
  • Image uploads are limited to 20 MB and to JPEG, PNG, GIF, WebP, and BMP detected by file signature.
  • Keep the four intentional MQTT notification degradation catches and include the exception object in each log call.
  • Do not revert unrelated dirty-worktree changes.

Task 1: Make Global Exception Responses Safe And Observable

Files:

  • Modify: water-common/water-common-web/pom.xml
  • Modify: water-common/water-common-web/src/main/java/org/dromara/common/web/handler/GlobalExceptionHandler.java:60-167
  • Create: water-common/water-common-web/src/test/java/org/dromara/common/web/handler/GlobalExceptionHandlerTest.java

Interfaces:

  • Consumes: ServiceException, BaseException, R<Void>, HttpServletRequest.

  • Produces: unchanged handler method signatures; generic system failures return R.fail() instead of e.getMessage().

  • Step 1: Add the test dependency and failing exception-handler tests

Add this dependency to water-common-web/pom.xml:

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

Create GlobalExceptionHandlerTest with these behaviors:

@ExtendWith(MockitoExtension.class)
class GlobalExceptionHandlerTest {

    @Mock
    private HttpServletRequest request;

    private GlobalExceptionHandler handler;

    @BeforeEach
    void setUp() {
        handler = new GlobalExceptionHandler();
        when(request.getRequestURI()).thenReturn("/app/v1/device/D01");
    }

    @Test
    void handleRuntimeException_hidesInternalMessage() {
        R<Void> result = handler.handleRuntimeException(
            new IllegalStateException("jdbc:mysql://internal-host/water"), request);

        assertThat(result.getCode()).isEqualTo(R.FAIL);
        assertThat(result.getMsg()).doesNotContain("internal-host");
    }

    @Test
    void handleBaseException_logsThrowableAndRequestUri() {
        Logger logger = (Logger) LoggerFactory.getLogger(GlobalExceptionHandler.class);
        ListAppender<ILoggingEvent> appender = new ListAppender<>();
        appender.start();
        logger.addAppender(appender);
        try {
            handler.handleBaseException(new BaseException("user", "业务校验失败"), request);

            ILoggingEvent event = appender.list.get(appender.list.size() - 1);
            assertThat(event.getLevel()).isEqualTo(Level.WARN);
            assertThat(event.getFormattedMessage()).contains("/app/v1/device/D01");
            assertThat(event.getThrowableProxy()).isNotNull();
        } finally {
            logger.detachAppender(appender);
        }
    }
}
  • Step 2: Run the tests and verify RED

Run:

mvn -pl water-common/water-common-web -am "-DskipTests=false" "-Dmaven.test.skip=false" "-Dtest=GlobalExceptionHandlerTest" "-Dsurefire.failIfNoSpecifiedTests=false" test

Expected: failures because runtime messages are currently returned unchanged and BaseException logs have no throwable at ERROR level.

  • Step 3: Implement the minimal handler changes

Apply these handler rules:

@ExceptionHandler(ServiceException.class)
public R<Void> handleServiceException(ServiceException e, HttpServletRequest request) {
    String requestURI = request.getRequestURI();
    String message = resolveErrorMessage(e.getMessage());
    log.warn("请求地址'{}',发生业务异常'{}'", requestURI, message, e);
    Integer code = e.getCode();
    return ObjectUtil.isNotNull(code) ? R.fail(code, message) : R.fail(message);
}

@ExceptionHandler(BaseException.class)
public R<Void> handleBaseException(BaseException e, HttpServletRequest request) {
    String requestURI = request.getRequestURI();
    String message = e.getMessage();
    log.warn("请求地址'{}',发生业务异常'{}'", requestURI, message, e);
    return R.fail(message);
}

@ExceptionHandler(RuntimeException.class)
public R<Void> handleRuntimeException(RuntimeException e, HttpServletRequest request) {
    String requestURI = request.getRequestURI();
    log.error("请求地址'{}',发生未知异常.", requestURI, e);
    return R.fail();
}

@ExceptionHandler(Exception.class)
public R<Void> handleException(Exception e, HttpServletRequest request) {
    String requestURI = request.getRequestURI();
    log.error("请求地址'{}',发生系统异常.", requestURI, e);
    return R.fail();
}
  • Step 4: Run the focused tests and verify GREEN

Run the command from Step 2. Expected: GlobalExceptionHandlerTest passes with zero failures.

  • Step 5: Record the Task 1 diff checkpoint
git diff -- water-common/water-common-web/pom.xml water-common/water-common-web/src/main/java/org/dromara/common/web/handler/GlobalExceptionHandler.java water-common/water-common-web/src/test/java/org/dromara/common/web/handler/GlobalExceptionHandlerTest.java

Task 2: Validate Image Upload Content And Size

Files:

  • Modify: water-modules/water-app/src/test/java/org/dromara/app/controller/AppControllerTest.java:131-169
  • Modify: water-modules/water-app/src/main/java/org/dromara/app/controller/AppController.java:593-612

Interfaces:

  • Consumes: MultipartFile and existing ISysOssService.upload(MultipartFile).

  • Produces: unchanged POST /app/v1/uploadImage; invalid files throw ServiceException before OSS is called.

  • Step 1: Write failing upload validation tests

Replace the fake valid PNG bytes with a real signature helper and add forged, oversized, and SVG cases:

private byte[] pngBytes() {
    return new byte[]{
        (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
        0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52
    };
}

@Test
void uploadImage_rejectsForgedImageContent() {
    AppController controller = newController();
    MockMultipartFile file = new MockMultipartFile(
        "file", "plant.png", "image/png", "not-an-image".getBytes(StandardCharsets.UTF_8));

    assertThatThrownBy(() -> callWithLogin(1L, () -> controller.uploadImage(file)))
        .isInstanceOf(ServiceException.class)
        .hasMessage("图片文件内容不合法");
    verify(ossService, never()).upload(any(MultipartFile.class));
}

@Test
void uploadImage_rejectsFileLargerThanTwentyMegabytes() {
    AppController controller = newController();
    MultipartFile file = mock(MultipartFile.class);
    when(file.isEmpty()).thenReturn(false);
    when(file.getSize()).thenReturn(20L * 1024 * 1024 + 1);

    assertThatThrownBy(() -> callWithLogin(1L, () -> controller.uploadImage(file)))
        .isInstanceOf(ServiceException.class)
        .hasMessage("图片大小不能超过20MB");
    verify(ossService, never()).upload(any(MultipartFile.class));
}

@Test
void uploadImage_rejectsSvg() {
    AppController controller = newController();
    MockMultipartFile file = new MockMultipartFile(
        "file", "plant.svg", "image/svg+xml", "<svg/>".getBytes(StandardCharsets.UTF_8));

    assertThatThrownBy(() -> callWithLogin(1L, () -> controller.uploadImage(file)))
        .isInstanceOf(ServiceException.class)
        .hasMessage("仅支持jpg、jpeg、png、gif、webp、bmp图片");
    verify(ossService, never()).upload(any(MultipartFile.class));
}
  • Step 2: Run upload tests and verify RED

Run:

mvn -pl water-modules/water-app -am "-DskipTests=false" "-Dmaven.test.skip=false" "-Dprofiles.active=dev" "-Dtest=AppControllerTest" "-Dsurefire.failIfNoSpecifiedTests=false" test

Expected: the forged and oversized tests fail because current code trusts Content-Type and has no endpoint-specific limit.

  • Step 3: Implement image validation

Add constants and mappings:

private static final long MAX_IMAGE_SIZE_BYTES = 20L * 1024 * 1024;
private static final Set<String> ALLOWED_IMAGE_EXTENSIONS =
    Set.of("jpg", "jpeg", "png", "gif", "webp", "bmp");
private static final Map<String, String> IMAGE_CONTENT_TYPES = Map.of(
    "jpg", "image/jpeg",
    "png", "image/png",
    "gif", "image/gif",
    "webp", "image/webp",
    "bmp", "image/bmp"
);

Call validateImageFile(file) before OSS. Implement it with FileUtil.extName and FileTypeUtil.getType(file.getInputStream()), normalize jpeg to jpg, and enforce extension/MIME/signature compatibility:

private void validateImageFile(MultipartFile file) {
    if (file == null || file.isEmpty()) {
        throw new ServiceException("上传图片不能为空");
    }
    if (file.getSize() > MAX_IMAGE_SIZE_BYTES) {
        throw new ServiceException("图片大小不能超过20MB");
    }
    String extension = StringUtils.lowerCase(FileUtil.extName(file.getOriginalFilename()));
    if (!ALLOWED_IMAGE_EXTENSIONS.contains(extension)) {
        throw new ServiceException("仅支持jpg、jpeg、png、gif、webp、bmp图片");
    }
    String contentType = StringUtils.lowerCase(file.getContentType());
    String normalizedExtension = "jpeg".equals(extension) ? "jpg" : extension;
    if (!IMAGE_CONTENT_TYPES.get(normalizedExtension).equals(contentType)) {
        throw new ServiceException("图片文件类型不匹配");
    }
    try (InputStream inputStream = file.getInputStream()) {
        String detectedType = StringUtils.lowerCase(FileTypeUtil.getType(inputStream));
        String normalizedDetectedType = "jpeg".equals(detectedType) ? "jpg" : detectedType;
        if (!normalizedExtension.equals(normalizedDetectedType)) {
            throw new ServiceException("图片文件内容不合法");
        }
    } catch (IOException e) {
        throw new ServiceException("读取图片文件失败");
    }
}
  • Step 4: Run AppController tests and verify GREEN

Run the command from Step 2. Expected: all existing and new upload tests pass.

  • Step 5: Record the Task 2 diff checkpoint
git diff -- water-modules/water-app/src/main/java/org/dromara/app/controller/AppController.java water-modules/water-app/src/test/java/org/dromara/app/controller/AppControllerTest.java

Task 3: Add Bounded Batch Query Service APIs

Files:

  • Modify: water-modules/water-app/src/main/java/org/dromara/app/service/IAppDeviceService.java
  • Modify: water-modules/water-app/src/main/java/org/dromara/app/service/impl/AppDeviceServiceImpl.java
  • Modify: water-modules/water-app/src/test/java/org/dromara/app/service/impl/AppDeviceServiceImplTest.java
  • Modify: water-modules/water-app/src/main/java/org/dromara/app/service/IAppSchedulingDeviceService.java
  • Modify: water-modules/water-app/src/main/java/org/dromara/app/service/impl/AppSchedulingDeviceServiceImpl.java
  • Create: water-modules/water-app/src/test/java/org/dromara/app/service/impl/AppSchedulingDeviceServiceImplTest.java

Interfaces:

  • Produces: List<AppDeviceVo> queryByDeviceNos(Collection<String> deviceNos).

  • Produces: Set<String> findBoundDeviceNos(Collection<String> deviceNos).

  • Step 1: Write failing service tests against the desired APIs

Add to AppDeviceServiceImplTest:

@Test
void queryByDeviceNos_delegatesToSingleBatchQuery() {
    AppDeviceServiceImpl service = new AppDeviceServiceImpl(
        appDeviceMapper, schedulingDeviceMapper, wateringLogMapper, wateringLogService);
    List<String> deviceNos = List.of("D01", "D02");
    List<AppDeviceVo> expected = List.of(new AppDeviceVo(), new AppDeviceVo());
    when(appDeviceMapper.selectVoByIds(deviceNos)).thenReturn(expected);

    assertThat(service.queryByDeviceNos(deviceNos)).isSameAs(expected);
    verify(appDeviceMapper).selectVoByIds(deviceNos);
}

Create AppSchedulingDeviceServiceImplTest:

@ExtendWith(MockitoExtension.class)
class AppSchedulingDeviceServiceImplTest {

    @Mock
    private AppSchedulingDeviceMapper mapper;

    @Test
    void findBoundDeviceNos_usesOneConstrainedQuery() {
        AppSchedulingDeviceServiceImpl service = new AppSchedulingDeviceServiceImpl(mapper);
        AppSchedulingDevice first = new AppSchedulingDevice();
        first.setDeviceNo("D01");
        AppSchedulingDevice duplicate = new AppSchedulingDevice();
        duplicate.setDeviceNo("D01");
        when(mapper.selectList(any(Wrapper.class))).thenReturn(List.of(first, duplicate));

        Set<String> result = service.findBoundDeviceNos(List.of("D01", "D02"));

        assertThat(result).containsExactly("D01");
        verify(mapper, times(1)).selectList(any(Wrapper.class));
    }
}
  • Step 2: Run service tests and verify RED

Run:

mvn -pl water-modules/water-app -am "-DskipTests=false" "-Dmaven.test.skip=false" "-Dprofiles.active=dev" "-Dtest=AppDeviceServiceImplTest,AppSchedulingDeviceServiceImplTest" "-Dsurefire.failIfNoSpecifiedTests=false" test

Expected: test compilation fails because the two batch APIs do not exist yet.

  • Step 3: Add the interfaces and minimal implementations

Add to IAppDeviceService and AppDeviceServiceImpl:

List<AppDeviceVo> queryByDeviceNos(Collection<String> deviceNos);
@Override
public List<AppDeviceVo> queryByDeviceNos(Collection<String> deviceNos) {
    if (deviceNos == null || deviceNos.isEmpty()) {
        return List.of();
    }
    return baseMapper.selectVoByIds(deviceNos);
}

Add to IAppSchedulingDeviceService and AppSchedulingDeviceServiceImpl:

Set<String> findBoundDeviceNos(Collection<String> deviceNos);
@Override
public Set<String> findBoundDeviceNos(Collection<String> deviceNos) {
    if (deviceNos == null || deviceNos.isEmpty()) {
        return Set.of();
    }
    return baseMapper.selectList(
            Wrappers.<AppSchedulingDevice>lambdaQuery()
                .select(AppSchedulingDevice::getDeviceNo)
                .in(AppSchedulingDevice::getDeviceNo, deviceNos))
        .stream()
        .map(AppSchedulingDevice::getDeviceNo)
        .filter(StringUtils::isNotBlank)
        .collect(Collectors.toSet());
}
  • Step 4: Run service tests and verify GREEN

Run the command from Step 2. Expected: both service test classes pass.

  • Step 5: Record the Task 3 diff checkpoint
git diff -- water-modules/water-app/src/main/java/org/dromara/app/service water-modules/water-app/src/test/java/org/dromara/app/service/impl

Task 4: Replace Controller N+1 Queries

Files:

  • Modify: water-modules/water-app/src/test/java/org/dromara/app/controller/AppControllerTest.java
  • Modify: water-modules/water-app/src/main/java/org/dromara/app/controller/AppController.java:175-195
  • Modify: water-modules/water-app/src/main/java/org/dromara/app/controller/AppController.java:357-400

Interfaces:

  • Consumes: IAppDeviceService.queryByDeviceNos from Task 3.

  • Consumes: IAppSchedulingDeviceService.findBoundDeviceNos from Task 3.

  • Preserves: GET /app/v1/scheduleDeviceList/{scheduleId} and GET /app/v1/schedule/{id} payloads.

  • Step 1: Write failing Controller interaction tests

Add these tests:

@Test
void scheduleDeviceList_usesSingleBatchBindingLookup() {
    AppController controller = newController();
    AppDeviceVo first = ownedDevice("D01", 99L);
    AppDeviceVo second = ownedDevice("D02", 99L);
    when(appDeviceService.queryList(any(AppDeviceBo.class))).thenReturn(List.of(first, second));
    when(appSchedulingDeviceService.findBoundDeviceNos(List.of("D01", "D02")))
        .thenReturn(Set.of("D01"));

    R<Map<String, Object>> result = callWithLogin(99L, () -> controller.scheduleDeviceList(10L));

    assertThat((List<AppDeviceVo>) result.getData().get("deviceList"))
        .extracting(AppDeviceVo::getDeviceNo)
        .containsExactly("D02");
    verify(appSchedulingDeviceService, never()).findByDeviceNo(anyString());
}

@Test
void getScheduleInfo_usesSingleBatchDeviceLookup() {
    AppController controller = newController();
    AppScheduleVo schedule = ownedSchedule(10L, 99L);
    AppSchedulingDeviceVo firstBinding = new AppSchedulingDeviceVo();
    firstBinding.setDeviceNo("D01");
    AppSchedulingDeviceVo secondBinding = new AppSchedulingDeviceVo();
    secondBinding.setDeviceNo("D02");
    List<AppDeviceVo> devices = List.of(ownedDevice("D01", 99L), ownedDevice("D02", 99L));
    when(appScheduleService.queryById(10L)).thenReturn(schedule);
    when(appScheduleDetailService.queryByScheduleIdByStatus(10L)).thenReturn(List.of());
    when(appSchedulingDeviceService.queryList(any(AppSchedulingDeviceBo.class)))
        .thenReturn(List.of(firstBinding, secondBinding));
    when(appDeviceService.queryByDeviceNos(List.of("D01", "D02"))).thenReturn(devices);

    R<AppScheduleVo> result = callWithLogin(99L, () -> controller.getScheduleInfo(10L));

    assertThat(result.getData().getDeviceNos()).isEqualTo(devices);
    verify(appDeviceService, never()).queryById("D01");
    verify(appDeviceService, never()).queryById("D02");
}
  • Step 2: Run AppController tests and verify RED

Run the Task 2 test command. Expected: interaction assertions fail because current methods query once per device/binding.

  • Step 3: Implement batch-backed Controller methods

Use one binding lookup in scheduleDeviceList:

List<String> deviceNos = appDeviceVos.stream().map(AppDeviceVo::getDeviceNo).toList();
Set<String> boundDeviceNos = appSchedulingDeviceService.findBoundDeviceNos(deviceNos);
List<AppDeviceVo> availableDevices = appDeviceVos.stream()
    .filter(device -> !boundDeviceNos.contains(device.getDeviceNo()))
    .toList();
Map<String, Object> result = new HashMap<>();
result.put("deviceList", availableDevices);
return R.ok(result);

Use one batch device lookup in getScheduleInfo:

List<String> deviceNos = bindings.stream()
    .map(AppSchedulingDeviceVo::getDeviceNo)
    .filter(StringUtils::isNotBlank)
    .distinct()
    .toList();
List<AppDeviceVo> devices = appDeviceService.queryByDeviceNos(deviceNos);
  • Step 4: Run AppController tests and verify GREEN

Run the Task 2 command. Expected: all AppControllerTest tests pass.

  • Step 5: Record the Task 4 diff checkpoint
git diff -- water-modules/water-app/src/main/java/org/dromara/app/controller/AppController.java water-modules/water-app/src/test/java/org/dromara/app/controller/AppControllerTest.java

Task 5: Reuse Schedule Details Across MQTT Notifications

Files:

  • Modify: water-modules/water-app/src/test/java/org/dromara/app/controller/AppControllerTest.java
  • Modify: water-modules/water-app/src/main/java/org/dromara/app/controller/AppController.java:200-287
  • Modify: water-modules/water-app/src/main/java/org/dromara/app/controller/AppController.java:763-794

Interfaces:

  • Preserves all schedule MQTT command payload fields.

  • Changes internal helper to consume prebuilt List<Map<String, Object>> details.

  • Step 1: Write a failing query-count test

Add an open-schedule test with two bindings:

@Test
void editScheduleStatus_queriesScheduleDetailsOnceForMultipleDevices() {
    AppController controller = newController();
    AppScheduleBo bo = new AppScheduleBo();
    bo.setId(10L);
    bo.setStatus("1");
    AppSchedulingDeviceVo first = new AppSchedulingDeviceVo();
    first.setDeviceNo("D01");
    AppSchedulingDeviceVo second = new AppSchedulingDeviceVo();
    second.setDeviceNo("D02");
    when(appScheduleService.queryById(10L)).thenReturn(ownedSchedule(10L, 99L));
    when(appScheduleService.updateStatusByBo(bo)).thenReturn(true);
    when(appSchedulingDeviceService.queryList(any(AppSchedulingDeviceBo.class)))
        .thenReturn(List.of(first, second));
    when(appScheduleDetailService.queryByScheduleIdByStatus(10L)).thenReturn(List.of());

    callWithLogin(99L, () -> controller.editScheduleStatus(bo));

    verify(appScheduleDetailService, times(1)).queryByScheduleIdByStatus(10L);
    verify(deviceCommandService, times(2)).sendScheduleBindCommand(anyString(), any());
}
  • Step 2: Run the test and verify RED

Run the Task 2 command. Expected: detail service is called twice.

  • Step 3: Build details once and reuse them

Change payload construction to:

private Map<String, Object> buildScheduleBindPayload(
        String deviceNo, List<Map<String, Object>> details) {
    Map<String, Object> payload = new HashMap<>();
    payload.put("deviceNo", deviceNo);
    payload.put("details", details);
    return payload;
}

private List<Map<String, Object>> loadScheduleDetailPayload(Long scheduleId) {
    return toScheduleDetailPayload(appScheduleDetailService.queryByScheduleIdByStatus(scheduleId));
}

Call loadScheduleDetailPayload(scheduleId) once before each device loop in add/update/open-status notification paths, then pass the result into buildScheduleBindPayload(deviceNo, details).

  • Step 4: Run AppController tests and verify GREEN

Run the Task 2 command. Expected: all tests pass and the detail service is called once for two devices.

  • Step 5: Record the Task 5 diff checkpoint
git diff -- water-modules/water-app/src/main/java/org/dromara/app/controller/AppController.java water-modules/water-app/src/test/java/org/dromara/app/controller/AppControllerTest.java

Task 6: Complete Type-Safety And Local Cleanup

Files:

  • Modify: water-modules/water-app/src/main/java/org/dromara/app/controller/AppController.java
  • Modify: water-modules/water-app/src/test/java/org/dromara/app/controller/AppControllerTest.java

Interfaces:

  • Removes the unused AppScheduleServiceImpl constructor dependency.

  • Preserves all endpoint return JSON shapes.

  • Step 1: Capture the current static-analysis failure

Run:

rg -n '^import .*\*;|\b(Map|List|Set)\s+[a-zA-Z][a-zA-Z0-9_]*\s*=|R<Map>' water-modules/water-app/src/main/java/org/dromara/app/controller/AppController.java

Expected: matches for wildcard imports, raw collections, and raw response maps.

  • Step 2: Replace raw types and wildcard imports

Use these concrete types:

R<Map<String, Object>>
List<AppDeviceVo>
List<AppWateringLogVo>
List<Map<String, Object>>
List<Object>
Map<String, Object>
Map<String, Long>

Replace org.dromara.app.domain.vo.*, org.dromara.app.service.*, org.springframework.web.bind.annotation.*, and java.util.* with explicit imports. Remove the unused AppScheduleServiceImpl appScheduleServiceimpl field and its test mock/constructor argument.

Replace Controller time formatting with:

private static final DateTimeFormatter SECOND_TIME_FORMATTER =
    DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

String startTime = LocalDateTime.now().format(SECOND_TIME_FORMATTER);

private String formatSecondTime(Date time) {
    return time == null ? null : time.toInstant()
        .atZone(ZoneId.systemDefault())
        .toLocalDateTime()
        .format(SECOND_TIME_FORMATTER);
}

Remove commented-out annotations and dead commented code only in sections touched by this plan.

  • Step 3: Re-run the static query and verify GREEN

Run the Step 1 command. Expected: no output and exit code 1 from rg because there are no matches.

  • Step 4: Run all water-app tests
mvn -pl water-modules/water-app -am "-DskipTests=false" "-Dmaven.test.skip=false" "-Dprofiles.active=dev" test

Expected: reactor BUILD SUCCESS, zero test failures.

  • Step 5: Record the Task 6 diff checkpoint
git diff -- water-modules/water-app/src/main/java/org/dromara/app/controller/AppController.java water-modules/water-app/src/test/java/org/dromara/app/controller/AppControllerTest.java

Task 7: Final Verification Against The Review Standard

Files:

  • Verify only; no planned production edits.

Interfaces:

  • Verifies the deliverables from Tasks 1-6 together.

  • Step 1: Run common-web tests

mvn -pl water-common/water-common-web -am "-DskipTests=false" "-Dmaven.test.skip=false" test

Expected: BUILD SUCCESS and GlobalExceptionHandlerTest passes.

  • Step 2: Run water-app tests
mvn -pl water-modules/water-app -am "-DskipTests=false" "-Dmaven.test.skip=false" "-Dprofiles.active=dev" test

Expected: BUILD SUCCESS with zero failures and errors.

  • Step 3: Run structural and diff checks
rg -n 'return fail\(|private <T> R<T> fail|toI18nErrorMessage|^import .*\*;|R<Map>|\b(Map|List|Set)\s+[a-zA-Z][a-zA-Z0-9_]*\s*=' water-modules/water-app/src/main/java/org/dromara/app/controller/AppController.java
git diff --check
git status --short

Expected: no forbidden Controller matches, git diff --check exits 0, and status contains only intentional task files plus pre-existing unrelated changes.

  • Step 4: Review the final diff by task scope
git diff -- water-common/water-common-web water-modules/water-app/src/main/java/org/dromara/app/controller/AppController.java water-modules/water-app/src/main/java/org/dromara/app/service water-modules/water-app/src/test

Expected: no route changes, no MQTT topic changes, no schema changes, and no unrelated file modifications.