GVic云槿
发布于 2025-02-11 / 36 阅读
0
0

数字化作业统计平台V1.2-文档


你可以在这里回顾ReleaseV1.0版本:数字化作业统计平台V1.0-文档 但V1.0版本已不再维护!


尊敬的评委:

您好!非常荣幸能够向您展示我们的科创项目。在此,我们衷心感谢您拨冗审阅我们的文档,给予我们这个宝贵的机会来分享我们的努力与成果。

我们的团队怀着对科技创新的热忱与执着,投入了大量的时间和精力,致力于打造一个具有创新性、实用性和前瞻性的项目。这个项目凝聚着我们的智慧与汗水,也承载着我们对未来的憧憬与期望。

在接下来的文档中,您将了解到我们项目的详细内容,包括项目的背景与意义、创新点与核心技术、实施过程与成果展示,以及未来的发展规划等方面。我们相信,这个项目将为相关领域带来新的思路和解决方案,为推动科技创新和社会进步贡献一份力量。

1.灵感

在日常的学习生活中,家庭作业是学生巩固知识、提升能力的重要环节。然而,收作业却常常成为一个令人头疼的问题。每天课代表在收作业时,总有部分同学不交,导致作业收不齐。老师为了检查谁没交作业,不得不花费大量时间与精力,这严重影响了上课进度和办公效率。

为了解决这一问题,我们团队经过深入思考和反复讨论,提出了一个创新的解决方案。我们设想让同学们在书的侧面贴上条形码,并将信息录入系统。当作业收起后,课代表只需把作业本摞成一摞,老师对着侧面的一堆条码进行拍照,上传到系统中。系统便会迅速分析出谁没有交作业,同时记录这些数据,并且在每周还能进行数据汇总。

通过这个智能作业管理系统,我们可以极大地简化检查和统计作业的复杂环节,减少所需时间,从而显著提高效率。这不仅能让老师更专注于教学工作,也能培养学生的自律意识和责任感。

2.使用方法

我们已经将项目部署到我们自己的服务器,在全球内使用互联网均访问,您可通过网址 http://kc.gvicyunjin.cn:44441/ 进行访问。我的域名(gvicyunjin.cn)已通过 ICP 备案(备案号:陕ICP备2024041476号-1),符合国内的法律法规要求,确保了网站的合法性与正规性,不存在钓鱼网站等安全隐患,请您放心使用。

需要说明的是,由于本项目当前处于测试环境,尚未为网页申请 SSL 证书,因此在访问时可能会出现不安全的提示。尽管这在测试阶段是正常情况,但在实际正式使用场景中,出于对用户隐私安全的考虑,建议按照通用数据保护条例(GDPR)等相关规定,及时申请 SSL 证书以实现 HTTPS 访问,从而更好地保护用户数据隐私。

本项目采用 Python 的 Flask 框架进行开发,框架中运用了加密机制对敏感的闪现消息等数据进行加密处理,有效保障了数据在传输和存储过程中的安全性,进一步增强了系统的安全性和可靠性。

使用详情:

  1. 班级注册(若首次使用):进入页面后,若未注册班级,会显示班级注册界面。在文本框中每行输入一个学生姓名,输入完成后点击 “注册班级并生成条码” 按钮。系统会自动为每个学生分配学号,并生成对应的条形码。

  2. 打印并粘贴条码:注册成功后,会显示班级成员及对应的条码。此时,班主任需截图并打印这些条码,将其粘贴到同学们的作业册子侧面。

  3. 作业检查:课代表收齐作业后,将作业摞成一摞交给班主任。班主任对着作业侧面的条码拍照,然后在平台页面中点击 “上传作业条码图片”,选择刚才拍摄的图片,点击 “检查作业” 按钮,系统会快速检测出未提交作业的学生名单,并显示结果。

  4. 查看历史记录:在平台首页,您可以查看每个学生的未交作业历史记录,了解学生的作业提交情况。

  5. 注销班级与清空记录:若班级有变动,可点击 “注销班级” 按钮删除班级数据;若想清空历史记录,点击 “清空历史记录” 按钮即可。

3.实现过程

3.1文件结构

homework_ckeck/
├── app.py             # Flask 主程序,含路由和逻辑
├── requirements.txt   # 项目依赖库清单
├── Dockerfile         # 构建 Docker 镜像
├── docker-compose.yml # 配置 Docker 多容器应用
├── templates/
│   └── index.html     # 主页面 HTML 模板
├── static/
│   └── barcodes/      # 存放生成的条码图片
├── uploads/           # 存储用户上传的条码图片
├── class_data.json    # 班级学生信息
└── history.json       # 学生未交作业历史记录

3.2.app.py

源码:

from flask import Flask, render_template, request, redirect, url_for, flash, send_from_directory, jsonify
from pyzbar.pyzbar import decode
from PIL import Image
import os
import json
import barcode
from barcode.writer import ImageWriter

app = Flask(__name__)
app.secret_key = '123456'

# 数据文件路径
data_file = "class_data.json"
history_file = "history.json"

# 加载班级数据
def load_class_data():
    if os.path.exists(data_file):
        try:
            with open(data_file, 'r', encoding='utf-8') as f:
                return json.load(f)
        except json.JSONDecodeError:
            return {}
    return {}

# 保存班级数据
def save_class_data(class_data):
    with open(data_file, 'w', encoding='utf-8') as f:
        json.dump(class_data, f, ensure_ascii=False, indent=4)

