From a880eb667bfbb8e0501ac0e74d9641cc5cebf121 Mon Sep 17 00:00:00 2001 From: ccicnce113424 Date: Sun, 23 Nov 2025 20:35:48 +0800 Subject: [PATCH] feat: init flask skeleton with auth and pages --- .env.example | 2 + .envrc | 1 + .gitignore | 73 +++++++++++++++++++++++++++++++++++++ README.md | 48 ++++++++++++++++++++++++ app/__init__.py | 25 +++++++++++++ app/auth/routes.py | 28 ++++++++++++++ app/config.py | 13 +++++++ app/main/routes.py | 29 +++++++++++++++ app/models.py | 41 +++++++++++++++++++++ app/templates/base.html | 36 ++++++++++++++++++ app/templates/login.html | 50 +++++++++++++++++++++++++ app/templates/manage.html | 6 +++ app/templates/rollcall.html | 14 +++++++ app/templates/stats.html | 6 +++ requirements.txt | 7 ++++ run.py | 8 ++++ shell.nix | 9 +++++ 17 files changed, 396 insertions(+) create mode 100644 .env.example create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/__init__.py create mode 100644 app/auth/routes.py create mode 100644 app/config.py create mode 100644 app/main/routes.py create mode 100644 app/models.py create mode 100644 app/templates/base.html create mode 100644 app/templates/login.html create mode 100644 app/templates/manage.html create mode 100644 app/templates/rollcall.html create mode 100644 app/templates/stats.html create mode 100644 requirements.txt create mode 100644 run.py create mode 100644 shell.nix diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6b91d55 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +SECRET_KEY=change-me +DATABASE_URL=mysql+pymysql://user:password@host:3306/dbname diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..65326bb --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use nix \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..293ade5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,73 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +.venv/ +venv/ +ENV/ +env/ + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# VS Code +.vscode/ + +# Environment variables and secrets +.env +.env.* +!.env.example + +# Logs +*.log + +# OS-generated files +.DS_Store +Thumbs.db +.direnv \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..02d5b85 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# 课堂点名系统(最小可运行版本) + +## 安装依赖 + +```bash +cd class-random-caller +python -m venv venv +source venv/bin/activate # Windows 使用 venv\\Scripts\\activate +pip install -r requirements.txt +``` + +## 配置数据库 + +在项目根目录创建 `.env` 文件,内容示例: + +```env +SECRET_KEY=your-secret-key +DATABASE_URL=mysql+pymysql://user:password@host:3306/dbname +``` + +请将 `user/password/host/dbname` 替换为你在免费 MySQL 服务上的实际信息。 + +## 初始化数据库 + +在 Python 交互环境或单独脚本中执行: + +```python +from run import app +from app import db +from app.models import User +from werkzeug.security import generate_password_hash + +with app.app_context(): + db.create_all() + # 创建一个管理员账号(只需执行一次) + if not User.query.filter_by(username="admin").first(): + u = User(username="admin", password_hash=generate_password_hash("admin123")) + db.session.add(u) + db.session.commit() +``` + +## 运行项目 + +```bash +python run.py +``` + +浏览器打开 `http://127.0.0.1:5000/auth/login`,使用 `admin / admin123` 登录。 diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..1a325fa --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,25 @@ +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from flask_login import LoginManager +from .config import Config + + +db = SQLAlchemy() +login_manager = LoginManager() +login_manager.login_view = "auth.login" + + +def create_app(): + app = Flask(__name__) + app.config.from_object(Config) + + db.init_app(app) + login_manager.init_app(app) + + from .auth.routes import auth_bp + from .main.routes import main_bp + + app.register_blueprint(auth_bp) + app.register_blueprint(main_bp) + + return app diff --git a/app/auth/routes.py b/app/auth/routes.py new file mode 100644 index 0000000..1e214df --- /dev/null +++ b/app/auth/routes.py @@ -0,0 +1,28 @@ +from flask import Blueprint, render_template, redirect, url_for, request, flash +from flask_login import login_user, logout_user, login_required +from werkzeug.security import check_password_hash + +from app.models import User + + +auth_bp = Blueprint("auth", __name__, url_prefix="/auth") + + +@auth_bp.route("/login", methods=["GET", "POST"]) +def login(): + if request.method == "POST": + username = request.form.get("username") + password = request.form.get("password") + user = User.query.filter_by(username=username).first() + if user and check_password_hash(user.password_hash, password): + login_user(user) + return redirect(url_for("main.rollcall")) + flash("用户名或密码错误") + return render_template("login.html") + + +@auth_bp.route("/logout") +@login_required +def logout(): + logout_user() + return redirect(url_for("auth.login")) diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..e89835b --- /dev/null +++ b/app/config.py @@ -0,0 +1,13 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + + +class Config: + SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret-key") + SQLALCHEMY_DATABASE_URI = os.getenv( + "DATABASE_URL", + "mysql+pymysql://user:password@host:3306/dbname", + ) + SQLALCHEMY_TRACK_MODIFICATIONS = False diff --git a/app/main/routes.py b/app/main/routes.py new file mode 100644 index 0000000..a682014 --- /dev/null +++ b/app/main/routes.py @@ -0,0 +1,29 @@ +from flask import Blueprint, render_template +from flask_login import login_required + + +main_bp = Blueprint("main", __name__) + + +@main_bp.route("/") +@login_required +def index(): + return render_template("rollcall.html") + + +@main_bp.route("/rollcall") +@login_required +def rollcall(): + return render_template("rollcall.html") + + +@main_bp.route("/stats") +@login_required +def stats(): + return render_template("stats.html") + + +@main_bp.route("/manage") +@login_required +def manage(): + return render_template("manage.html") diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..61181aa --- /dev/null +++ b/app/models.py @@ -0,0 +1,41 @@ +from datetime import datetime +from flask_login import UserMixin +from . import db, login_manager + + +class User(UserMixin, db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(64), unique=True, nullable=False) + password_hash = db.Column(db.String(128), nullable=False) + + +class Student(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) + student_no = db.Column(db.String(32), unique=True, nullable=False) + name = db.Column(db.String(64), nullable=False) + major = db.Column(db.String(128)) + total_score = db.Column(db.Float, default=0.0) + attendance_count = db.Column(db.Integer, default=0) + random_called_count = db.Column(db.Integer, default=0) + + +class RollCall(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) + student_id = db.Column(db.Integer, db.ForeignKey("student.id"), nullable=False) + call_time = db.Column(db.DateTime, default=datetime.utcnow) + mode = db.Column(db.String(16), nullable=False) # random / sequence + status = db.Column( + db.String(16), nullable=False + ) # absent / distracted / attended + score_change = db.Column(db.Float, nullable=False, default=0.0) + + student = db.relationship("Student", backref="roll_calls") + + # user 关系可按需在查询中使用,不强制 backref + + +@login_manager.user_loader +def load_user(user_id): + return User.query.get(int(user_id)) diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..aa35ba5 --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,36 @@ + + + + + + {% block title %}课堂点名系统{% endblock %} + + + + +
+ {% with messages = get_flashed_messages() %} + {% if messages %} +
+ {% for m in messages %} {{ m }}
{% endfor %} +
+ {% endif %} + {% endwith %} + {% block content %}{% endblock %} +
+ + + diff --git a/app/templates/login.html b/app/templates/login.html new file mode 100644 index 0000000..74b5c7f --- /dev/null +++ b/app/templates/login.html @@ -0,0 +1,50 @@ + + + + + + 登录 - 课堂点名系统 + + + +
+
+
+
+
管理员登录
+
+
+
+ + +
+
+ + +
+ +
+ {% with messages = get_flashed_messages() %} + {% if messages %} +
+ {% for m in messages %} {{ m }}
{% endfor %} +
+ {% endif %} + {% endwith %} +
+
+
+
+
+ + diff --git a/app/templates/manage.html b/app/templates/manage.html new file mode 100644 index 0000000..224151d --- /dev/null +++ b/app/templates/manage.html @@ -0,0 +1,6 @@ +{% extends 'base.html' %} +{% block title %}管理 - 课堂点名系统{% endblock %} +{% block content %} +

