fix(Q): 主动控制与设备回执配对去重, 消除APP操作产生两条动态
- 后端 DeviceActivityService.recordEvent 新增配对判定: source=panel/state 的回执, 若 6s 内同设备同物模型属性同动作已有 app/voice/scene 主动控制记录, 则视为同一次操作的 回执并跳过(回执判定前等 1.2s, 给 APP 上报留落库时间) - 生产验证: 仅面板操作保留 1 条 panel; APP 操作+回执只保留 1 条 app - Android 首页新增 30s 动态心跳(页面可见时): 兜底拉取后端侧产生的动态(如面板操作), 只刷动态列表不碰设备列表, 不会造成常用设备跳位
This commit is contained in:
parent
f3aaf8fc35
commit
3a91a50dec
@ -111,6 +111,7 @@ public class HomeFragment extends Fragment {
|
||||
if (deviceRefreshHandler != null) {
|
||||
deviceRefreshHandler.removeCallbacks(alignRefreshTask);
|
||||
}
|
||||
stopHeartbeat();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -125,18 +126,31 @@ public class HomeFragment extends Fragment {
|
||||
loadDevicesFromBackend();
|
||||
// 兜底:页面回到前台时同步一次近期动态(覆盖进程内事件丢失的情况)
|
||||
scheduleActivityRefresh();
|
||||
// 页面可见:启动动态心跳(兜底拉取后端侧产生的动态,如面板操作)
|
||||
startHeartbeat();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
stopHeartbeat();
|
||||
}
|
||||
|
||||
/** Tab 用 hide/show 切换时不走 onResume,需在此刷新主题色 */
|
||||
@Override
|
||||
public void onHiddenChanged(boolean hidden) {
|
||||
super.onHiddenChanged(hidden);
|
||||
if (!hidden && adapter != null && isAdded()) {
|
||||
if (hidden) {
|
||||
stopHeartbeat();
|
||||
return;
|
||||
}
|
||||
if (adapter != null && isAdded()) {
|
||||
adapter.setAccentColor(ThemeManager.getInstance(requireContext()).accent().value);
|
||||
adapter.notifyDataSetChanged();
|
||||
// 兜底:切回首页时同步一次近期动态
|
||||
scheduleActivityRefresh();
|
||||
startHeartbeat();
|
||||
}
|
||||
}
|
||||
|
||||
@ -164,6 +178,42 @@ public class HomeFragment extends Fragment {
|
||||
activityRefreshHandler.postDelayed(activityRefreshTask, ACTIVITY_REFRESH_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
// ==================== 近期动态心跳 ====================
|
||||
|
||||
/**
|
||||
* 心跳间隔:30 秒。
|
||||
*
|
||||
* <p>用途:面板/物理开关的操作,只有在手机能收到设备 DP 回流时才会被感知
|
||||
* (此时事件总线会立即通知,不必等心跳);但当操作发生在手机不在家/无蓝牙连接时,
|
||||
* 只有后端能感知(涂鸦云消息订阅打通后)。心跳作为兜底,让首页在停留期间
|
||||
* 也能把后端侧新产生的动态拉回来。</p>
|
||||
*
|
||||
* <p>只刷新动态列表,不触碰设备列表,因此不会造成常用设备跳位。</p>
|
||||
*/
|
||||
private static final long ACTIVITY_HEARTBEAT_MS = 30_000L;
|
||||
private final android.os.Handler heartbeatHandler =
|
||||
new android.os.Handler(android.os.Looper.getMainLooper());
|
||||
private final Runnable heartbeatTask = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isAdded() && getView() != null) {
|
||||
loadRecentActivities();
|
||||
}
|
||||
heartbeatHandler.postDelayed(this, ACTIVITY_HEARTBEAT_MS);
|
||||
}
|
||||
};
|
||||
|
||||
/** 启动心跳(页面可见时) */
|
||||
private void startHeartbeat() {
|
||||
heartbeatHandler.removeCallbacks(heartbeatTask);
|
||||
heartbeatHandler.postDelayed(heartbeatTask, ACTIVITY_HEARTBEAT_MS);
|
||||
}
|
||||
|
||||
/** 停止心跳(页面不可见时) */
|
||||
private void stopHeartbeat() {
|
||||
heartbeatHandler.removeCallbacks(heartbeatTask);
|
||||
}
|
||||
|
||||
// ==================== 刷新节流(避免高频全量重绑导致界面闪动) ====================
|
||||
|
||||
/**
|
||||
|
||||
@ -111,6 +111,16 @@ public class DeviceActivityService {
|
||||
String type = StringUtils.hasText(event.getType())
|
||||
? event.getType() : (isControl ? "control" : "state");
|
||||
|
||||
// 1.5) 主动控制 ↔ 回执 配对去重
|
||||
// APP 主动控制时先落一条 source=app 的动态,随后设备把同一变化回传上来
|
||||
// (source=panel)。二者是同一次操作,用户只应看到一条「APP 操作」。
|
||||
// 这里按「同设备 + 同物模型属性 + 同动作 + 时间窗内已有主动控制」判定为回执并跳过。
|
||||
if (isPassiveFeedback(event, type) && isPairedWithActiveCommand(device, s)) {
|
||||
log.debug("回执已与主动控制配对,跳过重复动态: deviceId={} dp={} action={}",
|
||||
device.getDeviceId(), s.dpCode, s.action);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) 去重键
|
||||
String dedupeKey = computeDedupeKey(event, s);
|
||||
|
||||
@ -265,6 +275,80 @@ public class DeviceActivityService {
|
||||
return event.getDeviceId() + ":" + dp + ":" + val + ":" + slot;
|
||||
}
|
||||
|
||||
// ==================== 主动控制 ↔ 回执 配对去重 ====================
|
||||
|
||||
/** 主动控制来源(这些来源算「我方发起的操作」,其回执不应重复展示) */
|
||||
private static final java.util.Set<String> ACTIVE_COMMAND_SOURCES =
|
||||
java.util.Set.of("app", "voice", "scene");
|
||||
|
||||
/** 回执与主动控制的配对时间窗(毫秒) */
|
||||
private static final long FEEDBACK_PAIR_WINDOW_MS = 6_000L;
|
||||
|
||||
/** 回执判定前的等待时间(毫秒):给 APP 侧的主动上报留出落库时间 */
|
||||
private static final long FEEDBACK_PAIR_DELAY_MS = 1_200L;
|
||||
|
||||
/**
|
||||
* 是否是「被动回执」事件(设备把状态变化回传上来)。
|
||||
*
|
||||
* <p>面板操作、物理开关按下、涂鸦原生面板、云端 DP 上报都归到这里。</p>
|
||||
*/
|
||||
private boolean isPassiveFeedback(DeviceEvent event, String type) {
|
||||
if (!"state".equals(type)) {
|
||||
return false;
|
||||
}
|
||||
String source = event.getSource();
|
||||
return "panel".equals(source) || "local".equals(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断这条回执是否与近期的主动控制是同一次操作。
|
||||
*
|
||||
* <p>判据:同设备、同物模型属性、同动作(调节类再比值),且时间窗内已存在
|
||||
* app/voice/scene 来源的主动控制记录。</p>
|
||||
*/
|
||||
private boolean isPairedWithActiveCommand(Device device, Semantic s) {
|
||||
// 回执与 APP 主动上报几乎同时到达,稍等一下再判定,避免"先到先判"漏配对
|
||||
try {
|
||||
Thread.sleep(FEEDBACK_PAIR_DELAY_MS);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
try {
|
||||
java.util.List<DeviceActivityLog> list = activityMapper.selectList(
|
||||
new LambdaQueryWrapper<DeviceActivityLog>()
|
||||
.eq(DeviceActivityLog::getDeviceId, device.getDeviceId())
|
||||
.in(DeviceActivityLog::getSource, ACTIVE_COMMAND_SOURCES)
|
||||
.ge(DeviceActivityLog::getCreateTime,
|
||||
java.time.LocalDateTime.now()
|
||||
.minusSeconds(FEEDBACK_PAIR_WINDOW_MS / 1000))
|
||||
.orderByDesc(DeviceActivityLog::getCreateTime)
|
||||
.last("LIMIT 10"));
|
||||
for (DeviceActivityLog row : list) {
|
||||
if (isSameChange(row, s)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("回执配对查询失败 deviceId={}: {}", device.getDeviceId(), e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 两处记录是否描述了同一个变化(属性一致 + 动作一致) */
|
||||
private boolean isSameChange(DeviceActivityLog row, Semantic s) {
|
||||
if (!java.util.Objects.equals(row.getDpCode(), s.dpCode)) {
|
||||
return false;
|
||||
}
|
||||
if (!java.util.Objects.equals(row.getAction(), s.action)) {
|
||||
return false;
|
||||
}
|
||||
if ("adjust".equals(s.action)) {
|
||||
// 调节类动作动作名相同但值可能不同(如 调到 30% vs 调到 60%)
|
||||
return java.util.Objects.equals(row.getValueText(), s.valueText);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ==================== 兼容旧接口 ====================
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user