328 lines
13 KiB
Markdown
328 lines
13 KiB
Markdown
# 蝶雅 dy-dashboard 云端部署文档
|
||
|
||
> 本文档为 **云端服务器(gyl.dieya.net.cn)** 的部署操作手册。
|
||
> 生成日期:2026-09-21 适用项目:`dy-dashboard`
|
||
> ⚠️ **本文档不涉及 11.79 生产环境**。生产升级需单独明确指令,流程见 `生产环境部署指南.md`。
|
||
|
||
---
|
||
|
||
## 一、目标环境(2026-09-21 实测)
|
||
|
||
| 项 | 值 |
|
||
|---|---|
|
||
| SSH | `ubuntu@82.156.135.91`(本机 id_rsa 免密) |
|
||
| 域名 | `gyl.dieya.net.cn` |
|
||
| 项目目录 | `/opt/dy-dashboard` |
|
||
| 是否 Git 仓库 | **否**(打包部署结构,**不要执行任何 git 命令**) |
|
||
| Python | `/opt/dy-dashboard/venv/bin/python3`(3.12.3) |
|
||
| 服务 | 系统单元 `dy-dashboard.service`(当前 active,MainPID 1271894) |
|
||
| 端口 | Gunicorn 18888(4 worker) |
|
||
| 数据库 | `127.0.0.1:3306`,库 `dy_data`,账号 `dy_app` / `dy_data_2026` |
|
||
| 备份目录 | `~/dy-backups/<用途>-<时间戳>/`(`/opt/dy-dashboard-backups` 属 root,**不可写**) |
|
||
| 磁盘 | 40G,当前 **23G 已用 / 16G 可用(60%)** |
|
||
| 表数量 | 52 张,985(本地) vs 986(云端)字段 |
|
||
|
||
---
|
||
|
||
## 二、授权与边界(先读)
|
||
|
||
1. **仅在明确点名"云端"时执行本流程**;生产 11.79 需单独指令。
|
||
2. 部署前**先确认本次改的是代码还是数据切片**,以及判断成功的业务指标。
|
||
3. 边界内不做的事:
|
||
- ❌ 不覆盖云端 `config.py`(内含云端专属 `VAULT_PATH`)
|
||
- ❌ 不删除云端独有列或表
|
||
- ❌ 不覆盖 `static/uploads`(云端用户上传图片优先保留)
|
||
- ❌ 不动 `/opt/dy-dashboard-backups`、`/opt/backups` 等备份目录
|
||
- ❌ 不在云端跑 `git` 命令
|
||
4. 若用户报的是"网页打不开/接口超时/502",**走故障排查流程,不要当部署任务做**。
|
||
|
||
---
|
||
|
||
## 三、部署前检查
|
||
|
||
```bash
|
||
# 1) 云端可达性 + 磁盘余量 + 服务状态
|
||
ssh ubuntu@82.156.135.91 'echo OK; df -h / | tail -1; systemctl is-active dy-dashboard.service; systemctl show -p MainPID --value dy-dashboard.service'
|
||
```
|
||
|
||
**磁盘**:余量不足时连 `mkdir` 都会报 `No space left on device`。
|
||
- 磁盘满会表现为「**页面能打开、点登录没反应**」——先看 `df -h /`,别往代码方向查。
|
||
- 只清明确属临时产物的东西:`sudo journalctl --vacuum-size=100M`、`/tmp` 里历次部署残留的 `*.tar.gz`/`*.sql`/临时目录。
|
||
- **备份一律不动**,删除备份必须经用户明确确认。
|
||
|
||
```bash
|
||
# 2) 本地差异清单(只读,务必先 dry-run)
|
||
cd ~/.openclaw/workspace/dy-dashboard
|
||
rsync -a --dry-run --out-format='%n' <排除项> ./ ubuntu@82.156.135.91:/opt/dy-dashboard/
|
||
```
|
||
**完成标准**:清单里只有本次要上线的功能文件。若混进 `.git`、`*.bak`、`tmp_*`,补齐排除项后再看一遍。
|
||
|
||
**排除项(完整清单)**
|
||
```
|
||
config.py .git/ venv logs backups tmp_cache static/uploads
|
||
*.sql *.tar.gz *.csv *.xlsx *.db *.bak*
|
||
tmp_csv/ tmp_inventory_*/ tmp_outbound*/ tmp_sync_*/ {CLOUD_DIR}/
|
||
*.pyc __pycache__/
|
||
deploy.sh cloud_deploy*.sh run_cloud_deploy*.py ssh_cloud.py upload_cloud.py
|
||
```
|
||
|
||
---
|
||
|
||
## 四、备份(写入前必做)
|
||
|
||
**时间戳只生成一次,全程复用。**
|
||
|
||
```bash
|
||
TS=$(date +%Y%m%d_%H%M%S)
|
||
ssh ubuntu@82.156.135.91 "TS=$TS bash -s" <<'OUTER'
|
||
set -e
|
||
BK="$HOME/dy-backups/deploy-$TS"
|
||
rm -rf "$BK" && mkdir -p "$BK"
|
||
cd /opt/dy-dashboard
|
||
# 代码备份
|
||
tar -czf "$BK/code-pre.tar.gz" \
|
||
--exclude=venv --exclude=__pycache__ --exclude='*.pyc' \
|
||
--exclude=logs --exclude=backups --exclude=tmp_cache \
|
||
--exclude=static/uploads .
|
||
# 数据库备份
|
||
mysqldump -udy_app -pdy_data_2026 --single-transaction --complete-insert \
|
||
--hex-blob --no-tablespaces --routines --triggers --events \
|
||
dy_data | gzip -9 > "$BK/dy_data_full.sql.gz"
|
||
# 自检
|
||
echo "代码包文件数: $(tar -tzf "$BK/code-pre.tar.gz" | wc -l)"
|
||
gzip -t "$BK/dy_data_full.sql.gz" && echo "数据库包 gzip OK"
|
||
ls -lh "$BK"
|
||
md5sum "$BK"/*
|
||
OUTER
|
||
```
|
||
|
||
> `--routines --triggers --events` 在受限账号上可能失败:检查返回码与文件是否非空,必要时去掉这些参数重试。
|
||
|
||
**并发守卫**:重跑备份前先 `pgrep -af 'mysqldump|tar -czf'`,确认没有并发的同类进程,否则会留下多个半成品目录,分不清该用哪个。
|
||
|
||
**完成标准**:只有一个本次时间戳的目录,其中文件完整、`gzip -t` 通过、md5 已记录。
|
||
|
||
---
|
||
|
||
## 五、代码同步
|
||
|
||
### 方式 A(推荐)—— 定向差异同步
|
||
|
||
```bash
|
||
cd ~/.openclaw/workspace/dy-dashboard
|
||
rsync -a --dry-run --out-format='%n' \
|
||
--exclude='config.py' --exclude='.git/' --exclude='venv' --exclude='logs' \
|
||
--exclude='backups' --exclude='tmp_cache' --exclude='static/uploads' \
|
||
--exclude='*.sql' --exclude='*.tar.gz' --exclude='*.csv' --exclude='*.xlsx' \
|
||
--exclude='*.db' --exclude='*.bak*' --exclude='tmp_csv/' \
|
||
--exclude='tmp_inventory_*/' --exclude='tmp_outbound*/' --exclude='tmp_sync_*/' \
|
||
--exclude='{CLOUD_DIR}/' --exclude='*.pyc' --exclude='__pycache__/' \
|
||
--exclude='deploy.sh' --exclude='cloud_deploy*.sh' --exclude='run_cloud_deploy*.py' \
|
||
--exclude='ssh_cloud.py' --exclude='upload_cloud.py' \
|
||
./ ubuntu@82.156.135.91:/opt/dy-dashboard/
|
||
```
|
||
确认清单无误后,**去掉 `--dry-run` 实跑**。注意 `rsync -a` **不加 `--delete`**。
|
||
|
||
### 方式 B —— 使用打包文件(谨慎)
|
||
|
||
本机桌面的 `dy-dashboard-<TS>.tar.gz` 是为"留档"打的**全量包**,含 `tmp_csv`、`dy_data_v1.0.sql` 等大体积非功能文件,**不建议直接解压覆盖云端**。若确要用:
|
||
|
||
```bash
|
||
# 云端解到临时目录 → 再用 rsync 从临时目录挑选功能文件同步
|
||
scp ~/桌面/dy-dashboard-<TS>.tar.gz ubuntu@82.156.135.91:/tmp/
|
||
ssh ubuntu@82.156.135.91 'mkdir -p /tmp/dy-stage && tar -xzf /tmp/dy-dashboard-<TS>.tar.gz -C /tmp/dy-stage'
|
||
# 随后按方式 A 的排除项,从 /tmp/dy-stage 同步到 /opt/dy-dashboard
|
||
```
|
||
|
||
### 整文件替换的红线
|
||
|
||
整文件替换前,必须先判断**云端文件是否等于本地"本次改动前"的版本**:
|
||
```bash
|
||
git show <本次提交>^:<路径> > /tmp/prev_version
|
||
ssh ubuntu@82.156.135.91 'cat /opt/dy-dashboard/<路径>' > /tmp/cloud_version
|
||
diff /tmp/prev_version /tmp/cloud_version
|
||
```
|
||
完全一致才可整文件替换;**有差异说明云端另有本地未同步的内容,必须回到锚点替换**,别把未授权的改动一起带上线。
|
||
|
||
---
|
||
|
||
## 六、数据库结构增量迁移
|
||
|
||
**原则:只增量,不覆盖数据;绝不删除云端独有列或表。**
|
||
|
||
```bash
|
||
Q="select table_name,column_name,column_type,is_nullable,ifnull(column_default,'~NULL~'),extra,column_comment,ifnull(character_set_name,''),ifnull(collation_name,'') from information_schema.columns where table_schema='dy_data' order by table_name,ordinal_position"
|
||
mysql -udy_app -pdy_data_2026 -N -B -e "$Q" > /tmp/local_cols.tsv
|
||
ssh ubuntu@82.156.135.91 "mysql -udy_app -pdy_data_2026 -N -B -e \"$Q\"" > /tmp/cloud_cols.tsv
|
||
diff /tmp/local_cols.tsv /tmp/cloud_cols.tsv
|
||
```
|
||
|
||
> 取数**必须用 `mysql -N -B -e`**,不要用 `mysqldump` 跑 select。
|
||
|
||
**生成 DDL 规则**
|
||
|
||
| 情况 | 动作 |
|
||
|---|---|
|
||
| 本地有、云端无 | `ADD COLUMN` |
|
||
| 两端都有但定义不同 | `MODIFY`(改 `NOT NULL` 前先确认该列 NULL 行数为 0) |
|
||
| **云端有、本地无** | **只列出并保留,不删除** |
|
||
|
||
列定义拼接顺序:`type` + `CHARACTER SET` + `COLLATE` + `NOT NULL/NULL` + `DEFAULT`(`~NULL~` 表示无默认;`NOT NULL` 且原本无默认时给 `DEFAULT ''`)+ `extra` + `COMMENT`。
|
||
|
||
应用后重新导出并 `diff`,期望只剩「云端独有列」;同时对比关键业务表行数,确认数据未变。
|
||
|
||
---
|
||
|
||
## 七、代码自检与缓存
|
||
|
||
```bash
|
||
ssh ubuntu@82.156.135.91 'cd /opt/dy-dashboard && \
|
||
venv/bin/python3 -m compileall -q app.py routes services && \
|
||
node --check static/js/app.js && \
|
||
gzip -9 -k -f static/js/app.js && gzip -t static/js/app.js.gz && \
|
||
find . -name __pycache__ -type d -exec rm -rf {} + 2>/dev/null; echo DONE'
|
||
```
|
||
|
||
> ⚠️ **改了 `static/js/app.js` 必须重新生成 `app.js.gz`**。浏览器带 gzip 头请求时会优先拿到预压缩缓存,不重新生成 → 前端改动"看不到效果"。
|
||
|
||
---
|
||
|
||
## 八、服务重载
|
||
|
||
```bash
|
||
# 常规代码更新:平滑重载(只换 worker)
|
||
ssh ubuntu@82.156.135.91 'kill -HUP $(systemctl show -p MainPID --value dy-dashboard.service)'
|
||
```
|
||
|
||
> **改了单元文件本身**(如给 `ExecStart` 加 `--max-requests`)时,HUP 不会应用新的 ExecStart,**必须**:
|
||
> ```bash
|
||
> sudo systemctl daemon-reload && sudo systemctl restart dy-dashboard
|
||
> ```
|
||
|
||
等 **3–8 秒**后确认:单元 `active`、日志出现新 worker 启动、无新错误、端口只有一组进程。
|
||
|
||
```bash
|
||
ssh ubuntu@82.156.135.91 'systemctl is-active dy-dashboard.service; journalctl -u dy-dashboard.service -n 30 --no-pager | tail -20; ss -ltnp | grep 18888'
|
||
```
|
||
|
||
---
|
||
|
||
## 九、验证(探针全绿才算完成)
|
||
|
||
```bash
|
||
# 1) 本机与公网探针
|
||
curl -s -o /dev/null -w 'local %{http_code}\n' http://127.0.0.1:18888/login
|
||
curl -sk -o /dev/null -w 'public %{http_code}\n' https://gyl.dieya.net.cn/login
|
||
```
|
||
云端本机探针需在云端执行:`ssh ubuntu@82.156.135.91 'curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:18888/login'`
|
||
|
||
```bash
|
||
# 2) 确认下发的 app.js 就是磁盘文件
|
||
curl -sk --compressed https://gyl.dieya.net.cn/static/js/app.js -o /tmp/served.js
|
||
ssh ubuntu@82.156.135.91 'cat /opt/dy-dashboard/static/js/app.js' > /tmp/disk.js
|
||
cmp /tmp/served.js /tmp/disk.js && echo "下发内容一致"
|
||
```
|
||
|
||
```bash
|
||
# 3) 数据类改动后:刷新缓存 + 复核取值分布与总数
|
||
ssh ubuntu@82.156.135.91 'cd /opt/dy-dashboard && venv/bin/python3 -c "from services.cache import refresh_all_caches; refresh_all_caches()"'
|
||
```
|
||
|
||
**完成标准**:探针全绿 + 下发内容与磁盘一致 + 数据与预期一致。
|
||
|
||
---
|
||
|
||
## 十、回滚
|
||
|
||
```bash
|
||
ssh ubuntu@82.156.135.91 "TS=$TS bash -s" <<'OUTER'
|
||
set -e
|
||
BK="$HOME/dy-backups/deploy-$TS"
|
||
cd /opt/dy-dashboard
|
||
# 1) 恢复代码(保留云端上传文件)
|
||
mv static/uploads /tmp/uploads-rollback && \
|
||
tar -xzf "$BK/code-pre.tar.gz" -C /opt/dy-dashboard && \
|
||
mkdir -p static/uploads && cp -a /tmp/uploads-rollback/. static/uploads/ && \
|
||
rm -rf /tmp/uploads-rollback
|
||
# 2) 恢复数据库(仅在数据被误改时执行,会覆盖当前数据!)
|
||
# gunzip -c "$BK/dy_data_full.sql.gz" | mysql -udy_app -pdy_data_2026 dy_data
|
||
# 3) 重载
|
||
kill -HUP $(systemctl show -p MainPID --value dy-dashboard.service)
|
||
echo ROLLBACK_DONE
|
||
OUTER
|
||
```
|
||
回滚后同样跑第九节的验证探针。
|
||
|
||
---
|
||
|
||
## 十一、本次待部署内容(2026-09-21 实测)
|
||
|
||
### 代码差异:15 项
|
||
|
||
| 文件 | 类型 |
|
||
|---|---|
|
||
| `run.py` | 入口 |
|
||
| `update_data.py` | 数据更新 |
|
||
| `backup.sh` | 备份脚本 |
|
||
| `routes/ai.py` | 路由 |
|
||
| `routes/inventory.py` | 路由 |
|
||
| `routes/planning.py` | 路由 |
|
||
| `routes/products.py` | 路由 |
|
||
| `routes/purchase.py` | 路由 |
|
||
| `services/ai_datasources.py` | 服务 |
|
||
| `services/db.py` | 服务 |
|
||
| `services/field_perms.py` | 服务 |
|
||
| `static/js/app.js` | 前端 |
|
||
| `static/js/app.js.gz` | 前端预压缩 |
|
||
|
||
### 数据库结构差异:**无需迁移**
|
||
|
||
- 本地 985 列 vs 云端 986 列,表清单 52 张完全一致。
|
||
- **无需 ADD / MODIFY**。
|
||
- 云端多出 1 列 `finished_products.待提数量`(int)——**保留,不删除**。
|
||
- `bom_packaging.updated_at`、`packaging_orders.计划下单时间` 列定义完全相同,仅**列顺序**有差异(无害);因此**不要用不带 `ordinal_position` 的裸 diff 判断差异**。
|
||
|
||
---
|
||
|
||
## 十二、记录同步
|
||
|
||
部署成功后:
|
||
|
||
1. 主动告知结果与备份位置(云端路径)。
|
||
2. 按 `work-record-sync` 写训练记录与四处索引/日志。
|
||
3. 部署类记录**另需同步到云端**:`rsync -a --exclude='.obsidian' <本地>/知识库/ ubuntu@82.156.135.91:/opt/dy-dashboard/vault/知识库/`,**不加 `--delete`**(云端独有笔记保留)。
|
||
|
||
---
|
||
|
||
## 十三、常见故障速查
|
||
|
||
| 现象 | 首查 | 说明 |
|
||
|---|---|---|
|
||
| 页面能开、点登录没反应 | `df -h /` | 磁盘满 → MySQL 无法提交 → 登录卡 60s |
|
||
| 改了前端但看不到效果 | `app.js.gz` | 未重新生成预压缩缓存 |
|
||
| 改了数据但页面显示旧值 | `refresh_all_caches()` | 手工 SQL 导入绕过导入脚本,不会自动刷缓存 |
|
||
| 服务起不来 | `journalctl -u dy-dashboard` | 看 Python 依赖是否缺失(必须用 venv) |
|
||
| 端口占用 | `ss -ltnp \| grep 18888` | 检查是否有孤儿 gunicorn 进程 |
|
||
| `No space left on device` | `/tmp` + journal | 清临时产物;备份需用户确认后才能删 |
|
||
|
||
---
|
||
|
||
## 十四、SSH 多行脚本写法约束(实测踩坑)
|
||
|
||
1. **用 `ssh <host> bash -s <<'OUTER'` 传入多行脚本**,脚本内再嵌套 `<<'PY'`。
|
||
**不要**把多行 Python 直接塞进单引号 SSH 参数——远端会把 stdin 当文件名,报 `/opt/dy-dashboard/-` 一类错误。
|
||
2. **结束标记必须独占一行**。写成 `OUTER 2>&1 | tail -20` 会找不到结束符(远端报"立即文档被文件结束符分隔")且脚本只执行一半。
|
||
要截取输出就把整个调用包起来:
|
||
```bash
|
||
{ ssh <host> bash -s <<'OUTER'
|
||
...
|
||
OUTER
|
||
} 2>&1 | tail -20
|
||
```
|
||
3. `<<'OUTER'` 带引号时**本地变量不展开**。需要传变量走 ssh 命令本身:
|
||
```bash
|
||
ssh <host> "TS=$TS bash -s" <<'OUTER'
|
||
... 脚本内用 $TS ...
|
||
OUTER
|
||
```
|