From 61759071dc7bc474b3bdbf25992e6c7271ae1cc6 Mon Sep 17 00:00:00 2001
From: Administrator <15274802129@163.com>
Date: Sun, 20 Sep 2026 15:30:37 +0800
Subject: [PATCH] feat(station): 添加手动控制面板功能

---
 src/main/resources/static/station-dashboard.html                                    |   40 ++++++++++--
 src/main/java/com/xcong/excoin/modules/station/controller/StationController.java    |   42 ++++++++++++++
 src/main/java/com/xcong/excoin/modules/station/service/impl/StationServiceImpl.java |   32 ++++++++++
 src/main/java/com/xcong/excoin/modules/station/service/StationService.java          |   18 ++++++
 4 files changed, 124 insertions(+), 8 deletions(-)

diff --git a/src/main/java/com/xcong/excoin/modules/station/controller/StationController.java b/src/main/java/com/xcong/excoin/modules/station/controller/StationController.java
index 3902ce7..b32b786 100644
--- a/src/main/java/com/xcong/excoin/modules/station/controller/StationController.java
+++ b/src/main/java/com/xcong/excoin/modules/station/controller/StationController.java
@@ -88,4 +88,46 @@
         stationService.updateConfig(apiKeyMd5, params);
         return Result.ok("参数已保存,请手动点击「启动」使新配置生效");
     }
+
+    @ApiOperation("取消多仓止盈止损挂单")
+    @PostMapping("/cancel-long-tp-sl")
+    public Result cancelLongTpSl(@RequestParam String apiKeyMd5) {
+        stationService.cancelLongTpSl(apiKeyMd5);
+        return Result.ok("指令已发送");
+    }
+
+    @ApiOperation("取消空仓止盈止损挂单")
+    @PostMapping("/cancel-short-tp-sl")
+    public Result cancelShortTpSl(@RequestParam String apiKeyMd5) {
+        stationService.cancelShortTpSl(apiKeyMd5);
+        return Result.ok("指令已发送");
+    }
+
+    @ApiOperation("生成多仓止盈止损挂单")
+    @PostMapping("/rebuild-long-tp-sl")
+    public Result rebuildLongTpSl(@RequestParam String apiKeyMd5) {
+        stationService.rebuildLongTpSl(apiKeyMd5);
+        return Result.ok("指令已发送");
+    }
+
+    @ApiOperation("生成空仓止盈止损挂单")
+    @PostMapping("/rebuild-short-tp-sl")
+    public Result rebuildShortTpSl(@RequestParam String apiKeyMd5) {
+        stationService.rebuildShortTpSl(apiKeyMd5);
+        return Result.ok("指令已发送");
+    }
+
+    @ApiOperation("多单平仓")
+    @PostMapping("/close-long")
+    public Result closeLong(@RequestParam String apiKeyMd5) {
+        stationService.closeLong(apiKeyMd5);
+        return Result.ok("指令已发送");
+    }
+
+    @ApiOperation("空单平仓")
+    @PostMapping("/close-short")
+    public Result closeShort(@RequestParam String apiKeyMd5) {
+        stationService.closeShort(apiKeyMd5);
+        return Result.ok("指令已发送");
+    }
 }
diff --git a/src/main/java/com/xcong/excoin/modules/station/service/StationService.java b/src/main/java/com/xcong/excoin/modules/station/service/StationService.java
index 49ec74f..8fdce89 100644
--- a/src/main/java/com/xcong/excoin/modules/station/service/StationService.java
+++ b/src/main/java/com/xcong/excoin/modules/station/service/StationService.java
@@ -25,6 +25,24 @@
     /** 更新策略参数(通过 MQ 发送 UPDATE_CONFIG 到目标 JAR,仅持久化,需手动启动生效) */
     void updateConfig(String apiKeyMd5, Map<String, Object> params);
 
