Files
water/docs/superpowers/plans/2026-06-24-bind-schedule-device-dispatch.md
2026-07-13 14:22:11 +08:00

11 KiB

Bind Schedule Device Dispatch 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: When the app binds a schedule to one or more devices, immediately publish that schedule's detail payload to each bound device.

Architecture: Keep the change inside the existing app module flow. AppController.addScheduleDevice remains the entry point, persists the relation as it does today, then reuses the existing IDeviceCommandService.sendScheduleBindCommand(...) path to publish a bindSchedule command with a small payload assembled from current schedule-detail data. Shared payload shaping should stay private to AppController unless implementation pressure proves otherwise.

Tech Stack: Java 17, Spring Boot 3, existing app controller/service layer, MQTT command publishing through IDeviceCommandService, Maven

Global Constraints

  • Only touch the schedule-device binding flow in water-modules/water-app
  • Reuse existing IDeviceCommandService.sendScheduleBindCommand(...)
  • Do not modify MQTT publisher, ACK handling, or topic routing
  • Published payload must contain deviceNo and details
  • details must follow the current device-facing schedule detail structure used by buildScheduleBindPayload
  • Binding uses synchronous failure semantics: command publish failure must fail the API call
  • Keep changes ASCII unless the target file already uses non-ASCII

Task 1: Add a focused regression test for schedule-device bind dispatch

Files:

  • Create: water-modules/water-app/src/test/java/org/dromara/app/controller/AppControllerTest.java
  • Modify: water-modules/water-app/pom.xml

Interfaces:

  • Consumes: AppController.addScheduleDevice(String body)

  • Produces: a regression test proving that binding a device triggers deviceCommandService.sendScheduleBindCommand(deviceNo, payload)

  • Step 1: Add the test dependency baseline for the app module

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
  • Step 2: Write the failing controller regression test
package org.dromara.app.controller;

import org.dromara.app.domain.bo.AppSchedulingDeviceBo;
import org.dromara.app.domain.vo.AppDeviceVo;
import org.dromara.app.domain.vo.AppScheduleDetailVo;
import org.dromara.app.domain.vo.AppScheduleVo;
import org.dromara.app.service.IAppDeviceService;
import org.dromara.app.service.IAppScheduleDetailService;
import org.dromara.app.service.IAppScheduleService;
import org.dromara.app.service.IAppSchedulingDeviceService;
import org.dromara.app.service.IAppWateringLogService;
import org.dromara.app.service.IDeviceCommandService;
import org.dromara.common.core.domain.R;
import org.dromara.system.service.ISysUserService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class AppControllerTest {

    @Mock private IAppDeviceService appDeviceService;
    @Mock private IAppScheduleService appScheduleService;
    @Mock private IAppScheduleDetailService appScheduleDetailService;
    @Mock private org.dromara.app.service.impl.AppScheduleServiceImpl appScheduleServiceimpl;
    @Mock private IAppSchedulingDeviceService appSchedulingDeviceService;
    @Mock private ISysUserService userService;
    @Mock private IAppWateringLogService appWateringLogService;
    @Mock private IDeviceCommandService deviceCommandService;

    @InjectMocks
    private AppController controller;

    @Test
    void addScheduleDevice_dispatchesSchedulePayloadAfterBinding() {
        AppScheduleVo schedule = new AppScheduleVo();
        schedule.setId(10L);
        schedule.setUserId(99L);
        schedule.setName("Morning");
        schedule.setStatus("1");

        AppDeviceVo device = new AppDeviceVo();
        device.setDeviceNo("D01");
        device.setUserId(99L);

        AppScheduleDetailVo detail = new AppScheduleDetailVo();
        detail.setId(101L);
        detail.setWeekday("1");
        detail.setTimeData("[{\"startTime\":\"08:00\",\"durationMin\":15}]");
        detail.setTriggerType("0");
        detail.setStatus("1");

        when(appScheduleService.queryById(10L)).thenReturn(schedule);
        when(appDeviceService.queryById("D01")).thenReturn(device);
        when(appScheduleDetailService.queryByScheduleIdByStatus(10L)).thenReturn(List.of(detail));
        when(appSchedulingDeviceService.insertByBo(any(AppSchedulingDeviceBo.class))).thenReturn(true);
        when(deviceCommandService.sendScheduleBindCommand(eq("D01"), anyMap())).thenReturn("cmd-1");

        R<Void> result = controller.addScheduleDevice("{\"scheduleId\":10,\"deviceNos\":[\"D01\"]}");

        assertThat(result.getCode()).isEqualTo(200);

        ArgumentCaptor<Map<String, Object>> payloadCaptor = ArgumentCaptor.forClass(Map.class);
        verify(deviceCommandService).sendScheduleBindCommand(eq("D01"), payloadCaptor.capture());
        Map<String, Object> payload = payloadCaptor.getValue();
        assertThat(payload.get("deviceNo")).isEqualTo("D01");
        assertThat(payload).containsKey("details").doesNotContainKeys("cmd", "schedule");
    }
}
  • Step 3: Run the test to verify it fails for the expected reason

