news 2026/9/15 23:12:23

C语言学生成绩管理系统DevC实现:从结构体到文件读写全攻略

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言学生成绩管理系统DevC实现:从结构体到文件读写全攻略

简介:面向C语言初学者的学生成绩管理系统DevC++完整项目,适合K12阶段或高校学生用于课程设计与综合实训。项目以结构体数组存储学生信息,覆盖学号、姓名、性别以及语文、数学、外语三门单科成绩,并按考试平均成绩占六成、同学互评分占一成、品德成绩占一成、任课教师评分占两成的权重自动计算综合测评总分与名次,名次允许并列且不会影响后续排序。系统具备信息录入、平均成绩与总分名次计算、全部信息显示、按学号查询/删除/修改等基础功能,同时扩展了按性别统计人数、从文件读入数据以及将结果保存到文件的能力,支持数据持久化。压缩包内共7个文件,包括main.c源代码、Dev工程文件、界面布局文件、学生数据csv样例、可直接运行的exe程序、Makefile.win编译脚本及编译中间文件,整体仅51KB,结构清晰紧凑。目前已有99人学习,对希望掌握结构体数组、模块化设计、菜单交互和文件操作的读者很有参考价值。

1. 学生成绩管理系统遇到 DevC,先把数据放哪想明白

很多人拿到“C语言 学生成绩管理系统(DevC项目)”这个题目时,第一反应是赶紧写一个菜单界面,再塞一堆 printf 和 scanf。但真正能跑起来的系统,一定是先把数据结构和存储方式定下来:学生信息用结构体还是二维数组?成绩存在内存里还是写进文件?课程门数是固定三门还是支持动态输入?DevC 只是一个编辑器加编译器的组合,它不会替你决定这些设计。这个项目的本质是让学生把结构体、数组、函数、文件读写、排序和指针这些 C 语言重点串成一条线。适合刚学完 C 语言基础、准备做大作业或者想练手的中级学习者,也适合要检查学生是否理解数组越界和内存管理的老师。先把数据模型定清楚,后面写代码才不会反复推翻。

2. 用 DevC 搭出学生成绩管理系统的骨架:结构体、菜单和文件

2.1 为什么 DevC 适合 C 语言学生成绩管理系统这类项目

DevC 是最常被点名用来做学生成绩管理系统的集成开发环境,原因很直接:它内置了 GCC 编译器,安装后不用额外配置环境变量,点一下“编译运行”就能出结果。对新手来说,最怕的是代码明明写对了,却因为 vscode 的 tasks.json 没配置皱眉。DevC 把这些细节藏起来了,让你把注意力放在 C 语言本身。

不过你也要清楚 DevC 的边界:它的调试器功能有限,单步调试时会经常出现“找不到调试程序”的提示。所以我的习惯是:主逻辑用 DevC 写,一旦遇到莫名崩溃,就换一种思路排查——检查数组下标、检查指针是否被赋过值、检查文件是否打开成功。DevC 自带的高亮和补全虽然比 vscode 弱,但应付几百行的学生管理系统足够了。

#include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX_STUDENTS 100 #define COURSE_COUNT 3 typedef struct { char id[20]; char name[20]; int scores[COURSE_COUNT]; float average; int rank; } Student;

上面的结构体是学生成绩管理系统的核心。用typedef struct定义Student类型,之后就能直接Student stu[MAX_STUDENTS]声明数组。注意idname用的是字符数组而不是指针,这是学生成绩系统里最常见的做法:数据量不大,固定长度就够,不用像链表那样每次 malloc。averagefloat是因为平均分可能带小数;rank在排序后回填名次用。

2.2 定义结构体和管理成绩数据的内存布局

如果你只建一个结构体,那只是第一步。学生成绩管理系统里更常见的设计是“学生数组 + 当前学生数”的组合:

Student students[MAX_STUDENTS]; int studentCount = 0;

studentCount保存当前实际录入的学生数量。所有功能函数都依赖它:遍历studentCount个元素,而不是遍历整个数组。这样可以避免对未初始化内存的操作,也避免输出一堆乱码。

这段代码的逻辑说明:MAX_STUDENTS用宏定义是方便以后改容量。如果题目说“学生人数不超过 30”,把宏改成 30 就行,不用满文件找循环上界。另一个常见做法是定义float scores[COURSE_COUNT],但分数用整数更符合成绩管理系统的输入场景,阅卷分数一般没有小数。平均分单独计算存进average,避免每次排序都重新算一遍。