+    /** 取消多仓止盈止损(通过 MQ 发送 CANCEL_LONG_TP_SL 到目标 JAR) */
+    void cancelLongTpSl(String apiKeyMd5);
+
+    /** 取消空仓止盈止损(通过 MQ 发送 CANCEL_SHORT_TP_SL 到目标 JAR) */
+    void cancelShortTpSl(String apiKeyMd5);
+
+    /** 生成多仓止盈止损(通过 MQ 发送 REBUILD_LONG_TP_SL 到目标 JAR) */
+    void rebuildLongTpSl(String apiKeyMd5);
+
+    /** 生成空仓止盈止损(通过 MQ 发送 REBUILD_SHORT_TP_SL 到目标 JAR) */
+    void rebuildShortTpSl(String apiKeyMd5);
+
+    /** 多单平仓(通过 MQ 发送 CLOSE_LONG 到目标 JAR) */
+    void closeLong(String apiKeyMd5);
+
+    /** 空单平仓(通过 MQ 发送 CLOSE_SHORT 到目标 JAR) */
+    void closeShort(String apiKeyMd5);
+
     /** 保存实例别名(空白/空串 = 清除别名),持久化到 strategy_status */
     void saveAlias(String apiKeyMd5, String alias);
 
diff --git a/src/main/java/com/xcong/excoin/modules/station/service/impl/StationServiceImpl.java b/src/main/java/com/xcong/excoin/modules/station/service/impl/StationServiceImpl.java
index 8bc76bd..b2ce1c0 100644
--- a/src/main/java/com/xcong/excoin/modules/station/service/impl/StationServiceImpl.java
+++ b/src/main/java/com/xcong/excoin/modules/station/service/impl/StationServiceImpl.java
@@ -67,6 +67,38 @@
         cmdProducer.sendCommand(apiKeyMd5, "UPDATE_CONFIG", JSON.toJSONString(params));
     }
 
+    // ==================== 手动控制指令 ====================
+
+    @Override
+    public void cancelLongTpSl(String apiKeyMd5) {
+        cmdProducer.sendCommand(apiKeyMd5, "CANCEL_LONG_TP_SL");
+    }
+
+    @Override
+    public void cancelShortTpSl(String apiKeyMd5) {
+        cmdProducer.sendCommand(apiKeyMd5, "CANCEL_SHORT_TP_SL");
+    }
+
+    @Override
+    public void rebuildLongTpSl(String apiKeyMd5) {
+        cmdProducer.sendCommand(apiKeyMd5, "REBUILD_LONG_TP_SL");
+    }
+
+    @Override
+    public void rebuildShortTpSl(String apiKeyMd5) {
+        cmdProducer.sendCommand(apiKeyMd5, "REBUILD_SHORT_TP_SL");
+    }
+
+    @Override
+    public void closeLong(String apiKeyMd5) {
+        cmdProducer.sendCommand(apiKeyMd5, "CLOSE_LONG");
+    }
+
+    @Override
+    public void closeShort(String apiKeyMd5) {
+        cmdProducer.sendCommand(apiKeyMd5, "CLOSE_SHORT");
+    }
+
     // ==================== 别名 ====================
 
     @Override
diff --git a/src/main/resources/static/station-dashboard.html b/src/main/resources/static/station-dashboard.html
index 2f2f7d8..c19fc8c 100644
--- a/src/main/resources/static/station-dashboard.html
+++ b/src/main/resources/static/station-dashboard.html
@@ -130,6 +130,10 @@
         .btn-outline { background: transparent; border: 1px solid var(--border); color: var(--text); }
         .btn:disabled { opacity: .4; cursor: not-allowed; }
 
