280 lines
10 KiB
Java
280 lines
10 KiB
Java
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();
|
||
}
|
||
}
|