我在 DevC 里调试这类项目时,经常看“监视”窗口里的studentCount。如果录入了 5 个学生后studentCount还是 0,那基本可以断定录入函数没正确更新计数器。提示:不要用sizeof(students)来判断数组长度,它对全局数组返回的是总字节数,容易在传参后变成指针大小。

2.3 写一个能反复使用的主菜单循环

主菜单是成绩管理系统的入口,很多人会写成一个无限循环加 switch。这个循环本身不难,难的是“怎么退出、怎么清理输入缓冲、怎么在菜单循环里体现增删改查的不同路径”。

void showMenu() { system("cls"); printf("======== 学生成绩管理系统 ========\n"); printf("1. 录入学生成绩\n"); printf("2. 显示所有学生\n"); printf("3. 查询学生成绩\n"); printf("4. 修改学生成绩\n"); printf("5. 删除学生记录\n"); printf("6. 按总分排序\n"); printf("7. 统计平均分和及格率\n"); printf("8. 保存到文件\n"); printf("9. 从文件读取\n"); printf("0. 退出系统\n"); printf("==================================\n"); printf("请输入选择:"); }

这个菜单设计的关键是序号分配:排序和统计单独列项,文件读写也单独留入口。考试版的“学生成绩管理系统”常常把保存文件放在每次操作后自动完成,但为了演示c语言文件读写操作代码,我一般手动让用户选择,这样能看到文件被改写的时机。

showMenu()用了system("cls")清屏,DevC 在 Windows 下支持这个命令。如果换到 vscode 的集成终端里,system("cls")同样有效。主循环的典型写法是:

