简介:面向计算机科学与技术专业学生的C++面向对象课程设计参考文档,以「学生选修课程系统」为题目,适合正在准备课程设计、需要完整范例对照的初学者,也适合想快速搭出选题框架的同学。压缩包内共1个doc文件,约259KB,是一份结构完整的课程设计报告,按需求分析、内容设计、调试分析、用户手册、设计总结与附录等章节依次展开。报告先说明课题目的与系统应实现的功能,再给出模块图与主程序流程图,涵盖学生登录、课程浏览、选课与退选、个人信息管理、成绩查询和数据统计等模块,并附用户认证、课程数据管理、选课冲突检查与数据持久化等关键代码思路。调试分析部分记录了实际完成情况、性能分析、上机遇到的问题及解决方案,还提出可扩充的教师管理、课程评价与教室预约方向。已有1400余人学习,可直接参照其章节安排与模块划分。
1. 学生选修课程系统设计:一份能直接交的 C++ 课程设计有几个前提
选题单发下来,十个做 C++ 课程设计的人里有六七个会挑学生选修课程系统设计——听起来功能明确、数据关系简单,好像两天就能写完。真动手才发现难点根本不在语法,而在两件事:一是八个功能(添加、查询、显示、编辑、删除、统计、保存、读取)之间怎么共用同一套数据结构,二是数据存进文件之后,下次运行读回来还对不对得上。C++ 课程设计最常见的“看起来写完了但一演示就出问题”,基本都出在这两处。这套方案面向三类读者:要在两三周内交一份完整课设报告的学生、要照着结构改一份课程设计作业的从业者、以及需要把“面向对象四大特性”落到代码里给导师看的同学。下面从需求边界一直讲到答辩时的验收顺序,中间贴的代码都能直接编译运行。
2. 需求分析与功能边界:八个操作怎么收敛成两条业务线
2.1 教师端与学生端的职责切分
一个选课系统最容易写乱的地方,是把所有人的所有操作平铺在一个菜单里。真正落到代码,通常会砍成两条业务线:教师端负责维护数据,学生端负责消费数据。
教师端的能力是“增删改查显统读存”八件套——添加课程或学生、按编号查询、显示全部、修改、删除、统计、从文件读、存到文件;学生端只有两件事:看全部可选课程,然后选课。这个切分不是为了好看,是因为两端的操作对象完全不同:教师端改的是course和student两份主数据,学生端只往xuanke里追加一条关联记录。把这两类写操作隔开,能避免学生在选课界面上误删课程这种事故。
菜单层面用两层选择实现就够了:主函数里一个a变量控制教师端/学生端/退出三个入口,进入教师端后再用一个内层switch分发八个功能。原项目在调试记录里专门提到过“进入教师端选择某项操作,操作完成后就退出系统”,根因就是少了这层循环。正确写法是把每个端的菜单包在while(true)里,功能执行完回到菜单,只有显式选择“返回”或“退出”才跳出。这个细节在报告里只是一句话,但在现场演示时差别巨大——不修的话,老师让你连续做两次添加,你就得重启程序重新输入全部数据。
学生端不需要登录鉴权。课程设计要求里写“学号验证身份”是很多人的惯性加戏,实际这份系统的学生端只是输入学号和课程编号做一次关联,加密码反而是给自己挖坑——密码存哪、怎么校验、忘了怎么办,全是额外工作量,答辩时还会被追问安全性。课程设计的评分点在设计方法,不在鉴权强度。
2.2 功能清单到验收口径的映射
需求分析写得好不好,看能不能把每个功能翻译成可当场验证的一句话。下面这张表是给导师看的,也是自己编码时逐条打勾用的:
| 功能 | 输入 | 输出/行为 | 边界条件 |
|---|---|---|---|
| 添加课程 | 课程代码、名称、性质、学时、学分、学期 | 追加到对象数组,计数加一 | 代码重复则拒绝并提示 |
| 添加学生 | 学号、姓名、性别、年龄、系别、班级、联系方式 | 追加到对象数组,计数加一 | 学号重复则拒绝并提示 |
| 查询 | 课程代码 / 学号 | 命中则打印该条完整信息 | 未命中提示“没有找到” |
| 显示 | 无 | 逐行打印全部记录 | 记录为 0 时提示“文件无记录” |
| 修改 | 原编号 + 新字段 | 覆盖该条记录 | 新编号与其他人重复则回滚 |
| 删除 | 编号 | 后续元素前移,计数减一 | 未命中提示“没有该项记录” |
| 统计 | 统计类型 | 学生人数/课程门数/某课选课人数及学号列表 | 选课文件缺失时提示 |
| 保存/读取 | 文件路径 | 对象数组与文本文件互转 | 文件打不开时给出明确提示 |
表里最值得盯的是“边界条件”这一列。课程设计拿高分和拿及格的分水岭,往往不是功能多不多,而是每条异常路径有没有被处理。八个功能里至少六条有异常分支,全写出来,报告的“程序性能分析”一节就有真东西可写,而不是干巴巴地罗列功能。
2.3 三个数据实体的字段设计
系统只有三个实体,字段定死了后面就不用返工。学生表的学号是唯一键,课程表的课程代码是唯一键,选课表只存两个编号:
const int maxn = 100; // 对象数组容量上限,课设规模够用 class student { public: int SID; // 学号,唯一键 char name[10]; // 姓名 char sex; // 性别,M/F int age; char SDEPT[10]; // 系别 char grade[10]; // 班级 char TEL[10]; // 联系方式 void put_student(); // 从键盘读入一条 void display(); // 按行输出 }; class course { public: int c_code; // 课程代码,唯一键 char c_name[10]; char c_nature[10]; // 必修/选修 int c_hour; // 总学时 int c_grade; // 学分 char c_semester[10];// 开课学期 void put_course(); void display(); }; class xuanke { public: int c_num; // 课程代码 int s_num; // 学号,两条编号合起来构成一条选课记录 void get(); };这段是整份系统的地基,几个地方值得在报告里点一句。char name[10]存中文姓名刚好卡在边界上,一个汉字占两字节,五个字就满了,输入时cin >> name又会在空格处截断,遇到“欧阳 明”这类带空格的输入直接错位——通常的做法是改用std::string,报告里写“使用 string 类替代字符数组,避免缓冲区溢出”,比用char数组更站得住。另外字段名混用SID和c_code两种风格,统一成一种更好看。选课类不存课程名和学生名,只存两个编号,这是典型的关系表思路,查详情时靠编号回主表找,省空间也避免改名后数据不一致。
3. 面向对象总体设计:四个类、三个文件和一套主循环
3.1 类的职责划分与继承关系的取舍
四个类的分工是:student和course只描述一条记录长什么样、怎么读写自己;xuanke描述一条关联;manage是管理者,持有三份对象数组和全部业务函数。
class manage : public student, public course, public xuanke { private: student st[maxn]; // 全部学生记录 course co[maxn]; // 全部课程记录 xuanke xk[maxn]; // 内存中的选课记录(以文件为准时可省) public: void c_read(); // 课程文件 -> 数组 void s_read(); // 学生文件 -> 数组 void add_course(); // 添加课程(含重复校验) void add_student(); void c_search(); // 按编号查课程 void s_search(); void c_display(); // 显示全部 void s_display(); void c_edit(); // 修改,含编号冲突回滚 void s_edit(); void c_delete(); // 删除,后续元素前移 void s_delete(); void xuank(); // 选课,写入选课文件 void cnt(); // 三档统计 void save(); // 数组 -> 文件 };这里的public多重继承是要在报告里解释清楚的:课设评分表里通常有“体现封装、继承、多态”一项,写class manage : public student, public course, public xuanke能让类图连出三条继承线,满足形式要求。但从工程角度看,管理者并非“是一种学生”,这是典型的 is-a 误用;manage内部已经把student、course都作为成员数组声明了,真正的复用靠的是组合,继承线并没有带来任何代码收益,反而在manage里访问SID、c_code时容易出现名字二义性。
我一般会这么处理:报告里的类图照继承画,代码里用组合实现,并在“可改进的地方”一节写明“管理者与被管理对象应为组合关系,当前继承设计仅为满足类层次展示,后续可改为组合以消除耦合”。这句话是加分项,说明你知道为什么这么写,而不是碰巧这么写。同时这也是答辩被问“为什么用多重继承”时的标准答法——先答设计意图,再答工程权衡,比硬撑强。
3.2 模块图与主菜单流程
主流程很薄,就三层:主菜单选身份 → 身份菜单选功能 → 功能执行完回菜单。
int main() { manage m; m.c_read(); // 先把两份主数据读进内存,后面全在数组上操作 m.s_read(); int a = 0; while (true) { cout << "1.教师端 2.学生端 3.退出" << endl; cin >> a; if (a == 3) break; if (a == 1) { while (true) { // 教师端内层循环,做完一项继续留在菜单 cout << "1添加 2查询 3显示 4修改 5删除 6统计 7保存 8返回" << endl; int b; cin >> b; if (b == 8) break; switch (b) { case 1: m.add_course(); break; // 实际可用二级菜单区分课程/学生 case 2: m.c_search(); break; case 3: m.c_display(); break; case 4: m.c_edit(); break; case 5: m.c_delete(); break; case 6: m.cnt(); break; case 7: m.save(); break; } } } else if (a == 2) { m.c_display(); // 学生端先看全部课程 m.xuank(); // 再录入一条选课记录 } } m.save(); // 退出前统一落盘,避免“改了没存” return 0; }代码里的两个while是整套交互的骨架。外层管身份切换,内层管功能连续调用,a == 3和b == 8是仅有的两个出口。这里有个容易忽略的点:m.c_read()和m.s_read()必须放在主循环之前调用一次,之后所有功能都只操作内存数组。如果按“每个功能函数内部各自打开文件”的写法,代码会散成十几份重复的ifstream,改一个字段名要改十几处,而且频繁开关文件很容易踩到没关文件句柄的坑。
save()放在return 0之前是最后一道保险。原项目调试记录里明确写过“当运行完程序时没有进行保存,下次读取发现上次修改的记录没显示”,这就是典型的后悔药没准备。更稳的做法是每个写操作(添加、删除、修改)完成后立刻调一次save(),代价是多次磁盘写,课设规模下完全无感。
3.3 对象数组的容量与计数策略
全系统只有两个全局计数c_cnt和s_cnt,分别表示当前有效的课程条数和学生条数。这两个变量的地位等同于数据库里的行数,所有遍历都以它们为上界,所有增删都必须同步维护它们。
容量定成maxn = 100是课设的常规选择,够放一个班的课程和几个班的学生。真正要写进代码的是超额保护:添加函数第一行判断if (c_cnt >= maxn),直接提示“记录已满”并返回。没有这行,第 101 条记录会越界写到数组外的内存上,表现是程序不崩但数据莫名其妙被改写,这种问题在现场演示时最难解释。
删除时的计数维护要和数组前移绑在一起看。删除下标i的元素,是把i+1到c_cnt-1的元素整体往前挪一格,然后c_cnt--。顺序不能反——如果先减计数再前移,最后一个元素会被漏掉。原项目调试记录里有“新加入的课程不会显示,最后发现添加记录时计数值没有跟着增加”,本质是同一个问题:数组内容和计数不同步。一条铁律是,任何一次co[...] = ...之后紧跟一行注释,写明这行的计数要怎么变。
4. 核心功能实现:添加、删除、修改、选课与统计的代码落地
4.1 添加与编号唯一性校验
添加的完整流程是:容量检查 → 读入新记录 → 全表扫描查重 → 写入数组并自增计数。
void add_course() { if (c_cnt >= maxn) { // 1. 容量保护,防止越界写 cout << "记录已满,无法继续添加!" << endl; return; } course c; c.put_course(); // 2. 从键盘读一条完整课程信息 for (int i = 0; i < c_cnt; i++) { // 3. 全表比对课程代码 if (co[i].c_code == c.c_code) { cout << "有重复,添加失败!" << endl; return; // 直接返回,不动数组也不动计数 } } co[c_cnt] = c; // 4. 先写入当前空位 c_cnt++; // 再自增,两步顺序固定 cout << "添加成功!" << endl; }四个步骤的顺序有讲究。第 3 步查重必须在写入之前,一旦先把c塞进数组再查重,失败时就得做一次回滚删除,多此一举。第 4 步的co[c_cnt] = c; c_cnt++;和co[c_cnt++] = c;等价,但拆成两行更适合初学者核对,也方便在中间打断点看计数。学生添加函数add_student()结构完全一样,只是查重键换成SID,这就是为什么两个函数应该复用同一套模板——如果时间允许,把查重和写入抽成一个带比较键的函数模板,代码量能砍掉一半。
学号用int还是string是个小争议点。用int做比较最省事,但前导零会丢(“007”变成 7),而且学生学号通常是十来位,int装得下但看着别扭。课设阶段用int没问题,报告里注明“学号采用整型存储,如实际学号含字母或前导零需改为 string”即可。
4.2 查询、显示与删除的数组前移
查询和显示是同一种遍历的两个变体,一个命中就停,一个走完全程。
void c_search() { int c_num; cout << "请输入要查询的课程代码:"; cin >> c_num; for (int i = 0; i < c_cnt; i++) { if (co[i].c_code == c_num) { co[i].display(); // 命中,打印这一条 return; // 命中即返回,不再继续扫 } } cout << "没有找到相关课程,查询失败!" << endl; // 循环走完仍未返回 } void c_display() { if (c_cnt == 0) { // 空表单独提示,避免打印表头后无内容 cout << "文件中无记录!" << endl; return; } for (int i = 0; i < c_cnt; i++) { co[i].display(); // 每条记录占一行 } }c_search里用return提前结束,比用break加标志位清爽。要注意的是循环结束后才输出“没找到”,这个位置关系决定了提示语只在真正未命中时出现。如果写成if (i == c_cnt)也可以,但前提是i在循环外声明,否则作用域出错——这是编译期就会拦下的问题,不算坑。
删除的关键动作是数组前移,也就是数据结构课上说的“顺序表删除”:
void c_delete() { int c_num; cout << "请输入要删除课程的代码:"; cin >> c_num; int i = 0; for (; i < c_cnt; i++) { if (co[i].c_code == c_num) break; // 先定位 } if (i == c_cnt) { // 走到末尾说明没这个编号 cout << "没有该项记录!" << endl; return; } for (int j = i; j < c_cnt - 1; j++) { co[j] = co[j + 1]; // 后面每个元素往前挪一格 } c_cnt--; // 有效长度减一 cout << "记录删除成功!" << endl; }前移循环的边界是j < c_cnt - 1,因为循环体里要读co[j+1],写到c_cnt-1时读的正是原来最后一条,再往后就越界了。这个-1是手写顺序表最容易漏的地方,漏了之后程序不一定崩,但会把数组后面那块内存里的垃圾数据搬进来,表现为删掉一条记录后列表末尾多出一条乱码。学生删除函数同理,只换比较键。
4.3 修改:先备份再覆盖的写法
修改是八个功能里唯一会同时改动“旧值”和“新值”的,也最容易写出“改失败了但原数据已经没了”这种事故。标准做法是先把原对象复制一份出来当后悔药:
void c_edit() { int c_num; cout << "请输入要修改课程的代码:"; cin >> c_num; int i = 0; for (; i < c_cnt; i++) { if (co[i].c_code == c_num) break; } if (i == c_cnt) { cout << "没有此项记录!" << endl; return; } co[i].display(); course backup = co[i]; // 关键:覆盖前留一份原值 co[i].put_course(); // 直接用新输入覆盖这一条 for (int j = 0; j < c_cnt; j++) { if (j == i) continue; // 跳过自己,否则必然“重复” if (co[j].c_code == co[i].c_code) { co[i] = backup; // 新编号撞了别人,整条回滚 cout << "存在相同编号的课程,修改失败!" << endl; return; } } cout << "修改成功!" << endl; }原项目在调试记录里把这个坑写得很清楚:“当找到了要修改的记录后,就将该记录读取了新内容,之前的内容被覆盖掉,结果不论有没有重复编号都会进行修改”。backup这一行就是全部的解法。第二个容易漏的是查重循环里的if (j == i) continue;——不跳过自己,新编号等于原编号时会被判成重复,导致“什么都不改也提示失败”。
这段还有个隐藏问题:put_course()是全字段覆盖,用户只想改个学时也得把课程代码、名称、性质、学期全部重打一遍,输错一个字段就得重来。更友好的写法是逐字段询问“是否修改(y/n)”,默认保留原值。课设阶段用全量覆盖没问题,但在报告的“可改进的地方”里写上这一条,说明你考虑过交互体验。
4.4 选课记录与三档统计
选课和统计都绕不开第三个文件。选课只追加一行两个编号,统计则要能回答三个问题:学生总数、课程门数、某门课有多少人修。
void xuank() { xuanke xk; xk.get(); // 读入 c_num 和 s_num ofstream out("xuanke_information.txt", ios::app); // app 模式追加,不清空原内容 if (!out) { cout << "选课文件打开失败!" << endl; return; } out << xk.c_num << " " << xk.s_num << endl; // 一行一条,空格分隔 out.close(); // 必须关闭,否则下次读不到最新内容 cout << "选课成功!" << endl; } void cnt_choose(int c_num) { ifstream in("xuanke_information.txt"); if (!in) { cout << "选课文件不存在或打开失败!" << endl; return; } int c, s, n = 0; while (in >> c >> s) { // 按行读,读到文件尾自动结束 if (c == c_num) { cout << s << " "; n++; } } cout << "\n选修该课程的人数为:" << n << endl; }选课这里用文本追加(ios::app)而不是二进制写,是为了让文件能直接用记事本打开核对——原项目最初用fwrite写选课文件,打开全是乱码,后来换成fstream才解决。out.close()那一行看着多余,实际是必须的:不关文件,缓冲区里的内容不落盘,紧接着的统计函数读到的就是旧文件,表现是“刚选完课,统计里没有这条记录”。
统计的三档里,前两档直接读全局计数就行,第三档才需要扫文件。这里有个口径问题值得单独说:学生人数对应s_cnt,课程门数对应c_cnt,两个变量名和含义必须严格对上。原项目代码里case 1打印的是c_cnt却标着“学生人数”,case 2打印s_cnt却标着“课程门数”,两个口径正好写反了——这种错误编译不报、运行不崩,只有手动数一遍数据才能发现,属于最难自查的一类。
5. 常见问题排查:文件读写和数组操作的六个翻车点
5.1 文本流与二进制流的选择
同样的“保存到文件”,fwrite和ofstream出来的是两种东西。fwrite(&obj, sizeof(obj), 1, fp)写的是内存字节的原始拷贝,用记事本打开是乱码,而且类里一旦加了std::string成员,写进去的是指针值,换个进程读回来直接野指针崩溃。ofstream <<写的是格式化文本,人能读、能改、能用 Excel 打开看。
课设场景下无脑选文本流。理由有三:一是答辩时老师要求“打开文件看看存进去没有”,文本流一秒就能证明;二是文本文件出问题时肉眼能看出哪一列错位;三是文本流的读写代码和cin/cout语法一致,不用额外学fseek、ftell那套。
代价是文本流读写要做类型转换,int读进来还是int,但char数组读进来遇到空格就断。所以字段里如果可能有空格(比如系别名“计算机 工程”),要么改用string配合getline,要么约定所有字段都不含空格。课设数据基本可控,约定无空格最省事,在用户手册里写一句输入规范即可。
5.2 读写时机:什么时候读、什么时候存
读和存的时机定错,会引出一连串“数据不见了”的假象。稳定的策略只有一条:进主循环前读一次,所有功能改内存,退出前存一次。
void c_read() { ifstream in("course_information.txt"); if (!in) { cout << "打开失败!" << endl; return; } // 首次运行文件不存在属正常 while (in >> co[c_cnt].c_code >> co[c_cnt].c_name >> co[c_cnt].c_nature >> co[c_cnt].c_hour >> co[c_cnt].c_grade >> co[c_cnt].c_semester) { c_cnt++; // 读成功一条,计数加一 } in.close(); } void save() { ofstream out("course_information.txt", ios::out); // out 模式会清空重写 if (!out) { cout << "打开失败!" << endl; return; } for (int i = 0; i < c_cnt; i++) { out << co[i].c_code << " " << co[i].c_name << " " << co[i].c_nature << " " << co[i].c_hour << " " << co[i].c_grade << " " << co[i].c_semester << endl; } out.close(); }while (in >> ...)这个写法本身就是循环条件:读成功继续,读到文件尾或格式不匹配就退出。计数c_cnt在循环体里自增,天然和实际读到的条数对齐,不需要额外统计行数。save()用ios::out会先清空文件再写,这是正确的——如果误用ios::app,每次保存都会在文件末尾再追加一份全量数据,跑三次程序就变成三份重复记录。
5.3 六个高频翻车点:现象、原因与改法
现象一:保存后打开文件全是乱码。原因是用了fwrite写二进制。改成ofstream按字段文本输出,每个字段之间用空格或制表符分隔,每条记录一行。改完顺手把读取端也换成ifstream >>,两端格式必须严格对称,多一个空格都会导致字段错位。
现象二:文件明明存在,ifstream却打不开。最常见的原因是上一次写文件之后没有close(),缓冲区还没落盘,或者句柄没释放。检查每个ofstream后面是否都有out.close(),尤其是函数中途return的分支——提前返回时析构虽然会关文件,但如果同一函数里先写后读,中间必须显式关一次。
现象三:修改失败后,原记录也没了。原因是先覆盖再校验。改用course backup = co[i];保存原值,校验不通过时co[i] = backup;整体回滚。同样的套路适用于任何“先破坏后验证”的流程,这个习惯在后面的项目里会反复用到。
现象四:添加了记录但列表里看不到。两个可能:一是添加时忘了c_cnt++,二是显示循环的上界写成了固定值 100 而不是c_cnt。前者看添加函数末尾有没有自增,后者看for循环条件。两者都属于“数组和计数不同步”,统一检查一遍所有对c_cnt的读写位置就能定位。
现象五:统计数据张冠李戴。学生人数打成了课程门数。原因是两个计数的变量名太像、含义没对齐。改法是给变量重命名成student_count和course_count,并且写统计函数时先把三个case的中文标签和变量一一念一遍再写代码。
现象六:删掉中间一条后,末尾多出一条乱码。前移循环的上界写成了j < c_cnt而不是j < c_cnt - 1,导致读了数组外的一块内存。把-1补上,同时在循环前加一行断言或注释说明“最后一次搬运的源下标是 c_cnt-1”。
6. 验收与答辩:按功能清单走一遍,比背代码管用
6.1 一份能现场演示的验收清单
答辩前自己跑一遍下面这个顺序,全程不重启程序,能走完基本就稳了:
| 步骤 | 操作 | 期望结果 |
|---|---|---|
| 1 | 首次运行(无数据文件) | 提示打开失败但不崩溃,可选择继续 |
| 2 | 添加 3 门课程、3 名学生 | 每次提示添加成功,计数正确 |
| 3 | 重复添加同一课程代码 | 提示重复,列表条数不变 |
| 4 | 显示全部 | 3 门课程、3 名学生逐行输出 |
| 5 | 按编号查询存在的和不存在的 | 前者打印详情,后者提示未找到 |
| 6 | 修改某课程,新代码与他人冲突 | 提示失败,且该记录保持原值 |
| 7 | 删除中间一条 | 列表条数减一,末尾无乱码 |
| 8 | 学生端选课两次,统计该课人数 | 显示 2 人及对应学号 |
| 9 | 退出程序,重新运行,显示全部 | 所有增删改结果都在 |
| 10 | 用记事本打开两个数据文件 | 内容可读,字段对齐 |
第 6 步和第 9 步是两个关键检查点。前者验证回滚逻辑真的生效,后者验证保存时机没出问题。这两处一旦出问题,前面的功能做得再全也救不回来——老师看到改一次数据丢一次,印象分直接见底。
6.2 答辩讲解的推荐顺序
讲的时候不要从#include开始念。推荐顺序是:先讲数据模型(三个实体、两个唯一键、一份关联),再讲类结构(四个类各管什么,为什么管理者用组合实现、继承只体现在类图上),然后挑一个功能走完整流程——建议挑删除,因为它同时涉及定位、前移、计数三个动作,最能体现对顺序表的理解;最后讲文件持久化,重点说清“读一次、改内存、存一次”的时机设计,以及为什么放弃fwrite改用文本流。
被问到“还有什么可以改进”时,准备三条就够:把char数组换成string消除输入截断;把继承改为组合降低耦合;给每个写操作后加自动保存,避免退出时忘记存盘。这三条都指向真实存在的问题,说出来是加分,不说被问到也答得上。
代码里那行course backup = co[i];是我做课设时被导师当场问出来的。当时改一条课程记录,新编号填重复了,提示“修改失败”,可原记录也跟着没了,只能重启重录一遍。后来每次写“先用新值覆盖、再做校验”的逻辑,我都会强制先复制一份原对象出来,这个习惯一直用到现在。希望帮到你。
本文还有配套的精品资源,点击获取