# Flask 应用工厂 from flask import Flask from app.models import db import os import pathlib def create_app(): app = Flask(__name__) # 使用 run.py 的目录作为项目根目录(绝对路径,不依赖工作目录) import sys if hasattr(sys, '_MEIPASS'): BASE_DIR = pathlib.Path(sys._MEIPASS) else: BASE_DIR = pathlib.Path(__file__).resolve().parent.parent # 存储到 app.config,各处引用,不再各自计算 app.config["BASE_DIR"] = BASE_DIR app.config["SECRET_KEY"] = "piano-practice-plan-secret-key-2026" app.config["DEBUG"] = True app.config["SQLALCHEMY_DATABASE_URI"] = ( f"sqlite:///{BASE_DIR / 'data' / 'piano_plans.db'}" ) app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False app.config["PDF_OUTPUT_DIR"] = BASE_DIR / "output" app.config["API_CONFIG_FILE"] = BASE_DIR / "config" / "api_config.json" # 初始化数据库 db.init_app(app) # 注册蓝图 from app.routes import main_bp from app.routes.templates import templates_bp from app.routes.goals import goals_bp app.register_blueprint(main_bp) app.register_blueprint(templates_bp) app.register_blueprint(goals_bp) from app.routes import student_goals app.register_blueprint(student_goals.student_goals_bp) # 创建数据库和目录 with app.app_context(): os.makedirs(os.path.join(BASE_DIR, "data"), exist_ok=True) os.makedirs(app.config["PDF_OUTPUT_DIR"], exist_ok=True) # 确保所有模型都被导入 from app.models import Student, Problem, StudentProblem, Template, PracticePlan from app.models import Goal, GoalRelation, StudentGoal # 新增目标相关模型 db.create_all() # 简单迁移:为已存在的数据库添加新字段(必须在init_default_templates之前) try: from sqlalchemy import text # 检查practice_time字段是否存在 result = db.session.execute(text("PRAGMA table_info(students)")) columns = [row[1] for row in result] if "practice_time" not in columns: db.session.execute( text( "ALTER TABLE students ADD COLUMN practice_time VARCHAR(20) DEFAULT '30-60分钟'" ) ) db.session.commit() if "wechat_nickname" not in columns: db.session.execute( text("ALTER TABLE students ADD COLUMN wechat_nickname VARCHAR(100)") ) db.session.commit() # 检查student_problems表的level字段 result2 = db.session.execute(text("PRAGMA table_info(student_problems)")) columns2 = [row[1] for row in result2] if "level" not in columns2: db.session.execute( text("ALTER TABLE student_problems ADD COLUMN level VARCHAR(20)") ) db.session.commit() # 检查users表是否存在 result3 = db.session.execute( text( "SELECT name FROM sqlite_master WHERE type='table' AND name='users'" ) ) if not result3.fetchone(): db.session.execute( text( """ CREATE TABLE users ( id INTEGER PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, password_hash VARCHAR(200) NOT NULL, role VARCHAR(20) DEFAULT 'user', created_at TIMESTAMP ) """ ) ) db.session.commit() # 检查templates表是否有sort_order字段 result4 = db.session.execute(text("PRAGMA table_info(templates)")) template_columns = [row[1] for row in result4] if "sort_order" not in template_columns: db.session.execute(text("ALTER TABLE templates ADD COLUMN sort_order INTEGER DEFAULT 0")) db.session.commit() # 检查practice_plans表是否有template_id字段 result5 = db.session.execute(text("PRAGMA table_info(practice_plans)")) plan_columns = [row[1] for row in result5] if "template_id" not in plan_columns: db.session.execute(text("ALTER TABLE practice_plans ADD COLUMN template_id INTEGER REFERENCES templates(id)")) db.session.commit() # 检查practice_plans表是否有is_typical字段 result6 = db.session.execute(text("PRAGMA table_info(practice_plans)")) plan_columns2 = [row[1] for row in result6] if "is_typical" not in plan_columns2: db.session.execute(text("ALTER TABLE practice_plans ADD COLUMN is_typical INTEGER DEFAULT 0")) db.session.commit() # 检查practice_plans表是否有created_by字段 if "created_by" not in plan_columns2: db.session.execute(text("ALTER TABLE practice_plans ADD COLUMN created_by INTEGER REFERENCES users(id)")) db.session.commit() if "updated_by" not in plan_columns2: db.session.execute(text("ALTER TABLE practice_plans ADD COLUMN updated_by INTEGER REFERENCES users(id)")) db.session.commit() if "updated_at" not in plan_columns2: db.session.execute(text("ALTER TABLE practice_plans ADD COLUMN updated_at TIMESTAMP")) db.session.commit() # 检查goals表是否有level字段 result7 = db.session.execute(text("PRAGMA table_info(goals)")) goal_columns = [row[1] for row in result7] if "level" not in goal_columns: db.session.execute(text("ALTER TABLE goals ADD COLUMN level VARCHAR(20) DEFAULT '入门'")) 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() # 检查student_goals表是否有新字段 result8 = db.session.execute(text("PRAGMA table_info(student_goals)")) sg_columns = [row[1] for row in result8] if "start_date" not in sg_columns: db.session.execute(text("ALTER TABLE student_goals ADD COLUMN start_date TIMESTAMP")) db.session.commit() if "assessment_date" not in sg_columns: db.session.execute(text("ALTER TABLE student_goals ADD COLUMN assessment_date TIMESTAMP")) db.session.commit() if "achievement_date" not in sg_columns: db.session.execute(text("ALTER TABLE student_goals ADD COLUMN achievement_date TIMESTAMP")) db.session.commit() if "comment" not in sg_columns: db.session.execute(text("ALTER TABLE student_goals ADD COLUMN comment TEXT")) db.session.commit() # 检查classes表的level字段 result9 = db.session.execute(text("PRAGMA table_info(classes)")) class_columns = [row[1] for row in result9] if "level" not in class_columns: db.session.execute(text("ALTER TABLE classes ADD COLUMN level VARCHAR(20) DEFAULT '启蒙'")) db.session.commit() # 删除不再使用的字段 # deadline 和 completed_at 已被 start_date, assessment_date, achievement_date 取代 # status 字段现在由日期计算,不再存储 except Exception as e: print(f"数据库迁移: {e}") # 初始化默认模板(必须在迁移之后) # 已禁用:如果需要默认模板,请手动创建 # from app.routes.templates import init_default_templates # init_default_templates() return app