int choice; do { showMenu(); scanf("%d", &choice); getchar(); // 吸收回车 switch (choice) { case 1: inputStudents(); break; case 2: printAllStudents(); break; case 3: queryStudent(); break; // 其余省略 case 0: printf("退出系统\n"); break; default: printf("输入错误\n"); } } while (choice != 0);

getchar()在这里很重要。如果只在scanf("%d")之后不加getchar(),那么用户输入1之后按下的回车会残留在缓冲区,下一次用到scanf("%s")录入姓名时,就会直接跳过输入。这是 C 语言学生成绩管理系统里最容易踩的坑。do...while循环保证菜单至少显示一次,直到用户选择 0 才退出。

中间章必须有表格和代码块,这章已有代码,后面加个表格对比。或者下章加表。下章要出现表格。

3. 在 DevC 里实现成绩录入、查询、修改与删除

3.1 录入与非法地址检测:scanf 与缓冲区清理

录入学生成绩时,你的第一反应可能是直接连续scanf学号、姓名、三科成绩。但 C 语言初学者最容易写出的问题代码是这种:

printf("请输入学号:"); scanf("%s", students[studentCount].id); printf("请输入姓名:"); scanf("%s", students[studentCount].name); for (int i = 0; i < COURSE_COUNT; i++) { printf("请输入第%d门成绩:", i + 1); scanf("%d", &students[studentCount].scores[i]); }

这段代码表面没问题,但它没有做两个重要的检查:一是学号是否重复,二是录入的是否真的是数字。如果用户输入了abc当作成绩,scanf会返回 0,但变量没有被赋值,后续用这个成绩计算平均分时就会出现随机值,甚至产生“非法地址”访问的错觉。

我一般会在成绩管理系统的录入函数里写成这样:

int inputStudents() { if (studentCount >= MAX_STUDENTS) { printf("学生人数已满,不能再录入\n"); return 0; } Student stu; printf("请输入学号:"); scanf("%s", stu.id); for (int i = 0; i < studentCount; i++) { if (strcmp(students[i].id, stu.id) == 0) { printf("学号重复,请重新检查\n"); return 0; } } printf("请输入姓名:"); scanf("%s", stu.name); char temp[10]; for (int i = 0; i < COURSE_COUNT; i++) { printf("请输入第%d门成绩:", i + 1); scanf("%s", temp); while (atoi(temp) == 0 && strcmp(temp, "0") != 0) { printf("成绩输入非法,请重新输入:"); scanf("%s", temp); } stu.scores[i] = atoi(temp); } stu.average = (stu.scores[0] + stu.scores[1] + stu.scores[2]) / 3.0f; students[studentCount] = stu; studentCount++; return 1; }

为什么这里用temp字符串加atoi?因为atoi遇到非数字字符会返回 0,我们可以先读成字符串,再判断它是不是合法的数字,这样比直接scanf("%d")更容易校验。while循环的条件里有一个细节:atoi(temp) == 0temp不是"0"时说明输入不是数字,需要重新输入。如果你输入0atoi返回 0,但strcmp判断它就是数字“0”,所以能正常通过。

这种写法的代价是牺牲了一点效率,但对成绩管理系统这种交互式程序来说,用户体验和健壮性比性能更重要。另一个常见改进是判断成绩范围在 0 到 100 之间,只需在 while 条件里追加atoi(temp) < 0 || atoi(temp) > 100即可。

3.2 按学号或姓名查询的成绩信息输出

查询功能要解决的是:输入一个学号,找到对应学生并打印信息;输入一个姓名,可能有多条记录,所以要把匹配的全部打印出来。

void queryStudent() { char keyword[20]; printf("请输入学号或姓名:"); scanf("%s", keyword); int found = 0; for (int i = 0; i < studentCount; i++) { if (strcmp(students[i].id, keyword) == 0 || strcmp(students[i].name, keyword) == 0) { printf("学号: %s, 姓名: %s, 成绩: %d %d %d, 平均分: %.2f\n", students[i].id, students[i].name, students[i].scores[0], students[i].scores[1], students[i].scores[2], students[i].average); found = 1; } } if (!found) { printf("没有找到这条记录\n"); } }

strcmp返回 0 表示两个字符串相等。查询逻辑是把传入的关键字和idname都做一次比较,这样既能支持按学号查,也支持按姓名查。很多初级版本会把学号查询和姓名查询分成两个函数,但实际 C 语言项目里合并成一个函数并用strcmp一次判断更简洁。

这里要提一个和检索词里“怎么检验非法地址c语言”相关的点:如果你在 DevC 里查询一个不存在的学号,然后继续访问students[i],只要i在 0 到studentCount-1之间,就不会产生非法地址。真正的内存问题往往出现在查询函数里使用了while (students[i].id != NULL)这种写法。id是字符数组,不是指针,永远不会是NULL,循环会一路读到数组末尾之外。记住:遍历边界一定依赖studentCount

3.3 修改和删除时的数组搬移问题

修改学生成绩比查询麻烦,因为要支持重新录入某科成绩并自动更新平均分。删除操作更讲究:删除第 i 个学生后,要把后面的学生依次往前搬移一位,最后把studentCount减一。

void deleteStudent() { char id[20]; printf("请输入要删除的学号:"); scanf("%s", id); int index = -1; for (int i = 0; i < studentCount; i++) { if (strcmp(students[i].id, id) == 0) { index = i; break; } } if (index == -1) { printf("未找到该学号\n"); return; } for (int i = index; i < studentCount - 1; i++) { students[i] = students[i + 1]; } studentCount--; printf("删除成功\n"); }

这个函数展示了 C 语言数组删除的标准姿势:从被删除位置的后面一个元素开始,逐个把元素往前复制,最后清空最后一个元素的位置(其实不清空也行,因为studentCount已经不会再访问它)。这里用的是students[i] = students[i + 1],结构体可以直接赋值,C 语言会完成整块内存的复制,不需要memcpy

有一个细节:删除后是否需要重置最后一个位置的averagerank?不需要。studentCount是访问边界的唯一依据。这也说明为什么在成绩管理系统中,维护好studentCount比清理失效数据更重要。

下表对比了修改和删除的实现差异:

操作核心做法需要更新的数量常见错误
修改找到目标下标,重新赋值 scores,重算 average只更新一个学生忘记重算平均分
删除把后面的所有元素前移一位需要移动后面全部元素删除后 studentCount 不减
插入(如果要求)从尾部倒序后移,腾出空位需要移动后一半元素从前往后搬会覆盖数据

修改功能里还有个数组越界的风险:如果修改后的分数超出 0-100,应该拒绝写入。如果你用atoi的方式录入,那么修改时也应该复用同样的校验逻辑。现实大作业里,很多人把录入和修改写成重复代码,这是导致项目冗长的主要原因。把“输入单科成绩并校验”提成一个独立函数,例如int inputScore(int courseIndex),录入和修改共用,代码会清爽很多。

4. 排序、统计和文件读写:让系统可保存可排行

4.1 用冒泡排序给学生成绩管理系统做总分排名

排序是学生成绩管理系统里仅次于增删查的高频需求。最常见的排序题版本是“按总分从高到低排名,同分按学号从小到大”。用冒泡排序可以直观地完成这个任务。

void sortByTotal() { for (int i = 0; i < studentCount - 1; i++) { for (int j = 0; j < studentCount - 1 - i; j++) { int totalA = students[j].scores[0] + students[j].scores[1] + students[j].scores[2]; int totalB = students[j + 1].scores[0] + students[j + 1].scores[1] + students[j + 1].scores[2]; if (totalA < totalB) { Student temp = students[j]; students[j] = students[j + 1]; students[j + 1] = temp; } } } for (int i = 0; i < studentCount; i++) { students[i].rank = i + 1; } printf("排序完成\n"); }

冒泡排序的每一轮把最小的总分“冒”到数组末尾。内层循环判断totalA < totalB时交换,实现降序。Student temp = students[j]整体交换结构体变量,是结构体赋值的一个绝佳例子。如果担心交换结构体的开销,可以交换索引下标,但那需要额外维护一个指针数组,对成绩管理系统这种规模没必要。

排序完成后,把rank字段回填为当前下标加一。这里有个容易被忽视的问题:如果两个学生总分相同,排名也会不同。学生成绩管理系统的考试版通常要求“同分并列”,那就要在做完冒泡排序后单独比较相邻元素的总分,相同总分共用同一名次。实际写代码时可以用下面的逻辑:

int currentRank = 1; for (int i = 0; i < studentCount; i++) { if (i > 0) { int prevTotal = students[i - 1].scores[0] + students[i - 1].scores[1] + students[i - 1].scores[2]; int curTotal = students[i].scores[0] + students[i].scores[1] + students[i].scores[2]; if (prevTotal != curTotal) { currentRank = i + 1; } } students[i].rank = currentRank; }

如果你对c语言必背100代码里的排序题比较熟悉,会发现这是典型冒泡的变体。换成 qsort 也能做,但 qsort 需要写比较函数,适合后面进阶。学生管理系统的初版用冒泡排序更容易对着代码讲清楚“为什么两次循环、为什么内层减 i”。

4.2 统计平均分和及格率时的数值陷阱

统计功能一般要求输出各科平均分、总分平均分、及格率、优秀率。数值陷阱主要集中在“整数除以整数得到整数”和“除数为 0”这两件事上。

void printStatistics() { if (studentCount == 0) { printf("当前没有学生数据\n"); return; } int sum[COURSE_COUNT] = {0}; int pass[COURSE_COUNT] = {0}; for (int i = 0; i < studentCount; i++) { for (int j = 0; j < COURSE_COUNT; j++) { sum[j] += students[i].scores[j]; if (students[i].scores[j] >= 60) { pass[j]++; } } } for (int j = 0; j < COURSE_COUNT; j++) { float avg = (float)sum[j] / studentCount; float passRate = (float)pass[j] / studentCount * 100; printf("第%d门课程平均分: %.2f, 及格率: %.2f%%\n", j + 1, avg, passRate); } }

这里第二层循环先累加每一科成绩,再统计及格人数。(float)sum[j] / studentCount是先强转成浮点数再做除法,如果直接写sum[j] / studentCount,两个整数相除会直接截断小数,导致平均分错误。及格率也同样需要强转,否则pass[j] / studentCount永远是 0 或 1,再乘 100 也只能得到 0 或 100。

另一个常见坑是:当你用%.2f格式化输出passRate时,如果passRate是 0,会输出0.00%,没有问题。但如果你在计算之前没有检查studentCount == 0,除零错误会让程序直接崩溃。在 DevC 里,除零不一定立刻报错,可能得到 inf 或 nan,扫描结果会让你误以为是“非法地址”问题。提示:所有可能被零除的地方,都要先检查除数。

表格对比下整数除法和浮点除法的区别:

表达式类型结果
3 / 2int / int1
3.0 / 2float / int1.5
(float)3 / 2cast 后1.5
1.0 * 3 / 2float1.5

4.3 C语言文件读写操作代码与 DevC 的文件路径问题

文件读写是“学生成绩管理系统”大作业从“演示程序”升级为“可用工具”的分水岭。常见的存储方式有两种:文本方式和二进制方式。文本方式用fprintffscanf,优点是可以在记事本里查看;二进制方式用fwritefread,读写快但不能直接阅读。对学生项目来说,我推荐文本方式,因为调试时可以直接打开 data.txt 看内容。

void saveToFile() { FILE *fp = fopen("students.txt", "w"); if (fp == NULL) { printf("文件打开失败\n"); return; } fprintf(fp, "%d\n", studentCount); for (int i = 0; i < studentCount; i++) { fprintf(fp, "%s %s %d %d %d %.2f\n", students[i].id, students[i].name, students[i].scores[0], students[i].scores[1], students[i].scores[2], students[i].average); } fclose(fp); printf("保存成功\n"); }

fopen("students.txt", "w")的第二个参数用"w"表示写模式。如果文件已存在则清空重写;"a"才是追加。很多学生在这里会把模式写成"r+",结果文件不存在时报错。正确流程是:保存用"w",读取用"r"

读取函数是对称的:

void loadFromFile() { FILE *fp = fopen("students.txt", "r"); if (fp == NULL) { printf("文件不存在,请先保存\n"); return; } fscanf(fp, "%d", &studentCount); for (int i = 0; i < studentCount; i++) { fscanf(fp, "%s %s %d %d %d %f", students[i].id, students[i].name, &students[i].scores[0], &students[i].scores[1], &students[i].scores[2], &students[i].average); } fclose(fp); printf("读取成功,共 %d 条数据\n", studentCount); }

fscanf的格式串和fprintf的格式串需要严格对应。如果保存时在姓名和成绩之间写了逗号分隔,读取时也要在格式串里加上逗号。这是文件读写过程中最容易出的问题:保存的文件你能看懂,但程序读不进去。

在 DevC 中,文件路径特别值得注意。DevC 默认的“当前工作目录”并不是源代码所在的目录,而是项目文件.dev所在目录。如果你只是打开.c单文件,工作目录默认是C:\Dev-Cpp或当前可执行文件所在目录。所以当你用相对路径"students.txt"时,文件可能会被写到奇怪的位置。我的做法是:在程序开头打印一下getcwd(NULL, 0)观察当前目录,或者干脆在保存前用绝对路径测试,等逻辑通了再改回相对路径。

#include <direct.h> char buffer[1024]; getcwd(buffer, sizeof(buffer)); printf("当前目录: %s\n", buffer);

这段在 Windows 的 DevC 下可用。getcwd是 POSIX 函数,Windows 上的移植版本在<direct.h>里声明。如果你在 vscode 配置 c 语言环境时用 GCC,也会发现工作目录取决于 launch.json 里的cwd设置。无论哪个环境,“文件找不到”先看当前目录,不要假设和源码同一个文件夹。

5. 从 DevC 调试到避坑:让成绩管理系统一次跑通

5.1 用 DevC 的单步调试定位非法地址

写完整个学生成绩管理系统后,最常见的报错是程序崩溃后跳出“程序已停止工作”。这个时候不要急着重写代码,先开 DevC 的调试模式。DevC 的调试功能藏在“调试”菜单里,通常点击调试按钮后它会在首个断点暂停。断点可以点击行号左侧的红色圆点设置。单步执行时,重点观察students数组的每一项和studentCount变量值是否同步变化。

我自己用的排查顺序是:先确认studentCount值是不是符合预期。如果录入 3 个人后studentCount变成了 300,那问题一定出在录入函数里,很可能是指针类型错误地写入越界。接着往下看students[i]的内存状态,如果name显示为不可读字符,就说明结构体被错误地赋值或者数组越界覆盖了数据。提示:DevC 调试器偶尔不刷新局部变量,可以加printf打印studentCount和关键学生信息辅助判断。

5.2 字符数组与 scanf 导致的隐藏陷阱

成绩管理系统里最隐蔽的 Bug 往往和缓冲区有关。典型场景是菜单选择用scanf("%d", &choice),之后录入姓名用的是scanf("%s", name)。用户输入数字后按回车,回车符留在输入流里,下一次scanf("%s")会把这个回车符读掉,导致直接跳过姓名输入。解决办法是在每个交互输入后积极清理缓冲区:

void clearInputBuffer() { int c; while ((c = getchar()) != '\n' && c != EOF); }

在调用scanf之后调用clearInputBuffer(),可以吸收掉残留的换行符。这里用getchar读到\nEOF为止,比fflush(stdin)更可移植。fflush(stdin)在部分编译器下是未定义行为,很多教学环境不推荐。

字符数组越界在成绩管理系统里也常遇到:如果学号定义为char id[20],但用户输入了一个 30 位的字符串,scanf会一直写下去,越过id的边界,把后面的内存覆盖掉。轻则影响namescores,重则让程序执行到非法指令。严格来说应该用scanf("%19s", id)限制最大长度,19是因为要在末尾保留\0的位置。这个习惯在真实项目中同样重要,比如处理网络输入时,防止缓冲区溢出。

5.3 把 DevC 项目迁移到 vscode 配置 c 语言环境

大作业或自用项目一旦超过几百行,很多人会考虑从 DevC 换到 vscode。vscode 配置 c 语言环境比 DevC 多几步,但好处是对长代码的支持更好,还能用 Git 保存版本。常见配置流程是:安装 C/C++ 扩展,安装 MinGW-w64 并添加bin目录到 PATH,然后新建launch.jsontasks.json.vscode下的配置我只给出最精简版供参考:

{ "version": "0.2.0", "configurations": [ { "name": "C Debug", "type": "cppdbg", "request": "launch", "program": "${fileDirname}/${fileBasenameNoExtension}.exe", "cwd": "${fileDirname}", "MIMode": "gdb", "miDebuggerPath": "C:/mingw64/bin/gdb.exe" } ] }

关键字段是programcwd。在 vscode 里运行成绩管理系统时,cwd决定了students.txt文件会被创建在哪。你需要把它设置成students.txt所在目录,否则程序会找不到文件。miDebuggerPath是你 MinGW 安装目录下的 gdb.exe 路径,没有它就无法断点调试。

迁移后最容易遇到的差异是:DevC 自带“编译运行”一键完成,vscode 需要先用 Ctrl+Shift+B 构建任务生成 exe,再按 F5 调试。构建任务可以这样简单定义:

{ "version": "2.0.0", "tasks": [ { "label": "Build C", "command": "gcc", "args": [ "-g", "${file}", "-o", "${fileDirname}/${fileBasenameNoExtension}.exe" ] } ] }

-g表示生成调试信息,没有这个参数,GDB 就无法定位到源码行。如果你已经学完学生成绩管理系统,想更进一步,可以试两个方向:一是在这个系统里加入qsort和函数指针,用qsort(students, studentCount, sizeof(Student), compareByAverage)代替手写冒泡;二是把所有while循环的读取改成fgets+sscanf,彻底规避%s的缓冲区风险。这些扩展对理解C语言 指针C语言内存管理的内核非常有帮助,也是翁恺 C 语言课程里反复强调的进阶路径。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 23:11:07

用Python打造30秒跨Excel文件搜索工具:不打开文件精确到单元格

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 23:10:10

从工程结构到答辩:安卓班课APP设计与开发实践

简介&#xff1a;这是一份面向安卓开发初学者与毕业设计学生的班课手机APP项目资料&#xff0c;内容涵盖客户端源码、毕业论文及完整设计思路&#xff0c;重点解决课程管理、作业提交、讨论互动与通知提醒等校园场景需求。资源共2000个文件&#xff0c;压缩包约20.29MB&#xf…

作者头像 李华
网站建设 2026/9/15 23:10:00

Spring事务失效的底层原理与排查实战:从代理机制到异常处理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 23:10:03

域名放别人网站风险大 选对服务商哪家好

域名放别人网站风险大 选对服务商哪家好 域名解析指向不明服务器,DNS劫持与挂马风险让你头疼?域名服务器搞不懂,怕被黑客利用打擦边球,选建站公司哪家好成了甲方最纠结的事。…

作者头像 李华
网站建设 2026/9/15 23:07:22

SpringBoot+Vue企业级家具商城系统全栈源码解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华