--- name: piano-practice-sys description: 钢琴练习方案生成系统的运维技能。用于系统的开发,维护、部署、数据备份恢复等操作。当用户提到钢琴方案系统的任何运维工作(查看数据、修改配置、备份恢复、部署上线等)时使用此技能。 --- # 钢琴练习方案生成系统 - 运维技能 > 版本:v1.3 | 更新日期:2026-04-26 > **核心原则:不删除,只备份后新增/替换** > **脚本优先原则:脚本报错 → 修复脚本,而非绕过脚本** ## 系统信息 | 项目 | 值 | |------|-----| | **项目路径** | `D:\F\NewI\opencode\daily-workspace\projects\青年钢琴集体课\练习方案系统` | | **本地启动** | 双击 `run.bat` | | **本地访问** | http://127.0.0.1:5001 | | **生产环境** | https://piano.yoin.fun | | **生产服务器** | 47.115.32.206 (新) | | **旧服务器** | 47.106.65.108 (仅保留 Gitea/FRP) | | **数据库** | SQLite (`data/piano_plans.db`) | | **备份目录** | `/opt/piano-plan/backups/` | ## 登录凭据 **必须先登录才能调用API!** ```python import requests s = requests.Session() s.post('https://piano.yoin.fun/api/login', json={ 'username': 'hmo', 'password': 'Dm19000o1st!' }) # 后续请求用这个session ``` --- ## 部署原则(铁律) | 操作 | 允许? | 说明 | |------|--------|------| | 删除容器 | ❌ 禁止 | 停止即可,容器配置是资产 | | 删除 volume | ❌ 禁止 | 数据资产,不可恢复 | | 删除 host 文件 | ❌ 禁止 | 先备份到 `/tmp/backup_YYYYMMDD/` | | 覆盖文件 | ⚠️ 先备份 | 任何覆盖操作前必须先备份 | | 停止容器 | ✅ 允许 | stop 是安全的 | | 启动新容器 | ✅ 允许 | 配合正确的挂载配置 | ### 脚本优先原则(铁律) > **当脚本执行失败时:修复脚本,而非绕过脚本。** | 错误行为 | 正确行为 | |---------|---------| | 脚本报错 → `docker rm` 手动清理 | 脚本报错 → 查看日志 → 修复脚本问题 → 重跑脚本 | | 挂载丢失 → 手动指定新挂载 | 挂载丢失 → 更新脚本的挂载配置 → 重跑脚本 | | 镜像加载失败 → `docker rmi` 清理 | 镜像加载失败 → 检查错误 → 重跑脚本 | | 容器启动失败 → `docker rm` 重来 | 容器启动失败 → 查看 `docker logs` → 修复配置 → 重跑脚本 | --- ## 生产环境部署 ### 架构 ``` 用户 → https://piano.yoin.fun → Nginx (容器) → piano容器:5001 ``` ### 关键路径 | 类型 | 宿主机/源 | 容器内路径 | 说明 | |------|-----------|------------|------| | Bind Mount | `/opt/piano-plan/个性化方案` | `/app/个性化方案` | 问题文件(15个md) | | Volume | `piano-plan-data` | `/app/data` | SQLite 数据库 | | Volume | `piano-plan-output` | `/app/output` | PDF 输出 | | Bind Mount | `/opt/piano-plan/config` | `/app/config` | API 配置 | ### 部署步骤(使用脚本!) **重要:遵循 DEPLOYMENT_SOP.md 的规范!** #### 3.1 本地构建 ```powershell # 1. 启动 Docker Desktop(Windows) Start-Process "C:\Program Files\Docker\Docker\Docker Desktop.exe" # 2. 等待 Docker 就绪 docker version # 看到 Server: Docker Desktop 即为就绪 # 3. 进入项目目录 cd "D:\F\NewI\opencode\daily-workspace\projects\青年钢琴集体课\练习方案系统" # 4. 构建 Docker 镜像 docker build -t piano-plan:latest . # 5. 保存镜像为 tar 文件 docker save piano-plan:latest -o piano-plan.tar ``` #### 3.2 上传到服务器 ```powershell # 6. 上传到服务器临时目录 scp -i ~/.ssh/id_rsa piano-plan.tar root@47.115.32.206:/opt/piano-plan/ ``` #### 3.3 服务器部署(使用脚本!) ```bash # 7. SSH 到服务器 ssh -i ~/.ssh/id_rsa root@47.115.32.206 # 8. 使用自动化部署脚本(会自动完成所有步骤并验证) bash /opt/piano-plan/deploy.sh /opt/piano-plan/piano-plan.tar ``` **脚本会自动完成**: - 检查镜像文件 - 停止并删除旧容器 - 加载新镜像 - 备份数据库 - 启动新容器(正确的挂载配置) - 验证部署 ### 验证清单(部署完成后必填) ``` [ ] 容器状态:running [ ] 服务响应:HTTP 200/302 [ ] 问题文件数量:15个 md 文件 [ ] 数据库记录:users, students, classes, student_problems, practice_plans 完整 [ ] templates 表存在且包含 AI提示词模板、报告导出模板 [ ] API 配置:provider, model, api_key 正确 [ ] 功能验证:能生成练习方案 ``` --- ## 证书管理 ```bash # 申请Let's Encrypt证书(停止nginx后执行) certbot certonly --standalone -d piano.yoin.fun # 证书位置 /etc/letsencrypt/live/piano.yoin.fun/ # 复制证书到nginx容器内 docker cp /etc/letsencrypt/live/piano.yoin.fun/cert.pem nginx_server:/etc/letsencrypt/live/piano.yoin.fun/ docker cp /etc/letsencrypt/live/piano.yoin.fun/chain.pem nginx_server:/etc/letsencrypt/live/piano.yoin.fun/ docker cp /etc/letsencrypt/live/piano.yoin.fun/fullchain.pem nginx_server:/etc/letsencrypt/live/piano.yoin.fun/ docker cp /etc/letsencrypt/live/piano.yoin.fun/privkey.pem nginx_server:/etc/letsencrypt/live/piano.yoin.fun/ ``` ### Nginx配置 ```nginx # /etc/nginx/conf.d/piano.yoin.fun.conf server { server_name piano.yoin.fun; location / { proxy_pass http://172.17.0.1:5001; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # SSE (Server-Sent Events) 支持 - 生成方案进度需要 location /api/generate-plan { proxy_buffering off; proxy_cache off; tcp_nodelay on; proxy_http_version 1.1; proxy_set_header Connection ''; proxy_buffers 8 32k; proxy_buffer_size 32k; proxy_max_temp_file_size 1024m; } listen 443 ssl; ssl_certificate /etc/letsencrypt/live/piano.yoin.fun/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/piano.yoin.fun/privkey.pem; } ``` ### 故障排查 ```bash # 检查容器状态 docker ps -a | grep piano # 查看容器日志 docker logs piano-plan --tail 50 # 进入容器 docker exec -it piano-plan sh # 检查挂载 docker inspect piano-plan --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{end}}' # 检查问题文件 docker exec piano-plan ls /app/个性化方案/ # 重启nginx docker exec nginx_server nginx -s reload ``` --- ## 回滚流程 ### 快速回滚(推荐) ```bash # 停止当前容器 docker stop piano-plan docker rm piano-plan # 使用旧镜像重新启动(如果镜像还在) docker run -d \ --name piano-plan \ -p 5001:5001 \ --restart unless-stopped \ -e FLASK_ENV=production \ -v /opt/piano-plan/个性化方案:/app/个性化方案 \ -v piano-plan-data:/app/data \ -v piano-plan-output:/app/output \ -v /opt/piano-plan/config:/app/config \ piano-plan:latest ``` ### 从备份恢复 ```bash # 恢复数据库 docker stop piano-plan cp /opt/piano-plan/backups/piano_plans.db.bak /var/lib/docker/volumes/piano-plan-data/_data/piano_plans.db docker start piano-plan ``` --- ## 数据保护规范 ### 必须保护的数据(绝对不删除) | 数据类型 | 存储位置 | 说明 | |----------|----------|------| | 用户数据 | piano-plan-data:/app/data | users 表 | | 学员数据 | piano-plan-data:/app/data | students, student_problems 表 | | 班级数据 | piano-plan-data:/app/data | classes 表 | | 练习方案 | piano-plan-data:/app/data | practice_plans 表 | | 问题文件 | /opt/piano-plan/个性化方案 | 15个md文件 | ### 备份操作 ```bash # 备份数据库(volume) docker cp piano-plan:/app/data/piano_plans.db /opt/piano-plan/backups/piano_plans.db.$(date +%Y%m%d) # 备份 API 配置 cp /opt/piano-plan/config/api_config.json /opt/piano-plan/backups/ # 列出所有备份 ls -la /opt/piano-plan/backups/ ``` --- ## 数据同步 ### 生产环境数据导出 ```python # 在服务器容器内执行 import sqlite3 conn = sqlite3.connect('/app/data/piano_plans.db') c = conn.cursor() # 导出用户 for row in c.execute("SELECT * FROM users"): print(row) # 导出班级 for row in c.execute("SELECT * FROM classes"): print(row) # 导出学生 for row in c.execute("SELECT * FROM students"): print(row) conn.close() ``` ### 本地数据导入生产 ```python # 创建同步脚本 sync_data.py import sqlite3 users = [ ('hyh', 'hash值', 'user', '时间戳'), # ... ] classes = [ ('26春(1)', '', 1, '时间戳'), # ... ] students = [ ('张三', '手机号', '微信昵称', '30分钟', '', 1, '时间戳'), # ... ] conn = sqlite3.connect('/app/data/piano_plans.db') c = conn.cursor() for u in users: c.execute("INSERT INTO users (username, password_hash, role, created_at) VALUES (?, ?, ?, ?)", u) for cls in classes: c.execute("INSERT INTO classes (name, description, active, created_at) VALUES (?, ?, ?, ?)", cls) for s in students: c.execute("INSERT INTO students (name, phone, wechat_nickname, practice_time, notes, class_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", s) conn.commit() conn.close() ``` 上传到服务器执行: ```bash docker cp sync_data.py piano-plan:/tmp/ docker exec piano-plan python /tmp/sync_data.py ``` --- ## 从生产环境同步数据到本地开发 ### 警告 - **生产环境数据只读**:不得修改生产环境数据 - **本地会被覆盖**:执行后本地数据会被生产数据完全替代 - **先备份本地**:每次同步前自动备份 ### 同步步骤 #### 1. 同步数据库(生产→本地) ```powershell # 1.1 备份本地数据库(手动) Copy-Item "data\piano_plans.db" "data\piano_plans.db.backup_$(Get-Date -Format 'yyyyMMdd_HHmmss')" # 1.2 从生产容器复制数据库到服务器 ssh -i ~/.ssh/id_rsa root@47.115.32.206 "docker cp piano-plan:/app/data/piano_plans.db /tmp/piano_plans_prod.db" # 1.3 下载到本地 scp -i ~/.ssh/id_rsa root@47.115.32.206:/tmp/piano_plans_prod.db data\ # 1.4 覆盖本地数据库 Copy-Item "data\piano_plans_prod.db" "data\piano_plans.db" -Force # 1.5 添加本地特有的字段(如果生产数据库没有) # 本地开发环境可能有 template_id, is_typical 等生产没有的字段 ``` ```python # add_local_cols.py - 添加本地特有字段 import sqlite3 conn = sqlite3.connect('data/piano_plans.db') cursor = conn.cursor() # 检查并添加 template_id cursor.execute("PRAGMA table_info(practice_plans)") columns = [col[1] for col in cursor.fetchall()] if 'template_id' not in columns: cursor.execute("ALTER TABLE practice_plans ADD COLUMN template_id INTEGER") if 'is_typical' not in columns: cursor.execute("ALTER TABLE practice_plans ADD COLUMN is_typical INTEGER DEFAULT 0") conn.commit() conn.close() ``` #### 2. 同步问题文件(生产→本地) ```powershell # 2.1 确保本地目录存在 New-Item -ItemType Directory -Path "D:\F\NewI\opencode\daily-workspace\个性化方案\针对性练习(拆分为单独文件)" -Force # 2.2 从服务器同步问题文件 scp -i ~/.ssh/id_rsa -r root@47.106.65.108:/opt/piano-plan/个性化方案/* "D:\F\NewI\opencode\daily-workspace\个性化方案\针对性练习(拆分为单独文件)\" ``` ### 一键同步脚本 ```python # sync_prod_to_local.py import sqlite3 import shutil import os from datetime import datetime LOCAL_DB = 'data/piano_plans.db' PROD_DB = 'data/piano_plans_prod.db' def sync_from_production(): # 1. 备份本地 backup = f"{LOCAL_DB}.backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}" shutil.copy2(LOCAL_DB, backup) print(f"本地已备份: {backup}") # 2. 覆盖本地数据库 shutil.copy2(PROD_DB, LOCAL_DB) print("生产数据库已覆盖本地") # 3. 添加本地特有字段 conn = sqlite3.connect(LOCAL_DB) cursor = conn.cursor() cursor.execute("PRAGMA table_info(practice_plans)") columns = [col[1] for col in cursor.fetchall()] if 'template_id' not in columns: cursor.execute("ALTER TABLE practice_plans ADD COLUMN template_id INTEGER") print("添加 template_id 字段") if 'is_typical' not in columns: cursor.execute("ALTER TABLE practice_plans ADD COLUMN is_typical INTEGER DEFAULT 0") print("添加 is_typical 字段") conn.commit() conn.close() print("完成") if __name__ == '__main__': sync_from_production() ``` ### 验证同步结果 ```python # verify_sync.py import sqlite3 conn = sqlite3.connect('data/piano_plans.db') cursor = conn.cursor() for table in ['classes', 'students', 'student_problems', 'practice_plans']: cursor.execute(f"SELECT COUNT(*) FROM {table}") print(f" {table}: {cursor.fetchone()[0]} 条") conn.close() ``` --- ## 快速命令参考 | 操作 | 命令 | |------|------| | 生产环境SSH | `ssh -i ~/.ssh/id_rsa root@47.115.32.206` | | 旧服务器SSH | `ssh -i ~/.ssh/id_rsa root@47.106.65.108` | | 查看容器 | `docker ps -a | grep piano` | | 查看日志 | `docker logs piano-plan --tail 50` | | 重启容器 | `docker restart piano-plan` | | 停止容器 | `docker stop piano-plan` | | 查看证书 | `docker exec nginx_server ls /etc/letsencrypt/live/` | | 测试HTTPS | `curl https://piano.yoin.fun` | ## 相关文档 - [完整API文档](../../projects/青年钢琴集体课/练习方案系统/docs/API.md) - [项目README](../../projects/青年钢琴集体课/练习方案系统/README.md) - [部署SOP(必读)](../../projects/青年钢琴集体课/练习方案系统/docs/DEPLOYMENT_SOP.md)