# 加载历史记录
def load_history():
    if os.path.exists(history_file):
        try:
            with open(history_file, 'r', encoding='utf-8') as f:
                return json.load(f)
        except json.JSONDecodeError:
            return {}
    return {}

# 保存历史记录
def save_history(history):
    with open(history_file, 'w', encoding='utf-8') as f:
        json.dump(history, f, ensure_ascii=False, indent=4)

# 条码生成
def generate_barcode(student_id, student_name):
    barcode_dir = "static/barcodes"
    if not os.path.exists(barcode_dir):
        os.makedirs(barcode_dir)

    ean = barcode.get('code128', str(student_id), writer=ImageWriter())
    filename = f"{barcode_dir}/{student_name}_{student_id}"
    ean.save(filename)
    filename += ".png"
    return filename

# Flask 路由
@app.route('/')
def index():
    class_data = load_class_data()
    barcodes = {}
    if class_data:
        for student_id, student_name in class_data.items():
            barcode_path = f"barcodes/{student_name}_{student_id}.png"
            barcodes[student_id] = barcode_path
    history = load_history()
    return render_template('index.html', class_data=class_data, barcodes=barcodes, history=history)

@app.route('/register', methods=['POST'])
def register():
    student_names = request.form['student_names'].strip()
    if student_names:
        students = student_names.splitlines()
        class_data = {str(i+1): name for i, name in enumerate(students)}
        save_class_data(class_data)

        # 自动为所有学生生成条码
        for student_id, student_name in class_data.items():
            generate_barcode(student_id, student_name)

        flash("班级成员注册成功并生成条码!", "success")
    else:
        flash("请输入班级成员姓名!", "error")
    return redirect(url_for('index'))

@app.route('/unregister', methods=['POST'])
def unregister():
    if os.path.exists(data_file):
        os.remove(data_file)
    if os.path.exists(history_file):
        os.remove(history_file)
    flash("班级数据已删除。", "success")
    return redirect(url_for('index'))

@app.route('/check_homework', methods=['POST'])
def check_homework():
    file = request.files['barcode_image']
    if file:
        file_path = os.path.join('uploads', file.filename)
        file.save(file_path)

        # 解码条码图片
        img = Image.open(file_path)
        barcodes = decode(img)

        if not barcodes:
            flash("未检测到任何条码!", "error")
        else:
            class_data = load_class_data()
            submitted_ids = [int(barcode_obj.data.decode('utf-8')) for barcode_obj in barcodes if barcode_obj.type == 'CODE128']
            missing_students = [name for student_id, name in class_data.items() if int(student_id) not in submitted_ids]

            if missing_students:
                missing_list = "\n".join(missing_students)
                flash(f"未交作业的学生:\n{missing_list}", "info")

                # 更新历史记录
                history = load_history()
                for student_name in missing_students:
                    if student_name in history:
                        history[student_name] += 1
                    else:
                        history[student_name] = 1
                save_history(history)
            else:
                flash("所有学生都已提交作业。", "success")
    else:
        flash("请上传条码图片!", "error")

    return redirect(url_for('index'))

@app.route('/clear_history', methods=['POST'])
def clear_history():
    if os.path.exists(history_file):
        os.remove(history_file)
    flash("历史记录已清空。", "success")
    return redirect(url_for('index'))

if __name__ == '__main__':
    if not os.path.exists('static/barcodes'):
        os.makedirs('static/barcodes')
    if not os.path.exists('uploads'):
        os.makedirs('uploads')
    app.run(host='0.0.0.0', port=44441)

解释:

导入必要的库

from flask import Flask, render_template, request, redirect, url_for, flash, send_from_directory, jsonify
from pyzbar.pyzbar import decode
from PIL import Image
import os
import json
import barcode
from barcode.writer import ImageWriter

这里导入了多个不同功能的库:

  • Flask 及其相关模块用于构建 Web 应用,像创建应用实例、处理请求、渲染模板、重定向和消息闪现等操作。

  • pyzbar 库中的 decode 函数用于解码条码图片里的条码信息。

  • PIL 库的 Image 模块可用于打开和处理图片。

  • os 库用于进行文件和目录操作。

  • json 库用于读写 JSON 文件。

  • barcode 库和 ImageWriter 用于生成条码图片。

创建 Flask 应用实例并设置密钥

app = Flask(__name__)
app.secret_key = '123456'
  • app = Flask(__name__):创建一个 Flask 应用实例,__name__ 参数能帮助 Flask 确定应用的根目录,进而定位静态文件和模板文件。

  • app.secret_key = '123456':设置应用的密钥,此密钥用于会话管理和消息闪现,确保数据在传输和存储过程中的安全性。

定义数据文件路径

data_file = "class_data.json"
history_file = "history.json"
  • data_file:指定用于存储班级学生信息的 JSON 文件的路径。

  • history_file:指定用于存储学生未交作业历史记录的 JSON 文件的路径。

数据加载和保存函数

# 加载班级数据
def load_class_data():
    if os.path.exists(data_file):
        try:
            with open(data_file, 'r', encoding='utf-8') as f:
                return json.load(f)
        except json.JSONDecodeError:
            return {}
    return {}

# 保存班级数据
def save_class_data(class_data):
    with open(data_file, 'w', encoding='utf-8') as f:
        json.dump(class_data, f, ensure_ascii=False, indent=4)

