Files
boss/android/app/src/main/java/com/hyzq/boss/OpsCenterActivity.java
2026-04-07 16:04:18 +08:00

280 lines
10 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.hyzq.boss;
import android.os.Bundle;
import android.widget.Button;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.LinkedHashMap;
import java.util.Map;
public class OpsCenterActivity extends BossScreenActivity {
private static final long REALTIME_RELOAD_THROTTLE_MS = 900L;
private LinearLayout contentRoot;
private @Nullable BossRealtimeClient realtimeClient;
private final Map<String, Long> recentRealtimeEventTimestamps = new LinkedHashMap<>();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
configureScreen("运维与修复", "运维会话、修复回放与 standby 切换");
setHeaderAction("刷新", v -> reload());
contentRoot = new LinearLayout(this);
contentRoot.setOrientation(LinearLayout.VERTICAL);
replaceContent(contentRoot);
realtimeClient = new BossRealtimeClient(apiClient, this::handleRealtimeEvent);
reload();
}
@Override
protected void onResume() {
super.onResume();
updateRealtimeSubscription();
}
@Override
protected void onPause() {
stopRealtimeUpdates();
super.onPause();
}
@Override
protected void onDestroy() {
stopRealtimeUpdates();
super.onDestroy();
}
@Override
protected void reload() {
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse ops = apiClient.getOpsSummary();
if (!ops.ok()) {
throw new IllegalStateException("OPS_LOAD_FAILED");
}
runOnUiThread(() -> render(ops.json));
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
replaceContent(BossUi.buildEmptyCard(this, "运维与修复加载失败:" + error.getMessage()));
});
}
});
}
private void updateRealtimeSubscription() {
if (apiClient != null && apiClient.hasSessionHints() && realtimeClient != null) {
realtimeClient.start();
return;
}
stopRealtimeUpdates();
}
private void stopRealtimeUpdates() {
if (realtimeClient != null) {
realtimeClient.stop();
}
}
void handleRealtimeEvent(BossRealtimeEvent event) {
if (event == null || event.eventName.isEmpty()) {
return;
}
if (!shouldReloadForRealtimeEvent(event)) {
return;
}
String eventFingerprint = BossRealtimeClient.buildEventFingerprint(event);
if (eventFingerprint.isEmpty()) {
return;
}
long now = System.currentTimeMillis();
if (isDuplicateRealtimeEvent(eventFingerprint, now)) {
return;
}
runOnUiThread(this::reload);
}
private boolean shouldReloadForRealtimeEvent(BossRealtimeEvent event) {
return "app.logs.updated".equals(event.eventName)
|| "project.context_risk.updated".equals(event.eventName);
}
private boolean isDuplicateRealtimeEvent(String eventFingerprint, long now) {
pruneRecentRealtimeEvents(now);
Long previousEventAt = recentRealtimeEventTimestamps.get(eventFingerprint);
if (previousEventAt != null && now - previousEventAt < REALTIME_RELOAD_THROTTLE_MS) {
return true;
}
recentRealtimeEventTimestamps.put(eventFingerprint, now);
return false;
}
private void pruneRecentRealtimeEvents(long now) {
java.util.Iterator<Map.Entry<String, Long>> iterator = recentRealtimeEventTimestamps.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Long> entry = iterator.next();
if (now - entry.getValue() >= REALTIME_RELOAD_THROTTLE_MS) {
iterator.remove();
}
}
}
private void render(JSONObject ops) {
replaceContent(contentRoot);
contentRoot.removeAllViews();
renderOpsTab(ops);
setRefreshing(false);
}
private void renderOpsTab(JSONObject ops) {
contentRoot.addView(BossUi.buildWechatMenuRow(
this,
"巡检状态",
ops.optString("mode", "idle").equals("active")
? "active当前存在风险线程或未关闭运维工单。"
: "idle当前没有高风险工单保持低频巡检。",
"这里只保留修复与验证的轻量入口。",
null,
null
));
JSONArray faults = ops.optJSONArray("faults");
if (faults == null || faults.length() == 0) {
contentRoot.addView(BossUi.buildEmptyCard(this, "当前没有运维故障。"));
} else {
for (int i = 0; i < faults.length(); i++) {
JSONObject fault = faults.optJSONObject(i);
if (fault == null) continue;
contentRoot.addView(buildFaultCard(fault, ops.optJSONArray("tickets")));
}
}
}
private LinearLayout buildFaultCard(JSONObject fault, @Nullable JSONArray tickets) {
LinearLayout card = new LinearLayout(this);
card.setOrientation(LinearLayout.VERTICAL);
card.addView(BossUi.buildWechatMenuRow(
this,
fault.optString("faultKey", "故障"),
fault.optString("summary", "暂无摘要"),
fault.optString("severity", "-")
+ " · " + fault.optString("status", "-")
+ " · " + fault.optString("nodeId", "-")
+ " · " + fault.optString("serviceName", "-")
+ " · 建议 " + fault.optString("suggestedNextAction", "暂无"),
null,
null
));
if (tickets != null) {
for (int i = 0; i < tickets.length(); i++) {
JSONObject ticket = tickets.optJSONObject(i);
if (ticket == null) continue;
if (!fault.optString("faultId").equals(ticket.optString("faultId"))) continue;
card.addView(buildTicketCard(ticket));
}
}
return card;
}
private LinearLayout buildTicketCard(JSONObject ticket) {
LinearLayout card = new LinearLayout(this);
card.setOrientation(LinearLayout.VERTICAL);
card.addView(BossUi.buildWechatMenuRow(
this,
ticket.optString("title", "修复工单"),
ticket.optString("actionSummary", "暂无动作摘要"),
ticket.optString("approvalStatus", "-")
+ " · " + ticket.optString("executionStatus", "-")
+ " · " + ticket.optString("targetNodeId", "-")
+ " · " + ticket.optString("updatedAt", "-"),
null,
null
));
if (ticket.optJSONObject("verification") != null) {
JSONObject verification = ticket.optJSONObject("verification");
card.addView(BossUi.buildWechatMenuRow(
this,
"验证结果",
verification.optString("summary", "暂无"),
verification.optString("status", "-")
+ " · " + verification.optString("verifiedAt", "-"),
null,
null
));
}
Button approve = BossUi.buildMiniActionButton(this, "批准修复", true);
approve.setOnClickListener(v -> approveTicket(ticket.optString("ticketId")));
Button verify = BossUi.buildMiniActionButton(this, "验证修复", false);
verify.setOnClickListener(v -> verifyTicket(ticket.optString("ticketId")));
card.addView(BossUi.buildInlineActionRow(this, approve, verify));
return card;
}
private void approveTicket(String ticketId) {
if (ticketId == null || ticketId.isEmpty()) {
showMessage("缺少 ticketId");
return;
}
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.approveRepairTicket(ticketId);
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> {
showMessage("修复工单已批准");
reload();
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
showMessage("批准失败:" + error.getMessage());
});
}
});
}
private void verifyTicket(String ticketId) {
if (ticketId == null || ticketId.isEmpty()) {
showMessage("缺少 ticketId");
return;
}
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.verifyRepairTicket(ticketId);
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> {
showMessage("修复结果已验证");
reload();
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
showMessage("验证失败:" + error.getMessage());
});
}
});
}
private String joinArray(@Nullable JSONArray values) {
if (values == null || values.length() == 0) return "-";
StringBuilder builder = new StringBuilder();
for (int i = 0; i < values.length(); i++) {
String value = values.optString(i);
if (value == null || value.isEmpty()) continue;
if (builder.length() > 0) builder.append(" · ");
builder.append(value);
}
return builder.length() == 0 ? "-" : builder.toString();
}
}