Files
skills/piano-practice-sys/SKILL.md
T
hmo 04db423416 Initial commit: skills library
- 70 skills with code and documentation
- Add .gitignore (ignore __pycache__, output/, temp/, venv/)
- Clean up test intermediates and caches
2026-04-26 19:27:40 +08:00

13 KiB
Raw Blame History

name, description
name description
piano-practice-sys 钢琴练习方案生成系统的运维技能。用于系统的开发,维护、部署、数据备份恢复等操作。当用户提到钢琴方案系统的任何运维工作(查看数据、修改配置、备份恢复、部署上线等)时使用此技能。

钢琴练习方案生成系统 - 运维技能

版本: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

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 本地构建

# 1. 启动 Docker DesktopWindows
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 上传到服务器

# 6. 上传到服务器临时目录
scp -i ~/.ssh/id_rsa piano-plan.tar root@47.115.32.206:/opt/piano-plan/

3.3 服务器部署(使用脚本!)

# 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 正确
[ ] 功能验证:能生成练习方案

证书管理

# 申请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配置

# /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;
}

故障排查

# 检查容器状态
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

回滚流程

快速回滚(推荐)

# 停止当前容器
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

从备份恢复

# 恢复数据库
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文件

备份操作

# 备份数据库(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/

数据同步

生产环境数据导出

# 在服务器容器内执行
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()

本地数据导入生产

# 创建同步脚本 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()

上传到服务器执行:

docker cp sync_data.py piano-plan:/tmp/
docker exec piano-plan python /tmp/sync_data.py

从生产环境同步数据到本地开发

警告

  • 生产环境数据只读:不得修改生产环境数据
  • 本地会被覆盖:执行后本地数据会被生产数据完全替代
  • 先备份本地:每次同步前自动备份

同步步骤

1. 同步数据库(生产→本地)

# 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 等生产没有的字段
# 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. 同步问题文件(生产→本地)

# 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\个性化方案\针对性练习(拆分为单独文件)\"

一键同步脚本

# 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()

验证同步结果

# 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
查看日志 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

相关文档