v1.1.0: Alpine 轻量级 Docker 开发环境 + 敏感凭证清理

This commit is contained in:
lxy
2026-07-27 13:59:18 +08:00
parent feb76081a0
commit ccca0fa62b
51 changed files with 6125 additions and 993 deletions
+117
View File
@@ -0,0 +1,117 @@
# Flux 开发环境
> 当前在 ws-flux-dev 容器内
> 工作目录: /workspace (= E:/wk-flux)
## 项目概述
贷款撮合平台 (Flux),核心模块:
| 模块 | 技术栈 | 说明 |
|------|--------|------|
| flux-api | Java 17 + Redkale + Maven | 后端 API,核心业务 |
| flux-admin | Vue 3 + Arco Design + bun | 管理后台 |
| flux-uniapp | UniApp + Vite | H5 前端 |
| flux-mock | Go | 三方模拟服务 |
| flux-test | Go | 业务链测试 (撞库等) |
| file-server | Go | 文件上传下载 |
| flux-wechat-api | — | 微信服务端 |
## 代理工具(优先使用)
> **容器内代理服务由 .bashrc 自动启动**,无需手动启动。
> **所有数据库和远程操作必须通过代理工具执行**,禁止直接用 mysql/ssh/redis-cli。
### MySQL 查询
```bash
# 查询 (默认表格输出)
mysql-proxy cli -c flux_dev -e "SELECT * FROM users LIMIT 10"
# JSON 输出 (便于解析)
mysql-proxy cli -c flux_dev -e "SHOW TABLES" -F json
# 执行 DML (加 -x)
mysql-proxy cli -c flux_dev -e "UPDATE table SET status=1 WHERE id=1" -x
```
### SSH 远程执行
```bash
# 执行命令
ssh-proxy exec -n flux_dev -c "docker ps"
# JSON 输出
ssh-proxy exec -n flux_dev -c "docker ps" -F json
# 生产服务器 (byr_pro = prod_baiyarong)
ssh-proxy exec -n byr_pro -c "cd /opt/flux-api && bash bin/restart.sh"
```
### Redis 操作
```bash
# 取值
redis-proxy get -c flux_dev -k "session:user:123"
# 通用命令 (最灵活)
redis-proxy run -c flux_dev -C "KEYS" -a "user:*"
# 设置值
redis-proxy set -c flux_dev -k "key" -v "value"
```
## 开发命令
```bash
# ===== flux-api (Java/Redkale/Maven) =====
cd /workspace/flux-api
mvn clean package # 构建
cmd /c bin\restart.bat # 重启 (Windows bat,容器内需调整)
# ===== flux-admin (Vue3/bun) =====
cd /workspace/flux-admin
bun install # 安装依赖
bun run dev # 开发模式
# ===== flux-uniapp (UniApp) =====
cd /workspace/flux-uniapp
npm install
npm run dev:h5 # H5 模式
# ===== Go 服务 =====
cd /workspace/flux-mock && go run *.go
cd /workspace/flux-test && go run test_quick.go qulaijie -mode=collide
cd /workspace/file-server && go run *.go
```
## 连接信息速查
详细配置见:
- 服务器: `/root/.dev-env/servers.yaml`
- 数据库: `/root/.dev-env/databases.yaml`
- 规范: `/root/.dev-env/norms.md`
**快速参考**:
| 环境 | MySQL | Redis |
|------|-------|-------|
| 测试 (flux_dev) | 见 databases.yaml(不入库) | 见 databases.yaml :6379 |
| 生产 (flux_prox) | 见 databases.yaml(不入库) | — |
## 项目知识库
Flux 专属文档位于 `/root/.dev-env/docs/`
| 文件 | 用途 |
|------|------|
| Redkale使用易错点.md | **flux-api 必读** — 框架踩坑记录 |
| MySQL慢查询分析.md | SQL 慢查询优化参考 |
## 开发规范
> **唯一事实来源**: `/root/.dev-env/norms.md`
> 本文件不重复规范内容,避免维护不一致。
执行任务前先读取 norms.md,关键规则:
- Git 提交、代码审查、命名规范、高危操作检查清单
+1
View File
@@ -0,0 +1 @@
{"permissions": {"allow": ["Bash(mysql-proxy *)", "Bash(ssh-proxy *)", "Bash(redis-proxy *)", "Bash(mongo-proxy *)", "Bash(mvn *)", "Bash(java *)", "Bash(javac *)", "Bash(go *)", "Bash(bun *)", "Bash(npm *)", "Bash(node *)", "Bash(cmd *)", "Bash(git *)", "Bash(docker *)", "Bash(curl *)", "Bash(wget *)", "Bash(cat *)", "Bash(ls *)", "Bash(grep *)", "Bash(find *)", "Read", "Write", "Edit"], "deny": []}}
+124
View File
@@ -0,0 +1,124 @@
# MySQL 慢查询分析
---
## 查看慢查询配置
```sql
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
```
---
## 查询慢日志(RDS/MySQL
```sql
-- 最近24小时慢查询
SELECT
db,
LEFT(REPLACE(REPLACE(sql_text, '\n', ' '), ' ', ' '), 200) as sql_preview,
query_time,
rows_examined,
rows_sent,
start_time
FROM mysql.slow_log
WHERE start_time > DATE_SUB(NOW(), INTERVAL 24 HOUR)
ORDER BY query_time DESC
LIMIT 30;
-- 查询所有慢记录
SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 20;
```
---
## 查看当前运行的长查询
```sql
-- 查看所有查询
SHOW FULL PROCESSLIST;
-- 筛选长查询(超过10秒)
SELECT
id, User, Host, db, Time, State,
LEFT(Info, 150) as sql_preview
FROM information_schema.processlist
WHERE Command = 'Query' AND Time > 10
ORDER BY Time DESC;
```
---
## 终止查询
```sql
-- 单个终止
KILL <process_id>;
-- 批量生成终止语句(按来源IP
SELECT CONCAT('KILL ', id, ';')
FROM information_schema.processlist
WHERE Host LIKE '39.99.243.191%' AND Command = 'Query';
```
---
## performance_schema 分析
```sql
-- 按总耗时排序
SELECT
SCHEMA_NAME,
LEFT(DIGEST_TEXT, 150) as sql_pattern,
COUNT_STAR as exec_count,
ROUND(SUM_TIMER_WAIT/1000000000000, 2) as total_sec,
ROUND(AVG_TIMER_WAIT/1000000000, 2) as avg_ms,
ROUND(MAX_TIMER_WAIT/1000000000000, 2) as max_sec
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME IS NOT NULL
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 20;
-- 按平均耗时排序
SELECT
SCHEMA_NAME,
LEFT(DIGEST_TEXT, 150) as sql_pattern,
COUNT_STAR as exec_count,
ROUND(AVG_TIMER_WAIT/1000000000, 2) as avg_ms,
ROUND(MAX_TIMER_WAIT/1000000000000, 2) as max_sec
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME IS NOT NULL
ORDER BY AVG_TIMER_WAIT DESC
LIMIT 15;
```
---
## 查看表大小
```sql
SELECT
table_schema,
table_name,
ROUND(data_length/1024/1024, 2) as data_MB,
ROUND(index_length/1024/1024, 2) as index_MB,
table_rows
FROM information_schema.tables
WHERE table_schema IN ('db1', 'db2')
ORDER BY data_length DESC
LIMIT 30;
```
---
## 常用连接
| 库 | 连接命令 |
|----|----------|
| suke 生产库(阿里云RDS | `mysql -hrm-8vbrd2wil14hclop3vm.mysql.zhangbei.rds.aliyuncs.com -uroot -pWFFGwffg233` |
| 二号机 | `mysql -h47.92.117.13 -uroot -px251119!` |
---
*创建时间:2026-03-04*
@@ -0,0 +1,382 @@
# Redkale FilterNode 使用易错点
> 创建时间:2026-02-03
> 基于:flux-api 项目实际代码
---
## ❌ 常见错误
### 1. LIKE 查询错误
**错误写法**
```java
// ❌ 错误:使用静态方法
node.and(PartnerFormRecord::getRealname, FilterNode.like(keyword));
```
**正确写法**
```java
// ✅ 正确:使用实例方法
node.like(PartnerFormRecord::getRealname, keyword);
```
---
### 2. 比较查询错误
**错误写法**
```java
// ❌ 错误:方法名拼写错误
node.and(PartnerFormRecord::getCreatedtime, FilterNode.greateEqual(starttime));
node.and(PartnerFormRecord::getCreatedtime, FilterNode.lessEqual(endtime));
```
**正确写法**
```java
// ✅ 正确:使用简短的方法名
node.ge(PartnerFormRecord::getCreatedtime, starttime);
node.le(PartnerFormRecord::getCreatedtime, endtime);
```
**参考方法**
- `ge()` - Greater or Equal>=
- `le()` - Less or Equal<=
- `gt()` - Greater Than>
- `lt()` - Less Than<
---
### 3. OR 条件错误
**错误写法**
```java
// ❌ 错误:在 and() 中混用 FilterNode 静态方法
node.and(
FilterNode.or(
PartnerFormRecord::getRealname, FilterNode.like(keyword),
PartnerFormRecord::getMobile, FilterNode.like(keyword)
)
);
```
**正确写法**
```java
// ✅ 正确:创建独立 OR 节点,链式调用
FilterNode orNode = new FilterNode();
orNode.or(PartnerFormRecord::getRealname, FilterNode.like(keyword))
.or(PartnerFormRecord::getMobile, FilterNode.like(keyword))
.or(PartnerFormRecord::getIdcard, FilterNode.like(keyword));
node.and(orNode);
```
---
### 4. 等值条件冗余
**错误写法**
```java
// ❌ 冗余:等值条件不需要特殊方法
node.and(PartnerFormRecord::getStatus, FilterNode.eq(1));
```
**正确写法**
```java
// ✅ 简洁:直接传值
node.and(PartnerFormRecord::getStatus, 1);
```
---
### 5. NOT 条件错误
**错误写法**
```java
// ❌ 错误:使用不存在的 notLike 方法
node.notLike(PartnerFormRecord::getStatus, 3);
```
**正确写法**
```java
// ✅ 正确:使用 notEq
node.notEq(PartnerFormRecord::getStatus, 3);
```
---
## ✅ 正确的模式
### 基础查询条件
```java
FilterNode node = new FilterNode();
// 固定条件
node.and(PartnerFormRecord::getDraftstatus, (short) 0);
// 可选条件
if (bean.getKeyword() != null && !bean.getKeyword().isEmpty()) {
node.like(PartnerFormRecord::getRealname, bean.getKeyword());
}
// 范围查询
if (bean.getStarttime() > 0) {
node.ge(PartnerFormRecord::getCreatedtime, bean.getStarttime());
}
if (bean.getEndtime() > 0) {
node.le(PartnerFormRecord::getCreatedtime, bean.getEndtime());
}
// 不等于
node.notEq(PartnerFormRecord::getStatus, (short) 3);
```
### OR 条件组合
```java
// 关键词:姓名 OR 电话 OR 身份证
if (bean.getKeyword() != null && !bean.getKeyword().isEmpty()) {
FilterNode orNode = new FilterNode();
orNode.or(PartnerFormRecord::getRealname, FilterNode.like(bean.getKeyword()))
.or(PartnerFormRecord::getMobile, FilterNode.like(bean.getKeyword()))
.or(PartnerFormRecord::getIdcard, FilterNode.like(bean.getKeyword()));
node.and(orNode);
}
```
### IN 条件
```java
// IN 查询
FilterNode node = new FilterNode();
node.in(PartnerFormRecord::getFormdataid, formIds.toArray());
List<PartnerFormRecord> list = dataSource.queryList(PartnerFormRecord.class, node);
```
---
## 📋 FilterNode 方法速查表
| 方法 | 说明 | 示例 |
|------|------|------|
| `and()` | AND 等值条件 | `node.and(Entity::getField, value)` |
| `like()` | LIKE 模糊查询 | `node.like(Entity::getField, "%keyword%")` |
| `ge()` | >= 大于等于 | `node.ge(Entity::getTime, timestamp)` |
| `le()` | <= 小于等于 | `node.le(Entity::getTime, timestamp)` |
| `gt()` | > 大于 | `node.gt(Entity::getField, value)` |
| `lt()` | < 小于 | `node.lt(Entity::getField, value)` |
| `notEq()` | != 不等于 | `node.notEq(Entity::getStatus, 3)` |
| `in()` | IN 查询 | `node.in(Entity::getId, ids.toArray())` |
| `or()` | OR 条件 | 需创建独立 FilterNode |
---
## 🔍 完整示例参考
### PartnerFormService.java
```java
@RestMapping(name = "list", comment = "分页查询表单记录")
public RetResult<Sheet<PartnerFormRecord>> list(FormRecordFilterBean bean, Flipper flipper) {
FilterNode node = new FilterNode();
node.notEq(PartnerFormRecord::getStatus, STATUS_DELETED);
if (!Utils.isEmpty(bean.getRealname())) {
node.like(PartnerFormRecord::getRealname, bean.getRealname());
}
if (bean.getDraftstatus() >= 0) {
node.and(PartnerFormRecord::getDraftstatus, bean.getDraftstatus());
}
if (bean.getStarttime() > 0) {
node.ge(PartnerFormRecord::getCreatedtime, bean.getStarttime());
}
if (bean.getEndtime() > 0) {
node.le(PartnerFormRecord::getCreatedtime, bean.getEndtime());
}
Sheet<PartnerFormRecord> sheet = dataSource.querySheet(PartnerFormRecord.class, flipper, node);
return render(sheet);
}
```
### PartnerCustomerService.java
```java
@RestMapping(name = "list", comment = "分页查询客户列表")
public RetResult<Sheet<PartnerCustomer>> list(PartnerCustomerFilterBean bean, Flipper flipper) {
FilterNode node = new FilterNode();
node.notEq(PartnerCustomer::getStatus, (short) 3);
if (!Utils.isEmpty(bean.getMobile())) {
node.and(PartnerCustomer::getMobile, bean.getMobile());
}
if (bean.getStatus() > 0) {
node.and(PartnerCustomer::getStatus, bean.getStatus());
}
if (bean.getStarttime() > 0) {
node.ge(PartnerCustomer::getCreatedtime, bean.getStarttime());
}
if (bean.getEndtime() > 0) {
node.le(PartnerCustomer::getCreatedtime, bean.getEndtime());
}
Sheet<PartnerCustomer> sheet = dataSource.querySheet(PartnerCustomer.class, flipper, node);
return render(sheet);
}
```
---
## ⚠️ 注意事项
1. **方法调用方式**:所有条件方法都是 `FilterNode` 实例的方法,不是静态方法
2. **方法命名**`ge`/`le` 而非 `greateEqual`/`lessEqual`
3. **OR 条件**:必须创建独立的 `FilterNode` 对象进行链式调用
4. **值比较**:等值条件直接传值,不需要 `FilterNode.eq()`
5. **LIKE 参数**:通常需要在参数中自行添加 `%` 通配符
---
## 📚 参考代码位置
- `flux-api/src/main/java/cn/casehub/partner/PartnerFormService.java`
- `flux-api/src/main/java/cn/casehub/partner/PartnerCustomerService.java`
- `flux-api/src/main/java/cn/casehub/partner/PartnerVisitService.java`
---
# Redkale JSON 使用要点
> 更新时间:2026-03-03
> 背景:速贷接口接入时错误使用 `@JsonProperty`Jackson 注解),Redkale 不支持
---
## ❌ 常见错误
### 1. 使用 Jackson 注解
**错误写法**
```java
// ❌ 错误:Redkale 不支持 Jackson 注解
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonProperty("mobile_md5")
private String mobileMd5;
```
**正确方案**:使用 Redkale 的 `@ConvertColumn(name = "xxx")` 进行字段重命名:
```java
// ✅ 正确:使用 @ConvertColumn 重命名
import org.redkale.convert.ConvertColumn;
@ConvertColumn(name = "mobile_md5")
private String mobileMd5; // Java驼峰,JSON下划线
```
---
## ✅ JsonConvert 使用方式
### 序列化(对象 → JSON
```java
// 方式1:实例方法
JsonConvert convert = JsonConvert.root();
String json = convert.convertTo(object);
// 方式2:静态调用
String json = JsonConvert.root().convertTo(object);
```
### 反序列化(JSON → 对象)
```java
// 单个对象
User user = JsonConvert.root().convertFrom(User.class, json);
// 泛型集合
List<User> users = JsonConvert.root().convertFrom(
new TypeToken<List<User>>() {}.getType(),
json
);
// Map(用于解析不确定结构的JSON)
Map<String, Object> map = JsonConvert.root().convertFrom(Map.class, json);
```
---
## 📋 字段映射规则
| Java 字段名 | JSON 字段名 | 说明 |
|------------|------------|------|
| `mobileMd5` | `mobileMd5` | 默认:驼峰 → 驼峰 |
| `mobile_md5` | `mobile_md5` | 默认:下划线 → 下划线 |
| `mobileMd5` + `@ConvertColumn(name="mobile_md5")` | `mobile_md5` | ✅ 重命名 |
| `mobileMd5` + `@ConvertColumn(ignore=true)` | - | 忽略字段 |
**结论**:使用 `@ConvertColumn(name = "xxx")` 可实现字段重命名。
---
## 🔧 跨命名风格转换方案
当第三方接口使用下划线,内部使用驼峰时:
### 方案1@ConvertColumn + Utils.copy(推荐)
```java
// 速贷请求BeanJava驼峰,JSON下划线)
public class SuDaiRequest {
@ConvertColumn(name = "mobile_md5")
private String mobileMd5;
@ConvertColumn(name = "city_id")
private String cityId;
}
// 内部Bean(驼峰)
public class CollideBean {
private String mobileMd5;
private String cityId;
}
// 转换器 - 使用 Utils.copy
public class Converter {
public static CollideBean toBean(SuDaiRequest req) {
return Utils.copy(new CollideBean(), req); // 一行搞定
}
}
```
### 方案2:手动映射(不推荐)
```java
// 速贷请求Bean(下划线)
public class SuDaiRequest {
private String mobile_md5;
private String city_id;
}
// 转换器 - 手动映射
public class Converter {
public static CollideBean toBean(SuDaiRequest req) {
CollideBean bean = new CollideBean();
bean.setMobileMd5(req.getMobile_md5()); // 手动映射
bean.setCityId(req.getCity_id());
return bean;
}
}
```
---
## ⚠️ 注意事项
1. **使用 @ConvertColumn**:Redkale 的字段重命名注解,不是 Jackson 的 @JsonProperty
2. **@ConvertColumn 用法**
- `@ConvertColumn(name = "xxx")` - 重命名JSON字段
- `@ConvertColumn(ignore = true)` - 忽略字段
3. **配合 Utils.copy**:重命名后,Java字段名一致,可用 `Utils.copy` 简化转换
+29
View File
@@ -0,0 +1,29 @@
# mysql-proxy 连接配置模板
# 用法:复制为 mysql-proxy.toml 后填入真实值(mysql-proxy.toml 已在 .gitignore,不入库)
# 真实密码/IP 请勿提交,参见 docs/03-运维/开发环境搭建手册.md
[server]
port = 3307
host = "0.0.0.0"
[pool]
default_max_connections = 5
idle_timeout_secs = 300
check_interval_secs = 60
[[connections]]
name = "flux_dev"
host = "<TEST_DB_HOST>"
port = 3306
user = "root"
password = "<TEST_DB_PASSWORD>"
database = "flux_dev"
[[connections]]
name = "flux_prox"
host = "<PROD_DB_HOST>"
port = 3306
user = "root"
password = "<PROD_DB_PASSWORD>"
database = "flux_prox"
max_connections = 10
+102
View File
@@ -0,0 +1,102 @@
# Flux 项目开发规范
> 本文件供 Claude Code / AI 助手阅读,统一开发规范认知
---
## Git 提交规范
```
<类型>: <简述>
```
**类型**: 新增 | 修复 | 优化 | 重构
**禁止**:
- 英文提交信息
- Co-Authored-By 尾巴
- 提及工具名称(如 "generated by Claude"
**示例**:
```
新增: 渠道价格匹配规则引擎
修复: 下游进件超时未回调处理
优化: SQL 慢查询添加联合索引
重构: UpstreamService 抽象三方对接接口
```
## 技术栈与启动命令
| 模块 | 技术 | 目录 | 启动命令 |
|------|------|------|---------|
| flux-api | Java 17 + Redkale + Maven | flux-api/ | `cmd /c bin\restart.bat``mvn clean package` 后运行 |
| flux-admin | Vue 3 + Arco Design Pro + Vite + bun | flux-admin/ | `bun install && bun run dev` |
| flux-uniapp | UniApp (H5) + Vite | flux-uniapp/ | `npm install && npm run dev:h5` |
| flux-mock | Go (模拟三方) | flux-mock/ | `go run *.go` |
| flux-test | Go (业务链测试) | flux-test/ | `go run test_quick.go {channel}` |
| file-server | Go (文件上传) | file-server/ | `go run *.go` |
| flux-wechat-api | 微信服务 | flux-wechat-api/ | — |
## 核心架构
```
上游渠道 (10个) → UpstreamService → 下游对接 (美信/小薇/易贷通/CryptoApi) → 三方机构
```
- **上游**: 小爱、速贷、趣来借、臻品借、微融花、腰贷钱包、演示、放心借、龙享花、可贷
- **下游**: 美信钱包、美信B、小薇钱包、易贷通、有鑫钱包、源融花、闪融花、Mock
- **加密**: AES-CBC/ECB, CBC模式, 部分Base64密钥
## 代码审查要点
### Redkale 框架 (flux-api)
- FilterNode 使用规范 → 见 `/root/.kms/Redkale使用易错点.md`
- JSON 字段映射用 `@ConvertColumn`,注意命名转换
- 数据库连接通过 `@Resource(name="lake")` 注入
- 自定义 Render 用 `cn.casehub.base.TplRender`
### 前端 (flux-admin)
- 组件库: Arco Design Pro
- 状态管理: Pinia
- 路由: Vue Router 4
### Go 服务
- 交叉编译: `$env:GOOS="linux"; $env:GOARCH="amd64"; go build`
- 配置文件: 各模块独立 config.json
## 命名规范
```
{业务域}-{类型}-{名称}
```
| 前缀 | 含义 |
|------|------|
| flux | Flux 架构核心项目 |
| 后缀 | 含义 |
|------|------|
| -api | 后端 API 服务 |
| -admin | 管理后台前端 |
| -web | 用户端 Web |
| -mp | 小程序 |
| -task | 定时任务 |
| -kit | 工具库/SDK |
## 高危操作检查清单
- [ ] 改 Docker/系统配置 → 先备份,验证语法,再重启
- [ ] 删数据库/容器 → 确认备份和影响范围
- [ ] 生产环境操作 → 必须先在测试环境验证
- [ ] 发版部署 → 通知相关人,准备回滚方案
- [ ] 数据库 DML → 先 SELECT 预览,确认 WHERE 条件
## 模型选择参考
| 场景 | 推荐模型 |
|------|---------|
| 关键决策/技术选型 | glm-5 / deepseek-r1 |
| 复杂推理 | deepseek-r1 |
| 图片理解 | kimi-k2.5 / qwen-max |
| 日常开发操作 | kimi-k2.5 / deepseek-v3 |
| 简单任务 | glm-4.7-flash / qwen-turbo |
+17
View File
@@ -0,0 +1,17 @@
# redis-proxy 连接配置模板
# 用法:复制为 redis-proxy.toml 后填入真实值(redis-proxy.toml 已在 .gitignore,不入库)
[server]
port = 3310
host = "0.0.0.0"
[pool]
idle_timeout_secs = 300
check_interval_secs = 60
[[connections]]
name = "flux_dev"
host = "<TEST_REDIS_HOST>"
port = 6379
password = "<REDIS_PASSWORD>"
db = 0
+25
View File
@@ -0,0 +1,25 @@
# ssh-proxy 连接配置模板
# 用法:复制为 ssh-proxy.toml 后填入真实值(ssh-proxy.toml 已在 .gitignore,不入库)
# 注意:server name 用短名(如 byr_pro),作为内部连接标识符
[server]
port = 3308
host = "0.0.0.0"
[pool]
idle_timeout_secs = 300
check_interval_secs = 60
[[servers]]
name = "flux_dev"
host = "<TEST_SERVER_HOST>"
port = 22
user = "root"
private_key = "/root/.ssh/id_ed25519"
[[servers]]
name = "byr_pro"
host = "<PROD_SERVER_HOST>"
port = 22
user = "root"
private_key = "/root/.ssh/id_ed25519"