Run: mvn -pl water-modules/water-app -DskipTests=false -Dtest=AppControllerTest test

Expected: FAIL because sendScheduleBindCommand(...) is never invoked and payload assertions cannot be satisfied yet

  • Step 4: Commit the failing test baseline
git add water-modules/water-app/pom.xml water-modules/water-app/src/test/java/org/dromara/app/controller/AppControllerTest.java
git commit -m "test: cover schedule bind device dispatch"

Task 2: Implement schedule payload assembly and dispatch in AppController

Files:

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

Interfaces:

  • Consumes: IAppScheduleService.queryById(Long), IAppScheduleDetailService.queryByScheduleIdByStatus(Long), IDeviceCommandService.sendScheduleBindCommand(String, Map<String, Object>)

  • Produces: addScheduleDevice(...) publishes a bindSchedule command after each successful relation insert

  • Step 1: Implement the minimum controller change to make the test pass

for (String deviceNo : deviceNos) {
    if (StringUtils.isBlank(deviceNo)) {
        continue;
    }
    assertDeviceOwned(deviceNo);
    AppSchedulingDeviceBo schedulingDeviceBo = new AppSchedulingDeviceBo();
    schedulingDeviceBo.setDeviceNo(deviceNo);
    schedulingDeviceBo.setScheduleId(scheduleId);
    appSchedulingDeviceService.insertByBo(schedulingDeviceBo);
    deviceCommandService.sendScheduleBindCommand(deviceNo, buildScheduleBindPayload(scheduleId, deviceNo));
}
private Map<String, Object> buildScheduleBindPayload(Long scheduleId, String deviceNo) {
    List<AppScheduleDetailVo> details = appScheduleDetailService.queryByScheduleIdByStatus(scheduleId);

    List<Map<String, Object>> detailPayload = new ArrayList<>();
    for (AppScheduleDetailVo detail : details) {
        List<Object> timeSlots = new ArrayList<>();
        JSONArray timeArray = JSONUtil.parseArray(detail.getTimeData());
        for (Object timeSlot : timeArray) {
            timeSlots.add(timeSlot);
        }

        Map<String, Object> item = new HashMap<>();
        item.put("weekday", detail.getWeekday());
        item.put("timeData", timeSlots);
        item.put("triggerType", detail.getTriggerType());
        item.put("status", detail.getStatus());
        detailPayload.add(item);
    }

    Map<String, Object> payload = new HashMap<>();
    payload.put("deviceNo", deviceNo);
    payload.put("details", detailPayload);
    return payload;
}
  • Step 2: Run the focused regression test to verify it passes

Run: mvn -pl water-modules/water-app -DskipTests=false -Dtest=AppControllerTest test

Expected: PASS with AppControllerTest green

  • Step 3: Refactor only if needed to remove duplication inside schedule bind payload shaping
private List<Map<String, Object>> toScheduleDetailPayload(List<AppScheduleDetailVo> details) {
    List<Map<String, Object>> detailPayload = new ArrayList<>();
    for (AppScheduleDetailVo detail : details) {
        List<Object> timeSlots = new ArrayList<>();
        JSONArray timeArray = JSONUtil.parseArray(detail.getTimeData());
        for (Object timeSlot : timeArray) {
            timeSlots.add(timeSlot);
        }
        Map<String, Object> item = new HashMap<>();
        item.put("weekday", detail.getWeekday());
        item.put("timeData", timeSlots);
        item.put("triggerType", detail.getTriggerType());
        item.put("status", detail.getStatus());
        detailPayload.add(item);
    }
    return detailPayload;
}
  • Step 4: Re-run the focused regression test after refactor

Run: mvn -pl water-modules/water-app -DskipTests=false -Dtest=AppControllerTest test

Expected: PASS

  • Step 5: Commit the implementation
git add 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
git commit -m "feat: dispatch schedule payload when binding device"

Task 3: Run module-level verification and review edge behavior

Files:

  • Modify: none required unless verification reveals defects

Interfaces:

  • Consumes: completed controller change and regression test

  • Produces: fresh verification evidence for the implementation claim

  • Step 1: Run the focused controller regression test

Run: mvn -pl water-modules/water-app -DskipTests=false -Dtest=AppControllerTest test

Expected: PASS

  • Step 2: Run a compile pass for the app module with tests skipped

Run: mvn -pl water-modules/water-app -DskipTests compile

Expected: BUILD SUCCESS

  • Step 3: Inspect the diff to confirm scope stayed local

Run: git diff --stat

Expected: only the app controller, app module test baseline, the new controller test, and planning/spec docs changed for this task

  • Step 4: Commit any verification-driven follow-up if needed
git add -A
git commit -m "chore: verify schedule bind dispatch change"