# 蝶雅 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-.tar.gz` 是为"留档"打的**全量包**,含 `tmp_csv`、`dy_data_v1.0.sql` 等大体积非功能文件,**不建议直接解压覆盖云端**。若确要用: ```bash # 云端解到临时目录 → 再用 rsync 从临时目录挑选功能文件同步 scp ~/桌面/dy-dashboard-.tar.gz ubuntu@82.156.135.91:/tmp/ ssh ubuntu@82.156.135.91 'mkdir -p /tmp/dy-stage && tar -xzf /tmp/dy-dashboard-.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 bash -s <<'OUTER'` 传入多行脚本**,脚本内再嵌套 `<<'PY'`。 **不要**把多行 Python 直接塞进单引号 SSH 参数——远端会把 stdin 当文件名,报 `/opt/dy-dashboard/-` 一类错误。 2. **结束标记必须独占一行**。写成 `OUTER 2>&1 | tail -20` 会找不到结束符(远端报"立即文档被文件结束符分隔")且脚本只执行一半。 要截取输出就把整个调用包起来: ```bash { ssh bash -s <<'OUTER' ... OUTER } 2>&1 | tail -20 ``` 3. `<<'OUTER'` 带引号时**本地变量不展开**。需要传变量走 ssh 命令本身: ```bash ssh "TS=$TS bash -s" <<'OUTER' ... 脚本内用 $TS ... OUTER ```