feat: 目标和问题统一分类体系(综合/乐理相关/演奏能力/其他),添加数据库迁移

This commit is contained in:
hmo
2026-04-23 21:57:00 +08:00
parent 5f1dcc08fb
commit f83769fa20
4 changed files with 49 additions and 6 deletions
+23
View File
@@ -131,6 +131,29 @@ def create_app():
if "level" not in goal_columns: if "level" not in goal_columns:
db.session.execute(text("ALTER TABLE goals ADD COLUMN level VARCHAR(20) DEFAULT '入门'")) db.session.execute(text("ALTER TABLE goals ADD COLUMN level VARCHAR(20) DEFAULT '入门'"))
db.session.commit() db.session.commit()
# 检查goals表是否有category字段
if "category" not in goal_columns:
db.session.execute(text("ALTER TABLE goals ADD COLUMN category VARCHAR(20) DEFAULT '综合'"))
db.session.commit()
# 迁移problems表分类:旧分类 -> 新分类
# 技术类/技术类(手型)/技术类(生理限制) -> 演奏能力
# 识谱类 -> 乐理相关
# 综合类 -> 综合类 (保持不变)
# 其他 -> 其他 (保持不变)
category_mapping = {
'技术类': '演奏能力',
'技术类(手型)': '演奏能力',
'技术类(生理限制)': '演奏能力',
'识谱类': '乐理相关',
}
for old_cat, new_cat in category_mapping.items():
db.session.execute(
text("UPDATE problems SET category = :new_cat WHERE category = :old_cat"),
{"new_cat": new_cat, "old_cat": old_cat}
)
db.session.commit()
except Exception as e: except Exception as e:
print(f"数据库迁移: {e}") print(f"数据库迁移: {e}")
+7 -2
View File
@@ -6,6 +6,9 @@ import re
db = SQLAlchemy() db = SQLAlchemy()
# 问题和目标的统一分类体系
ITEM_CATEGORIES = ['综合', '乐理相关', '演奏能力', '其他']
class User(db.Model): class User(db.Model):
"""管理员用户表""" """管理员用户表"""
@@ -149,7 +152,7 @@ class Problem(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
no = db.Column(db.String(10), unique=True, nullable=False) # 编号:01, 02... no = db.Column(db.String(10), unique=True, nullable=False) # 编号:01, 02...
name = db.Column(db.String(100), nullable=False) # 问题名称 name = db.Column(db.String(100), nullable=False) # 问题名称
category = db.Column(db.String(50), default="技术类") # 分类 category = db.Column(db.String(20), default="综合") # 分类:综合/乐理相关/演奏能力/其他
content = db.Column(db.Text) # 问题详细内容 content = db.Column(db.Text) # 问题详细内容
created_at = db.Column(db.DateTime, default=datetime.now) created_at = db.Column(db.DateTime, default=datetime.now)
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now) updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
@@ -159,7 +162,7 @@ class Problem(db.Model):
"id": self.id, "id": self.id,
"no": self.no, "no": self.no,
"name": self.name, "name": self.name,
"category": self.category, "category": self.category or "综合",
} }
@@ -198,6 +201,7 @@ class Goal(db.Model):
name = db.Column(db.String(100), nullable=False) name = db.Column(db.String(100), nullable=False)
content = db.Column(db.Text) content = db.Column(db.Text)
level = db.Column(db.String(20), default="入门") # 启蒙/入门/进阶/熟练/精通 level = db.Column(db.String(20), default="入门") # 启蒙/入门/进阶/熟练/精通
category = db.Column(db.String(20), default="综合") # 分类:综合/乐理相关/演奏能力/其他
created_at = db.Column(db.DateTime, default=datetime.now) created_at = db.Column(db.DateTime, default=datetime.now)
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now) updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
@@ -207,6 +211,7 @@ class Goal(db.Model):
"name": self.name, "name": self.name,
"content": self.content, "content": self.content,
"level": self.level, "level": self.level,
"category": self.category or "综合",
"created_at": self.created_at.isoformat() if self.created_at else None, "created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None,
} }
+4 -1
View File
@@ -25,7 +25,8 @@ def create_goal():
goal = Goal( goal = Goal(
name=data["name"], name=data["name"],
content=data.get("content", ""), content=data.get("content", ""),
level=data.get("level", "入门") level=data.get("level", "入门"),
category=data.get("category", "综合")
) )
db.session.add(goal) db.session.add(goal)
db.session.commit() db.session.commit()
@@ -48,6 +49,8 @@ def update_goal(goal_id):
goal.content = data["content"] goal.content = data["content"]
if "level" in data: if "level" in data:
goal.level = data["level"] goal.level = data["level"]
if "category" in data:
goal.category = data["category"]
db.session.commit() db.session.commit()
return jsonify(goal.to_dict()) return jsonify(goal.to_dict())
+15 -3
View File
@@ -46,6 +46,15 @@
<option value="精通">精通</option> <option value="精通">精通</option>
</select> </select>
</div> </div>
<div class="mb-3">
<label class="form-label">分类</label>
<select class="form-select" id="goal-category">
<option value="综合">综合(涉及多方面)</option>
<option value="乐理相关">乐理相关</option>
<option value="演奏能力">演奏能力</option>
<option value="其他">其他</option>
</select>
</div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">目标内容 (Markdown)</label> <label class="form-label">目标内容 (Markdown)</label>
<textarea class="form-control" id="goal-content" rows="8"></textarea> <textarea class="form-control" id="goal-content" rows="8"></textarea>
@@ -111,6 +120,7 @@ async function loadGoals() {
grid.innerHTML = goalsWithChildren.map(g => { grid.innerHTML = goalsWithChildren.map(g => {
const level = g.level || '入门'; const level = g.level || '入门';
const category = g.category || '综合';
const childNames = g.children && g.children.length > 0 const childNames = g.children && g.children.length > 0
? g.children.map(c => escapeHtml(c.name)).join(', ') ? g.children.map(c => escapeHtml(c.name)).join(', ')
: ''; : '';
@@ -120,6 +130,7 @@ async function loadGoals() {
<div class="card-body"> <div class="card-body">
<h6 class="card-title">${escapeHtml(g.name)}</h6> <h6 class="card-title">${escapeHtml(g.name)}</h6>
<div class="mb-1"> <div class="mb-1">
<span class="badge bg-primary">${category}</span>
<span class="badge bg-secondary">${level}</span> <span class="badge bg-secondary">${level}</span>
</div> </div>
${childNames ? `<div class="small text-muted mb-2">子目标: ${childNames}</div>` : ''} ${childNames ? `<div class="small text-muted mb-2">子目标: ${childNames}</div>` : ''}
@@ -140,14 +151,14 @@ async function saveGoal() {
const id = document.getElementById('goal-id').value; const id = document.getElementById('goal-id').value;
const name = document.getElementById('goal-name').value; const name = document.getElementById('goal-name').value;
const level = document.getElementById('goal-level').value; const level = document.getElementById('goal-level').value;
const category = document.getElementById('goal-category').value;
const content = document.getElementById('goal-content').value; const content = document.getElementById('goal-content').value;
if (!name) { alert('请输入目标名称'); return; } if (!name) { alert('请输入目标名称'); return; }
const method = id ? 'PUT' : 'POST'; const method = id ? 'PUT' : 'POST';
const url = id ? `${API_BASE}/${id}` : API_BASE; const url = id ? `${API_BASE}/${id}` : API_BASE;
const payload = {name, level, content}; const payload = {name, level, category, content};
console.log('Saving goal:', payload);
try { try {
const res = await fetch(url, { const res = await fetch(url, {
@@ -174,10 +185,10 @@ function editGoal(id) {
fetch(`${API_BASE}/${id}`) fetch(`${API_BASE}/${id}`)
.then(r => r.json()) .then(r => r.json())
.then(g => { .then(g => {
console.log('Editing goal:', g);
document.getElementById('goal-id').value = g.id; document.getElementById('goal-id').value = g.id;
document.getElementById('goal-name').value = g.name; document.getElementById('goal-name').value = g.name;
document.getElementById('goal-level').value = g.level || '入门'; document.getElementById('goal-level').value = g.level || '入门';
document.getElementById('goal-category').value = g.category || '综合';
document.getElementById('goal-content').value = g.content || ''; document.getElementById('goal-content').value = g.content || '';
document.getElementById('goalModalTitle').textContent = '编辑目标'; document.getElementById('goalModalTitle').textContent = '编辑目标';
new bootstrap.Modal(document.getElementById('goalModal')).show(); new bootstrap.Modal(document.getElementById('goalModal')).show();
@@ -298,6 +309,7 @@ document.getElementById('goalModal').addEventListener('hidden.bs.modal', () => {
document.getElementById('goal-id').value = ''; document.getElementById('goal-id').value = '';
document.getElementById('goal-name').value = ''; document.getElementById('goal-name').value = '';
document.getElementById('goal-level').value = '入门'; document.getElementById('goal-level').value = '入门';
document.getElementById('goal-category').value = '综合';
document.getElementById('goal-content').value = ''; document.getElementById('goal-content').value = '';
document.getElementById('goalModalTitle').textContent = '新建目标'; document.getElementById('goalModalTitle').textContent = '新建目标';
}); });