268 lines
11 KiB
Markdown
268 lines
11 KiB
Markdown
# 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**
|
|
|
|
```xml
|
|
<dependency>
|
|
<groupId>org.springframework.boot</groupId>
|
|
<artifactId>spring-boot-starter-test</artifactId>
|
|
<scope>test</scope>
|
|
</dependency>
|
|
```
|
|
|
|
- [ ] **Step 2: Write the failing controller regression test**
|
|
|
|
```java
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```java
|
|
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));
|
|
}
|
|
```
|
|
|
|
```java
|
|
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**
|
|
|
|
```java
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "chore: verify schedule bind dispatch change"
|
|
```
|