This commit is contained in:
Your Name
2026-04-16 13:05:07 +08:00
commit 32efff1670
99 changed files with 9974 additions and 0 deletions

View File

@@ -0,0 +1,179 @@
# domainCheck Linux 部署说明
本文档用于将 `domain-api``domainCheck Worker` 部署到 Linux并由 `systemd` 托管。
## 一、建议目录结构
```text
/opt/domaincheck
├── domain-api
├── domain-web
└── domainCheck
```
说明:
- `domain-api` 提供 Web 后台接口
- `domain-web` 为前端静态文件项目
- `domainCheck` 保留当前检测核心与 Worker
## 二、准备 Python 环境
建议使用 Python 3.11。
```bash
cd /opt/domaincheck/domainCheck
python3.11 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cd /opt/domaincheck/domain-api
pip install fastapi uvicorn pydantic-settings psycopg2-binary redis openpyxl python-multipart
```
## 三、准备配置文件
### 1. domainCheck/.env
`domain-api` 默认会读取 `/opt/domaincheck/domainCheck/.env`
至少确认下面这些配置正确:
```env
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=domain
DB_USER=postgres
DB_PASSWORD=postgres
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
```
### 2. 可选的 domain-api 环境变量
可以参考 `.env.example`
常用项:
- `API_HOST`
- `API_PORT`
- `WORKER_MODE`
- `WORKER_SERVICE_NAME`
- `API_SERVICE_NAME`
- `ADMIN_USERNAME`
- `ADMIN_PASSWORD`
## 四、部署 systemd
模板文件:
- `deploy/systemd/domain-api.service`
- `deploy/systemd/domain-worker.service`
复制到系统目录:
```bash
sudo cp /opt/domaincheck/domain-api/deploy/systemd/domain-api.service /etc/systemd/system/domaincheck-api.service
sudo cp /opt/domaincheck/domain-api/deploy/systemd/domain-worker.service /etc/systemd/system/domaincheck-worker.service
```
然后按实际机器修改:
- `WorkingDirectory`
- `ExecStart`
- `User`
- `Group`
- `Environment`
## 五、启动顺序
```bash
sudo systemctl daemon-reload
sudo systemctl enable domaincheck-api
sudo systemctl enable domaincheck-worker
sudo systemctl start domaincheck-api
sudo systemctl start domaincheck-worker
```
查看状态:
```bash
sudo systemctl status domaincheck-api
sudo systemctl status domaincheck-worker
```
查看日志:
```bash
journalctl -u domaincheck-api -n 200 --no-pager
journalctl -u domaincheck-worker -n 200 --no-pager
```
## 六、联调检查
### 1. API 健康检查
```bash
curl http://127.0.0.1:8100/health
```
期望看到:
- `status=ok`
- `worker_mode=linux-systemd`
### 2. Web 后台系统设置
在 Web 后台里确认:
- `Worker 运行模式 = linux-systemd`
- `Worker 服务名 = domaincheck-worker`
- `API 服务名 = domaincheck-api`
### 3. 运行中心
进入 `运行中心`,确认:
- API 在线
- Worker 在线
- 进程数大于 0
- 最近启动时间正常
### 4. 运行 API 自测脚本
```bash
cd /opt/domaincheck/domain-api
python deploy/linux/smoke_test.py --base-url http://127.0.0.1:8100
```
如果同时希望把 Web 首页一起纳入检查:
```bash
python deploy/linux/smoke_test.py --base-url http://127.0.0.1:8100 --web-url http://127.0.0.1
```
## 七、上线建议
- 先保持 `Windows 桌面版 + Web/Linux` 并行一段时间
- 先让 Web 后台接管设置、导入、筛选、导出、日志
- 再让 Linux Worker 接管主检测任务
- 确认稳定后,再逐步淡出桌面检测端
## 八、联调诊断采集
如需导出一份联调诊断包,可执行:
```bash
cd /opt/domaincheck/domain-api/deploy/linux
bash collect_diagnostics.sh /opt/domaincheck
```
会输出:
- `diagnostics_dir=...`
- `diagnostics_archive=...`
把生成的归档包发回即可继续排障。

View File

