feat: rename system to 有音个性化教学系统; update docs

This commit is contained in:
hmo
2026-04-30 20:56:54 +08:00
parent 2a8d8a87d7
commit 90e93bb2b0
18 changed files with 285 additions and 27 deletions
+3 -3
View File
@@ -164,8 +164,8 @@ piano-plan/
--- ---
> **版本**v1.5.1 > **版本**v1.5.6
> **创建时间**2026-04-17 > **创建时间**2026-04-17
> **最后更新**2026-04-28 > **最后更新**2026-04-30
> >
> **重要更新**v1.5.1 - PDF水印配置保存修复;{student_goals}占位符修复 > **重要更新**v1.5.6 - Nginx限流(防爬虫);每日自动备份(30天)
+1 -1
View File
@@ -287,7 +287,7 @@ def generate_pdf(plan_id, student_name, content, output_dir, rendered_report=Non
pdf.add_table(table_data) pdf.add_table(table_data)
else: else:
# 使用结构化内容 # 使用结构化内容
pdf.add_title(f"钢琴练习方案 - {student_name}") pdf.add_title(f"有音个性化教学 - {student_name}")
pdf.add_heading("学员信息") pdf.add_heading("学员信息")
pdf.add_paragraph(f"学员姓名:{student_name}") pdf.add_paragraph(f"学员姓名:{student_name}")
pdf.add_paragraph(f"每日练习时间:{content.get('practice_time', 'N/A')}") pdf.add_paragraph(f"每日练习时间:{content.get('practice_time', 'N/A')}")
+1 -1
View File
@@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}API设置 - 钢琴练习方案系统{% endblock %} {% block title %}API设置 - 有音个性化教学系统{% endblock %}
{% block content %} {% block content %}
<div class="card"> <div class="card">
+3 -3
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>{% block title %}钢琴练习方案管理系统{% endblock %}</title> <title>{% block title %}有音个性化教学系统{% endblock %}</title>
<!-- 公共CSS --> <!-- 公共CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
@@ -70,7 +70,7 @@
<nav class="mobile-nav-toggle navbar navbar-dark bg-dark d-flex d-md-none" style="position:fixed;top:0;left:0;right:0;z-index:1050;padding:10px;"> <nav class="mobile-nav-toggle navbar navbar-dark bg-dark d-flex d-md-none" style="position:fixed;top:0;left:0;right:0;z-index:1050;padding:10px;">
<div class="container-fluid d-flex justify-content-between align-items-center"> <div class="container-fluid d-flex justify-content-between align-items-center">
<div> <div>
<span class="navbar-brand mb-0"><i class="bi bi-music-note-beamed"></i> 钢琴方案</span> <a href="/" class="navbar-brand mb-0 text-white text-decoration-none"><i class="bi bi-music-note-beamed"></i> 有音个性化教学系统</a>
<small id="mobileUserDisplay" class="d-block text-white-50" style="font-size:10px;"></small> <small id="mobileUserDisplay" class="d-block text-white-50" style="font-size:10px;"></small>
</div> </div>
<button class="btn btn-outline-light btn-sm" onclick="toggleMobileNav()"> <button class="btn btn-outline-light btn-sm" onclick="toggleMobileNav()">
@@ -84,7 +84,7 @@
<!-- 侧边栏 --> <!-- 侧边栏 -->
<div class="col-md-2 sidebar p-0 collapsed" id="sidebar"> <div class="col-md-2 sidebar p-0 collapsed" id="sidebar">
<div class="p-3 text-center border-bottom border-secondary d-none d-md-block"> <div class="p-3 text-center border-bottom border-secondary d-none d-md-block">
<h5><i class="bi bi-music-note-beamed"></i> 钢琴方案</h5> <h5><a href="/" class="text-white text-decoration-none"><i class="bi bi-music-note-beamed"></i> 有音个性化教学系统</a></h5>
<small id="currentUserDisplay" class="text-light"></small> <small id="currentUserDisplay" class="text-light"></small>
</div> </div>
<nav class="nav flex-column"> <nav class="nav flex-column">
+1 -1
View File
@@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}班级管理 - 钢琴练习方案系统{% endblock %} {% block title %}班级管理 - 有音个性化教学系统{% endblock %}
{% block content %} {% block content %}
<div class="card mb-3"> <div class="card mb-3">
+2 -2
View File
@@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}首页 - 钢琴练习方案系统{% endblock %} {% block title %}首页 - 有音个性化教学系统{% endblock %}
{% block content %} {% block content %}
<div class="row g-4 mb-4"> <div class="row g-4 mb-4">
@@ -39,7 +39,7 @@
<div class="card"> <div class="card">
<div class="card-body text-center py-5"> <div class="card-body text-center py-5">
<i class="bi bi-music-note-beamed text-muted" style="font-size: 48px;"></i> <i class="bi bi-music-note-beamed text-muted" style="font-size: 48px;"></i>
<h4 class="mt-3 text-muted">欢迎使用钢琴练习方案管理系统</h4> <h4 class="mt-3 text-muted">欢迎使用有音个性化教学系统</h4>
<p class="text-muted">从左侧菜单选择功能</p> <p class="text-muted">从左侧菜单选择功能</p>
</div> </div>
</div> </div>
+1 -1
View File
@@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}钢琴练习方案管理系统{% endblock %} {% block title %}有音个性化教学系统{% endblock %}
{% block page_css %} {% block page_css %}
<style> <style>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录 - 钢琴练习方案系统</title> <title>登录 - 有音个性化教学系统</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
<style> <style>
@@ -18,7 +18,7 @@
<div class="login-card"> <div class="login-card">
<div class="text-center mb-4"> <div class="text-center mb-4">
<i class="bi bi-music-note-beamed" style="font-size: 48px; color: #667eea;"></i> <i class="bi bi-music-note-beamed" style="font-size: 48px; color: #667eea;"></i>
<h4 class="mt-3">钢琴练习方案系统</h4> <h4 class="mt-3">有音个性化教学系统</h4>
<p class="text-muted">请登录继续</p> <p class="text-muted">请登录继续</p>
</div> </div>
+1 -1
View File
@@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}方案详情 - 钢琴练习方案系统{% endblock %} {% block title %}方案详情 - 有音个性化教学系统{% endblock %}
{% block extra_css %} {% block extra_css %}
<style> <style>
+1 -1
View File
@@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}编辑方案 - 钢琴练习方案系统{% endblock %} {% block title %}编辑方案 - 有音个性化教学系统{% endblock %}
{% block page_css %} {% block page_css %}
<style> <style>
+1 -1
View File
@@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}方案管理 - 钢琴练习方案系统{% endblock %} {% block title %}方案管理 - 有音个性化教学系统{% endblock %}
{% block page_css %} {% block page_css %}
<style> <style>
+1 -1
View File
@@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}问题配置 - 钢琴练习方案系统{% endblock %} {% block title %}问题配置 - 有音个性化教学系统{% endblock %}
{% block page_css %} {% block page_css %}
<style> <style>
+1 -1
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>初始设置 - 钢琴练习方案系统</title> <title>初始设置 - 有音个性化教学系统</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
<style> <style>
+1 -1
View File
@@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}数据统计 - 钢琴练习方案系统{% endblock %} {% block title %}数据统计 - 有音个性化教学系统{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid py-4"> <div class="container-fluid py-4">
+1 -1
View File
@@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ student.name }} - 学员详情 - 钢琴练习方案系统{% endblock %} {% block title %}{{ student.name }} - 学员详情 - 有音个性化教学系统{% endblock %}
{% block content %} {% block content %}
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
+1 -1
View File
@@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}模板管理 - 钢琴练习方案系统{% endblock %} {% block title %}模板管理 - 有音个性化教学系统{% endblock %}
{% block page_css %} {% block page_css %}
<style> <style>
+1 -1
View File
@@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}用户管理 - 钢琴练习方案系统{% endblock %} {% block title %}用户管理 - 有音个性化教学系统{% endblock %}
{% block content %} {% block content %}
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
+262 -4
View File
@@ -1,11 +1,219 @@
# 钢琴练习方案系统 - 部署 SOP # 钢琴练习方案系统 - 部署 SOP
> 版本:v1.5.2 > 版本:v1.5.6
> 日期:2026-04-28 > 日期:2026-04-30
> 核心原则:**不删除,只备份后新增/替换** > 核心原则:**不删除,只备份后新增/替换**
--- ---
## 重要更新(v1.5.6
### 🔒 安全防护:Nginx 限流配置
新增 Nginx 限流规则,防止爬虫和恶意请求:
**限流规则:**
| 路由 | 限流规则 | 说明 |
|------|----------|------|
| `/` (全站) | 30请求/秒, burst=50 | 正常用户够用 |
| `/api/generate-plan` | 10请求/秒, burst=20 | 重点防护(AI生成接口) |
**超过限制返回:** `429 Too Many Requests`
**配置文件:**
- 主配置:`/etc/nginx/nginx.conf`(限流zone定义)
- 站点配置:`/etc/nginx/conf.d/piano.yoin.fun.conf`(限流规则应用)
**完整的 nginx.conf 限流部分(http{} 块中添加):**
```nginx
# 限流配置 - 防止爬虫和恶意请求
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=general_limit:10m rate=30r/s;
limit_req_log_level warn;
limit_req_status 429;
```
**完整的站点配置(piano.yoin.fun.conf):**
```nginx
server {
listen 80;
server_name piano.yoin.fun;
return 301 https://$server_name$request_uri;
}
server {
server_name piano.yoin.fun;
location / {
limit_req zone=general_limit burst=50 nodelay;
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;
proxy_buffering off;
proxy_cache off;
tcp_nodelay on;
proxy_http_version 1.1;
proxy_set_header Connection '';
}
location /api/generate-plan {
limit_req zone=api_limit burst=20 nodelay;
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;
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
# 1. 备份当前配置
docker exec nginx_server cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak
docker exec nginx_server cp /etc/nginx/conf.d/piano.yoin.fun.conf /etc/nginx/conf.d/piano.yoin.fun.conf.bak
# 2. 使用 sed 或直接编辑修改配置
# 主配置:在 http{} 块中插入限流zone定义
# 站点配置:在 location{} 块中添加 limit_req 指令
# 3. 测试配置
docker exec nginx_server nginx -t -c /etc/nginx/nginx.conf
# 4. 重载nginx
docker exec nginx_server nginx -s reload
```
**常用命令:**
```bash
# 查看限流配置
docker exec nginx_server cat /etc/nginx/conf.d/piano.yoin.fun.conf | grep limit_req
# 检查nginx状态
docker exec nginx_server nginx -t
# 重载配置
docker exec nginx_server nginx -s reload
# 查看nginx日志(限流日志)
docker exec nginx_server tail -50 /var/log/nginx/error.log
```
---
### 💾 数据备份:每日自动备份
新增服务器本地每日自动备份机制:
**备份配置:**
- 备份路径:`/opt/backups/piano-db/`
- 执行时间:每天凌晨 3:00
- 保留期限:30 天
- 备份方式:SQLite `.backup` 命令(保证一致性)
- 服务状态:crond 运行中
**备份脚本位置:** `/opt/backups/backup_piano_db.sh`(宿主机,不受容器重建影响)
**完整脚本内容:**
```bash
#!/bin/bash
# Piano Plan 数据库每日备份脚本
# 保留30天
# 自包含:部署后依然有效,不依赖容器内预存文件
BACKUP_DIR="/opt/backups/piano-db"
RETENTION_DAYS=30
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="piano_plans_${TIMESTAMP}.db"
SCRIPT_FILE="/tmp/backup_piano_$$.py"
mkdir -p "$BACKUP_DIR"
# 创建临时Python脚本(写在宿主机,然后复制到容器)
cat > "$SCRIPT_FILE" << 'PYEOF'
import sqlite3
conn = sqlite3.connect('/app/data/piano_plans.db')
backup = sqlite3.connect('/tmp/piano_backup.db')
conn.backup(backup)
backup.close()
conn.close()
PYEOF
# 复制到容器并执行
docker cp "$SCRIPT_FILE" piano-plan:/tmp/backup_piano.py
rm -f "$SCRIPT_FILE"
docker exec piano-plan python3 /tmp/backup_piano.py
# 复制备份到宿主机
docker cp piano-plan:/tmp/piano_backup.db "$BACKUP_DIR/$BACKUP_FILE"
# 清理容器内文件
docker exec piano-plan rm -f /tmp/backup_piano.py /tmp/piano_backup.db
# 验证
if [ -f "$BACKUP_DIR/$BACKUP_FILE" ]; then
SIZE=$(du -h "$BACKUP_DIR/$BACKUP_FILE" | cut -f1)
echo "[$(date)] Backup OK: $BACKUP_FILE ($SIZE)"
# 删除30天前的备份
find "$BACKUP_DIR" -name "piano_plans_*.db" -mtime +${RETENTION_DAYS} -delete
echo "[$(date)] Cleanup done (retained $RETENTION_DAYS days)"
else
echo "[$(date)] Backup FAILED"
exit 1
fi
ls -lh "$BACKUP_DIR"
```
**设置 cron 任务:**
```bash
# 添加到 crontab
(crontab -l 2>/dev/null; echo '0 3 * * * /opt/backups/backup_piano_db.sh >> /opt/backups/backup.log 2>&1') | crontab -
# 验证
crontab -l
```
**常用命令:**
```bash
# 查看备份
ls -lh /opt/backups/piano-db/
# 手动执行备份
/opt/backups/backup_piano_db.sh
# 查看 cron 配置
crontab -l
# 删除旧的 cron 任务(如果需要)
crontab -e # 编辑模式删除对应行
```
**备份恢复:**
```bash
# 停止容器
docker stop piano-plan
# 复制备份文件到容器
docker cp /opt/backups/piano-db/piano_plans_XXXXXXXX_XXXXXX.db piano-plan:/app/data/piano_plans.db
# 启动容器
docker start piano-plan
```
---
## 重要更新(v1.5.2 ## 重要更新(v1.5.2
### ✨ 导出预览功能 ### ✨ 导出预览功能
@@ -447,6 +655,7 @@ A: 检查是否执行了 migrate_goals_v3.py 迁移脚本,该脚本创建 stud
| 版本 | 日期 | 变更 | | 版本 | 日期 | 变更 |
|------|------|------| |------|------|------|
| v1.5.6 | 2026-04-30 | Nginx限流配置(防爬虫/恶意请求);每日自动备份(30天保留) |
| v1.5.5 | 2026-04-28 | 修复容器时区(TZ=Asia/Shanghai);学员列表"x个方案"可点击跳转最新方案详情 | | v1.5.5 | 2026-04-28 | 修复容器时区(TZ=Asia/Shanghai);学员列表"x个方案"可点击跳转最新方案详情 |
| v1.5.4 | 2026-04-28 | PDF正文字体12pt、表格11pt | | v1.5.4 | 2026-04-28 | PDF正文字体12pt、表格11pt |
| v1.5.3 | 2026-04-28 | PDF行间距修复(7mm→2mm | | v1.5.3 | 2026-04-28 | PDF行间距修复(7mm→2mm |
@@ -467,5 +676,54 @@ A: 检查是否执行了 migrate_goals_v3.py 迁移脚本,该脚本创建 stud
--- ---
> **最后更新**2026-04-28 ## 十二、Ops 运维脚本
> **更新原因**:v1.5.2 - 导出预览功能;目标换行修复;居中语法支持
### 12.1 备份脚本 (scripts/ops/backup_piano_db.sh)
**位置**`scripts/ops/backup_piano_db.sh`(项目目录)
**上传到服务器**
```bash
scp -i ~/.ssh/id_rsa scripts/ops/backup_piano_db.sh root@47.115.32.206:/opt/backups/backup_piano_db.sh
chmod +x /opt/backups/backup_piano_db.sh
```
**设置定时任务**
```bash
ssh -i ~/.ssh/id_rsa root@47.115.32.206
(crontab -l 2>/dev/null; echo '0 3 * * * /opt/backups/backup_piano_db.sh >> /opt/backups/backup.log 2>&1') | crontab -
```
**验证设置**
```bash
crontab -l
ls -lh /opt/backups/piano-db/
```
### 12.2 脚本特性
- **自包含**:不依赖容器内预存文件
- **幂等性**:可重复执行
- **验证**:备份后自动验证文件存在
- **清理**:自动删除 30 天前的备份
### 12.3 恢复备份
```bash
# 1. 停止容器
docker stop piano-plan
# 2. 列出可用备份
ls -lh /opt/backups/piano-db/
# 3. 复制备份到容器
docker cp /opt/backups/piano-db/piano_plans_XXXXXXXX_XXXXXX.db piano-plan:/app/data/piano_plans.db
# 4. 启动容器
docker start piano-plan
```
---
> **最后更新**2026-04-30
> **更新原因**v1.5.6 - Nginx限流配置;每日自动备份