# 加载历史记录
def load_history():
    if os.path.exists(history_file):
        try:
            with open(history_file, 'r', encoding='utf-8') as f:
                return json.load(f)
        except json.JSONDecodeError:
            return {}
    return {}

# 保存历史记录
def save_history(history):
    with open(history_file, 'w', encoding='utf-8') as f:
        json.dump(history, f, ensure_ascii=False, indent=4)
  • load_class_data():检查 class_data.json 文件是否存在,若存在则读取其内容并解析为 Python 对象;若文件不存在或解析出错,则返回空字典。

  • save_class_data(class_data):将班级学生信息以 JSON 格式保存到 class_data.json 文件中,ensure_ascii=False 可确保非 ASCII 字符能正确保存,indent=4 使文件内容格式更清晰。

  • load_history():检查 history.json 文件是否存在,若存在则读取其内容并解析为 Python 对象;若文件不存在或解析出错,则返回空字典。

  • save_history(history):将学生未交作业历史记录以 JSON 格式保存到 history.json 文件中,同样采用 ensure_ascii=Falseindent=4

条码生成函数

def generate_barcode(student_id, student_name):
    barcode_dir = "static/barcodes"
    if not os.path.exists(barcode_dir):
        os.makedirs(barcode_dir)

    ean = barcode.get('code128', str(student_id), writer=ImageWriter())
    filename = f"{barcode_dir}/{student_name}_{student_id}"
    ean.save(filename)
    filename += ".png"
    return filename
  • 该函数接收学生编号和姓名作为参数。

  • 首先检查 static/barcodes 目录是否存在,若不存在则创建该目录。

  • 利用 barcode 库生成 code128 类型的条码,条码内容为学生编号。

  • 将生成的条码保存为 PNG 图片,文件名包含学生姓名和编号。

  • 最后返回生成的条码图片的完整文件名。

Flask 路由

首页路由
@app.route('/')
def index():
    class_data = load_class_data()
    barcodes = {}
    if class_data:
        for student_id, student_name in class_data.items():
            barcode_path = f"barcodes/{student_name}_{student_id}.png"
            barcodes[student_id] = barcode_path
    history = load_history()
    return render_template('index.html', class_data=class_data, barcodes=barcodes, history=history)
  • 该路由处理根路径 / 的请求。

  • 加载班级学生信息和学生未交作业历史记录。

  • 若班级学生信息存在,为每个学生生成对应的条码图片路径。

  • 渲染 index.html 模板,并将班级学生信息、条码图片路径和历史记录传递给模板。

班级注册路由
@app.route('/register', methods=['POST'])
def register():
    student_names = request.form['student_names'].strip()
    if student_names:
        students = student_names.splitlines()
        class_data = {str(i+1): name for i, name in enumerate(students)}
        save_class_data(class_data)

        # 自动为所有学生生成条码
        for student_id, student_name in class_data.items():
            generate_barcode(student_id, student_name)

        flash("班级成员注册成功并生成条码!", "success")
    else:
        flash("请输入班级成员姓名!", "error")
    return redirect(url_for('index'))
  • 该路由处理 POST 请求,用于班级注册。

  • 从表单中获取学生姓名列表,去除首尾空格后检查是否为空。

  • 若不为空,将学生姓名转换为字典形式的班级学生信息并保存到 class_data.json 文件中。

  • 为每个学生生成条码图片。

  • 使用 flash 函数显示注册成功或失败的消息。

  • 最后重定向到首页。

班级注销路由
@app.route('/unregister', methods=['POST'])
def unregister():
    if os.path.exists(data_file):
        os.remove(data_file)
    if os.path.exists(history_file):
        os.remove(history_file)
    flash("班级数据已删除。", "success")
    return redirect(url_for('index'))
  • 该路由处理 POST 请求,用于班级注销。

  • 检查 class_data.jsonhistory.json 文件是否存在,若存在则删除。

  • 使用 flash 函数显示注销成功的消息。

  • 重定向到首页。

作业检查路由
@app.route('/check_homework', methods=['POST'])
def check_homework():
    file = request.files['barcode_image']
    if file:
        file_path = os.path.join('uploads', file.filename)
        file.save(file_path)

        # 解码条码图片
        img = Image.open(file_path)
        barcodes = decode(img)

        if not barcodes:
            flash("未检测到任何条码!", "error")
        else:
            class_data = load_class_data()
            submitted_ids = [int(barcode_obj.data.decode('utf-8')) for barcode_obj in barcodes if barcode_obj.type == 'CODE128']
            missing_students = [name for student_id, name in class_data.items() if int(student_id) not in submitted_ids]

            if missing_students:
                missing_list = "\n".join(missing_students)
                flash(f"未交作业的学生:\n{missing_list}", "info")

                # 更新历史记录
                history = load_history()
                for student_name in missing_students:
                    if student_name in history:
                        history[student_name] += 1
                    else:
                        history[student_name] = 1
                save_history(history)
            else:
                flash("所有学生都已提交作业。", "success")
    else:
        flash("请上传条码图片!", "error")

    return redirect(url_for('index'))
  • 该路由处理 POST 请求,用于检查作业提交情况。

  • 从请求中获取上传的条码图片,若图片存在则保存到 uploads 目录。

  • 打开图片并解码其中的条码信息。

  • 若未检测到条码,显示错误消息;若检测到条码,对比班级学生信息,找出未交作业的学生。

  • 若有未交作业的学生,更新历史记录并显示提示信息;若所有学生都已提交作业,显示成功消息。

  • 若未上传图片,显示错误消息。

  • 最后重定向到首页。

