ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

学生信息管理系统开发实战:Flask+SQLite构建CRUD应用

学生信息管理系统开发实战:Flask+SQLite构建CRUD应用 简介这是一份基于C控制台的学生信息管理系统完整源码面向程序设计初学者和需要完成课程设计的在校生用于解决多类型学生信息统一管理不便、手工维护效率低的问题可帮助快速掌握增删改查、统计与文件存储等基础功能。系统针对小学生、中学生、大学生三类对象分别扩展信息字段小学生增加英语、数学、语文成绩中学生增加地理、历史成绩与家庭住址大学生增加专业、家庭地址和联系方式同时提供添加、查询、显示、编辑、删除、统计、保存、读取八项菜单操作功能覆盖完整代码逻辑清晰便于逐段阅读理解。压缩包为RAR格式仅一个文件即一个cpp源程序包大小仅4KB代码量精简无需复杂配置即可编译运行。目前已有11712人浏览学习读者可直接获得的是一份完整可运行的C实现既能对照调试理解文件读写与菜单交互也可在此基础上扩展图形界面或数据库适用于课程设计、期末项目或日常编程练习。1. 学生信息管理系统到底在管什么从一张 Excel 表说起新学期开学教务老师手里拿着一张几百行的 Excel 表上面是全校学生的学号、姓名、性别、年龄、班级。她要按班级排序打印名单要筛出某个班男生女生各多少人要改一个转学生的班级信息。每次操作都要重新筛选、排序、复制粘贴一个不小心还会把公式拖坏。这时候你就知道一个学生信息管理系统解决的不是“有没有数据库”的问题而是把这类重复劳动固化成几个按钮的事。学生信息管理系统的核心就是一套针对“学号、姓名、性别、年龄、班级”五个字段的增删改查。听起来简单但实际写起来涉及字段类型选择、数据库约束、表单校验、搜索分页、CSV 导入导出等一连串决策。这篇文章面向的是正在做课程设计、毕业设计或者刚入职想拿小项目练手的前端/后端新人。我会用 Python Flask 加 SQLite 作为默认技术栈因为它零配置、开箱即跑同时把每个决策背后的理由和踩坑点讲清楚让你做完能 confidently 说“这个系统是我自己设计并实现的”。2. 数据模型先行学生表的字段、类型与约束这样定很多新手拿到“学生信息管理系统”这个题目第一件事就是打开 IDE 写页面。这是典型的顺序错误。页面是数据的投影数据模型没定清楚后面每改一次字段都要动前端、动后端、动数据库三重返工。我一般会先用十分钟把表结构想明白再开始写代码。2.1 五个核心字段的选型为什么学号必须用 TEXT 而不是 INTEGER学生信息管理系统最核心的字段就是标题里写的那五个学号、姓名、性别、年龄、班级。我第一次做这类系统时想当然地把学号设成了 INTEGER 类型结果导入数据时发现 20230001 变成了 1前导零全部丢失。这是血泪教训学号看起来是数字但它实际上是一个标识符不是用来参与加减乘除的。字段选型我建议这样定字段类型约束理由student_idTEXTPRIMARY KEY学号定长且可能含前导零用 TEXT 保存原样nameTEXTNOT NULL姓名没有长度计算需求genderTEXTCHECK IN (男, 女)枚举值用 CHECK 约束防止脏数据ageINTEGERCHECK BETWEEN 3 AND 100年龄是整数但要限制合理范围class_nameTEXTNOT NULL注意 class 是 SQL 关键字用 class_name这里有一个很容易被忽略的坑字段名不要用 class因为 CLASS 在 SQL 标准里是保留字。不同数据库对保留字的处理不一样MySQL 里可以用反引号包住SQLite 里直接写也不会报错但换到其他数据库就可能翻车。统一用 class_name 从源头规避。2.2 SQLite 还是 MySQL课程设计够用和上生产线的分界线选数据库是个让新手纠结的问题。我的建议很简单如果这个系统是课程设计、毕业设计或者内部工具SQLite 完全够用如果是要部署到公网、多人同时写入、日均请求上千再考虑 MySQL。两者的差别主要体现在并发和运维上。SQLite 是嵌入式数据库整个库就是一个文件备份直接复制文件即可不需要安装服务、不需要配账号密码。缺点是并发写入能力弱多个进程同时写同一个库文件可能会报 database is locked。MySQL 是独立服务支持并发连接但你要额外装服务端、建库建用户、处理连接池学习成本高出一截。对于“学生信息管理系统”这个体量的题目我推荐 SQLite。原因是这个系统的写入频率极低——无非就是开学录一批、转学改几条、毕业删一批并发写几乎不存在。SQLite 的零运维特性可以让你把精力放在业务逻辑上。如果你未来想换 MySQL只需要把数据库连接层替换掉SQL 语句本身基本不用改。2.3 建表 SQL 与初始化脚本给数据库上约束定好字段就要写建表 SQL。我用一个schema.sql文件保存表结构然后用命令行一次性初始化。这样做的好处是表结构进入版本控制以后改字段有迹可循。-- schema.sql CREATE TABLE IF NOT EXISTS students ( student_id TEXT PRIMARY KEY, name TEXT NOT NULL, gender TEXT NOT NULL CHECK (gender IN (男, 女)), age INTEGER NOT NULL CHECK (age BETWEEN 3 AND 100), class_name TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_students_class ON students(class_name);初始化命令sqlite3 students.db schema.sqlIF NOT EXISTS保证脚本重复执行不会报错CHECK (gender IN (男, 女))在数据库层面拦截非法性别值即使前端表单被绕过脏数据也进不来idx_students_class索引是为了后面按班级搜索时不走全表扫描。注意 SQLite 的 TEXT 主键默认走 B-Tree 索引所以学号字段不需要额外建索引。2.4 年龄该存静态值还是按出生日期算一个字段引发的长期维护问题这是学生信息管理系统里最有争议的一个点。标题明确要求“年龄”字段所以很多初学者就直接存一个整数。但你想过没有今年存 20明年这个学生就 21 了你总不能每年开学手动把所有年龄字段加一。常见的做法是存出生日期birth_date然后查询时用当前日期减去出生日期动态计算年龄。但这里有个现实约束题目要求的是“年龄”你的表单里如果放一个日期选择器录入人员会嫌麻烦。折中方案是数据库表里同时存 birth_date 和 ageage 作为冗余字段在新增和编辑时由后端根据 birth_date 计算后写入。如果你坚持只存 age 字段问题会在第二年的数据核对时爆发。我见过一个真实的翻车案例系统跑了一年年龄字段全部没更新最后写了个一次性脚本把所有人的年龄加了 1结果转学生和复读生被加错了。所以我的建议是至少在设计文档里说明 age 是“入学时年龄”并在字段注释里写清楚别让后人背锅。3. 用 Flask 把增删改查跑通核心路由与参数校验数据模型确定后后端实现是最重要的一环。Flask 是 Python 生态里最简单直接的 Web 框架一个文件就能起步。这一章我按新增、查询、编辑、删除四个方向拆开讲每个路由都会附上完整代码和参数说明。3.1 项目结构与最小依赖一个 app.py 起步学生信息管理系统不需要复杂的项目结构我个人习惯用三层目录但初期可以压缩成一个 app.py 加两个模板文件。依赖只需要 Flask 一个包pip install flask项目结构如下student_management/ ├── app.py # 后端主逻辑 ├── schema.sql # 建表语句 ├── students.db # SQLite 数据库文件 └── templates/ ├── index.html # 列表页 └── form.html # 新增/编辑表单页先写数据库连接函数。不要在每个路由里重复写连接代码封装成一个函数# app.py import sqlite3 from flask import Flask, render_template, request, redirect, url_for, flash app Flask(__name__) app.secret_key dev-secret # flash 消息需要 secret_key生产环境改用环境变量 DB_PATH students.db def get_db(): conn sqlite3.connect(DB_PATH) conn.row_factory sqlite3.Row # 让查询结果支持按字段名访问 return connconn.row_factory sqlite3.Row这行很关键。不加的话查询结果是元组你得用row[0]、row[1]这种下标访问代码可读性极差加上之后可以用row[student_id]访问模板里也能直接写字段名。app.secret_key是 Flask 的 session 和 flash 机制的必要配置不设的话flash()会报错。3.2 新增学生POST 路由里的校验顺序新增是 CRUD 里最容易出问题的环节。我的原则是前端校验只负责体验后端校验才是安全底线。先看代码app.route(/add, methods[POST]) def add_student(): student_id request.form.get(student_id, ).strip() name request.form.get(name, ).strip() gender request.form.get(gender, ) age request.form.get(age, ) class_name request.form.get(class_name, ).strip() # 校验顺序先查空再查类型再查业务规则 if not all([student_id, name, gender, age, class_name]): flash(所有字段均为必填) return redirect(url_for(index)) try: age_int int(age) if not 1 age_int 120: raise ValueError except ValueError: flash(年龄必须是 1 到 120 之间的整数) return redirect(url_for(index)) try: conn get_db() conn.execute( INSERT INTO students (student_id, name, gender, age, class_name) VALUES (?, ?, ?, ?, ?), (student_id, name, gender, age_int, class_name) ) conn.commit() conn.close() flash(学生添加成功) except sqlite3.IntegrityError: flash(f学号 {student_id} 已存在请检查后重试) return redirect(url_for(index))代码里有三个值得说明的细节。第一是request.form.get()而不是request.form[]前者在字段缺失时返回None而不会抛异常配合.strip()可以同时处理空白输入。第二是学号重复的捕获INTEGRITY ERROR是数据库层面的唯一约束拦截这比你先查一遍再插入要可靠因为查和插之间存在时间窗口并发时可能漏掉。第三是校验顺序——空值、类型、业务规则这个顺序不能乱否则用户会看到一个莫名其妙的报错。这里还有一个隐藏的坑SQLite 的INTEGER PRIMARY KEY字段在插入NULL时会自动分配自增 ID但你如果用了TEXT PRIMARY KEY且插入空字符串它不会报错而是把空字符串当作合法主键存进去。所以前面用not all(...)做空值校验是非常必要的。3.3 查询列表搜索、分页与排序查询列表是系统的门面。很多学生信息管理系统只做一个全表SELECT *数据量到几百条就开始卡。我一般会做三件事关键字搜索、分页、排序。app.route(/) def index(): keyword request.args.get(keyword, ).strip() page request.args.get(page, 1, typeint) per_page 10 conn get_db() if keyword: like f%{keyword}% rows conn.execute( SELECT * FROM students WHERE student_id LIKE ? OR name LIKE ? OR class_name LIKE ? ORDER BY student_id LIMIT ? OFFSET ?, (like, like, like, per_page, (page - 1) * per_page) ).fetchall() total conn.execute( SELECT COUNT(*) FROM students WHERE student_id LIKE ? OR name LIKE ? OR class_name LIKE ?, (like, like, like) ).fetchone()[0] else: rows conn.execute( SELECT * FROM students ORDER BY student_id LIMIT ? OFFSET ?, (per_page, (page - 1) * per_page) ).fetchall() total conn.execute(SELECT COUNT(*) FROM students).fetchone()[0] conn.close() total_pages (total per_page - 1) // per_page return render_template(index.html, studentsrows, totaltotal, pagepage, total_pagestotal_pages, keywordkeyword)request.args.get(page, 1, typeint)的typeint参数很实用它会在page不是数字时自动返回默认值 1省去了手动 try-catch。LIMIT ? OFFSET ?是分页的标准写法注意OFFSET的值是(page - 1) * per_page不是page * per_page这是新手最容易写错的地方会导致第一页和第二页数据重复。搜索用三个字段的LIKE模糊匹配覆盖学号、姓名、班级三个检索入口基本能满足日常需求。count 查询和列表查询拆开是因为分页时你需要知道总页数而LIMIT之后的结果集长度不等于总记录数。3.4 编辑和删除处理不存在与重复提交编辑和删除在逻辑上有一个共同点它们都必须处理“目标记录不存在”的情况。很多新手直接按主键更新更新影响行数为 0 时也不提示用户以为改成功了实际上什么都没发生。app.route(/edit/student_id, methods[GET, POST]) def edit_student(student_id): conn get_db() if request.method GET: row conn.execute(SELECT * FROM students WHERE student_id ?, (student_id,)).fetchone() conn.close() if row is None: flash(该学生不存在) return redirect(url_for(index)) return render_template(form.html, studentrow) # POST 更新逻辑 name request.form.get(name, ).strip() gender request.form.get(gender, ) age request.form.get(age, ) class_name request.form.get(class_name, ).strip() try: age_int int(age) except ValueError: flash(年龄必须是整数) return redirect(url_for(edit_student, student_idstudent_id)) cursor conn.execute( UPDATE students SET name ?, gender ?, age ?, class_name ? WHERE student_id ?, (name, gender, age_int, class_name, student_id) ) conn.commit() conn.close() if cursor.rowcount 0: flash(该学生不存在更新失败) else: flash(学生信息更新成功) return redirect(url_for(index))cursor.rowcount是 Python DB-API 提供的标准属性表示受影响的行数。用它来判断更新是否命中是一条被低估的经验。删除路由同理但我要强调一个交互细节删除操作必须用 POST 请求不能用 GET。原因是 GET 请求会被浏览器预取、被搜索引擎爬虫抓取如果把删除写在 GET 路由里爬虫可能把整个数据库删空。这是我见过最严重的教学事故之一真的有人这么干过。app.route(/delete/student_id, methods[POST]) def delete_student(student_id): conn get_db() cursor conn.execute(DELETE FROM students WHERE student_id ?, (student_id,)) conn.commit() conn.close() if cursor.rowcount 0: flash(该学生不存在可能已被删除) else: flash(学生信息删除成功) return redirect(url_for(index))4. 前端页面表格展示、表单校验与分页导航后端接口写好后前端决定这个系统“好不好用”。学生信息管理系统的前端不需要花哨但要满足三个基本要求表格信息密且可读、表单校验即时反馈、分页导航必须带上搜索条件。我用 Flask 自带的 Jinja2 模板引擎来实现。4.1 列表页模板表格加操作按钮列表页是用户进入系统的第一屏。表格列的顺序我按“学号、姓名、性别、年龄、班级”来排操作列固定在右侧。这里有一个经验操作列的按钮不要用文字链接要用带颜色的按钮区分编辑和删除降低误操作概率。!-- templates/index.html -- !DOCTYPE html html langzh-CN head meta charsetUTF-8 title学生信息管理系统/title style table { border-collapse: collapse; width: 100%; margin: 20px 0; } th, td { border: 1px solid #ddd; padding: 8px; text-align: left; } .btn { padding: 4px 8px; text-decoration: none; border-radius: 3px; } .btn-edit { background: #4CAF50; color: white; } .btn-delete { background: #f44336; color: white; } /style /head body h1学生信息管理系统/h1 form methodget action/ input typetext namekeyword value{{ keyword }} placeholder搜索学号/姓名/班级 button typesubmit搜索/button a href/add新增学生/a /form p共 {{ total }} 名学生/p table tr th学号/thth姓名/thth性别/thth年龄/thth班级/thth操作/th /tr {% for s in students %} tr td{{ s[student_id] }}/td td{{ s[name] }}/td td{{ s[gender] }}/td td{{ s[age] }}/td td{{ s[class_name] }}/td td a classbtn btn-edit href/edit/{{ s[student_id] }}编辑/a form methodpost action/delete/{{ s[student_id] }} styledisplay:inline; onsubmitreturn confirm(确认删除学生 {{ s[name] }} 吗); button classbtn btn-delete typesubmit删除/button /form /td /tr {% else %} trtd colspan6暂无学生数据/td/tr {% endfor %} /table /body /html注意删除表单里的onsubmitreturn confirm(...)这是删除前的二次确认能挡掉大多数手滑操作。{% else %}是 Jinja2 的循环无数据分支用来提示空表状态。模板里的{{ s[student_id] }}依赖后端row_factory sqlite3.Row的设置如果后端没加那一行这里会报错或者显示空值。4.2 表单页新增与编辑共用一套模板新增和编辑的表单长得很像理论上不应该写两个 HTML。Jinja2 里可以通过判断student变量是否被传入来决定表单的提交地址和初始值。!-- templates/form.html -- !DOCTYPE html html langzh-CN head meta charsetUTF-8 title{{ 编辑学生 if student else 新增学生 }}/title /head body h1{{ 编辑学生 if student else 新增学生 }}/h1 form methodpost action{{ /edit/ student[student_id] if student else /add }} p label学号/label input typetext namestudent_id value{{ student[student_id] if student else }} {% if student %}readonly{% endif %} required {% if student %}small学号不可修改/small{% endif %} /p p label姓名/label input typetext namename value{{ student[name] if student else }} required /p p label性别/label select namegender required option value请选择/option option value男 {% if student and student[gender] 男 %}selected{% endif %}男/option option value女 {% if student and student[gender] 女 %}selected{% endif %}女/option /select /p p label年龄/label input typenumber nameage min1 max120 value{{ student[age] if student else }} required /p p label班级/label input typetext nameclass_name value{{ student[class_name] if student else }} placeholder例如计算机2301班 required /p button typesubmit保存/button a href/返回列表/a /form /body /html编辑时学号用readonly属性锁住这是个容易被忽略的业务规则学号是学生的唯一标识修改学号等于删掉旧记录再插入一条新记录会破坏数据的连续性。更麻烦的是如果别的表以外键引用了这个学号比如成绩表改学号会导致关联数据断裂。min1 max120是 HTML5 原生数字校验用户在浏览器就会被拦截但后端也要保留同样的校验——因为请求可以被伪造绕过前端。4.3 搜索与分页的链接拼接这地方最容易写出 Bug分页导航看起来简单但有一个高频 Bug在搜索结果里翻页时分页链接必须带上当前的 keyword 参数否则翻到第二页就丢失了搜索条件展示出全部数据。这个坑我见过无数次。p {% if page 1 %} a href/?page{{ page - 1 }}{% if keyword %}keyword{{ keyword }}{% endif %}上一页/a {% endif %} 第 {{ page }} / {{ total_pages }} 页 {% if page total_pages %} a href/?page{{ page 1 }}{% if keyword %}keyword{{ keyword }}{% endif %}下一页/a {% endif %} /p在模板里拼接 URL 参数时注意?和的使用第一个参数前用?后面的参数用。如果当前没有搜索词链接里就不能出现keyword这种空参数。这里的技巧是用 Jinja2 的{% if keyword %}条件判断有才拼没有就不拼。如果你觉得模板里写这个太啰嗦可以改用 Flask 的url_for加request.args构造完整查询串但那是后话。至少在这个阶段模板条件拼接对新手来说最直白、最好排查。5. 学生信息管理系统常见问题与避坑5 条血泪经验这个系统虽然简单但坑一点都不少。下面几条是我在开发和帮别人调试时遇到的高频问题每一条都按“现象 → 原因 → 解决”写清楚建议直接收藏。5.1 学号前导零凭空消失现象通过表单添加学号为20230001的学生保存后在列表页看到的是20230001但用 SQLite 命令行查询发现存进去的值变成了20230001的整数形式或者某些前端组件显示时自动丢掉前导零。原因学号字段类型用了 INTEGERSQLite 在存储时会去除前导零另一种情况是模板里用了某类型转换函数把字符串转成了数字。我在 2.1 节强调过学号是标识符不是数字这个教训来自一个真实项目导入数据时 2000 多条学号全部被转成整数后来不得不用正则批量补回前导零。解决建表时学号字段固定用 TEXT后端从表单取值后不要做int()转换前端 input 的 type 设为text而不是number。如果数据已经被污染用 SQL 更新加printf(%08d, student_id)补零但前提是原学号长度统一且未丢失高位信息。5.2 年龄字段存了静态值第二年全乱现象系统上线用了一年第二年开学发现所有学生的年龄都没有变。新生录入时参考老学生的年龄结果把新生的年龄也填成去年的值数据整体失真。原因设计表结构时直接存了静态年龄没有考虑时间维度。年龄是随日期变化的派生数据不应该作为永久存储字段。我在 2.4 节已经讲过这个方案争议这里再补充一个折中做法如果你因为题目要求必须显示年龄字段可以在表里加birth_date查询时动态计算SELECT student_id, name, gender, class_name, CAST(strftime(%Y, now) AS INTEGER) - CAST(strftime(%Y, birth_date) AS INTEGER) AS age FROM students;解决表结构改成存birth_date列表页展示动态计算的 age表单录入时用日期选择器。如果嫌改造量大至少要在代码注释和设计文档里标注“age 是入学年龄每年需手动维护”避免后人接手时踩坑。5.3 用 f-string 拼 SQL被注入后哭都来不及现象程序运行正常但某天数据库里的数据突然全部消失或者搜索框里输入 OR 11能把所有学生数据带出来。原因开发时图省事用了类似fSELECT * FROM students WHERE name {keyword}的写法。SQL 注入的本质是把用户输入当作 SQL 代码执行。学生信息管理系统看似低风险但只要有输入框就有注入面。解决所有 SQL 语句一律用参数化查询也就是?占位符加元组传参如 3.2 节代码所示。不要相信任何用户输入包括下拉框——虽然gender来自select但攻击者可以直接 POST 一个gender恶意值。参数化查询是底线不是可选项。5.4 导出 CSV 用 Excel 打开全是乱码现象点击导出按钮生成 CSV 文件用记事本打开一切正常用 Excel 打开后中文字段全部变成䏿之类的乱码。原因CSV 文件本身是 UTF-8 编码而 Windows 版 Excel 默认按 ANSIGBK解析 CSV导致编码不匹配。这个问题不在数据库而在导出时的编码声明缺失。解决导出 CSV 时在文件开头写入 UTF-8 BOM字节序标记也就是三个字节\xef\xbb\xbf。Excel 看到 BOM 就会按 UTF-8 解析。在 Python 里可以这样处理output.write(\ufeff) # BOM 字符注意这个 BOM 不是全角空格而是 Unicode 的零宽不换行空格不会显示在表格里只影响解析方式。我在第 6 章会给出完整的导出代码。5.5 直接删除没有二次确认手滑就丢数据现象用户想编辑某条记录结果点到了旁边的删除按钮一条学生数据被误删。系统没有任何提示删完就找不回来了。原因一是没有前端 confirm 弹窗二是删除操作用了 GET 请求浏览器地址栏直接访问也触发删除。我在 3.4 节强调过 GET 删数据的危险性这里再补充一点SQLite 默认没有开启外键约束即使你建了关联表误删也不会被拦截。解决删除必须用 POST 表单加onsubmitreturn confirm(...)代码在 4.1 节已经给出。如果数据重要建议在应用层写一个软删除机制也就是给表加一个deleted字段删除时执行UPDATE students SET deleted 1 WHERE student_id ?查询时默认过滤deleted 0。这样误删后还可以做后悔药恢复成本极低。6. 从能跑到好用批量导入导出、接口验证与部署前检查CRUD 跑通只是及格线。真正让一个学生信息管理系统“可用”的是批量导入导出、接口冒烟测试和部署前检查这三个环节。这一章我把它们一次补齐。6.1 批量导入用 CSV 文件把几百条记录一次塞进去手工一条条录入几百个学生是不现实的。常见的做法是支持 CSV 导入让用户从 Excel 里整理好数据后一键上传。核心逻辑是读取文件内容逐行插入但要做两个处理跳过表头、捕获重复学号。app.route(/import, methods[POST]) def import_csv(): file request.files.get(csv_file) if not file or not file.filename.endswith(.csv): flash(请上传 CSV 文件) return redirect(url_for(index)) content file.read().decode(utf-8) import csv from io import StringIO reader csv.reader(StringIO(content)) conn get_db() success 0 duplicate 0 for row_number, row in enumerate(reader): if row_number 0 and row[0] 学号: continue # 跳过表头 if len(row) 5: continue # 跳过格式不完整的行 student_id, name, gender, age, class_name row[:5] try: age_int int(age) except ValueError: continue try: conn.execute( INSERT INTO students (student_id, name, gender, age, class_name) VALUES (?, ?, ?, ?, ?), (student_id.strip(), name.strip(), gender.strip(), age_int, class_name.strip()) ) success 1 except sqlite3.IntegrityError: duplicate 1 conn.commit() conn.close() flash(f导入完成成功 {success} 条重复 {duplicate} 条跳过格式错误若干) return redirect(url_for(index))注意file.read().decode(utf-8)这一步CSV 文件如果是 Excel 导出的 GBK 编码这里会报UnicodeDecodeError。建议在导入页面提示用户“请将文件另存为 UTF-8 编码”或者在代码里尝试utf-8失败后回退到gbk。导入过程中逐条捕获IntegrityError而不是一次性执行完再查这样单条重复不会导致整个事务回滚。6.2 导出带 BOM 的 CSV修复 Excel 乱码的最后一公里导入解决了录入效率导出解决数据分发。这里给出完整导出代码注意对比 5.4 节提到的 BOM 问题app.route(/export) def export_csv(): conn get_db() rows conn.execute(SELECT student_id, name, gender, age, class_name FROM students ORDER BY student_id).fetchall() conn.close() import csv from io import StringIO output StringIO() writer csv.writer(output) writer.writerow([学号, 姓名, 性别, 年龄, 班级]) for row in rows: writer.writerow([row[student_id], row[name], row[gender], row[age], row[class_name]]) response app.response_class( \ufeff output.getvalue(), mimetypetext/csv, headers{Content-Disposition: attachment; filenamestudents_export.csv} ) return response\ufeff output.getvalue()是关键的修复代码。\ufeff是 Unicode BOM 的 Python 字符串写法写入文件后 Excel 能自动识别出 UTF-8 编码。如果你用记事本打开导出的 CSV 看到最前面有个看不到的字符那就是 BOM 的正常表现不要去掉它。另外注意文件名students_export.csv里不要带空格和中文否则某些浏览器下载时会出现文件名乱码。6.3 用 curl 验证接口不用开浏览器也能做冒烟测试开发完接口后我习惯用 curl 做一轮快速验证确认路由、参数、响应状态都正常。这比在浏览器里手动点更快也更容易复现问题。以下是我每次必跑的几条命令# 启动服务 python app.py # 另开终端验证新增学生 curl -X POST -d student_id20230001name张三gender男age19class_name计算机2301班 http://127.0.0.1:5000/add # 验证搜索 curl http://127.0.0.1:5000/?keyword张三 # 验证编辑 curl -X POST -d name张三丰gender男age20class_name计算机2302班 http://127.0.0.1:5000/edit/20230001 # 验证删除 curl -X POST http://127.0.0.1:5000/delete/20230001-d参数表示 POST 表单数据-X POST指定请求方法。注意编辑和删除的接口如果是/edit/student_id这种路径必须保证 URL 中的学号和表单里的学号一致。我一般会在测试时故意传一个不存在的学号确认接口会不会返回友好提示而不是抛 500 错误。这一步能帮你提前发现rowcount 0分支的 Bug。6.4 部署前必须检查的三件事学生信息管理系统做完之后如果要部署到服务器上有三个检查项是我吃了亏之后总结出来的第一关闭调试模式。Flask 的app.run(debugTrue)在开发时很方便但生产环境开着调试模式等于把服务器后门打开攻击者可以通过 Werkzeug 的调试器在服务器上执行任意代码。部署时要改成app.run(host0.0.0.0, port8000, debugFalse)或者用 Gunicorn 等 WSGI 服务器托管。第二设置正式密钥不要用代码里的明文secret_key。我的做法是把密钥放到环境变量export SECRET_KEY$(python -c import secrets; print(secrets.token_hex(32)))然后在代码里读取app.secret_key os.environ.get(SECRET_KEY, fallback-dev-key)。环境变量方式的好处是密钥不进代码库即使源码泄露也不会被直接利用。第三数据备份。SQLite 备份就是复制文件你可以加一个简单的时间戳备份命令sqlite3 students.db .backup backups/students_$(date %Y%m%d_%H%M%S).db加到 crontab 里每天凌晨执行一次。这个系统结构简单但学生数据如果丢了补录的人力成本远超服务器成本。最后分享一个我的工作习惯每次改完数据库结构我都会先备份旧库再跑schema.sql初始化新表。遇到database is locked就检查是不是有别的进程没关连接。学生信息管理系统是我反复练过很多次的项目每一次都发现新问题但也正因为它的边界足够清晰才适合用来建立对数据模型、参数校验和部署流程的完整感觉。希望这些经验能帮你少走一段弯路做出一个能真正交付的系统。希望帮到你。本文还有配套的精品资源点击获取
返回列表