
简介这份源码包是C#编写的在线考试系统完整项目面向需要学习C# Web开发或部署在线考试平台的学生、教师与开发者。系统基于VS2010与SQL Server 2008实现学生、教师、管理员三角色管理涵盖自动生成试卷、教师自拟题目、自动批改、学生信息与成绩管理等功能。压缩包共133个文件以C#源码47个cs、ASP.NET页面39个aspx为主辅以数据库文件mdf/ldf、样式配置及少量图片素材包体约1.66MB便于本地还原与调试。已有1379人学习下载。通过阅读源码可深入理解C#分层架构、数据库表设计、题库随机抽取及评分规则实现也可直接修改题目与权限作为课程设计或毕业设计的实用参考。1. 拿到一套C#在线考试系统源码先别急着编译从网盘拉下来解压后这套C#在线考试系统源码其实是个 WinForms 桌面工程不少人的第一反应是“不是在线考试吗怎么没有网页”。它确实不是 B/S 网页而是 C/S 架构Windows 客户端 SQL Server。教师端负责题目自拟、批量导入、手工组卷学生端负责随机抽题、在线答题、自动交卷管理员端管账号和考试场次。这套源码把三角色的逻辑放在一个解决方案里主线清晰特别适合课程设计、毕业设计拿来改界面后直接交付。但最忌拿到手就按 F5 跑先得把数据库脚本建起来否则登录窗体都弹不出来。2. 三角色权限体系与自动批改四个模块怎么咬合这套系统的入口是一个登录窗体角色管理没那么玄users 表里一个 Role int 字段就分完了。真正决定代码质量的是登录后每个角色看到的界面和能执行的 SQL 不一样。下面把登录分发、题库设计、抽题算法、自动批改四块拆开讲这也是你改代码时最可能动刀子的位置。2.1 三角色入口一个 users 表用 Role 字段区分常见做法是登录时查 users 表拿到 Role 后路由到不同的窗体。密码存储一般是 MD5 十六进制字符串不是明文这也是课程设计里最通用的做法。C# 端封装的 MD5 方法长这样private string Md5(string input) { using (System.Security.Cryptography.MD5 md5 System.Security.Cryptography.MD5.Create()) { byte[] bytes md5.ComputeHash(Encoding.UTF8.GetBytes(input)); StringBuilder sb new StringBuilder(); foreach (byte b in bytes) sb.Append(b.ToString(x2)); return sb.ToString(); } }这段代码把用户输入的密码先转 UTF-8 字节再算 MD5最后转成 32 位小写十六进制。注意 x2 的意思是每个字节固定输出两位小写十六进制这样出来的结果和 SQL Server 端HASHBYTES(MD5, 123456)转出的小写格式能对齐否则两边算出来不一样登录永远失败。登录分发逻辑的核心代码private void btnLogin_Click(object sender, EventArgs e) { string userName txtUserName.Text.Trim(); string pwdMd5 Md5(txtPassword.Text.Trim()); string sql SELECT UserID, UserName, Role FROM users WHERE UserNameu AND Passwordp; using (SqlConnection conn new SqlConnection(AppConfig.ConnStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(u, userName); cmd.Parameters.AddWithValue(p, pwdMd5); conn.Open(); using (SqlDataReader reader cmd.ExecuteReader()) { if (reader.Read()) { int role reader.GetInt32(2); GlobalCache.LoginUser new UserModel { UserId reader.GetInt32(0), UserName reader.GetString(1), Role role }; if (role 0) new FrmAdmin().Show(); else if (role 1) new FrmTeacher().Show(); else new FrmStudent().Show(); this.Hide(); } else { MessageBox.Show(用户名或密码错误); } } } }逻辑说明先用参数化 SQL 查询用户避免字符串拼接导致 SQL 注入读到用户后把当前登录人信息放进 GlobalCache方便后续窗体直接拿 UserId 和 Role。参数说明u 是用户名p 是 MD5 后的密码Role 用 0、1、2 分别对应管理员、教师、学生。这里有个容易翻车的地方new 出来的子窗体关闭时如果直接 Application.Exit() 会让整个进程退出但只 Hide 登录窗体又会让任务管理器里残留进程。我一般建议在 Program.cs 里把主窗体设计成带容器的 MainForm登录窗体 Dialog 方式弹出这样角色切换更干净也方便演示时来回退出。2.2 题目自拟题库表与题型约束“题目自拟”在教师端的体现就是题库表 questions你可以手工录入也可以从 Excel 导入。这张表是整套系统的地基字段设计直接决定后面抽题和批改好不好写。这套源码里常见的字段如下字段类型说明QuestionIDint 自增题库主键Typeint1-单选 2-多选 3-判断 4-简答Contentnvarchar(500)题干文本OptionA ~ OptionDnvarchar(200)选项内容非选择题可为空RightAnswernvarchar(50)单选存“A”多选存“ABD”判断存“T/F”Scoredecimal(4,1)单题分值Difficultyint1容易、2中等、3难Statusint1可用0停用抽题时过滤这里有个容易忽略的点判断题的 RightAnswer 在导入模板里最好统一成“T/F”或“对/错”其中一种不要混着录。自动批改的代码通常只认一种格式混了就会出现有的题永远判不对。多选的答案顺序也要约定好按字母排序存比如正确选项是“A、B、D”就存“ABD”学生作答时如果选“ADB”批改逻辑就要做字符集合比较而不是简单字符串相等。2.3 随机抽题组卷ORDER BY NEWID() 的分层抽取教师组卷时如果选择“随机生成”系统就会按题型和难度从题库里抽取题目。SQL Server 里的随机抽取最常见的方式是ORDER BY NEWID()每行生成一个随机 GUID排序后取前 N 条。核心 SQL 是这样DECLARE PaperId int 202504001; DECLARE Type int 1; -- 1 为单选题 DECLARE Num int 20; -- 本次抽题数量 DECLARE Score decimal(4,1) 5; -- 单题分值 INSERT INTO paper_details(PaperID, QuestionID, Score) SELECT PaperId, QuestionID, Score FROM ( SELECT TOP (Num) q.QuestionID FROM questions q WHERE q.Type Type AND q.Difficulty 3 AND q.Status 1 ORDER BY NEWID() ) AS t;逻辑说明内存层先按题型过滤再按难度过滤最后用 NEWID() 打乱顺序取前 Num 条。参数说明PaperId 是试卷主键你要先往 papers 表插一条记录拿到自增 IDType 和 Num 决定抽什么题、抽多少Score 是这批题的单题分值。需要注意的是TOP (Num)括号不能省SQL Server 里参数化 TOP 必须带括号否则直接报语法错误。这套源码抽完题后 paper_details 就是一张试卷的快照之后不管题库怎么改已经生成的试卷不受影响这点对考试公平性很重要。如果你的版本是 MySQL这段 SQL 需要改成ORDER BY RAND()但大数据量下性能会差一些课程设计规模不用担心。2.4 自动批改边界哪些题能自动判哪些必须人工复核自动批改不是全自动。单选、多选、判断这类客观题可以机器判简答题基本只能交人工。把这层边界理清楚你才能理解为什么这套源码的成绩表里既有 AutoScore 又有 FinalScore 两个字段。自动批改的核心循环decimal autoScore 0m; DataTable dt ds.Tables[answers]; // 包含学生的作答和标准答案 foreach (DataRow row in dt.Rows) { int qType Convert.ToInt32(row[Type]); if (qType 4) continue; // 简答题走人工评分 string stdAns row[RightAnswer].ToString().Trim().ToUpper(); string stuAns row[StudentAnswer].ToString().Trim().ToUpper(); if (stdAns.Equals(stuAns)) { autoScore Convert.ToDecimal(row[Score]); } }逻辑说明循环里先判断题型简答题直接跳过不参与自动计分客观题把学生答案和标准答案都去空格、转大写后再比较。这里用 Trim() 是血泪经验Excel 导入的答案很容易带前后空格不去掉就会出现学生明明选对了却判 0 分。参数说明如果多选支持“ABD”和“ADB”都算对这段代码是不够的需要把字符串拆成字符集合用 SetEquals 之类的逻辑判断。主观题的 ManualScore 字段默认是 0教师在成绩管理界面里手动录入后最终成绩才完整。3. 从解压到跑起来环境配置、建库、第一次上线这套源码要跑起来关键不在 C# 代码而在环境。下面按顺序走一遍照着做就能在半小时内把系统拉起来不至于第一次打开就面对一堆红色报错。3.1 修改连接字符串App.config 里的三处关键参数源码的连接字符串一般放在 App.config 或主窗体的构造函数里。WinForms 程序最常见的配置是这样connectionStrings add nameOnlineExam connectionStringData Source.;Initial CatalogOnlineExam;User IDsa;Password123456;TrustServerCertificateTrue providerNameSystem.Data.SqlClient / /connectionStrings参数说明Data Source 是数据库地址点号表示本机默认实例如果是命名实例要写成计算机名\SQLEXPRESSInitial Catalog 是数据库名要和初始化脚本里创建的库名一致User ID 和 Password 是 SQL Server 登录账号。如果本机用的是 Windows 身份验证把User IDsa;Password123456;整段换成Integrated SecurityTrue;就行。注意改完 App.config 要重启程序连接字符串只在程序启动时读一次运行中修改不会生效。很多新手在这个坑里浪费半小时改完配置不重启就直接 F5。3.2 数据库从零初始化脚本与账号准备别指望 Handy 的界面帮你建库这份源码的库是脚本形成的。用 SQL Server Management Studio 打开源码里的 Database.sql 执行核心是这几步IF DB_ID(OnlineExam) IS NULL BEGIN CREATE DATABASE OnlineExam; END GO USE OnlineExam; GO IF OBJECT_ID(users) IS NOT NULL DROP TABLE users; IF OBJECT_ID(questions) IS NOT NULL DROP TABLE questions; IF OBJECT_ID(papers) IS NOT NULL DROP TABLE papers; IF OBJECT_ID(paper_details) IS NOT NULL DROP TABLE paper_details; IF OBJECT_ID(exam_sessions) IS NOT NULL DROP TABLE exam_sessions; IF OBJECT_ID(answers) IS NOT NULL DROP TABLE answers; GO CREATE TABLE users( UserID int IDENTITY(1,1) PRIMARY KEY, UserName nvarchar(50) UNIQUE NOT NULL, Password nvarchar(64) NOT NULL, Role int NOT NULL DEFAULT 2, RealName nvarchar(50) NULL, ClassName nvarchar(100) NULL ); GO CREATE TABLE questions( QuestionID int IDENTITY(1,1) PRIMARY KEY, Type int NOT NULL, Content nvarchar(500) NOT NULL, OptionA nvarchar(200) NULL, OptionB nvarchar(200) NULL, OptionC nvarchar(200) NULL, OptionD nvarchar(200) NULL, RightAnswer nvarchar(50) NOT NULL, Score decimal(4,1) NOT NULL, Difficulty int NOT NULL DEFAULT 2, Status int NOT NULL DEFAULT 1, CreateTime datetime NOT NULL DEFAULT GETDATE() ); GO INSERT INTO users(UserName, Password, Role, RealName) VALUES(admin, CONVERT(varchar(32), HASHBYTES(MD5, 123456), 2), 0, 系统管理员);逻辑说明这个脚本先建库然后删旧表重建最后插入一个管理员账号。参数说明HASHBYTES 是 SQL Server 计算 MD5 的函数CONVERT 的第三个参数 2 表示把结果转成不带 0x 前缀的十六进制字符串这样和 C# 端的 MD5 结果对得上。papers、exam_sessions、answers 等表按同样的思路在源码脚本里补齐字段对应我第四节列的结构。注意这段脚本会清空所有表数据。如果数据库里已经有考试记录执行前务必先备份。实际项目里我更习惯把 DROP 语句单独放一个文件正式建库脚本只跑 CREATE避免手滑。3.3 教师出题手工录入与 Excel 批量导入的细节题目自拟有两种录题方式界面上逐题录入或者用 Excel 模板批量导入。批量导入的代码普遍用 NPOI 读 xlsx因为 NPOI 是开源库服务器上不装 Office 也能跑。关键循环长这样using NPOI.SS.UserModel; using NPOI.XSSFUserModel; using (FileStream fs File.OpenRead(题目模板.xlsx)) { IWorkbook workbook new XSSFWorkbook(fs); ISheet sheet workbook.GetSheetAt(0); for (int i 1; i sheet.LastRowNum; i) { IRow row sheet.GetRow(i); if (row null) continue; // 空白行直接跳过 string content row.GetCell(0)?.ToString()?.Trim(); if (string.IsNullOrEmpty(content)) continue; // 题干为空的行跳过 // GetCell(1) 题型GetCell(2)~GetCell(5) 选项A-D // GetCell(6) 正确答案GetCell(7) 难度GetCell(8) 分值 InsertQuestion(content, row.GetCell(1)?.ToString()?.Trim(), row.GetCell(2)?.ToString()?.Trim(), row.GetCell(3)?.ToString()?.Trim(), row.GetCell(4)?.ToString()?.Trim(), row.GetCell(5)?.ToString()?.Trim(), row.GetCell(6)?.ToString()?.Trim(), row.GetCell(7)?.ToString()?.Trim(), row.GetCell(8)?.ToString()?.Trim()); } }逻辑说明这个循环从第二行开始读第一行是表头。GetCell(0)?.ToString()的写法是必须的xlsx 里空单元格返回 null直接调用 ToString() 会报空引用异常。LastRowNum 是最后有内容行的下标Row.GetCell 返回的单元格如果没内容也可能是 null所以每一列都用带 ? 的链式调用。参数说明列顺序是模板里约定的一旦模板顺序变了这里的下标全部要跟着调。3.4 学生交卷事务避免半截答案进库学生交卷这个动作表面上是把 DataGridView 或界面控件里的答案写进数据库实际上必须保证“写答案”和“标记已交卷”这两件事同时成功。很多源码这里翻车写到一半报错学生既交不了卷成绩也没存上。用事务包起来的写法是标准做法using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); SqlTransaction tx conn.BeginTransaction(); try { foreach (var item in answers) { string sql INSERT INTO answers(ExamID, QuestionID, StudentAnswer) VALUES(eid, qid, ans); using (SqlCommand cmd new SqlCommand(sql, conn, tx)) { cmd.Parameters.AddWithValue(eid, examId); cmd.Parameters.AddWithValue(qid, item.QuestionId); cmd.Parameters.AddWithValue(ans, item.StudentAnswer ?? ); cmd.ExecuteNonQuery(); } } string update UPDATE exam_sessions SET IsSubmit1, SubmitTimeGETDATE(), AutoScorescore WHERE ExamIDeid AND IsSubmit0; using (SqlCommand ucmd new SqlCommand(update, conn, tx)) { ucmd.Parameters.AddWithValue(score, autoScore); ucmd.Parameters.AddWithValue(eid, examId); if (ucmd.ExecuteNonQuery() 0) throw new Exception(试卷已提交请勿重复操作); } tx.Commit(); } catch { tx.Rollback(); throw; } }逻辑说明先把所有答案插入 answers 表再更新 exam_sessions 的提交状态。关键在 UPDATE 语句里带上了IsSubmit0条件如果影响行数为 0说明这条考试记录已经提交过直接抛异常回滚从数据库层面拦住重复交卷。参数说明answers 里的 StudentAnswer 用??把 null 转成空字符串避免空值在批改比较时出问题。autoScore 是在事务外先算好的客观题分数事务里只负责写入。4. 常见问题与排查记录四个踩过的坑课程设计类源码大家都知道能跑和能演示是两回事。下面几个坑是我实际用这套系统时踩过的每个都按现象、原因、解决三步写清楚遇到类似问题可以直接照方抓药。4.1 批量导入 Excel 后中文全部变成问号现象通过“Excel 导入题库”功能导了 50 道题界面上看到的中文全变成了“”英文和数字正常数据库里也是问号。原因这套源码的导入模板如果是 xlsx一般不会乱码乱码多数出在 CSV 模板场景。CSV 文件用 Excel 打开后默认按 ANSI 编码保存而源码里用 StreamReader 默认按 UTF-8 读取两边编码不一致中文就牺牲了。另外还有种情况是单元格里本身有全角空格或特殊字符NPOI 读取时GetCell(0).ToString()把格式信息也带出来了不过那个一般不会乱码。解决优先用 xlsx 模板而不是 CSV 模板如果用 CSV读取时要指定编码常见做法是把new StreamReader(path)改成new StreamReader(path, Encoding.GetEncoding(GB2312))。导入前先用记事本打开模板确认编码比在代码里反复试验快得多。如果已经有乱码数据进库写一条DELETE FROM questions WHERE Content LIKE N%???%清理掉再重新导入。4.2 学生明明答对了自动批改却是 0 分现象单选题选了 A标准答案也是 A交卷后成绩单显示这道题 0 分。原因最常见的两个来源。一是标准答案或学生答案带首尾空格Excel 模板里单元格不小心按了空格存进数据库没被注意到二是判断题的标准答案是“T”或“F”而界面里的选项文本是“正确 / 错误”学生端存进 answers 表的是“正确”批改代码拿“T”和“正确”做字符串比较永远不相等。解决先把批改代码里的比较统一成Trim().ToUpper()这能解决八成问题。判断题的答案规范在 Excel 模板里就定好数据库存“T/F”界面选项的 Value 也绑成“T/F”显示文本才用“正确 / 错误”不要把两件事混在一个字段里。历史脏数据用 SQL 清洗UPDATE questions SET RightAnswer T WHERE RightAnswer 正确再把学生答案表同步改掉。4.3 交卷按钮点两次出现两条考试记录现象学生作答完点了一下“交卷”界面卡了半秒又点了一下结果成绩单里出现两条相同记录分数还被算了两次。原因界面上没有做防重复提交按钮点击事件里直接执行插入逻辑第一次请求没返回时第二次点击已经进来数据库端也没有唯一约束把关。这就是典型的前端控制不住、后端也不设防。解决两层保险。第一层是界面按钮点击后立刻btnSubmit.Enabled false; Application.DoEvents();防止用户物理双击第二层在数据库answers 表加联合唯一索引UNIQUE(ExamID, QuestionID)同一道题只能有一条作答记录。最关键的是交卷事务里那条 UPDATE 语句要带IsSubmit0条件影响行数为 0 就说明已经交过卷直接回滚并提示“请勿重复提交”。4.4 演示前一天发现学生账号密码忘了现象要演示学生端但记不住当时给学生设的密码试了十几次都不对。原因密码是 MD5 存储的不可逆谁都没法从数据库反推明文。这就暴露了一个设计问题系统里没有“重置密码”功能管理员端只能看账号列表改不了密码。解决不用重新写功能直接跑一条 UPDATE 把指定学生的密码重置掉。注意 MD5 要和 C# 端算法一致SQL Server 里这样写UPDATE users SET Password CONVERT(varchar(32), HASHBYTES(MD5, 123456), 2) WHERE UserName student01;执行完再用 student01 / 123456 登录。从那以后我每次交付这套系统前都会顺手把学生账号密码统一重置成 123456并把这条 SQL 存在项目目录里一个叫 reset_password.sql 的文件中省得演示现场翻手机找笔记。5. 快速验收三条 SQL 核一遍自动批改和成绩统计系统跑起来后别急着开始录 500 道题。先用三条 SQL 把自动批改、成绩统计和题库质量核一遍确认核心逻辑没毛病再往库里灌数据。这三条 SQL 也可以作为你理解这套源码数据流向的索引。5.1 按考试场次统计已交卷人数、平均分、及格率验收自动批改有没有生效最直接的方式是看一场考试的交卷人数和平均分。如果一场考试有 30 个学生已交卷人数是 30但平均分是 0那基本可以断定批改环节挂掉了。这条 SQL 适合跑在 exam_sessions 表上SELECT p.PaperName, COUNT(es.ExamID) AS 应考人数, SUM(CASE WHEN es.SubmitTime IS NOT NULL THEN 1 ELSE 0 END) AS 已交卷, CAST(AVG(es.AutoScore) AS DECIMAL(4,1)) AS 客观题平均分, SUM(CASE WHEN es.AutoScore 60 THEN 1 ELSE 0 END) AS 及格人数 FROM exam_sessions es JOIN papers p ON es.PaperID p.PaperID WHERE p.PaperName LIKE N%期中% GROUP BY p.PaperName;逻辑说明COUNT 统计所有发了卷的学生SUM 加 CASE 统计已经交卷的人AVG 算已交卷学生的客观题平均分。参数说明及格线 60 是按百分制算的如果你的试卷满分不是 100这里要按比例调整。这套源码通常一张卷对应一场考试如果一场考试有多张卷子GROUP BY 的粒度就需要改成试卷 ID 场次 ID否则统计口径会混。5.2 人工评分后重算最终成绩简答题是人工评分的教师在界面里给完 ManualScore 后最终成绩不会自动变。很多版本需要点一个“重算”按钮但有时候忘了点成绩单导出的还是旧分数。与其依赖界面上那只按钮不如直接跑 SQL 把最终成绩算一遍UPDATE es SET es.FinalScore ISNULL(( SELECT SUM(q.Score) FROM answers a JOIN questions q ON a.QuestionID q.QuestionID WHERE a.ExamID es.ExamID AND q.Type 4 AND a.IsRight 1 ), 0) ISNULL(( SELECT SUM(a.ManualScore) FROM answers a WHERE a.ExamID es.ExamID AND a.ManualScore IS NOT NULL ), 0) FROM exam_sessions es;逻辑说明FinalScore 由两块组成客观题部分找 IsRight1 的作答按问题分值求和主观题部分把人工分加起来。ISNULL 的作用是当某一块没有记录时按 0 算避免整个表达式变成空值。参数说明如果你的源码里客观题分值不存 q.Score 而是直接冗余在 answers 表里把 SUM 的参数换成 answers 表里的分值字段即可。提示执行这条 UPDATE 前最好先备份 exam_sessions 和 answers 两张表或者加个 exe 事务包一下。人工评分有时候是批量改的跑错了还能回滚。5.3 查题库里有没有重复题和空题抽题系统最大的隐患不是题量少而是重复题和空题。重复题意味着同一张试卷里可能出现两道一模一样的题干空题直接导致学生看到一个没有内容的选项。这两类数据用肉眼很难发现SQL 一查就现形SELECT Content, COUNT(*) AS 重复次数 FROM questions GROUP BY Content HAVING COUNT(*) 1;这条 SQL 专门找完全重复的题干。参数说明如果两题的文字描述一样但选项顺序不同GROUP BY Content 是查不出来的需要再按 Content OptionA OptionB 组合查。空题用另一条SELECT QuestionID, Type, Content FROM questions WHERE Content IS NULL OR LEN(Content) 0;逻辑说明LEN 会忽略尾随空格所以LEN(Content)0能把纯空格题也查出来。查出问题数据后删掉之前先确认这个题有没有被抽进 paper_details如果已经进卷了直接删题会把试卷中间抽出一个空位正确做法是先删 paper_details 里的关联行再删题库记录。6. 给评委演示之前先做一遍数据清理课程设计答辩最怕的就是演示翻车。翻了题的重复数据、上次班学生考过的旧记录、忘了密码的账号都会让现场变得很难看。所以每次演示前我都会强制走一遍清理流程把系统恢复到“考了一次试但还没人交卷”的干净状态。核心就是下面这段 SQLUSE OnlineExam; GO DELETE FROM answers; DELETE FROM exam_sessions; DELETE FROM paper_details; GO DBCC CHECKIDENT(exam_sessions, RESEED, 0); DBCC CHECKIDENT(answers, RESEED, 0); GO UPDATE users SET Password CONVERT(varchar(32), HASHBYTES(MD5, 123456), 2) WHERE Role 2; GO SELECT UserName, RealName, Role FROM users;逻辑说明这个脚本把答题记录、考试场次、试卷明细全部清空但保留题库和用户表这样演示时你不需要重新录题只需要新建一场考试让学生进入。删除顺序要从子表到主表先 answers再 exam_sessions最后 paper_details避免残留孤儿数据。DBCC CHECKIDENT 把自增 ID 重置回 0演示时新产生的记录从 1 开始编号界面上看起来整整齐齐。最后把学生账号密码统一重置成 123456避免现场发现学生端登不进去。这套源码的表大多用逻辑关联没有强外键约束所以 DELETE 可以按任意顺序执行。如果你的版本里加了外键或者级联删除需要把顺序反过来先删主表会导致外键冲突报错。另外如果演示前教师刚导入了 500 道题不要执行DELETE FROM questions清理脚本特意没碰题库就是不想让你好不容易准备的题目一夜回到解放前。从那以后我每次拿这套系统出去演示或者交给别人验收都会强制走一遍清库、重置密码、查重复题三步流程。不是为了秀操作而是为了不让自己在现场被脏数据搞得手忙脚乱。希望帮到你。本文还有配套的精品资源点击获取