简介:基于Python的运动会管理系统源码,是面向软件工程课程设计的完整项目,适合课设答辩、技术学习或快速搭建赛事管理平台,核心解决选手报名、赛程编排、成绩记录与报表生成的自动化问题。压缩包共326个文件,包含162个Python源文件、90个编译后字节码文件,以及20个Vue文件、27个JavaScript文件和HTML、CSS等前端资源,整体仅855KB,以Python为后端主体,融合Vue等前端技术实现前后端协同;内部webapi目录展示了接口组织方式,配合多套环境配置、dockerfile部署文件与readme说明,工程结构完整。项目已有344人学习下载,通过梳理源码可掌握从数据库交互到前端页面的完整调用链路,理解运动员管理、比赛项目编排、成绩录入查询等模块的具体实现,还能借鉴pyc编译优化、.gitignore版本控制等开发细节,为课设或同类赛事系统开发提供可复用、可扩展的参考。
1. 一份运动会管理系统源码,为什么到你手上就跑不起来
很多人拿到的运动会管理系统源码,解压出来双击运行,第一眼看到的多半不是界面,而是一段 Traceback。原因往往不是代码写得差,而是这类源码把界面、业务逻辑、数据存储揉在同一个文件里,数据模型设计得将就,排名逻辑只覆盖了“没有并列名次”的理想情况。这个标题真正要解决的事情有两件:把运动会管理系统的数据模型和核心业务规则讲透,让你能读懂别人的源码、能改得动它;再给出一条从零跑通、能扩展的落地路径。这篇文章适合正在做 Python 课程设计或毕业设计的学生,也适合想快速搭一套内部赛事管理工具的老师或社团负责人。全文不依赖任何现成框架,只靠 Python 标准库就能把系统撑起来。
2. 运动会的数据库怎么建:四张表撑起数据模型,直接给建表 SQL
2.1 先想清楚三件事:谁参赛、比什么、成绩怎么存
运动会管理系统不是一个“存储成绩的表格”,它背后是一个典型的多对多数据模型。一个运动员属于某个班级,可以报多个项目;一个项目里有多个运动员参加。如果只建一张“成绩表”把运动员、项目、成绩全塞进去,很快会发现两个问题:一个运动员报了三项,就要手动维护三行冗余数据;某个项目有预赛和决赛两轮,成绩字段根本没法表达轮次。所以这个系统的地基,是四张表而不是一张表。
我一般这样拆:班级表存班级信息,运动员表存个人信息并外键关联班级,项目表存赛事项目定义,成绩表存运动员在某项目上的成绩记录。成绩表本质上是运动员表和项目表的关联表,只是额外带上了成绩和轮次字段。这样设计之后,“运动员张三报了100米和跳远”这个关系天然地表达为成绩表里的两行记录,而不是在运动员表里不断加列。
建表语句是整套系统的地基,字段类型和约束直接决定后面业务逻辑怎么写。下面是建议的建表 SQL,SQLite 语法,MySQL 改一下类型关键字即可。
CREATE TABLE classes ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL ); CREATE TABLE athletes ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_no TEXT UNIQUE NOT NULL, name TEXT NOT NULL, gender TEXT CHECK(gender IN ('M', 'F')), class_id INTEGER NOT NULL REFERENCES classes(id) ); CREATE TABLE events ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, category TEXT CHECK(category IN ('track', 'field')), gender TEXT CHECK(gender IN ('M', 'F', 'mixed')) ); CREATE TABLE results ( id INTEGER PRIMARY KEY AUTOINCREMENT, athlete_id INTEGER NOT NULL REFERENCES athletes(id), event_id INTEGER NOT NULL REFERENCES events(id), round_no INTEGER DEFAULT 1, score REAL NOT NULL, score_text TEXT, UNIQUE(athlete_id, event_id, round_no) );这段建表 SQL 里有三个设计决策值得记住。第一,score用 REAL 类型存数值,score_text存原始录入文本,后面的排名算法只认数值,原始文本留着做校验和追溯。第二,results表加了UNIQUE(athlete_id, event_id, round_no)约束,同一运动员在同一个项目的同一轮次里只能有一条成绩记录,这能从数据库层面挡住重复录入。第三,category字段区分径赛和田赛,因为这两类项目的排名方向完全相反。以后要加教职工组、趣味项目,只需要在events表里扩展类型,不用动表结构。
2.2 为什么选 SQLite 而不是 MySQL:零配置、单文件、内建驱动
课程设计阶段最常见的选型纠结是用 MySQL 还是 SQLite。我会直接推荐 SQLite,理由是它够用且省事。Python 标准库自带sqlite3模块,不需要额外安装驱动,也不需要启动数据库服务;整个数据库就是一个文件,答辩演示时拷走这个文件就带走了全部数据。更关键的是,SQLite 支持完整的事务、外键约束和标准 SQL,业务逻辑写法和 MySQL 没有本质区别,后期如果要迁移到 MySQL,改连接方式、把自增关键字换一下就行。
真正需要注意的是连接参数的设置。sqlite3默认返回的是元组,不设置row_factory的话,后续代码里写row["name"]这种字典式访问会直接报错。还有一个隐藏问题是外键约束默认关闭,不手动打开的话,REFERENCES形同虚设,删了运动员,成绩表里会留下孤儿数据。
import sqlite3 from pathlib import Path DB_PATH = Path(__file__).parent / "sports_meet.db" def get_conn(db_path=DB_PATH): conn = sqlite3.connect(str(db_path), timeout=10) conn.row_factory = sqlite3.Row conn.execute("PRAGMA foreign_keys = ON") return conn def init_db(): with get_conn() as conn: conn.executescript(""" CREATE TABLE IF NOT EXISTS classes ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL ); CREATE TABLE IF NOT EXISTS athletes ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_no TEXT UNIQUE NOT NULL, name TEXT NOT NULL, gender TEXT CHECK(gender IN ('M', 'F')), class_id INTEGER NOT NULL REFERENCES classes(id) ); CREATE TABLE IF NOT EXISTS events ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, category TEXT CHECK(category IN ('track', 'field')), gender TEXT CHECK(gender IN ('M', 'F', 'mixed')) ); CREATE TABLE IF NOT EXISTS results ( id INTEGER PRIMARY KEY AUTOINCREMENT, athlete_id INTEGER NOT NULL REFERENCES athletes(id), event_id INTEGER NOT NULL REFERENCES events(id), round_no INTEGER DEFAULT 1, score REAL NOT NULL, score_text TEXT, UNIQUE(athlete_id, event_id, round_no) ); """)timeout=10这个参数在 SQLite 里很关键,它表示当数据库文件被其他连接锁住时,等待 10 秒而不是立刻抛database is locked错误。PRAGMA foreign_keys = ON写在每次连接上,因为 SQLite 的外键约束是连接级别的,只开一次不够。executescript一次执行多条语句,适合初始化建表;注意它内部会先提交一次事务,所以不要在循环里反复调用它建表,初始化只跑一次就够了。
2.3 预置种子数据:没有数据,管理系统就是空壳
建好表之后要立即塞入种子数据,否则后面写查询、测排名都会因为没有数据而抓瞎。种子数据至少要覆盖一个班级、两名以上运动员、两个不同类别的项目。这样后面的成绩录入和排名逻辑能直接在真实数据结构上调试。下面的代码演示了批量插入的推荐写法,使用executemany避免在循环里逐条执行 SQL。
def seed_data(): with get_conn() as conn: conn.executemany( "INSERT OR IGNORE INTO classes (name) VALUES (?)", [("计算机2401",), ("软件2402",)] ) conn.executemany( "INSERT OR IGNORE INTO athletes (student_no, name, gender, class_id) VALUES (?, ?, ?, ?)", [ ("2024001", "张明", "M", 1), ("2024002", "李婷", "F", 1), ("2024003", "王浩", "M", 2), ] ) conn.executemany( "INSERT OR IGNORE INTO events (name, category, gender) VALUES (?, ?, ?)", [ ("100米", "track", "M"), ("跳远", "field", "M"), ] )INSERT OR IGNORE是幂等插入的关键。脚本重复执行时不会因为唯一约束冲突而报错,这在开发阶段反复调试很实用。注意class_id直接写死成了 1 和 2,依赖的是classes表自增主键的生成顺序,这种做法在种子数据里可以接受,但在正式业务代码里,插入操作必须通过查询拿到真实的外键 ID,而不是硬编码。这也是新手最容易埋雷的地方。
3. 成绩录入与排名算法:并列名次处理才是真正的分水岭
3.1 成绩不是随便存字符串:单位归一化是纪律问题
运动会现场录入成绩的场景远比想象的乱。裁判报“11秒5”,录入的人可能打成11.5、11秒5、11''5、11.5s,中长跑项目还有人习惯用2:15.3表示 2 分 15 秒 3。如果这些值都以文本形式存进数据库,排名时按字符串排序,9.9会排在10.5前面,因为字符串比较是一位一位比过去的。这个问题在真实比赛里出现一次就足以让整套系统失去信任。
解决办法是录入时统一换算成秒,用浮点数存score字段,原始文本存进score_text。换算函数是系统的第一个纪律关口,所有录入入口都必须走这个函数,不能在界面上各写各的解析逻辑。下面这段函数覆盖了运动会最常见的三种手写格式。
def normalize_score(raw: str, category: str = "track") -> float | None: """将手写成绩归一化为秒。支持 '12.5' '12.5s' "12''5" '2:15.3'。""" if raw is None: return None text = raw.strip().lower().replace("秒", "") if not text: return None try: if ":" in text: minutes, seconds = text.split(":", 1) return int(minutes) * 60 + float(seconds) if "'" in text: parts = text.replace('"', "'").split("'") if len(parts) == 2: minutes, seconds = parts return int(minutes) * 60 + float(seconds) return float(parts[0]) return float(text.replace("s", "")) except (ValueError, AttributeError): return None这个函数的处理顺序有讲究。先剥离中文单位“秒”,再处理带冒号的格式,然后处理带分秒符号的格式,最后才是去掉s后缀的纯数字。为什么最后处理s后缀?因为12s5这种非标准格式会被float()直接拒绝,这样反而能让非法输入在入口处暴露出来。category参数目前只是预留,田赛类项目比如跳远,录入的是米数,不需要换算,但保留这个参数让调用处语义更明确。判断一个成绩是否合法,要看normalize_score返回的是不是None,不能拿原始字符串去判断。
3.2 径赛取最小时间,田赛取最大成绩:两种相反的排序方向
排名规则在运动会系统里是分水岭。径赛项目例如 100 米,成绩越短越好,排序时按score升序,排第一的是最小值;田赛项目例如跳远、铅球,成绩越大越好,排序时按score降序,排第一的是最大值。这个方向如果不分清楚,把径赛的逻辑直接套到田赛上,跳远的冠军会变成跳得最近的人。
一种常见的错误是把reverse=True这个参数写死在排序代码里。正确做法是从项目表里查出category,再决定排序方向。下面是带完整流程的成绩排名函数,返回结果里直接带上名次,前端展示时不用再做二次计算。
def get_ranked_results(conn, event_id): event = conn.execute( "SELECT category FROM events WHERE id = ?", (event_id,) ).fetchone() if event is None: return [] rows = conn.execute(""" SELECT a.student_no, a.name, c.name AS class_name, r.score, r.score_text FROM results r JOIN athletes a ON a.id = r.athlete_id JOIN classes c ON c.id = a.class_id WHERE r.event_id = ? """, (event_id,)).fetchall() records = [dict(row) for row in rows] reverse = (event["category"] == "field") records.sort(key=lambda r: r["score"], reverse=reverse) return attach_ranks(records)注意reverse的取值逻辑:当项目是田赛时取True,降序排列;径赛时取False,升序排列。查询语句里用了两次JOIN,把运动员的班级信息也带出来了,因为排行榜展示时永远要显示“哪个班、谁、多少成绩”,单独查运动员表会造成 N+1 次查询的问题。dict(row)将sqlite3.Row转成普通字典,方便后续在attach_ranks里直接修改。
3.3 并列名次:第一名有两个,下一个名次必须是第三名
排名里最容易被新手写错的是并列名次。一场 100 米比赛两个选手同时跑出 12.5 秒,按运动会惯例两人都是第一名,下一个名次是第三名,而不是第二名。直接enumerate排序后的列表逐个标名次,会出现同一个成绩拿到两个不同名次的低级错误。处理并列名次的正确思路是:先排序,再按成绩分组,每组内部共享同一个名次,下一组的名次等于“当前组起点索引 + 组内人数”。
def attach_ranks(records): """按排序后的列表附加名次,同分同名次,下一名次按人数跳过。""" n = len(records) ranks = [0] * n i = 0 rank = 1 while i < n: j = i while j < n and records[j]["score"] == records[i]["score"]: j += 1 for k in range(i, j): ranks[k] = rank rank = j + 1 i = j for idx, r in enumerate(records): r["rank"] = ranks[idx] return records这个算法里最耐看的是rank = j + 1这一行。j是并列组末尾的下标,比如第 0、1 名并列,循环结束时j = 2,那么下一组的名次就是 3。很多人会写成rank += 1,结果变成第二名,这是血泪教训。排序之前不要先把名次填进去,因为排序前的顺序没有意义,一定要等排序完成后在最终顺序上做分组。这个函数独立出来还有个好处:将来要支持“不并列”的规则线,把分组逻辑替换成rank = i + 1即可,查询层和展示层都不受影响。
4. 把业务跑成闭环:从控制台菜单到图形界面的迁移路径
4.1 先做控制台闭环,再做界面:最小可用的菜单主循环
很多人的开发顺序搞反了,第一件事就去拖界面控件,结果写了三百行界面代码,业务逻辑还没跑通。我的习惯是先用控制台把整个系统的业务闭环打通,确认数据能进能出、排名正确,再考虑套界面。控制台版本的好处是没有任何界面框架的干扰,逻辑出错时一眼就能看出是哪一层的问题。
def main_loop(): conn = get_conn() while True: print("\n运动会管理系统") print("1. 录入成绩") print("2. 查看项目排名") print("3. 查看班级总分") print("0. 退出") choice = input("请选择: ").strip() if choice == "1": entry_score(conn) elif choice == "2": show_ranking(conn) elif choice == "3": show_class_score(conn) elif choice == "0": break else: print("无效选项,请重新输入")这段主循环把整个系统的骨架立起来了,后续每个功能都是往entry_score、show_ranking这样的函数里填实现。choice变量用字符串接收输入,再用if/elif分流,而不是先转成int,因为用户可能直接回车或者输入字母,int()会在这些情况抛ValueError导致程序崩溃。每个操作做完之后循环回到菜单,这个“回到菜单”的体验决定了系统像不像一个“系统”,而不是一个脚本。
4.2 控制台版本跑通之后:Tkinter 还是 Flask
控制台闭环跑通之后,下一个问题是用什么界面。课设场景里最常见的两个选择是 Tkinter 和 Flask。Tkinter 是 Python 标准库自带的 GUI 框架,生成的桌面程序双击即用,适合评委席单机录入;Flask 是 Web 框架,做成网页后可以在局域网内用浏览器访问,适合多人同时录入、大屏实时展示排名。这两个方向的决定不应该在看心情,而是看使用场景。
| 方案 | 优点 | 代价 | 适合场景 |
|---|---|---|---|
| 控制台 | 开发最快,零依赖 | 演示效果差 | 业务验证、自用 |
| Tkinter | 桌面程序,免部署 | 界面布局代码量大 | 单机单评委录入 |
| Flask | 多终端访问,可上大屏 | 需要理解 HTTP 与模板 | 多人同时录入、展示 |
如果你只需要在运动会当天在一台电脑上录入成绩,Tkinter 足够;如果希望老师在办公室用电脑查、裁判在操场用手机录,那得走 Flask。我个人建议课设优先走 Flask,因为 Web 形式的系统在答辩演示时不挑机器,任何带浏览器的设备都能展示,而且业务逻辑层完全复用前面写的函数,只需要加一层路由和模板渲染。
4.3 用 Flask 包成网页版:业务层保持独立是底线
从控制台迁移到 Flask,最常见翻车姿势是:把数据库查询、排名计算、界面渲染全部塞进一个路由函数里。这样做的后果是代码没法复用,以后加一个“按班级筛选排名”的功能,得复制一整套逻辑。正确做法是 Flask 只负责接收请求和返回响应,业务逻辑全部调用已经写好的函数。下面是最小可行的 Flask 接入方式。
from flask import Flask, request, render_template app = Flask(__name__) @app.route("/event/<int:event_id>") def event_ranking(event_id): conn = get_conn() records = get_ranked_results(conn, event_id) conn.close() return render_template("ranking.html", records=records) @app.route("/score/entry", methods=["POST"]) def score_entry(): athlete_id = request.form["athlete_id"] event_id = request.form["event_id"] raw_score = request.form["score"] score = normalize_score(raw_score) if score is None: return "成绩格式不合法,请重新输入", 400 with get_conn() as conn: conn.execute( """INSERT OR REPLACE INTO results (athlete_id, event_id, round_no, score, score_text) VALUES (?, ?, 1, ?, ?)""", (athlete_id, event_id, score, raw_score), ) return "成绩已保存"路由函数里没有一行排名逻辑,排名逻辑仍然在get_ranked_results里,这是刻意的。INSERT OR REPLACE与唯一约束配合,同一运动员同一项目同一轮次的成绩会被覆盖更新,省去了先查后改的两步操作。注意raw_score原样从表单取出后直接交给normalize_score校验,校验失败时返回 400 状态码和可读的错误信息,而不是让数据库报异常。with get_conn() as conn在这里负责事务提交,函数正常结束时自动 commit,异常时自动回滚。还有一点容易被忽略:连接用完要 close,Web 应用里每个请求都新建连接的话,不关闭会很快耗尽文件描述符。
5. 避坑指南:运动会管理系统最常见的五个翻车现场
5.1 中文乱码:Windows 控制台的编码战争
现象:控制台版本在 Windows 上运行,界面里的中文全部变成锟斤拷或方块字,但同一个脚本在 macOS 上完全正常。原因:Windows 控制台默认使用 GBK 编码,而 Python 3 源文件默认 UTF-8,两者直接碰撞。解决:不要试图在代码里到处加# -*- coding: utf-8 -*-,那是 Python 2 时代的语法,对这个场景没有任何帮助。最省事的方案是给控制台重新设置编码,在程序入口加上一句:
import sys if sys.platform == "win32": sys.stdout.reconfigure(encoding="utf-8", errors="replace")errors="replace"是兜底策略,极个别字符无法转换时用?代替,保证程序不会因为编码问题崩溃。这个坑的隐蔽之处在于:不在sys.platform判断里包一层的话,macOS 和 Linux 上reconfigure的编码参数行为会引入新问题。
5.2 排名结果与真实排名不符:问题多半出在并列与田赛
现象:100 米比赛两个人成绩相同,系统给出的名次是 1、2、3,而不是 1、1、3。原因:排名函数用了enumerate逐个标名次,完全没过并列检测。解决:换成本文第 3 章的attach_ranks实现。这个 bug 的隐蔽点是排序结果看着没问题,因为成绩确实是从小到大排的,只要不看名次列,很容易漏掉。验证方法是在测试数据里刻意造两条相同成绩,比如都填 12.5,跑完排名逻辑后检查名次序列是否等于[1, 1, 3]。
5.3 手写成绩格式不统一:数据库里全是 12秒5 和 10.5s
现象:录入时有人填12秒5,有人填12.5s,排名结果错乱。原因:成绩直接以字符串存库,排序按字典序而不是数值序。解决:所有录入入口强制走normalize_score,入库的score字段一定是浮点数,原始文本只存放在score_text里供以后核验。这道防线必须在所有入口统一,包括控制台录入、Web 表单、批量导入,少一个入口就会脏数据。排查技巧:查询里检查score_text LIKE '%秒%'或者score_text LIKE '%s%',能快速找出绕过归一化函数的录入记录。
5.4 多窗口同时录入触发 database is locked
现象:Flask 网页版启动后,两个浏览器窗口同时提交成绩,其中一个窗口报database is locked。原因:SQLite 对同一时刻的写操作是串行的,两个连接同时要求写锁,其中一个会被拒绝。解决:连接时设置timeout=10,让请求等待锁释放而不是立刻报错。更进一步,写操作应该集中在一个短事务里,不要在事务里做查询、排名计算这些事情占着锁不放。如果这个问题在部署后频繁出现,说明 SQLite 已经不适合这个并发量,迁移到 MySQL 或 PostgreSQL 是正道。
5.5 删除运动员后成绩表里出现孤儿数据
现象:从运动员表删掉一个人,但排行榜里他依然在列,点进去发现关联信息全是空的。原因:SQLite 的外键约束默认关闭,删除时没有级联清理成绩表。解决:每次连接都会执行PRAGMA foreign_keys = ON,并且在建表语句中明确外键关系。另一种思路是删除前先手动清掉该运动员的results记录,再删运动员,两个操作放在同一个事务里:
with get_conn() as conn: conn.execute("DELETE FROM results WHERE athlete_id = ?", (athlete_id,)) conn.execute("DELETE FROM athletes WHERE id = ?", (athlete_id,))两条删除语句在同一个事务里,要么都成功,要么都回滚,不会出现删了运动员留下成绩的中间状态。这个坑的麻烦之处在于:单机开发时数据量小,孤儿数据未必影响功能;一旦进入真实演示阶段,删除一个人之后排行榜出现空行,现场气氛会非常尴尬。
6. 进阶玩法:用批量导入和成绩册导出,把课设做成一款能用的工具
6.1 用 CSV 批量导入成绩:兼容 Excel 的 BOM 头和数据清洗
运动会现场的实际情况是:裁判在 Excel 里记录原始成绩,赛后需要一次性导入系统。逐条手动录入几十个项目几百条成绩效率太低,批量导入是系统从“能跑”变成“好用”的关键一步。最常见的工作流是:裁判按模板填写 Excel,另存为 CSV 文件,然后导入系统。这一步最大的坑是 Excel 在 Windows 上生成的 CSV 自带 UTF-8 BOM 头,直接用open()读取时第一列的表头会出现一个看不见的\ufeff字符,导致DictReader解析出来的键名不符合预期。
def import_results_csv(conn, csv_path, default_event_id): imported = 0 with open(csv_path, encoding="utf-8-sig") as f: reader = csv.DictReader(f) for row in reader: student_no = row.get("学号", "").strip() raw_score = row.get("成绩", "").strip() if not student_no or not raw_score: continue athlete = conn.execute( "SELECT id FROM athletes WHERE student_no = ?", (student_no,), ).fetchone() if athlete is None: continue score = normalize_score(raw_score) if score is None: print(f"跳过非法成绩: {student_no} {raw_score}") continue conn.execute( """INSERT OR REPLACE INTO results (athlete_id, event_id, round_no, score, score_text) VALUES (?, ?, 1, ?, ?)""", (athlete["id"], default_event_id, score, raw_score), ) imported += 1 conn.commit() return importedencoding="utf-8-sig"是处理 BOM 的官方解法,读出来的表头不带\ufeff。逐行处理时先查运动员 ID,查不到就跳过,而不是中断整个导入流程,这样可以保证一条坏数据不影响剩下几百条正常数据。每一条导入都先过normalize_score,返回None的记录直接被过滤并打印日志,方便赛后人工核对。default_event_id参数让一份 CSV 可以对应一个项目,避免 CSV 里每行再单独指定项目带来的格式复杂度。
6.2 一键导出成绩册:名次、班级、姓名排成一张正式表格
导入成绩之后,最实用的功能是把某项目的排名结果导出成成绩册,供打印和公示。这里可以直接复用get_ranked_results,导出逻辑只关心“把内存中的数据变成文件”,不重新计算排名,保证导出和页面显示的结果完全一致。导出文件同样用 CSV 加上utf-8-sig编码,Excel 打开时中文不会乱码。
def export_ranking_csv(conn, event_id, output_path): records = get_ranked_results(conn, event_id) with open(output_path, "w", encoding="utf-8-sig", newline="") as f: writer = csv.writer(f) writer.writerow(["名次", "班级", "姓名", "学号", "成绩"]) for r in records: writer.writerow([ r["rank"], r["class_name"], r["name"], r["student_no"], r["score_text"] or f"{r['score']}", ])这里刻意用score_text作为导出成绩列,因为原始录入的“11.5s”比归一化后的“11.5”更贴近裁判习惯。没有原始文本时再用数值兜底。newline=""是 CSV 写入的固定搭配,不设置的话,Windows 上每行末尾会多一个空行。
做到这一步,这个系统已经不再是“跑得起来但没法用”的课设代码了。批量导入解决录入效率,成绩册导出解决公示需求,这中间的所有数据都走同一条业务逻辑通道,不会出现“导入的成绩排名和手动录入的不一致”这种信任崩塌问题。回看整个过程,我踩得最深的一个坑就是一开始急着写界面,结果改了三天界面,最后发现排名逻辑是错的,整段推翻重来。做管理系统这类项目,正确的顺序没有捷径:先让数据模型和业务逻辑经得起推敲,再用任何界面去包装它。这个顺序想明白了,系统做起来会顺畅很多。希望帮到你。
本文还有配套的精品资源,点击获取