清空历史记录路由
@app.route('/clear_history', methods=['POST'])
def clear_history():
    if os.path.exists(history_file):
        os.remove(history_file)
    flash("历史记录已清空。", "success")
    return redirect(url_for('index'))
  • 该路由处理 POST 请求,用于清空学生未交作业历史记录。

  • 检查 history.json 文件是否存在,若存在则删除。

  • 使用 flash 函数显示清空成功的消息。

  • 重定向到首页。

启动应用

if __name__ == '__main__':
    if not os.path.exists('static/barcodes'):
        os.makedirs('static/barcodes')
    if not os.path.exists('uploads'):
        os.makedirs('uploads')
    app.run(host='0.0.0.0', port=44441)
  • 检查 static/barcodesuploads 目录是否存在,若不存在则创建。

  • 当脚本作为主程序运行时,启动 Flask 应用,监听 0.0.0.0 地址的 44441 端口,使得应用可以被外部访问。

3.3.requirements.txt

用于列出python所有依赖库或模块,便于构建docker容器时安装

Flask
Pillow
pyzbar
python-barcode

3.4.dockerfile

# 使用基础镜像
FROM python:3.9-slim

# 安装 zbar 库的依赖
RUN apt-get update && apt-get install -y \
    zbar-tools \
    libzbar0 \
    && rm -rf /var/lib/apt/lists/*

# 设置工作目录
WORKDIR /app

# 复制当前目录的内容到工作目录
COPY . /app

# 使用阿里云 pypi 镜像源安装依赖
RUN pip install -i https://mirrors.aliyun.com/pypi/simple/ --no-cache-dir Flask Pillow pyzbar python-barcode

# 暴露端口 44441
EXPOSE 44441

# 运行 Flask 应用
CMD ["python", "app.py"]

一、基础镜像选择

FROM python:3.9-slim

使用 Python 3.9 的 slim 版本作为基础镜像,这个镜像通常比较小,包含了基本的 Python 运行环境,可以减少最终构建的 Docker 镜像的大小。

二、安装 zbar 库依赖

RUN apt-get update && apt-get install -y \
    zbar-tools \
    libzbar0 \
    && rm -rf /var/lib/apt/lists/*

这一步更新了软件包列表并安装了 zbar-toolslibzbar0,这些是用于在容器中处理条形码的依赖库。安装完成后,清理了软件包列表缓存以减小镜像大小。

三、设置工作目录

WORKDIR /app

将容器内的工作目录设置为 /app,后续的操作都将在这个目录下进行。

四、复制项目文件

COPY. /app

将当前目录(构建上下文)下的所有文件复制到容器的 /app 目录中,确保项目的代码和相关文件在容器内可用。

五、安装项目依赖

RUN pip install -i https://mirrors.aliyun.com/pypi/simple/ --no-cache-dir Flask Pillow pyzbar python-barcode

使用阿里云的 PyPI 镜像源安装项目所需的依赖库,包括 Flask、Pillow、pyzbar 和 python-barcode。--no-cache-dir 参数确保不使用缓存,每次构建都重新安装依赖,以保证依赖的一致性。

六、暴露端口

EXPOSE 44441

声明容器将在运行时监听 44441 端口,以便外部可以访问容器内运行的应用程序。

七、运行应用

CMD ["python", "app.py"]

在容器启动时,执行 python app.py 命令来运行 Flask 应用程序。

3.5./templates/index.html

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>数字化作业统计平台</title>
    <style>
        /* 全局样式,设置渐变色背景 */
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 0;
            background: linear-gradient(to bottom, #e0f7fa, #b2ebf2);
            display: flex;
            flex-direction: column;
            align-items: center;
            min-height: 100vh;
        }

        /* 标题样式 */
        h1 {
            text-align: center;
            color: #333;
            margin: 20px 0;
        }

        /* 小标题样式 */
        h2 {
            color: #666;
            margin: 15px 0;
            text-align: center;
        }

        /* 通用容器样式 */
        .container {
            background: linear-gradient(to bottom, #ffdab9, #ffb6c1);
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            width: 80%;
            max-width: 600px;
            margin-bottom: 20px;
            display: flex;
            flex-direction: column;
            align-items: center;
        }

        /* 文本区域样式 */
        textarea {
            width: 100%;
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 4px;
            box-sizing: border-box;
            margin-bottom: 10px;
        }

        /* 按钮样式,设置圆角 */
        button {
            padding: 10px 20px;
            background-color: #007BFF;
            color: white;
            border: none;
            border-radius: 20px;
            cursor: pointer;
            transition: background-color 0.3s ease;
            margin: 5px;
        }

        /* 按钮悬停效果 */
        button:hover {
            background-color: #0056b3;
        }

        /* 条码展示大容器样式 */
        .barcode-container {
            background: linear-gradient(to bottom, #d8bfd8, #dda0dd);
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            width: 80%;
            max-width: 1200px;
            margin-bottom: 20px;
        }

        /* 网格容器样式 */
        .grid-container {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
            gap: 20px;
        }

        /* 网格项样式,去除背景 */
        .grid-item {
            padding: 10px;
            border-radius: 10px;
            text-align: center;
            box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
            background: transparent;
        }

        /* 图片样式 */
        img {
            max-width: 100%;
            height: auto;
            border-radius: 4px;
        }

        /* 表格样式 */
        table {
            width: 100%;
            border-collapse: collapse;
            background: linear-gradient(to bottom, #98fb98, #90ee90);
            border-radius: 10px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
        }

        /* 表格表头样式 */
        th,
        td {
            padding: 10px;
            text-align: center;
            border-bottom: 1px solid #ccc;
        }

        /* 表格表头样式 */
        th {
            background-color: rgba(255, 255, 255, 0.3);
        }

        /* 弹窗样式 */
        .modal {
            display: none;
            position: fixed;
            z-index: 1;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            overflow: auto;
            background-color: rgba(0, 0, 0, 0.4);
        }

        /* 弹窗内容样式 */
        .modal-content {
            background: linear-gradient(to bottom, #add8e6, #87ceeb);
            margin: 15% auto;
            padding: 20px;
            border: 1px solid #888;
            width: 80%;
            max-width: 400px;
            border-radius: 10px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
        }

        /* 弹窗关闭按钮样式 */
        .close {
            color: #aaa;
            float: right;
            font-size: 28px;
            font-weight: bold;
        }

        /* 弹窗关闭按钮悬停效果 */
        .close:hover,
        .close:focus {
            color: black;
            text-decoration: none;
            cursor: pointer;
        }
    </style>
</head>

<body>
    <!-- 弹窗元素 -->
    <div id="myModal" class="modal">
        <div class="modal-content">
            <span class="close">&times;</span>
            <p id="modal-message"></p>
        </div>
    </div>

    <h1>数字化作业统计平台</h1>

    {% with messages = get_flashed_messages(with_categories=true) %}
        {% if messages %}
            <script>
                // 显示弹窗
                var modal = document.getElementById('myModal');
                var span = document.getElementsByClassName("close")[0];
                var message = "";
                {% for category, message in messages %}
                    message += "<strong>{{ category }}:</strong> {{ message }}<br>";
                {% endfor %}
                document.getElementById('modal-message').innerHTML = message;
                modal.style.display = "block";

                // 点击关闭按钮关闭弹窗
                span.onclick = function () {
                    modal.style.display = "none";
                }

                // 点击窗口外关闭弹窗
                window.onclick = function (event) {
                    if (event.target == modal) {
                        modal.style.display = "none";
                    }
                }
            </script>
        {% endif %}
    {% endwith %}

    {% if not class_data %}
    <div class="container">
        <h2>注册班级</h2>
        <form action="/register" method="post">
            <textarea name="student_names" rows="10" placeholder="每行输入一个学生姓名"></textarea><br><br>
            <button type="submit">注册班级并生成条码</button>
        </form>
    </div>
    {% endif %}

    {% if class_data %}
    <div class="barcode-container">
        <h2>班级成员及条码</h2>
        <div class="grid-container">
            {% for student_id, student_name in class_data.items() %}
            <div class="grid-item">
                <p>{{ student_id }}. {{ student_name }}</p>
                <img src="/static/{{ barcodes[student_id] }}" alt="条码">
            </div>
            {% endfor %}
        </div>
    </div>

    <div class="container">
        <h2>注销班级</h2>
        <form action="/unregister" method="post">
            <button type="submit">注销班级</button>
        </form>
    </div>

    <div class="container">
        <h2>上传作业条码图片</h2>
        <form action="/check_homework" method="post" enctype="multipart/form-data">
            <input type="file" name="barcode_image" accept="image/*"><br><br>
            <button type="submit">检查作业</button>
        </form>
    </div>

    <div class="container">
        <h2>历史记录</h2>
        <table>
            <thead>
                <tr>
                    <th>学生姓名</th>
                    <th>未交作业次数</th>
                </tr>
            </thead>
            <tbody>
                {% for student_name, count in history.items() %}
                <tr>
                    <td>{{ student_name }}</td>
                    <td>{{ count }}</td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
    </div>

    <div class="container">
        <h2>清空历史记录</h2>
        <form action="/clear_history" method="post">
            <button type="submit">清空历史记录</button>
        </form>
    </div>
    {% endif %}
</body>

</html>

1. HTML 头部部分

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>数字化作业统计平台</title>
    <style>
        /* 全局样式,设置渐变色背景 */
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 0;
            background: linear-gradient(to bottom, #e0f7fa, #b2ebf2);
            display: flex;
            flex-direction: column;
            align-items: center;
            min-height: 100vh;
        }

        /* 标题样式 */
        h1 {
            text-align: center;
            color: #333;
            margin: 20px 0;
        }

        /* 小标题样式 */
        h2 {
            color: #666;
            margin: 15px 0;
            text-align: center;
        }

        /* 通用容器样式 */
        .container {
            background: linear-gradient(to bottom, #ffdab9, #ffb6c1);
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            width: 80%;
            max-width: 600px;
            margin-bottom: 20px;
            display: flex;
            flex-direction: column;
            align-items: center;
        }

        /* 文本区域样式 */
        textarea {
            width: 100%;
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 4px;
            box-sizing: border-box;
            margin-bottom: 10px;
        }

        /* 按钮样式,设置圆角 */
        button {
            padding: 10px 20px;
            background-color: #007BFF;
            color: white;
            border: none;
            border-radius: 20px;
            cursor: pointer;
            transition: background-color 0.3s ease;
            margin: 5px;
        }

        /* 按钮悬停效果 */
        button:hover {
            background-color: #0056b3;
        }

        /* 条码展示大容器样式 */
        .barcode-container {
            background: linear-gradient(to bottom, #d8bfd8, #dda0dd);
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            width: 80%;
            max-width: 1200px;
            margin-bottom: 20px;
        }

        /* 网格容器样式 */
        .grid-container {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
            gap: 20px;
        }

        /* 网格项样式,去除背景 */
        .grid-item {
            padding: 10px;
            border-radius: 10px;
            text-align: center;
            box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
            background: transparent;
        }

        /* 图片样式 */
        img {
            max-width: 100%;
            height: auto;
            border-radius: 4px;
        }

        /* 表格样式 */
        table {
            width: 100%;
            border-collapse: collapse;
            background: linear-gradient(to bottom, #98fb98, #90ee90);
            border-radius: 10px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
        }

        /* 表格表头样式 */
        th,
        td {
            padding: 10px;
            text-align: center;
            border-bottom: 1px solid #ccc;
        }

        /* 表格表头样式 */
        th {
            background-color: rgba(255, 255, 255, 0.3);
        }

        /* 弹窗样式 */
        .modal {
            display: none;
            position: fixed;
            z-index: 1;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            overflow: auto;
            background-color: rgba(0, 0, 0, 0.4);
        }

        /* 弹窗内容样式 */
        .modal-content {
            background: linear-gradient(to bottom, #add8e6, #87ceeb);
            margin: 15% auto;
            padding: 20px;
            border: 1px solid #888;
            width: 80%;
            max-width: 400px;
            border-radius: 10px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
        }

        /* 弹窗关闭按钮样式 */
        .close {
            color: #aaa;
            float: right;
            font-size: 28px;
            font-weight: bold;
        }

        /* 弹窗关闭按钮悬停效果 */
        .close:hover,
        .close:focus {
            color: black;
            text-decoration: none;
            cursor: pointer;
        }
    </style>
</head>
  • <!DOCTYPE html>:声明文档类型为 HTML5。

  • <html lang="zh-CN">:指定页面语言为中文(中国大陆)。

  • <meta> 标签:

    • charset="UTF-8":设置字符编码为 UTF - 8,确保页面能正确显示各种字符。

    • name="viewport":让页面在不同设备上有良好的显示效果,width=device-width 使页面宽度适应设备屏幕宽度,initial-scale=1.0 设置初始缩放比例为 1。

  • <title> 标签:设置页面标题为 “数字化作业统计平台”。

  • <style> 标签:定义页面的 CSS 样式,包括:

    • 全局样式:为 body 设置渐变色背景,使用 Flexbox 布局让页面内容垂直居中,最小高度为 100vh 以占满整个屏幕高度。

    • 标题和小标题样式:设置 h1h2 的文本颜色、对齐方式和外边距。

    • 容器样式:为 .container.barcode-container 设置渐变色背景、内边距、圆角、阴影和宽度等属性。

    • 表单元素样式:设置 textarea 的宽度、内边距、边框和圆角等;为 button 设置背景颜色、文本颜色、圆角和悬停效果。

    • 网格布局样式:使用 CSS Grid 布局为 .grid-container.grid-item 设置样式,使班级成员信息和条码图片能以网格形式展示。

    • 表格样式:为 tablethtd 设置背景颜色、边框和内边距等,让历史记录表格更美观。

    • 弹窗样式:定义 .modal.modal-content 的样式,实现弹窗的显示和关闭效果。

2. HTML 主体部分

弹窗元素
<body>
    <!-- 弹窗元素 -->
    <div id="myModal" class="modal">
        <div class="modal-content">
            <span class="close">&times;</span>
            <p id="modal-message"></p>
        </div>
    </div>

    <h1>数字化作业统计平台</h1>

    {% with messages = get_flashed_messages(with_categories=true) %}
        {% if messages %}
            <script>
                // 显示弹窗
                var modal = document.getElementById('myModal');
                var span = document.getElementsByClassName("close")[0];
                var message = "";
                {% for category, message in messages %}
                    message += "<strong>{{ category }}:</strong> {{ message }}<br>";
                {% endfor %}
                document.getElementById('modal-message').innerHTML = message;
                modal.style.display = "block";

                // 点击关闭按钮关闭弹窗
                span.onclick = function () {
                    modal.style.display = "none";
                }

                // 点击窗口外关闭弹窗
                window.onclick = function (event) {
                    if (event.target == modal) {
                        modal.style.display = "none";
                    }
                }
            </script>
        {% endif %}
    {% endwith %}
  • 弹窗结构:使用 <div> 元素创建一个弹窗,id="myModal" 用于在 JavaScript 中获取该元素,class="modal" 应用弹窗样式。

  • Jinja2 模板代码

    • {% with messages = get_flashed_messages(with_categories=true) %}:获取 Flask 应用中闪现的消息及其类别。

    • {% if messages %}:判断是否有闪现消息。

    • {% for category, message in messages %}:遍历所有闪现消息。

  • JavaScript 代码

    • 获取弹窗元素和关闭按钮元素。

    • 将闪现消息拼接成 HTML 字符串,并插入到弹窗的消息显示区域。

    • 设置弹窗的 display 属性为 block 以显示弹窗。

    • 为关闭按钮和窗口点击事件添加监听器,点击关闭按钮或窗口外区域时隐藏弹窗。

班级注册部分
    {% if not class_data %}
    <div class="container">
        <h2>注册班级</h2>
        <form action="/register" method="post">
            <textarea name="student_names" rows="10" placeholder="每行输入一个学生姓名"></textarea><br><br>
            <button type="submit">注册班级并生成条码</button>
        </form>
    </div>
    {% endif %}
  • 条件判断{% if not class_data %} 检查是否存在班级数据,如果不存在则显示班级注册表单。

  • 表单结构

    • <form> 标签:设置表单的提交地址为 /register,提交方法为 post

    • <textarea> 标签:用于输入学生姓名,每行一个姓名。

    • <button> 标签:点击后提交表单进行班级注册并生成条码。

班级数据存在时的部分
    {% if class_data %}
    <div class="barcode-container">
        <h2>班级成员及条码</h2>
        <div class="grid-container">
            {% for student_id, student_name in class_data.items() %}
            <div class="grid-item">
                <p>{{ student_id }}. {{ student_name }}</p>
                <img src="/static/{{ barcodes[student_id] }}" alt="条码">
            </div>
            {% endfor %}
        </div>
    </div>

    <div class="barcode-container">
        <h2>班级成员及条码</h2>
        <div class="grid-container">
            {% for student_id, student_name in class_data.items() %}
            <div class="grid-item">
                <p>{{ student_id }}. {{ student_name }}</p>
                <img src="/static/{{ barcodes[student_id] }}" alt="条码">
            </div>
            {% endfor %}
        </div>
    </div>
    <div class="container">
        <h2>注销班级</h2>
        <form action="/unregister" method="post">
            <button type="submit">注销班级</button>
        </form>
    </div>
    <div class="container">
        <h2>上传作业条码图片</h2>
        <form action="/check_homework" method="post" enctype="multipart/form-data">
            <input type="file" name="barcode_image" accept="image/*"><br><br>
            <button type="submit">检查作业</button>
        </form>
    </div>
    <div class="container">
        <h2>历史记录</h2>
        <table>
            <thead>
                <tr>
                    <th>学生姓名</th>
                    <th>未交作业次数</th>
                </tr>
            </thead>
            <tbody>
                {% for student_name, count in history.items() %}
                <tr>
                    <td>{{ student_name }}</td>
                    <td>{{ count }}</td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
    </div>
    <div class="container">
        <h2>清空历史记录</h2>
        <form action="/clear_history" method="post">
            <button type="submit">清空历史记录</button>
        </form>
    </div>
    {% endif %}
  • 班级成员及条码展示

    • 容器结构:使用 .barcode-container 作为外层容器,设置了渐变色背景、圆角和阴影等样式。

    • 网格布局.grid-container 采用 CSS Grid 布局,根据屏幕宽度自动排列 .grid-item

    • 循环展示:通过 {% for student_id, student_name in class_data.items() %} 遍历班级数据,为每个学生生成一个 .grid-item,显示学生编号、姓名和对应的条码图片。

  • 注销班级功能

    • 表单结构:包含在 .container 容器内,表单的 action 属性设置为 /unregister,提交方法为 post。点击 “注销班级” 按钮后,会向该地址发送请求,执行注销班级的操作。

  • 上传作业条码图片功能

    • 表单结构:同样使用 .container 容器,表单的 action 属性为 /check_homeworkenctype="multipart/form-data" 表示表单用于上传文件。

    • 文件输入<input type="file" name="barcode_image" accept="image/*"> 允许用户选择一张图片文件,点击 “检查作业” 按钮后,会将图片上传到服务器进行作业检查。

  • 历史记录展示

    • 表格结构:使用 <table> 元素展示历史记录,<thead> 定义表头,包含 “学生姓名” 和 “未交作业次数” 两列。

    • 数据填充:通过 {% for student_name, count in history.items() %} 遍历历史记录数据,为每个学生生成一行表格数据。

  • 清空历史记录功能

    • 表单结构:在 .container 容器内,表单的 action 属性为 /clear_history,提交方法为 post。点击 “清空历史记录” 按钮后,会向该地址发送请求,清空历史记录。

4.私有化部署

  1. 基于 Ubuntu 系统的部署(以 Ubuntu 22.04 为例)

    • 安装 Docker

      • 打开终端,更新软件包索引,执行命令:sudo apt update

      • 安装 Docker 的依赖包,执行命令:sudo apt install apt-transport-https ca-certificates curl software-properties-common

      • 添加 Docker 的官方 GPG 密钥,执行命令:curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

      • 添加 Docker 的软件源,执行命令:echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

      • 再次更新软件包索引,执行命令:sudo apt update

      • 安装 Docker 引擎,执行命令:sudo apt install docker.io

      • 将当前用户添加到 docker 用户组(这样可以无需使用 sudo 运行 Docker 命令),执行命令:sudo usermod -aG docker $USER,然后重新登录系统使设置生效。

    • 连接服务器与准备文件:使用 finalshell 或其他 ssh 工具连接到服务器。将项目文件夹【homework-ckack】上传到服务器的任意位置,并授予 777 权限(若为 root 用户可忽略此步骤)。

    • 获取权限与构建容器:获取根用户权限,执行命令:su root。进入上传文件的根目录,使用 ls 命令确认能看到 Dockerfile 文件。在终端输入以下指令构建 docker 容器:docker build -t homework-checker . ,根据网络情况,构建过程大约需要 3 - 10 分钟。

    • 启动容器与访问平台:构建完成后,在终端输入以下指令启动 docker 容器:docker run -d -p 44441:44441 --name homework-checker homework-checker 。在服务器的安全组或防火墙中放行 44441 端口,以保证请求不会被拦截。通过 http://{服务器 IP}:44441 访问应用程序。

  2. 基于 Windows 系统的部署

    • 安装 Docker:访问 Docker 官方网站(https://www.docker.com/ ),根据系统版本下载并安装适合的 Docker Desktop 应用程序。下载完成后,双击安装包,按照安装向导的提示完成安装。安装完成后,打开 Docker Desktop 确保 Docker 服务已启动。

    • 准备项目文件:将项目文件夹【homework-ckack】复制到电脑的合适位置。

    • 进行项目部署:打开命令提示符(CMD)或 PowerShell,导航到项目文件夹所在目录。依次执行构建 Docker 容器(docker build -t homework-checker. )、启动 Docker 容器(docker run -d -p 44441:44441 --name homework-checker homework-checker )。确保 Windows 防火墙已放行 44441 端口(可在防火墙设置中添加入站规则)。通过 http://localhost:44441http://{电脑本地 IP}:44441 访问应用程序。

  3. 基于 Mac 系统的部署

    • 安装 Docker

      • 打开浏览器,访问 Docker 官方网站(https://www.docker.com/ ),下载适用于 Mac 的 Docker Desktop 安装包。

      • 下载完成后,双击安装包并按照安装向导提示进行安装,过程中可能需输入 Mac 用户密码授权。

      • 安装完成后启动 Docker Desktop,等待图标显示为运行状态,表明 Docker 环境准备就绪。

    • 准备项目文件:将项目文件夹【homework-ckack】复制到 Mac 电脑合适位置,如 “下载” 或 “文档” 文件夹。

    • 进行项目部署

      • 打开 “终端” 应用程序(可通过 “聚焦搜索” 找到)。

      • 在终端中,使用 cd 命令导航到项目文件夹【homework-ckack】所在目录。例如,若项目文件夹在 “下载” 文件夹中,输入 cd ~/Downloads/homework-ckack 并回车。

      • 确认在项目文件夹目录后,输入指令构建 Docker 容器:docker build -t homework-checker. ,等待 Docker 构建容器,约 3 - 10 分钟。

      • 构建完成后,输入指令启动 Docker 容器:docker run -d -p 44441:44441 --name homework-checker homework-checker ,以后台运行方式启动容器,并将容器 44441 端口映射到 Mac 电脑 44441 端口。

      • 若 Mac 系统开启防火墙,需在防火墙设置中允许 Docker 应用访问网络,并确保 44441 端口已放行,可在 “系统偏好设置” -> “安全性与隐私” -> “防火墙” 中设置。

      • 完成上述步骤后,打开浏览器,在地址栏输入 http://localhost:44441http://{Mac 电脑本地 IP 地址}:44441 (可在 “系统偏好设置” -> “网络” 中查看本地 IP 地址),访问数字化作业统计平台。

5.未来计划

目前,我们的程序已基本实现通过在同学们的书侧面贴条码,老师拍照条码即可快速确定未交作业的学生,极大地减轻了收作业的工作负担。

对于未来的计划:

1.我们将引入设置多班级功能,可注册多个班级,让更多的师生共同使用我们这套系统。同时,我们还会为每科老师分别设置独立的账号密码,例如,语文老师有专属的语文账号密码,数学老师有自己的数学账号密码。不同老师上传作业情况后,班主任将拥有一个管理平台,在该平台上可以清晰地看到哪个同学具体哪一科未交作业。

2.我们将采用 token 技术与服务器进行验证。这样,老师在一次登录后,在特定时间段内可凭借 token 验证无需再次登录直接打开系统。

3.此外,后续我们还会引入每周、每月、每学期的统计量,并运用图表、折线等数据统计方式,直观地展示学生的作业完成情况,为教学管理提供更有力的支持。

4.为了保证这个数据安全,我们后续还会进行数据容灾方面的优化。在数据冗余方面,我们会对服务器设置 RAID 阵列,将数据存在阵列中,以保证即使硬盘损坏,数据也不会丢失。同时,我们还会进行数据灾备,设置两个异地服务器,让数据在异地服务器之间进行同步。这样,即使有一台服务器由于一些不可控因素如断电、火灾、海啸等问题损坏,我们的数据还能在另外一台服务器上恢复,服务也可以在另外一台服务器上继续进行,从而最大程度地保证用户的数据安全。

6.感谢

在数字化作业统计平台的开发之旅中,我们曾于服务器调试与网页构建的重重困难里徘徊挣扎,无数次想要放弃却又一次次选择坚持。指导老师倾囊相授、悉心指引,网络资源如 CSDN、菜鸟编程等平台以及 zbar 库、flask 库、PILLOW 库的原作者提供的帮助,如同点点星光照亮我们前行的道路。 因为坚持,我们收获成长,未来也将秉持这种探索、积极且坚持不懈的精神,于生活中砥砺前行。我们会努力汲取科学文化知识,钻研辩证思维,立志在科技与网络领域发光发热,为国家贡献自己的力量,不负梦想,成就未来!

再次感谢评委们耐心观看我们的成果展示!


开发团队:

开发者:郭子路

指导老师:薛仙

学校:西安市第六十六中学



评论