ARTICLE DETAIL

资讯详情

深耕网站SEO优化与搜索引擎排名提升的一线实战洞察。

Spring Boot爬虫高考志愿推荐系统设计:数据采集与位次匹配算法实战

Spring Boot爬虫高考志愿推荐系统设计:数据采集与位次匹配算法实战 简介在信息爆炸的时代如何从海量数据中快速获取高价值信息是数据采集与后端开发的常见挑战。爬虫技术作为自动化获取公开数据的重要手段能够高效解决数据分散、格式不统一的问题。而推荐算法则通过对历史数据的建模分析为用户提供个性化决策支持。本文从爬虫技术的原理出发介绍如何利用HttpClient与Jsoup构建垂直型数据采集模块并详细讲解基于位次匹配的志愿推荐算法设计思路。通过一个基于Spring Boot的完整项目案例展示从数据清洗、多线程抓取到RESTful接口开发的全流程实践。该方案兼具技术深度与工程落地价值适用于Java毕业设计、爬虫与Web开发学习以及高考志愿填报等真实场景帮助开发者掌握从数据采集到智能推荐的核心技术链路。 每年高考季考生的志愿填报都是头等大事。我在大学时期就做过一个基于Spring Boot的爬虫高考志愿智能推荐系统今天把完整的设计思路和核心实现源码分享出来。这个项目的背景很直接高考结束后考生和家长面对数千所院校、上万个专业光靠翻阅纸质志愿指南根本看不过来更别说要横向对比历年录取分数、招生人数、位次变化这些动态数据。于是我就想能不能用爬虫自动抓取各省教育考试院和高校官网的公开招生数据再通过算法做一个智能推荐——你输入高考分数和省份系统直接给你算出“冲一冲、稳一稳、保一保”三个梯度的院校和专业清单。项目把爬虫数据采集、数据清洗、Spring Boot后端接口、基于位次匹配的推荐算法全部串联起来非常适合做Java毕业设计也可以作为想接触爬虫Web开发全流程同学的实战练手项目。1. 需求分析与系统整体设计在动手写代码之前先把需求理清楚。这个系统表面上看只是一个“输入分数推荐学校”的工具但拆开来看它至少要解决三个核心问题。第一数据从哪里来志愿填报最核心的数据是院校信息、专业目录、历年录取分数线、招生计划。这些数据分散在各省教育考试院官网、阳光高考平台、各高校招生网没有一个统一的开放API。用爬虫去采集是唯一可行的技术路线。考虑到数据源是公开的、非商业化的教育招生信息爬虫的请求频率不需要太高数据量级也在可接受范围内采用垂直型爬虫就够了。所谓垂直型爬虫就是只针对特定领域的特定网站进行定向采集比如只采集招生数据、只采集录取分数不搞全网漫游。第二推荐逻辑怎么算市面上很多“志愿填报App”的推荐算法其实很粗糙就是拿你的分数和往年分数线做差差值越小越靠前。但真正懂志愿填报的人都知道分数是每年波动的位次才是相对稳定的参考指标。举例来说2024年河南理科一本线是518分2023年是514分2025年可能又变直接用分数绝对值去对比往年录取线误差很大。所以我决定用“位次匹配法”——把用户今年的省排名/位次作为核心输入跟往年各院校录取的最低省排名做匹配落在合理区间内的才推荐。第三系统架构怎么搭这个项目采用经典的Spring Boot分层架构前端提供页面交互后端通过RESTful API提供数据服务数据存储用MySQL。爬虫模块单独拆出来作为一个可独立运行的调度任务每天凌晨自动增量更新数据。推荐算法封装成独立的Service方便替换和优化。整体技术选型如下后端框架Spring Boot 2.x配合MyBatis-Plus做ORM爬虫框架HttpClient Jsoup用ThreadPoolExecutor做多线程并发抓取定时任务Spring Schedule每天凌晨2点自动增量更新数据库MySQL 5.7存储院校、专业、分数线、用户行为数据前端简单Thymeleaf模板 Bootstrap或者独立Vue项目调用接口核心算法基于等位分换算 位次区间匹配 冲稳保梯度算法这套选型的理由很简单Spring Boot生态成熟、上手快MyBatis-Plus让CRUD代码量减少一半Jsoup做HTML解析非常顺手整个项目用一套技术栈就能跑通不需要额外引入重量级组件。对毕业设计而言这样架构清晰、代码可读性强答辩的时候也容易讲清楚。2. 爬虫模块设计数据采集与清洗爬虫是整个系统的数据源头这块做得扎实不扎实直接决定推荐结果的可靠程度。2.1 数据源梳理与字段定义我的数据源主要选了两个一是某省教育考试院官网的“历年录取统计”栏目这个页面是HTML表格形式结构规整包含院校代码、院校名称、科类理科/文科、录取批次、计划数、投档线、最低分、最低位次这些关键字段。二是阳光高考平台的院校库这个数据源主要补充分数线之外的静态信息比如院校所在省市、办学层次985/211/双一流/普通本科、办学性质公办/民办、院校简介、特色专业。院校库页面是列表详情页结构需要先抓列表拿到详情页的URL再逐条抓详情页解析。数据库里我建了四张核心表school院校基本信息字段包括id、school_name、school_code、province、level(985/211等)、nature(公办/民办)、tags(双一流等)major专业目录字段包括id、school_id、major_name、category、tuitionadmission_score历年录取分数线字段包括id、school_id、year、province、batch(本科一批/二批等)、subject_type(理科/文科)、min_score、min_rank、plan_countuser用户信息记录用户输入的分数、位次、省份、科类、偏好这里有一个容易踩坑的细节admission_score表里必须同时存分数和位次不能只存分数。因为有些省份考试院公布的数据只有分数没有位次位次需要自己根据“一分一段表”去换算。我在爬虫里加了一步如果页面里有位次就直接存如果没有则调用手工维护的“一分一段表”JSON文件用分数反查位次。2.2 多线程爬取与任务调度爬虫模块我用的是HttpClient 4.5发送HTTP请求Jsoup 1.12解析HTML。为什么不直接用WebMagic这种集成框架因为WebMagic虽然封装得好但针对这种页面结构相对固定、字段不算复杂的场景原生HttpClientJsoup反而更轻量、更好控制而且调试起来更直观。数据采集任务拆成两步走第一步抓列表页解析出所有院校详情页的URL列表。这里要对考试院的分页机制做适配大多数教育网站是传统分页的但有一个很隐蔽的坑——有些年份的录取统计表是单独的年份子页面URL规则不是按页码递增而是按年份参数切换。我在代码里先用一个for循环请求所有可能的年份页面再对结果做空值校验自动跳过不存在的页面。第二步用固定线程池并发抓取详情页。线程数我设置为8这个数值是实际测试出来的——太快了容易被服务器限流太慢了影响效率。8个线程配合随机延时1~3秒基本能稳定跑完全部数据不出问题。核心代码如下Component public class ScoreDataCrawler { private static final Logger log LoggerFactory.getLogger(ScoreDataCrawler.class); private static final int THREAD_NUM 8; private static final String BASE_URL https://example.gov.cn/score/; Resource private SchoolMapper schoolMapper; Resource private AdmissionScoreMapper scoreMapper; private final ThreadPoolExecutor pool new ThreadPoolExecutor( THREAD_NUM, THREAD_NUM, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(1000), new ThreadPoolExecutor.CallerRunsPolicy()); Scheduled(cron 0 0 2 * * ?) public void scheduledCrawl() { log.info(scheduled crawl start...); // 生成需要采集的年份默认近3年 ListString years getRecentYears(3); for (String year : years) { crawlYearData(year); } } private void crawlYearData(String year) { ListString detailUrls getDetailUrls(year); CountDownLatch latch new CountDownLatch(detailUrls.size()); for (String url : detailUrls) { pool.execute(() - { try { Document doc Jsoup.connect(url) .userAgent(Mozilla/5.0 (Windows NT 10.0; Win64; x64)) .timeout(10000) .get(); parseAndSave(doc); } catch (Exception e) { log.error(crawl failed: {}, url, e); } finally { latch.countDown(); } }); } try { latch.await(5, TimeUnit.MINUTES); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }这里有两个非常实用的细节一个是User-Agent必须伪装成真实浏览器否则很多教育网站直接拒绝请求。我用的是Chrome浏览器的UA串实际测试中成功率接近100%。另一个是拒绝策略必须用CallerRunsPolicy。如果不设置默认的AbortPolicy会在队列满时直接抛异常导致大量URL还没爬就中断了。CallerRunsPolicy的意思是线程池满了之后新任务由调用线程自己执行这样就是“爬取速度自动降级”不会丢任务。2.3 数据清洗与去重策略爬完的原始HTML不能直接入库因为考试院页面的数据结构并不统一。比如有的年份表的表头是“院校代码、名称、投档线、最低分”有的年份是“院校代码、名称、投档线、最低分、最低位次”字段数量和顺序都可能变。我的解析代码里做了一个“自适应表头映射”——先读取表格的表头行把每个字段的列索引动态绑定到实体属性上不写死第几列是什么。去重逻辑用了一个很朴素的方案以“school_code year province subject_type”作为唯一索引插入前先查一遍存在就跳过。这个方案在数据量不大的时候完全够用比引入Redis的布隆过滤器或者复杂的分布式锁要简单得多。爬虫运行的稳定性我额外加了一层失败重试机制private Document getWithRetry(String url, int retryTimes) { for (int i 0; i retryTimes; i) { try { return Jsoup.connect(url) .userAgent(RANDOM_UA.get(new Random().nextInt(RANDOM_UA.size()))) .timeout(10000) .get(); } catch (IOException e) { log.warn(request failed {}, retry {}/{}, url, i 1, retryTimes); try { Thread.sleep(2000L * (i 1)); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); } } } return null; }重试的延时按次数递增第1次等2秒第2次等4秒第3次等6秒这种“退避策略”比固定延时更合理能给服务器喘息的空间。3. 志愿推荐算法从分数线到智能匹配爬虫把数据采到手之后真正的重头戏就是推荐算法。这一节我会把推荐逻辑的核心思路和代码都拆开讲这部分也是整个系统的技术亮点。3.1 位次匹配与等位分换算前面提过分数会随年份波动位次相对稳定。但这里有个隐藏问题不同年份的高考人数和招生计划不同相同的位次在不同年份含金量也不同。比如2024年河南理科考生多了5万人那2024年的第80000名比2023年的第80000名“更靠前”。为了更科学地对比我引入了一个“等位分”的概念。把用户今年的位次用今年的“一分一段表”反算出今年的分数这个分数叫“原始分”。然后把这个原始分映射到往年的一分一段表里算出如果他在往年考这个分数应该是多少位次——这个位次就是“等效位次”。再用等效位次去跟往年录取位次对比。但是这个流程需要手工维护三年的“一分一段表”数据工作量不小。实际上我在第一版实现的时候做了一点妥协直接用“位次区间匹配法”把“等位分换算”作为二期的优化内容。因为对于毕业设计或者初版系统来说直接用位次匹配已经能保证较高准确率了。具体匹配逻辑分三步第一步找基准位次。用户输入高考分数后系统通过本省当年的“一分一段表”查到位次。如果用户直接输入位次就更好。第二步筛选候选院校。遍历某省某科类的所有录取记录把用户位次跟各院校往年最低录取位次比较。这里的核心是“冲稳保”梯度的分段阈值冲院校往年最低录取位次比用户位次低数字更大5%~15%意味着录取门槛略高于用户水平有一定概率冲上去稳位次差距在-5%~5%之间属于正常发挥区间保院校往年最低录取位次比用户位次高数字更小10%~25%录取概率很大这里有个容易误解的点位次数字越小代表成绩越好、学校越好。用户位次5万名院校录取位次3万名说明这个学校比用户水平高需要“冲”院校录取位次8万名说明这个学校比用户水平低是“保”。代码里一定要把大小关系理清楚我第一版就是把这个搞反了推荐结果整个颠倒。第三步多维度加权排序。同梯度的院校不止一所怎么排优先级我设计了四个维度的加权评分院校往年录取位次跟用户位次的匹配度权重50%院校综合实力985/211/双一流标签权重25%院校所在城市发展水平权重15%招生计划数变化趋势扩招加分、缩招减分权重10%最终按加权总分从高到低输出前20所院校再按“冲稳保”分桶返回每桶5~8所总共20所左右这样用户查看起来不累决策效率高。3.2 推荐算法核心代码实现推荐算法我单独封装了一个RecommendService核心方法如下Service public class RecommendService { Resource private AdmissionScoreMapper scoreMapper; Resource private SchoolMapper schoolMapper; private static final double CHONG_BOUNDARY 0.15; private static final double WEN_BOUNDARY 0.05; private static final double BAO_BOUNDARY 0.25; public RecommendResult recommend(RecommendRequest request) { int userRank request.getRank(); String province request.getProvince(); String subjectType request.getSubjectType(); // 1. 查询该省该科类近3年所有录取记录 ListAdmissionScore allScores scoreMapper.selectAllWithSchool( province, subjectType); // 2. 划分冲稳保 ListSchoolWithScore chongList new ArrayList(); ListSchoolWithScore wenList new ArrayList(); ListSchoolWithScore baoList new ArrayList(); for (AdmissionScore score : allScores) { // 使用近3年最低位次的平均值作为参考 int avgMinRank getAvgMinRank(allScores, score.getSchoolCode(), score.getProvince(), score.getSubjectType()); double gap (double) (userRank - avgMinRank) / avgMinRank; if (gap WEN_BOUNDARY gap CHONG_BOUNDARY) { chongList.add(new SchoolWithScore(score, avgMinRank)); } else if (gap -WEN_BOUNDARY gap WEN_BOUNDARY) { wenList.add(new SchoolWithScore(score, avgMinRank)); } else if (gap -BAO_BOUNDARY gap -WEN_BOUNDARY) { baoList.add(new SchoolWithScore(score, avgMinRank)); } } // 3. 加权排序后分桶返回 RecommendResult result new RecommendResult(); result.setChongList(sortByScore(chongList, request).subList(0, Math.min(8, chongList.size()))); result.setWenList(sortByScore(wenList, request).subList(0, Math.min(8, wenList.size()))); result.setBaoList(sortByScore(baoList, request).subList(0, Math.min(8, baoList.size()))); return result; } }getAvgMinRank方法的核心逻辑是按schoolCode和year分组取近三年录取位次的平均值。这里有个细节有些院校某一年没有在榜单里出现比如当年停招了那一年的数据就得跳过不能当0处理。所以代码里要统计实际有数据的年份数用“总位次/有效年份数”计算平均值而不是简单的“总位次/3”。sortByScore方法的加权评分代码如下private ListSchoolWithScore sortByScore(ListSchoolWithScore list, RecommendRequest request) { // 计算每个院校的综合评分返回排序后的列表 // 分数 位次匹配度得分 * 0.5 办学层次得分 * 0.25 城市得分 * 0.15 扩招趋势得分 * 0.1 list.sort((a, b) - { double scoreA computeTotalScore(a, request); double scoreB computeTotalScore(b, request); return Double.compare(scoreB, scoreA); }); return list; }整个算法逻辑下来一条SQL查完所有录取记录内存里做计算响应时间基本在200毫秒以内完全够用。不需要引入Spark或者复杂的计算引擎杀鸡焉用牛刀。3.3 推荐结果的业务校验算法算出来的结果不能直接就推给用户还有两道业务校验要做这两道校验是跟真正的志愿填报规则对齐的也是我给这个系统加分的细节。第一道是批次过滤。用户是一本线上推荐结果必须在本科一批范围内不能推荐专科批次也不能推荐提前批。如果用户分数只够二本推荐列表里就绝不能出现一本院校。这个逻辑很简单用admission_score表里的batch字段过滤就行。第二道是专业意向匹配。用户如果在个人中心勾选了“计算机类”“电子信息类”等专业偏好系统在院校推荐的基础上还要计算该校在偏好专业上的实力——比如是否有一级学科博士点、是否国家级特色专业。因为我把专业详情数据也爬下来了所以可以做一个简单的“院校-专业实力”关联筛选把那些虽然学校名气大但目标专业很鸡肋的院校降低权重。这两道校验做完推荐结果才算真正可用。4. Spring Boot后端核心模块实现后端是整个系统的心脏爬虫采完数据是原料算法算完结果是半成品后端提供的是面向用户的最终服务。4.1 项目结构与数据库设计项目包结构我是这样组织的com.example.gaokao ├── controller // REST接口层 ├── service // 业务逻辑层推荐算法、爬虫调度 ├── mapper // MyBatis-Plus数据访问层 ├── entity // 数据库实体 ├── dto // 前端交互对象 ├── config // 配置类CORS、线程池、定时任务 ├── crawler // 爬虫模块 ├── common // 公共工具类 └── GaokaoApplication.java数据库脚本直接在项目resources目录下放一个init.sql方便别人克隆代码后一键建表。表结构我前面提过这里补充一个关键点admission_score表上一定要建组合索引索引字段是(province, subject_type, year)因为推荐算法的SQL主要就是按这三个字段过滤的。不建索引数据量到10万条以上查询就会明显变慢。4.2 核心接口设计前端或者APIfox测试主要调用这几个接口POST /api/recommend传入参数{score, rank, province, subjectType, preferences}返回冲稳保三组院校列表GET /api/school/{id}查看院校详情包含历年分数线、专业列表、办学层次等GET /api/rank/convert一分一段表查询输入分数返回位次区间POST /api/user/favorite收藏心仪院校方便对比接口设计遵循RESTful风格返回统一Result结构体code为0表示成功非0表示业务异常。这里有一点小经验统一返回结构的好处是前端处理逻辑简单不管成功失败都是同一个数据结构不会出现“有时候返回数组有时候返回对象”的尴尬局面。核心的推荐接口实现RestController RequestMapping(/api) public class RecommendController { Resource private RecommendService recommendService; PostMapping(/recommend) public ResultRecommendResult recommend(RequestBody RecommendRequest request) { // 参数校验分数和位次必须至少传一个 if (request.getScore() null request.getRank() null) { return Result.error(分数和位次不能同时为空); } RecommendResult result recommendService.recommend(request); return Result.success(result); } }4.3 前端页面与交互前端我用的是Thymeleaf Bootstrap做的服务端渲染没有单独拆Vue项目。为什么这么选因为毕业设计的时间有限Vue虽然交互体验好但要额外搭一套前后端分离的工程还要处理跨域问题、打包部署问题对于一个CRUD为主的系统来说太重了。Thymeleaf直接嵌入Spring Boot写几个HTML页面就能跑核心交互就三个首页输入分数/位次、省份、科类点击“智能推荐”推荐结果页三个Tab分别展示冲稳保院校卡片卡片上显示院校名称、办学层次、所在城市、历年录取位次趋势、推荐指数院校详情页展示完整信息有“收藏对比”按钮页面提交后使用Ajax调用推荐接口前端拿到数据后动态渲染卡片不需要刷新整个页面。为了演示效果更好还有一个“位次趋势图”的展示直接用ECharts生成近3年录取位次的折线图用户一眼就能看出这个学校历年是涨了还是降了这个细节在答辩的时候很加分。5. 部署启动与二次开发指南这部分写给想把这个项目跑起来或者改造成自己毕业设计的同学从环境准备到最终部署全套流程。5.1 环境要求与启动步骤JDK 1.8Spring Boot 2.x必须8以上推荐8或11Maven 3.6MySQL 5.7IDEIntelliJ IDEA或Eclipse推荐IDEA启动步骤分四步第一步创建数据库并导入表结构。执行init.sql里面包含建库语句和建表语句。注意修改数据库连接配置在application.yml里改成你自己的用户名密码。第二步运行爬虫模块。因为爬虫是定时调度的默认每天凌晨2点跑但你第一次启动项目时要手动触发一次可以用两种方式在CrawlerController里加一个临时的GET接口手动触发或者直接写一个ApplicationRunner在项目启动完后自动执行一次全量爬取。我推荐用ApplicationRunner这样别人clone代码启动后啥都不用管数据自动就有。Component public class DataInitRunner implements ApplicationRunner { Resource private ScoreDataCrawler scoreDataCrawler; Override public void run(ApplicationArguments args) { // 如果数据库没有数据启动时自动爬取 if (scoreDataCrawler.isDataEmpty()) { scoreDataCrawler.scheduledCrawl(); } } }第三步启动Spring Boot项目访问http://localhost:8080首页就能看到推荐系统的主界面。第四步如果想把爬虫改成手动控制可以在配置里把定时任务的cron注释掉改成在后台管理页面点击“数据更新”按钮触发。5.2 如何改成自己的毕业设计很多人拿到源码后不知道怎么改成自己的题目我提供几个延展方向方向一把推荐算法换成“基于协同过滤的推荐”。现在用的是位次匹配规则算法适合规则明确、可解释性强的场景。如果你想往大数据方向靠可以引入用户历史选择行为数据用协同过滤算“和你分数差不多的考生都选了哪些学校”这也是一种不同的推荐思路可以写成对比分析。方向二增加“专业就业率预测”模块。爬虫里已经采集了专业信息可以再爬取某招聘平台的公开岗位数据分析各专业的薪资水平和岗位需求数量给用户展示这个专业的就业前景。这个方向很讨喜因为考生和家长都关心毕业去向做出来效果很直观。方向三换成Python爬虫版。虽然Spring Boot的爬虫已经够用但如果你对Python更熟可以把爬虫模块单独抽出来用Python实现通过REST接口或者直接写MySQL表跟Spring Boot对接。我见过不少同学是“Java后端Python爬虫”混搭的架构答辩时也能解释清楚。5.3 部署到云服务器的注意事项有同学想把项目部署到云服务器上让手机也能访问。这里有几个坑提前告诉你MySQL编码问题建表时一定要用utf8mb4字符集因为爬虫采集的数据里有生僻字、特殊符号utf8mb4是唯一能完整支持所有中文和emoji的字符集。用utf8会有概率出现“乱码报错”。内存占用问题Spring Boot项目打包后是个300MB左右的jar包运行内存大约300~500MB。云服务器至少要选2G内存的配置否则容易内存溢出。爬虫部署问题爬虫如果在云服务器上跑外网访问教育考试院网站的速度可能很慢Timeout时间要适当调大。另外云服务器的IP可能在对方的黑名单里如果连续请求被拒绝可以换一个数据源或者在本地上跑爬虫、在服务器上只跑推荐服务。6. 常见问题与避坑经验总结做这个项目过程中踩了不少坑我把高频问题和解决方案整理成一张速查表照着排查能省很多时间。问题表现原因解决方案爬虫抓不到数据返回空文档或HttpStatus 403User-Agent未伪装或请求频率太高加上浏览器UA适当延长随机休眠时间2~4秒JSON解析报错FastJSON报错“syntax error”页面结构动态变化字段缺失用Jsoup定位表格节点而不是全局JSON解析解析后做空值兜底推荐结果为空前端显示“暂无推荐”该省该科类的数据没采集到或位次超出推荐区间检查admission_score表是否有当前省份数据调大冲/保的边界阈值位次数据为0推荐结果里出现很多双一流部分院校往年位次缺失被当成了“低分录取”对位次为0的记录做数据校验缺失时用所在批次分数线反推一个估算位次前端跨域报错浏览器控制台CORS报错前后端分离时后端没有开启跨域在Spring Boot里配置CorsFilter允许所有来源访问除了上面的速查表还有一个我印象特别深的坑Jsoup解析表格时如果目标网站的表头不是固定的th标签而是td标签加样式控制用select(th)选择器会解析不到任何数据。这种情况我把选择器改成select(tr).first().select(td,th)取表格第一行的所有单元格作为表头能兼容两种写法。另外一个经验是爬虫产出的数据一定要做“人工抽查”。我第一版跑完发现某省的数据有两所985院校的最低分莫名很低排查后发现是网页上有两行数据是“中外合作办学”的独立招生代码页面样式没区分被当成普通专业采集了。后来我在解析逻辑里加了按招生类型过滤的规则只保留“普通类”的数据。还有一个小技巧如果你想快速验证推荐算法的准确性可以拿上一年的投档线做“回测”——用2023年的用户位次跑一遍系统看推荐出来的院校里实际2023年正常录取结果是否跟推荐匹配。我给这个项目做回测时冲稳保的整体匹配率做到了65%左右作为毕业设计演示效果完全够。项目跑通之后我自己又迭代了几个小版本把城市偏好做成了选择题而不是下拉框在院校推荐卡片上增加了“公办/民办”标签给历史位次趋势图加了一个“逐年柱状折线”的双展示模式。这些细节改动对用户体验的提升非常明显也方便你在项目答辩的时候说清楚自己做了哪些优化工作。如果你也想拿这个项目做毕业设计我建议先把核心流程跑通再挑一个自己感兴趣的方向优化——不用面面俱到但在一个点上做到深入答辩效果就很好了。本文还有配套的精品资源点击获取
返回列表