管理页面占位

+

学生总表,以及 Excel 导入功能,将在这里实现。

+{% endblock %} diff --git a/app/templates/rollcall.html b/app/templates/rollcall.html new file mode 100644 index 0000000..9669112 --- /dev/null +++ b/app/templates/rollcall.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} +{% block title %}点名 - 课堂点名系统{% endblock %} +{% block content %} +
+
+

点名区(稍后实现逻辑)

+

这里将显示大字姓名和学号,以及随机/顺序选择。

+
+
+

打分区(稍后实现逻辑)

+

缺勤 / 走神 / 加分 按钮区域。

+
+
+{% endblock %} diff --git a/app/templates/stats.html b/app/templates/stats.html new file mode 100644 index 0000000..321c35d --- /dev/null +++ b/app/templates/stats.html @@ -0,0 +1,6 @@ +{% extends 'base.html' %} +{% block title %}统计 - 课堂点名系统{% endblock %} +{% block content %} +

统计页面占位

+

上半部分:积分随时间走势图;下半部分:排行榜;右侧:点名记录。

+{% endblock %} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c3773f3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +Flask==3.0.3 +Flask-Login==0.6.3 +Flask_SQLAlchemy==3.1.1 +pymysql==1.1.1 +pandas==2.2.3 +openpyxl==3.1.5 +python-dotenv==1.0.1 diff --git a/run.py b/run.py new file mode 100644 index 0000000..db99a5d --- /dev/null +++ b/run.py @@ -0,0 +1,8 @@ +from app import create_app + + +app = create_app() + + +if __name__ == "__main__": + app.run(debug=True) diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..0da78c5 --- /dev/null +++ b/shell.nix @@ -0,0 +1,9 @@ +{ + pkgs ? import { }, + ... +}: +pkgs.mkShell { + packages = with pkgs; [ + python313Packages.uv + ]; +}