接口加密
This commit is contained in:
19
.idea/php.xml
generated
Normal file
19
.idea/php.xml
generated
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="MessDetectorOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PHPCSFixerOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PHPCodeSnifferOptionsConfiguration">
|
||||||
|
<option name="highlightLevel" value="WARNING" />
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PhpStanOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PsalmOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## 前置
|
## 前置
|
||||||
|
|
||||||
1. 云端配置 `HY_TRANSIT_API_TOKEN`,部署 `routes/hy.php` 与 `xk_hy_transit_cloud.sql`。
|
1. 云端配置 `HY_TRANSIT_API_TOKEN`(与 Go `XK_API_TOKEN` 一致),可选 `HY_TRANSIT_PAYLOAD_ENCRYPT=true`(默认开启 result 传输加密),部署 `routes/hy.php` 与 `xk_hy_transit_cloud.sql`。
|
||||||
2. 本机执行 [`sql/hy_transit_schema.sql`](../sql/hy_transit_schema.sql)。
|
2. 本机执行 [`sql/hy_transit_schema.sql`](../sql/hy_transit_schema.sql)。
|
||||||
3. 内网部署 `xk-hy-forward-go`。
|
3. 内网部署 `xk-hy-forward-go`。
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# 编辑 .env 填写 XK_API_TOKEN、HY_*、FORWARD_BASE_URL、FILE_UPLOAD_VIA_FORWARD、MYSQL_DSN
|
# 编辑 .env 填写 XK_API_TOKEN、HY_*、FORWARD_BASE_URL、FILE_UPLOAD_VIA_FORWARD、MYSQL_DSN
|
||||||
|
# XK_API_TOKEN 同时用于鉴权 Header 与解密云端 result(SHA256 派生 AES-128 密钥)
|
||||||
```
|
```
|
||||||
|
|
||||||
程序启动时自动 `godotenv.Load()` 读取 `.env`。
|
程序启动时自动 `godotenv.Load()` 读取 `.env`。
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## 前置
|
## 前置
|
||||||
|
|
||||||
1. 复制环境配置:`cp .env.example .env`,填写 `XK_API_TOKEN`、`HY_*`、`FORWARD_BASE_URL`、`MYSQL_DSN`。
|
1. 复制环境配置:`cp .env.example .env`,填写 `XK_API_TOKEN`、`HY_*`、`FORWARD_BASE_URL`、`MYSQL_DSN`。云端 `HY_TRANSIT_API_TOKEN` 须与 `XK_API_TOKEN` 一致;默认开启 `/api/hy/*` 响应 `result` 传输加密(`HY_TRANSIT_PAYLOAD_ENCRYPT`),Go 客户端自动解密。
|
||||||
2. 本机 MySQL 已执行 [`sql/hy_transit_schema.sql`](../../sql/hy_transit_schema.sql)。若库已存在且 Web 需展示**推送明文**,另执行 [`sql/hy_push_log_plain_migration.sql`](../../sql/hy_push_log_plain_migration.sql)。
|
2. 本机 MySQL 已执行 [`sql/hy_transit_schema.sql`](../../sql/hy_transit_schema.sql)。若库已存在且 Web 需展示**推送明文**,另执行 [`sql/hy_push_log_plain_migration.sql`](../../sql/hy_push_log_plain_migration.sql)。
|
||||||
3. 内网已启动 **xk-hy-forward-go**(`forward.exe` 或 `go run .`)。
|
3. 内网已启动 **xk-hy-forward-go**(`forward.exe` 或 `go run .`)。
|
||||||
4. 云端已部署 `xk_hy_transit_cloud.sql` 与 `routes/hy.php`。
|
4. 云端已部署 `xk_hy_transit_cloud.sql` 与 `routes/hy.php`。
|
||||||
@@ -47,10 +47,12 @@ go run ./cmd/transit
|
|||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 首页 | `/` | 导航 |
|
| 首页 | `/` | 导航 |
|
||||||
| 文件日志 | `/logs` | app / pull / push |
|
| 文件日志 | `/logs` | app / pull / push |
|
||||||
| 同步流水 | `/runs` | 三表关联 + JSON 详情 |
|
| 同步流水 | `/runs` | 三表关联 + JSON 详情;可一键清空本机三表 |
|
||||||
| 测试执行 | `/test` | POST 触发一次 sync |
|
| 测试执行 | `/test` | POST 触发一次 sync |
|
||||||
|
|
||||||
环境变量:`LOG_WEB_ADDR`(默认 `127.0.0.1:8765`)、`LOG_WEB_ALLOW_TEST`(默认 `true`)。Cron 与 Web 测试共用互斥锁,不会并行执行两次 sync。
|
API:`POST /api/runs/truncate` — 清空本机 `hy_push_log` / `hy_push_record` / `hy_sync_job`(需 MySQL 已连接;sync 运行中返回 409)。仅影响本机 `xk_hy_transit` 审计库。
|
||||||
|
|
||||||
|
环境变量:`LOG_WEB_ADDR`(默认 `127.0.0.1:8765`)、`LOG_WEB_ALLOW_TEST`(默认 `true`)。Cron 与 Web 测试共用互斥锁,不会并行执行两次 sync;截断与 sync 亦互斥。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
19
internal/db/truncate.go
Normal file
19
internal/db/truncate.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
// TruncateAuditTables 清空本机审计三表(不可恢复)。
|
||||||
|
// 顺序:hy_push_log → hy_push_record → hy_sync_job。
|
||||||
|
func (s *Store) TruncateAuditTables() error {
|
||||||
|
if err := s.Ping(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, q := range []string{
|
||||||
|
`TRUNCATE TABLE hy_push_log`,
|
||||||
|
`TRUNCATE TABLE hy_push_record`,
|
||||||
|
`TRUNCATE TABLE hy_sync_job`,
|
||||||
|
} {
|
||||||
|
if _, err := s.db.Exec(q); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -4,3 +4,9 @@
|
|||||||
[push] 2026/05/22 08:57:01 chromedp: using browser C:\Program Files\Google\Chrome\Application\chrome.exe
|
[push] 2026/05/22 08:57:01 chromedp: using browser C:\Program Files\Google\Chrome\Application\chrome.exe
|
||||||
[push] 2026/05/22 08:57:01 chromedp: html_len=87 data_url_len=152
|
[push] 2026/05/22 08:57:01 chromedp: html_len=87 data_url_len=152
|
||||||
[push] 2026/05/22 08:57:02 chromedp: pdf bytes=5205
|
[push] 2026/05/22 08:57:02 chromedp: pdf bytes=5205
|
||||||
|
[push] 2026/05/22 09:18:12 chromedp: using browser C:\Program Files\Google\Chrome\Application\chrome.exe
|
||||||
|
[push] 2026/05/22 09:18:12 chromedp: html_len=87 data_url_len=152
|
||||||
|
[push] 2026/05/22 09:18:13 chromedp: pdf bytes=5205
|
||||||
|
[push] 2026/05/22 09:18:22 chromedp: using browser C:\Program Files\Google\Chrome\Application\chrome.exe
|
||||||
|
[push] 2026/05/22 09:18:22 chromedp: html_len=87 data_url_len=152
|
||||||
|
[push] 2026/05/22 09:18:23 chromedp: pdf bytes=5205
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ func (s *Server) routes() {
|
|||||||
s.mux.HandleFunc("/api/log", s.handleAPILog)
|
s.mux.HandleFunc("/api/log", s.handleAPILog)
|
||||||
s.mux.HandleFunc("/api/runs", s.handleAPIRuns)
|
s.mux.HandleFunc("/api/runs", s.handleAPIRuns)
|
||||||
s.mux.HandleFunc("/api/runs/record", s.handleAPIRecord)
|
s.mux.HandleFunc("/api/runs/record", s.handleAPIRecord)
|
||||||
|
s.mux.HandleFunc("/api/runs/truncate", s.handleAPIRunsTruncate)
|
||||||
s.mux.HandleFunc("/api/test/status", s.handleAPITestStatus)
|
s.mux.HandleFunc("/api/test/status", s.handleAPITestStatus)
|
||||||
s.mux.HandleFunc("/api/test/sync", s.handleAPITestSync)
|
s.mux.HandleFunc("/api/test/sync", s.handleAPITestSync)
|
||||||
|
|
||||||
@@ -97,6 +98,7 @@ func (s *Server) handleAPIMeta(w http.ResponseWriter, r *http.Request) {
|
|||||||
"mysqlOk": s.deps.MySQLOK,
|
"mysqlOk": s.deps.MySQLOK,
|
||||||
"mysqlError": s.deps.MySQLErr,
|
"mysqlError": s.deps.MySQLErr,
|
||||||
"allowTest": s.deps.AllowTest && s.deps.Runner != nil,
|
"allowTest": s.deps.AllowTest && s.deps.Runner != nil,
|
||||||
|
"allowTruncate": s.deps.Store != nil && s.deps.MySQLOK,
|
||||||
"hasRunner": s.deps.Runner != nil,
|
"hasRunner": s.deps.Runner != nil,
|
||||||
"stepOptions": hy.StepSelectOptions(),
|
"stepOptions": hy.StepSelectOptions(),
|
||||||
"testStepOptions": hy.TestStepSelectOptions(),
|
"testStepOptions": hy.TestStepSelectOptions(),
|
||||||
@@ -161,6 +163,31 @@ func (s *Server) handleAPIRecord(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, detail)
|
writeJSON(w, detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleAPIRunsTruncate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
writeErr(w, 405, "POST only")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.deps.Store == nil || !s.deps.MySQLOK {
|
||||||
|
writeErr(w, 503, "MySQL 未连接")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if syncgate.IsRunning() {
|
||||||
|
writeErr(w, 409, "已有同步任务在执行中")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := s.deps.Store.TruncateAuditTables(); err != nil {
|
||||||
|
applog.Appf("web truncate audit tables failed: %v", err)
|
||||||
|
writeErr(w, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
applog.Appf("web truncate audit tables ok")
|
||||||
|
writeJSON(w, map[string]any{
|
||||||
|
"ok": true,
|
||||||
|
"tables": []string{"hy_push_log", "hy_push_record", "hy_sync_job"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleAPITestStatus(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleAPITestStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
writeJSON(w, enrichTestStatus(syncgate.GetStatus()))
|
writeJSON(w, enrichTestStatus(syncgate.GetStatus()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
<select id="step"></select>
|
<select id="step"></select>
|
||||||
</label>
|
</label>
|
||||||
<button class="primary" id="load">刷新</button>
|
<button class="primary" id="load">刷新</button>
|
||||||
|
<button type="button" class="danger" id="truncate" title="清空本机三表全部数据">清空三表</button>
|
||||||
</div>
|
</div>
|
||||||
<h3 style="color:var(--muted);font-size:0.95rem">同步任务</h3>
|
<h3 style="color:var(--muted);font-size:0.95rem">同步任务</h3>
|
||||||
<div style="overflow:auto;margin-bottom:1.5rem">
|
<div style="overflow:auto;margin-bottom:1.5rem">
|
||||||
@@ -37,11 +38,41 @@
|
|||||||
const anchorFromUrl = qs('anchor_date');
|
const anchorFromUrl = qs('anchor_date');
|
||||||
if (anchorFromUrl) document.getElementById('anchor').value = anchorFromUrl;
|
if (anchorFromUrl) document.getElementById('anchor').value = anchorFromUrl;
|
||||||
|
|
||||||
|
const truncateBtn = document.getElementById('truncate');
|
||||||
|
let pageMeta = null;
|
||||||
|
|
||||||
|
function updateTruncateButton(meta, syncRunning) {
|
||||||
|
if (!truncateBtn) return;
|
||||||
|
if (!meta || !meta.allowTruncate) {
|
||||||
|
truncateBtn.disabled = true;
|
||||||
|
truncateBtn.title = 'MySQL 未连接,无法清空';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (syncRunning) {
|
||||||
|
truncateBtn.disabled = true;
|
||||||
|
truncateBtn.title = '同步任务执行中,请稍后再试';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
truncateBtn.disabled = false;
|
||||||
|
truncateBtn.title = '清空本机 hy_push_log / hy_push_record / hy_sync_job(不可恢复)';
|
||||||
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const m = await loadMeta();
|
pageMeta = await loadMeta();
|
||||||
if (m && m.stepOptions) fillStepSelect(document.getElementById('step'), m.stepOptions);
|
if (pageMeta && pageMeta.stepOptions) fillStepSelect(document.getElementById('step'), pageMeta.stepOptions);
|
||||||
|
updateTruncateButton(pageMeta, false);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
async function refreshSyncGate() {
|
||||||
|
try {
|
||||||
|
const s = await api('/api/test/status');
|
||||||
|
updateTruncateButton(pageMeta, !!(s && s.running));
|
||||||
|
} catch (_) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setInterval(refreshSyncGate, 2000);
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
const anchor = document.getElementById('anchor').value;
|
const anchor = document.getElementById('anchor').value;
|
||||||
const step = document.getElementById('step').value;
|
const step = document.getElementById('step').value;
|
||||||
@@ -83,7 +114,26 @@ async function load() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('load').onclick = () => load().catch(e => alert(e.message));
|
document.getElementById('load').onclick = () => load().catch(e => alert(e.message));
|
||||||
|
|
||||||
|
truncateBtn.onclick = async () => {
|
||||||
|
const tables = 'hy_push_log、hy_push_record、hy_sync_job';
|
||||||
|
if (!confirm('将清空本机审计库三表(' + tables + '),不可恢复。\n仅影响本机 xk_hy_transit,不动云端数据。\n\n确定继续?')) return;
|
||||||
|
const typed = prompt('请输入 TRUNCATE 以确认清空:');
|
||||||
|
if (typed !== 'TRUNCATE') {
|
||||||
|
if (typed !== null) alert('已取消:确认词不正确');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await api('/api/runs/truncate', { method: 'POST' });
|
||||||
|
alert('三表已清空');
|
||||||
|
await load();
|
||||||
|
} catch (e) {
|
||||||
|
alert(e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
load().catch(e => alert(e.message));
|
load().catch(e => alert(e.message));
|
||||||
|
refreshSyncGate();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ button {
|
|||||||
background: #21262d;
|
background: #21262d;
|
||||||
}
|
}
|
||||||
button.primary { background: #238636; border-color: #2ea043; }
|
button.primary { background: #238636; border-color: #2ea043; }
|
||||||
|
button.danger { background: #da3633; border-color: var(--err); }
|
||||||
|
button.danger:hover:not(:disabled) { filter: brightness(1.08); }
|
||||||
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -51,3 +51,4 @@
|
|||||||
[app] 2026/05/22 09:04:48 web test sync done step=all date=2026-05-22
|
[app] 2026/05/22 09:04:48 web test sync done step=all date=2026-05-22
|
||||||
======== END web-test step=all date=2026-05-22 ok=true ========
|
======== END web-test step=all date=2026-05-22 ok=true ========
|
||||||
[app] 2026/05/22 09:12:46 file upload via forward: http://127.0.0.1:16001/mng/file/auth/upload (cloud fileUploadUrl ignored)
|
[app] 2026/05/22 09:12:46 file upload via forward: http://127.0.0.1:16001/mng/file/auth/upload (cloud fileUploadUrl ignored)
|
||||||
|
[app] 2026/05/22 09:19:37 file upload via forward: http://127.0.0.1:16001/mng/file/auth/upload (cloud fileUploadUrl ignored)
|
||||||
|
|||||||
Reference in New Issue
Block a user