dy-dashboard/云端部署指南.md
2026-10-28 11:14:17 +08:00

328 lines
13 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 蝶雅 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
```