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
deviceNoanddetails detailsmust follow the current device-facing schedule detail structure used bybuildScheduleBindPayload- 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 abindSchedulecommand 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"