@@ -0,0 +1,60 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_DIR="${1:-/opt/domaincheck}"
OUT_DIR="${2:-$BASE_DIR/diagnostics}"
STAMP="$(date +%Y%m%d_%H%M%S)"
BUNDLE_DIR="$OUT_DIR/diag_$STAMP"
mkdir -p "$BUNDLE_DIR"
write_cmd() {
local name="$1"
shift
{
echo "# command: $*"
echo
"$@"
} > "$BUNDLE_DIR/$name.txt" 2>&1 || true
}
copy_if_exists() {
local src="$1"
local dest="$2"
if [ -f "$src" ]; then
cp "$src" "$dest"
fi
}
write_cmd "systemctl_api" systemctl status domaincheck-api --no-pager
write_cmd "systemctl_worker" systemctl status domaincheck-worker --no-pager
write_cmd "journal_api" journalctl -u domaincheck-api -n 200 --no-pager
write_cmd "journal_worker" journalctl -u domaincheck-worker -n 200 --no-pager
write_cmd "api_health_curl" curl -sS http://127.0.0.1:8100/health
write_cmd "api_preflight_curl" curl -sS http://127.0.0.1:8100/api/v1/runtime/preflight
write_cmd "api_runtime_curl" curl -sS http://127.0.0.1:8100/api/v1/runtime/status
write_cmd "ps_processes" ps -ef
write_cmd "ss_listen" ss -lntp
write_cmd "df_h" df -h
write_cmd "free_h" free -h
copy_if_exists "$BASE_DIR/domainCheck/.env" "$BUNDLE_DIR/domainCheck.env"
copy_if_exists "$BASE_DIR/domain-api/.env" "$BUNDLE_DIR/domain-api.env"
if [ -d "$BASE_DIR/domain-api/runtime" ]; then
cp -r "$BASE_DIR/domain-api/runtime" "$BUNDLE_DIR/domain-api-runtime"
fi
if [ -f "$BASE_DIR/domainCheck/logs/app.log" ]; then
tail -n 300 "$BASE_DIR/domainCheck/logs/app.log" > "$BUNDLE_DIR/app_tail.log" 2>&1 || true
fi
if [ -f "$BASE_DIR/domainCheck/detect_worker.log" ]; then
tail -n 500 "$BASE_DIR/domainCheck/detect_worker.log" > "$BUNDLE_DIR/detect_worker_tail.log" 2>&1 || true
fi
ARCHIVE="$OUT_DIR/diag_$STAMP.tar.gz"
tar -czf "$ARCHIVE" -C "$OUT_DIR" "diag_$STAMP"
echo "diagnostics_dir=$BUNDLE_DIR"
echo "diagnostics_archive=$ARCHIVE"

View File

@@ -0,0 +1,103 @@
from __future__ import annotations
import argparse
import json
import sys
from datetime import datetime
import requests
def _fetch_json(session: requests.Session, url: str, timeout: int = 15) -> tuple[bool, str, dict | None]:
try:
response = session.get(url, timeout=timeout)
response.raise_for_status()
return True, f"{response.status_code}", response.json()
except Exception as exc:
return False, str(exc), None
def _fetch_text(session: requests.Session, url: str, timeout: int = 15) -> tuple[bool, str]:
try:
response = session.get(url, timeout=timeout)
response.raise_for_status()
return True, f"{response.status_code}"
except Exception as exc:
return False, str(exc)
def main() -> int:
if hasattr(sys.stdout, "reconfigure"):
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
parser = argparse.ArgumentParser(description="domainCheck Web/API smoke test")
parser.add_argument("--base-url", default="http://127.0.0.1:8100", help="domain-api base URL")
parser.add_argument("--web-url", default="", help="optional domain-web URL, e.g. http://127.0.0.1:3200")
parser.add_argument("--output", default="", help="optional JSON report output path")
args = parser.parse_args()
base_url = args.base_url.rstrip("/")
session = requests.Session()
checks: list[dict[str, object]] = []
endpoints = [
("health", f"{base_url}/health"),
("runtime_status", f"{base_url}/api/v1/runtime/status"),
("runtime_preflight", f"{base_url}/api/v1/runtime/preflight"),
("dashboard_overview", f"{base_url}/api/v1/dashboard/overview"),
("settings_export", f"{base_url}/api/v1/settings/export"),
("settings_backups", f"{base_url}/api/v1/settings/backups"),
("detect_status", f"{base_url}/api/v1/detect/status"),
("imports_summary", f"{base_url}/api/v1/imports/summary"),
("exports", f"{base_url}/api/v1/exports"),
("logs_latest", f"{base_url}/api/v1/logs/latest"),
]
for name, url in endpoints:
ok, message, payload = _fetch_json(session, url)
checks.append(
{
"name": name,
"url": url,
"ok": ok,
"message": message,
"payload_excerpt": payload if ok and name in {"health", "runtime_preflight", "settings_export"} else None,
}
)
if args.web_url:
web_url = args.web_url.rstrip("/")
ok, message = _fetch_text(session, web_url)
checks.append(
{
"name": "web_home",
"url": web_url,
"ok": ok,
"message": message,
"payload_excerpt": None,
}
)
passed = all(bool(item["ok"]) for item in checks)
report = {
"generated_at": datetime.now().isoformat(timespec="seconds"),
"base_url": base_url,
"web_url": args.web_url.rstrip("/") if args.web_url else "",
"ok": passed,
"checks": checks,
}
if args.output:
with open(args.output, "w", encoding="utf-8") as handle:
json.dump(report, handle, ensure_ascii=False, indent=2)
print(json.dumps(report, ensure_ascii=False, indent=2))
return 0 if passed else 1
if __name__ == "__main__":
sys.exit(main())