# 云客数据镜像 — 服务器部署 runbook(1Panel 服务器,2026-09-03 实测落地版) > 形态:1Panel 管理,OpenResty 站点 + 公网高位端口,复用 1Panel MySQL, > 数据从本地整体搬迁。本地开发栈(五服务 docker-compose.yml)不在本文范围。 > **实测拓扑**:服务器在路由器 NAT 后(网卡 192.168.100.x,公网 IP 为路由器, > 公网 80 端口被同网其他设备占用)。 ## 架构 ``` 用户浏览器 ──https──▶ 1Panel OpenResty(host 网络, 80/443) │ 反向代理 ▼ 127.0.0.1:5173 frontend(nginx 容器) ├─ 静态页面(Vue SPA) └─ /api/* ──容器网络──▶ backend:8000(FastAPI,不映射宿主机) │ scheduler(Playwright+Chromium)│ 抓 crm2.yunkecn.com ────────┤ ▼ 宿主机内网IP:3306 1Panel MySQL(yunke 库) 媒体:./yunke-server/media(bind mount) ``` - **站点只有一个**:前端容器内 nginx 已把 `/api` 反代到 backend,浏览器只跟 一个域名说话(不要给后端单独建站,会跨域+双证书)。 - **调度**:每晚 02:00 全量 + 每 4h 消息增量(北京时间,compose 已设 TZ)。 ## 0. 前置条件核对 - [ ] ECS 已装 1Panel + Docker;建议 2C4G 起步,磁盘 ≥ 20GB(库 2.2GB + 媒体 2.5GB + 镜像 2.6GB) - [ ] 1Panel 应用商店已装 **OpenResty**(建站依赖) - [ ] 1Panel MySQL 版本 **8.x**;已建库 `yunke` + 专用用户,库权限设 「**所有区域(%)**」(容器从 docker 网段连入;3306 不对公网开,无风险) - [ ] 对外访问走「1Panel 域名管理加 `公网IP:高位端口`(实测 32002)+ 路由器端口转发到本机同端口」;**3306 / 5173 / 8000 不对公网开** (公网 80 已被其他设备占用,域名+HTTPS 为将来可选项) - [ ] Docker Hub 镜像加速:1Panel「容器→设置」里配加速器(拉 python/node/nginx 基础镜像用);apt/pip/npm/playwright 源已在 Dockerfile 内置国内镜像 - [ ] 服务器可访问 `crm2.yunkecn.com` 和 `*.oss.yunkecn.com`(抓取+媒体下载) - [ ] 从本地 `.env` 拿到云客凭据:`YK_USERID` / `YK_COMPANY` / `YK_COOKIE` ## 1. 拿代码 ```bash # 部署目标与自建 Git 同一台机器——clone 走本机回环,不出公网; # 后续升级 git pull 同样走 localhost,快且不依赖外网 git clone http://127.0.0.1:25311/xinyouhe/yunke.git /opt/yunke cd /opt/yunke ``` > 可选:想在 1Panel 界面管理这个栈,用「容器→编排→创建编排」选路径 > `/opt/yunke`,指定 `docker-compose.server.yml`。命令行 `docker compose` 与 > 1Panel 编排管理的是同一套容器,不冲突。 ## 2. 配 .env(仓库根目录) ```bash cd /opt/yunke && cat > .env <<'EOF' # ---- 1Panel MySQL(在 1Panel「数据库」里建的 yunke 库)---- MYSQL_HOST=172.19.x.x # 宿主机内网 IP(ECS 控制台看,不是公网 IP) MYSQL_PORT=3306 MYSQL_DATABASE=yunke MYSQL_USER=yunke MYSQL_PASSWORD=<1Panel 里建库时设的密码> # ---- 云客会话(从本地 .env 原样搬)---- YK_USERID=<32位userId> YK_COMPANY=<公司码> YK_COOKIE=user=xxx; userToken=yyy YUNKE_BASE_URL=https://crm2.yunkecn.com # ---- 查看后台鉴权(公网必填!)---- API_TOKEN= # ---- 凭据加密密钥(必填)---- ENCRYPTION_KEY= # ---- 调度(北京时间)---- FULL_SYNC_HOUR=2 MESSAGE_INTERVAL_HOURS=4 EOF chmod 600 .env ``` ## 3. 起栈 ```bash docker compose -f docker-compose.server.yml --env-file .env up -d --build ``` 首次构建 5-15 分钟(后端镜像 ~2.6GB 含 Chromium;源已配国内镜像)。 此时库还是空的,backend 日志可能有表不存在的报错——正常,下一步导数据。 ```bash docker compose -f docker-compose.server.yml ps # 三服务 Up ``` ## 4. 数据搬迁(本地 → 服务器,压缩后约 3GB) ### 4.1 本地导出(Windows,仓库根目录) ```bash # MySQL 一致性快照(--single-transaction 不锁表;ngram 全文索引随表定义导出) docker compose exec mysql mysqldump -uroot -p<本地root密码> \ --single-transaction --routines --triggers --default-character-set=utf8mb4 \ yunke | gzip > yunke.sql.gz # 媒体(2.5GB) tar czf media.tar.gz -C yunke-server media/ ``` 本地 root 密码在本地根目录 `.env` 的 `MYSQL_ROOT_PASSWORD`。 ### 4.2 传输 ```bash scp -P 22 yunke.sql.gz media.tar.gz root@:/opt/yunke/ ``` (1Panel 文件管理上传也行,但 3GB 大文件 scp 更稳。) ### 4.3 服务器导入 ```bash cd /opt/yunke # MySQL:1Panel MySQL 容器名在「容器」列表看(形如 1Panel-mysql-xxxx) gunzip yunke.sql.gz docker exec -i <1Panel-mysql容器名> mysql -uyunke -p<密码> \ --default-character-set=utf8mb4 yunke < yunke.sql # ⚠️ 86 万行 message 的 ngram 全文索引导入时重建,约 12 分钟——慢是正常, # 不要中断;期间导入脚本看似卡在索引阶段属预期。 # 媒体:解到 bind mount 目录(compose 已挂 ./yunke-server/media)。 # 包内根目录是 media/(打包时 -C yunke-server),必须 -C 解到 yunke-server 下 tar xzf media.tar.gz -C yunke-server # 数据自带 alembic_version=head,不需要跑 alembic upgrade。 ``` ### 4.4 导入后验证 + 补差量 ```bash docker exec -it <1Panel-mysql容器名> mysql -uyunke -p<密码> yunke \ -e "SELECT COUNT(*) FROM message; SELECT COUNT(*) FROM customer;" # 数字应与本地一致(message ≈ 86 万) # 补齐 dump 之后新产生的消息(增量,水位已随数据搬来) docker compose -f docker-compose.server.yml exec scheduler \ python scripts/scrape_via_page.py --entity messages --max-conversations 200 ``` ## 5. 站点与对外访问(实测:1Panel 端口模式) 1. 1Panel「网站→创建网站→反向代理」:代理地址 `127.0.0.1:5173`(已建则跳过) 2. 1Panel「网站→设置→域名管理」添加一条 `公网IP:高位端口`(实测 `219.157.255.213:32002`)→ 站点多监听该端口 3. 路由器端口转发:`外部 32002 → 192.168.100.132:32002` 4. 访问 `http://219.157.255.213:32002`,别人零配置直接用 5. 502 排查:栈是否 Up、`curl 127.0.0.1:5173` 是否通(OpenResty 是 host 网络, 1Panel 默认) 将来要 HTTPS:买真实域名 → 1Panel 证书走 **DNS 验证**(HTTP 验证需要 80, 已被占)→ 路由器加 443 转发。 前端登录鉴权即 `.env` 的 `API_TOKEN`(`Authorization: Bearer ` 或 `?token=`;**公网部署绝不能留空**)。 ## 6. 验收清单 - [ ] `docker compose -f docker-compose.server.yml ps` 三服务 Up - [ ] `curl http://127.0.0.1:5173/` 返回前端页面 - [ ] `curl http://127.0.0.1:8000/api/health` 返回 `{"status":"ok"}` - [ ] 域名 HTTPS 可打开,客户列表/聊天记录有数据(抽查语音列表→聊天记录) - [ ] 不带 token 调 `/api/*` 返回 401(鉴权生效) - [ ] scheduler 日志无 cookie 过期告警:`docker compose -f docker-compose.server.yml logs scheduler` - [ ] 安全组复核:3306 / 5173 / 8000 未对公网开放 ## 7. 日常运维 | 操作 | 命令 | |---|---| | 看调度日志 | `docker compose -f docker-compose.server.yml logs -f scheduler` | | 手动触发全量 | `docker compose -f docker-compose.server.yml exec scheduler bash scripts/full_sync_all.sh` | | 换云客 cookie | 改 `.env` 的 `YK_COOKIE` → `docker compose -f docker-compose.server.yml up -d`(重建注入 env) | | 备份 MySQL | `docker exec <1Panel-mysql容器名> mysqldump -uyunke -p<密码> --single-transaction yunke | gzip > /opt/backups/yunke-$(date +%F).sql.gz` | | 备份媒体 | `tar czf /opt/backups/media-$(date +%F).tgz -C yunke-server media/` | | 升级版本 | `git pull && docker compose -f docker-compose.server.yml --env-file .env up -d --build` | - 备份可挂 1Panel「计划任务」(Shell 脚本),如每日 03:00 跑上面两条。 - **升级时若有新 alembic 迁移**:加跑 `docker compose -f docker-compose.server.yml exec backend alembic upgrade head` (类型变更类迁移在 MySQL 上锁大表写入,避开抓取窗口:北京 02:00 全量、 每 4h 增量;增量耗时约 15-30 分钟)。 ### 数据分析板块首部署附加步骤(一次性,2026-09-04) alembic 链自当前线上版本起新增 3 支迁移:`a7d3e9f1c2b4` msg_daily_stats 日汇总表、`f0a1b2c3d4e5` 分析板块四新表 + `message.fetched_at` 索引、 `c3d4e5f6a7b8` 五表 `updated_at` 窄索引。 - **迁移随 `up -d --build` 自动跑**,平时无需手动。⚠️ 其中 `ix_message_fetched_at` 要在 86 万行 message 表上建索引,约十几秒 (InnoDB online DDL,量级同 c53f90b1e6a4)——**避开北京 02:00 全量抓取 窗口**执行升级:先停 scheduler 容器再 up,或白天低峰手动跑 `sudo docker compose -f docker-compose.server.yml exec backend alembic upgrade head`。 - 部署完成后在**夜间低峰**跑两条一次性回填,此后每 4h 的 analytics tick 自动增量维护,不用再手动: ```bash sudo docker compose -f docker-compose.server.yml exec backend python -m app.sync.cli staff-daily-stats sudo docker compose -f docker-compose.server.yml exec backend python -m app.sync.cli sensitive-scan --full ``` 前者全量重建 stat_daily_staff(分钟级);后者逐词扫全库并建扫描水位 (高频词分钟级)。**首跑必须带 `--full`**:不建水位,增量扫描会拒绝扫库。 - **顺序**:sensitive-scan --full 宜赶在调度器首次 4h analytics tick 之前; tick 首次在进程启动 4h 后才触发,部署当天跑回填通常天然满足。万一 tick 先到,每 4h 记一条 ERROR 日志(analytics tick failed,无扫描水位),且该 tick 整笔回滚——同笔的 staff rollup 增量刷新一并停摆,由夜间 full_sync_all.sh step 12(staff-daily-stats 全量重建)自愈兜底;补跑 --full 后即恢复,属预期提示非故障。 - 升级收尾 `sudo docker compose -f docker-compose.server.yml restart frontend` (让 nginx 重新解析 backend 容器 IP,防漂移后 502,同既有升级流程)。 - 浏览器验收:登录后依次打开 `/analytics/dashboard`、`/analytics/churn`、 `/analytics/unreplied`、`/analytics/sensitive` 四页;各页角标「数据截至」 在上述回填完成后才会显示。 - 起步词库 6 词(私单/飞单/加私人微信/红包/转账/离职)由 sensitive-scan 幂等自动补种,无需手插;后续加新词默认只扫增量,要扫历史消息需重跑 `--full`。 ## 8. 本地收尾(部署验证通过后) ```bash docker stop yunke-scheduler-1 # 停本地调度,避免两端同时抓取触发云客风控 ``` - 本地栈保留作开发环境,连本地 MySQL;此后本地改代码 → `git push` → 服务器 `git pull` 升级,两边数据各自独立。 - 本地 backend/frontend(8000/5173)按需启停,不影响服务器。 ## 9. 已知注意事项 1. **cookie 过期**:云客会话定期失效,scheduler 日志出现 `⚠️ cookie 可能已过期` → 浏览器重新复制 `YK_COOKIE` 并按上表换。 2. **时区**:server 编排已设 `TZ=Asia/Shanghai`,02:00 即北京时间全量。 3. **搜索性能**:message.text 已建 ngram FULLTEXT 索引(迁移 b41e8fa0c7d2), 数据搬迁导入时索引重建约 12 分钟属正常;重导后无需手工处理。 4. **服务器需能访问云客域名**(抓取 + OSS 媒体下载),ECS 出网正常即满足。 5. **MySQL 权限**:yunke 库用户须允许 `%` 或 docker 网段来源;3306 靠安全组 兜底不对公网暴露。