+        /* ===== 手动控制面板 ===== */
+        .manual-actions { display: flex; flex-wrap: wrap; gap: 8px; }
+        .manual-actions .btn { padding: 7px 14px; }
+
         /* ===== 内容区(Tabs) ===== */
         .content { flex: 1; overflow-y: auto; padding: 16px 24px; }
         .tabs {
@@ -365,8 +369,8 @@
             <h2 style="color:var(--dim)">← 选择一个实例</h2>
         </div>
         <div class="btn-group">
-            <button class="btn btn-success" id="btnStart" disabled onclick="showConfirm('start')">▶ 启动</button>
-            <button class="btn btn-danger" id="btnStop" disabled onclick="showConfirm('stop')">⏹ 停止</button>
+            <button class="btn btn-success" id="btnStart" disabled onclick="showConfirm('start','启动')">▶ 启动</button>
+            <button class="btn btn-danger" id="btnStop" disabled onclick="showConfirm('stop','停止')">⏹ 停止</button>
         </div>
     </div>
 
@@ -391,6 +395,17 @@
     <!-- 概览 Tab -->
     <div class="tab-content active" data-tab="overview">
         <div class="stats-grid" id="statsGrid"></div>
+        <div class="chart-container">
+            <h3>⚙️ 手动控制 <span style="font-size:11px;color:var(--dim);font-weight:400">(实时下发 MQ 指令,策略运行中可用)</span></h3>
+            <div class="manual-actions">
+                <button class="btn btn-outline" id="btnCancelLongTpSl" disabled onclick="showConfirm('cancel-long-tp-sl','取消多仓止盈止损')">取消多仓止盈止损</button>
+                <button class="btn btn-outline" id="btnCancelShortTpSl" disabled onclick="showConfirm('cancel-short-tp-sl','取消空仓止盈止损')">取消空仓止盈止损</button>
+                <button class="btn btn-outline" id="btnRebuildLongTpSl" disabled onclick="showConfirm('rebuild-long-tp-sl','生成多仓止盈止损')">生成多仓止盈止损</button>
+                <button class="btn btn-outline" id="btnRebuildShortTpSl" disabled onclick="showConfirm('rebuild-short-tp-sl','生成空仓止盈止损')">生成空仓止盈止损</button>
+                <button class="btn btn-danger" id="btnCloseLong" disabled onclick="showConfirm('close-long','多单平仓')">多单平仓</button>
+                <button class="btn btn-danger" id="btnCloseShort" disabled onclick="showConfirm('close-short','空单平仓')">空单平仓</button>
+            </div>
+        </div>
         <div class="chart-container">
             <h3>📈 盈亏趋势(最近 PNL_SNAPSHOT 事件)</h3>
             <div class="pnl-summary" id="pnlSummary"></div>
@@ -1205,24 +1220,33 @@
 function updateActionButtons(inst) {
     const startBtn = $('btnStart');
     const stopBtn = $('btnStop');
+    // 手动控制按钮(止盈止损/平仓):需策略运行中才有持仓/挂单可操作
+    const manualIds = ['btnCancelLongTpSl', 'btnCancelShortTpSl', 'btnRebuildLongTpSl',
+        'btnRebuildShortTpSl', 'btnCloseLong', 'btnCloseShort'];
+    const setManual = (disabled) => {
+        manualIds.forEach(id => { const b = $(id); if (b) b.disabled = disabled; });
+    };
     if (!inst) {
         startBtn.disabled = true;
         stopBtn.disabled = true;
+        setManual(true);
         return;
     }
     const running = inst.state === 'ACTIVE' || inst.state === 'OPENING' || inst.state === 'WAITING_KLINE';
     startBtn.disabled = running;   // 运行中不可重复启动
     stopBtn.disabled = !running;   // 未运行不可停止
+    setManual(!running);           // 仅运行中可下发手动指令
 }
 
-function showConfirm(action) {
+function showConfirm(action, label) {
     if (!selectedMd5) return;
-    pendingAction = action;
-    const verb = action === 'start' ? '启动' : '停止';
+    pendingAction = { path: action, label: label || action };
+    const verb = pendingAction.label;
+    const isDanger = action === 'stop' || action === 'close-long' || action === 'close-short';
     $('modalTitle').textContent = `确认${verb}`;
-    $('modalMsg').textContent = `确定要${verb}实例 ${selectedMd5.substring(0, 12)}... 的策略吗?`;
+    $('modalMsg').textContent = `确定要对实例 ${selectedMd5.substring(0, 12)}... 执行「${verb}」吗?`;
     $('modalConfirmBtn').textContent = verb;
-    $('modalConfirmBtn').className = 'btn ' + (action === 'start' ? 'btn-success' : 'btn-danger');
+    $('modalConfirmBtn').className = 'btn ' + (isDanger ? 'btn-danger' : 'btn-success');
     $('modalOverlay').classList.add('show');
 }
 
@@ -1272,7 +1296,7 @@
     closeModal();
 
     try {
-        const msg = await api(API + '/' + action + '?apiKeyMd5=' + encodeURIComponent(selectedMd5), { method: 'POST' });
+        const msg = await api(API + '/' + action.path + '?apiKeyMd5=' + encodeURIComponent(selectedMd5), { method: 'POST' });
         toast(msg || '指令已发送', 'success');
         // 延迟刷新等待 ACK
         setTimeout(refreshInstanceList, 2000);

--
Gitblit v1.9.1