SpringBoot+Vue实现形成性考核管理系统:从业务建模到实战部署全解析
第一次在需求会上听到“形成性考核管理系统”这个名字时我第一反应是这不就是给老师做个录成绩的系统嘛SpringBootVue一套组合拳下来前后端分离表格一摆搞定了。但真正把需求一条条捋清楚再跑完第一轮联调之后我才意识到这玩意儿真正难的地方不在CRUD而在“形成性考核”这四个字背后的一整套教学评价思路。这篇文章我不会去贴那种“项目简介 功能列表 截图”式的演示稿而是把这套系统从业务设计、数据库建模、后端SpringBoot实现、前端Vue实现到联调部署阶段踩过的真实问题完整拆开讲一遍。项目本身不算复杂但里面涉及的知识点覆盖面很广SpringBoot配置、JWT鉴权、MyBatis-Plus分页、Vue路由守卫、动态菜单、Excel批量导入、成绩权重计算……适合正在做毕设的同学也适合公司内部要快速搭建教学管理类系统的后端和前端开发。我会尽量讲清楚每一步“为什么这么做”而不只是“怎么做”。1. 项目背景与整体设计思路1.1 先搞清楚“形成性考核”的业务本质传统考核方式大家都不陌生平时成绩占30%期末卷面占70%期末一考定乾坤。这种模式下学生平时学得怎么样、作业交没交、课堂参与度高不高基本没有数据沉淀学期末老师凭印象给个平时分学生有意见也说不出所以然。形成性考核Formative Assessment的核心逻辑正好相反它把评价拆到整个教学过程中强调对学习过程的持续观察、记录和反馈。放到系统里就意味着同一个学生的成绩不是一条记录而是一堆过程性数据出勤情况、课堂表现、章节测验、课后作业、小组项目、期中阶段性测试……每一项都要单独记录、单独评分最后再按权重折算成总评。这就带来第一个设计上的转变系统不能设计成“成绩单管理”而要设计成“考核项管理”。老师的核心操作不是期末写一个分数而是维护好一门课程下有哪些考核项、每个考核项权重是多少、按什么规则计分。学生端看到的也不是一个最终分数而是一个持续更新的过程性评价视图哪项得分高哪项拖了后腿下一阶段该往哪补。这个视角一确定后面的表结构、接口设计、前端页面组织就全都顺了。1.2 为什么选SpringBootVue而不是其他方案这套系统我选的是SpringBoot Vue前后端分离架构。先说后端SpringBoot在这个场景下几乎是零思考的默认选择Maven管理依赖、内嵌Tomcat一键启动、生态里要什么有什么。这个项目涉及用户登录、角色权限、课程管理、成绩录入、Excel导入、统计报表Spring Boot加MyBatis-Plus、Sa-Token或JWT、EasyExcel这些成熟组件都能直接拿到现成方案开发效率非常高。前端选Vue而不是React更多是从团队和业务形态考虑。这类管理系统的主要界面就是表单、表格、弹窗、统计图Vue的模板语法和双向绑定在这种场景下非常顺手Vue Router做路由守卫控制页面权限也很直观。如果团队里有人已经熟悉Vue那上手速度会快很多后续维护成本也低。版本选择上要注意一个坑SpringBoot 3.x 必须搭配JDK17以上而目前很多高校机房和公司生产环境还在用JDK8如果环境不允许升级那就老老实实选SpringBoot 2.7.18这是2.x系列的最终维护版本稳定性和兼容性都经过大量验证。我这次开发环境是JDK8所以选了2.7.18前端对应用Vue3 Vite Element Plus。如果你只有JDK8生态想让SpringBoot也跑2.x那也没问题下面的代码和方案同样适用。1.3 整体功能模块划分结合业务需求系统按角色切分成三条主链路管理员端维护基础数据。包括用户管理教师账号维护、学生账号导入、院系/班级管理、课程开设与排课、学期学期设置。教师端日常教学考核的核心操作区。包括我的课程、考核项配置、成绩录入、成绩复核与发布、带权重的总评计算、班级成绩统计导出、过程性评价反馈。学生端查看个人考核进度。包括我的课程、各考核项得分明细、过程性评价曲线、阶段排名可选、成绩确认与反馈申诉。整体架构上后端按传统的Controller-Service-Mapper三层组织前端按页面组件状态管理拆分。登录时签发JWT前端存到本地存储请求时通过Axios拦截器自动带token后端通过拦截器做统一鉴权。这套链路是前后端分离项目的标准设计但这个项目里有一个特殊点——同一门课、同一个学生、不同考核项之间都有独立权限所以后端接口在做数据查询时不能只校验“是否登录”还得校验“当前登录教师是否真的教这门课”。这一点在代码里需要非常小心。2. 数据库设计与核心业务模型2.1 核心表结构与建表SQL数据库设计是这个项目的地基。如果你把表结构设计成“一张成绩表 一个总分字段”那所有后续功能都会越写越别扭因为那些过程性明细、权重、状态流转全都没有落点。我设计时会拆成这几张核心表。第一张是用户表不做复杂拆分统一放user表用role字段区分admin、teacher、student三种角色。字段包括user_id、username、passwordBCrypt加密存储、real_name、role、class_id学生所属班级、teacher_no或student_no、email、phone、status、create_time。第二张是课程表course表存course_id、course_name、course_code、term_id、teacher_id、credit、description、status。这里有一个容易忽略的点一门课只挂一个负责教师但很多真实业务里会有多个老师共同授课比如一个讲理论、一个带实验。如果系统面向的场景比较复杂建议把授课关系拆成独立的course_teacher表而不是直接放course表里这样后续扩展“助教”“多教师评教”都很方便。第三张是选课表student_course表记录学生和课程的关联字段包括id、student_id、course_id、status。第四张是考核项表assessment_item表存课程下的考核项配置。字段包括item_id、course_id、item_name、item_type、weight、max_score、sort_order、status。比如“课堂出勤”权重10%、“课后作业”权重30%、“期中测试”权重30%、“期末综合实践”权重30%。第五张是成绩明细表score_record表保存每个学生–课程–考核项的具体得分。字段包括record_id、student_id、course_id、item_id、scoredecimal、comment、recorded_by、record_date、status。这张表是所有报表的数据来源一定要加合理的唯一约束和索引。简单建表SQL如下CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(64) NOT NULL, password varchar(128) NOT NULL, real_name varchar(64) DEFAULT NULL, role varchar(16) NOT NULL COMMENT admin/teacher/student, class_id bigint DEFAULT NULL, status tinyint DEFAULT 1, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE course ( id bigint NOT NULL AUTO_INCREMENT, course_name varchar(128) NOT NULL, course_code varchar(64) DEFAULT NULL, term_id bigint DEFAULT NULL, teacher_id bigint DEFAULT NULL, credit decimal(4,1) DEFAULT NULL, status tinyint DEFAULT 1, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE student_course ( id bigint NOT NULL AUTO_INCREMENT, student_id bigint NOT NULL, course_id bigint NOT NULL, status tinyint DEFAULT 1, PRIMARY KEY (id), UNIQUE KEY uk_student_course (student_id, course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE assessment_item ( id bigint NOT NULL AUTO_INCREMENT, course_id bigint NOT NULL, item_name varchar(128) NOT NULL, item_type varchar(32) DEFAULT NULL COMMENT attendance/homework/quiz/performance/project, weight decimal(5,2) NOT NULL, max_score decimal(5,2) DEFAULT NULL, sort_order int DEFAULT 0, status tinyint DEFAULT 1, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE score_record ( id bigint NOT NULL AUTO_INCREMENT, student_id bigint NOT NULL, course_id bigint NOT NULL, item_id bigint NOT NULL, score decimal(5,2) NOT NULL, comment varchar(255) DEFAULT NULL, recorded_by bigint DEFAULT NULL, record_date date DEFAULT NULL, status tinyint DEFAULT 1 COMMENT 1草稿 2已发布 3已确认, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_stu_course_item (student_id, course_id, item_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;2.2 权重计算模型与成绩算法形成性考核的最终“总评成绩”不是简单加和而是按权重折算。举个例子某门课设置了5个考核项考勤10%、课堂表现20%、课后作业30%、小组项目20%、期中测试20%。学生各项目得分分别是90、85、92、88、86则总评 90×10% 85×20% 92×30% 88×20% 86×20% 88.2。这个东西看起来就是个小学数学但在系统里有一个坑浮点数精度问题。用double直接算很可能出现88.19999999999这种结果。所以业务代码里所有和分数、权重相关的计算必须用BigDecimal不能偷懒用double或float。权重本身在数据库里用decimal(5,2)在服务端统一用BigDecimal做乘加运算最后再四舍五入保留两位。成绩状态流转上我设计了三态草稿、已发布、已确认。教师录入成绩后默认是草稿学生不可见教师批量确认提交后变成已发布学生端可以看到分数学生如果对分数没有异议可以点确认按钮状态变成已确认这条成绩就锁定了。这个状态设计在真实教学场景里很有必要否则老师刚录完还没检查完学生那边就已经看到了很容易引发投诉。2.3 唯一索引是防止重复成绩的关键score_record表里一定要加上(student_id, course_id, item_id)三个字段的联合唯一索引。这个设计很多人会忽略但实际使用时价值很大。教师在Excel导入或者页面多次点击保存时如果没有唯一索引约束很可能对同一个学生的同一个考核项生成两条分数记录最后统计总评时成绩对不上。有唯一索引兜底后录入接口可以选择“发生冲突则更新分数”天然就是一个幂等保存逻辑不管点几次提交结果都是一致的。3. 后端SpringBoot核心实现细节3.1 工程初始化与依赖选型我这里的SpringBoot版本用的2.7.18JDK8核心依赖包括Web、MyBatis-Plus、MySQL驱动、Lombok、Validation、JWT、EasyExcel。pom.xml里几个关键依赖如下dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdcom.alibaba/groupId artifactIdeasyexcel/artifactId version3.3.2/version /dependency注意jjwt 0.9.1在JDK8下没问题但如果你用了JDK9以上里面依赖的javax.xml.bind需要额外引入不然启动会报错。SpringBoot项目的配置类里除了常规数据源还需要重点配置MyBatis-Plus的分页插件这是一个高频出错点后面专门讲。还要注意默认的Jackson序列化如果不对时间格式做配置Java里的LocalDateTime返回给前端会变成一大串数字前端解析起来很痛苦我用一个全局配置统一了格式Configuration public class JacksonConfig { Bean public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { return builder - { builder.simpleDateFormat(yyyy-MM-dd HH:mm:ss); builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); builder.deserializers(new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); }; } }配置完成后后端返回给前端的时间字段就是2025-01-15 09:30:00这种格式前端不用再做额外处理。3.2 JWT登录鉴权与角色权限控制登录逻辑不复杂用户在登录接口传入用户名密码后端校验通过后签发JWT返回给前端前端后续每次请求在Authorization头里带上token。JWT的核心工具类我直接用的jjwt生成token时放入userId和role设置过期时间一般设置为7天足够一个考核周期使用。为了不让鉴权逻辑散落在各个Controller里我用一个HandlerInterceptor统一处理。拦截器里先放行登录接口和静态资源其余请求从Header里解析token、解析用户信息放ThreadLocal里方便后续业务代码使用。角色权限再用自定义注解处理后台管理接口加了RequireRole(admin)教师端数据接口加了RequireRole(teacher)通过AOP做校验。这里有一个实际教训拦截器里解析token、查用户信息时尽量做一层缓存比如用本地Caffeine缓存或者Redis否则每个请求都查一次数据库在高并发下数据库压力会很大。形成性考核系统的并发量本身不高但如果在期末集中录入成绩的几天里几十个老师同时操作再加上学生端查询数据库连接数还是会有波动缓存是必要的。3.3 成绩录入与自动折算的实现要点成绩录入是这个系统最核心的接口。教师端有两种录入方式一种是在页面上逐条录入另一种是下载Excel模板、批量导入。逐条录入时后端接口的幂等性就靠前面说的唯一索引保证落库逻辑很简单。批量导入我用的EasyExcel读取每个Sheet时先校验模板头是否符合预期再逐行校验学生学号是否存在、该学生是否选了这门课最后组装成List批量插入。注意批量插入不要一条一条insertMyBatis-Plus的saveBatch或者自己写XML的foreach都能实现1000条数据一次提交也就几百毫秒。成绩录入完成后总评成绩的自动折算不能在前端算必须后端算因为前端JS的浮点数问题更严重。我提供一个calculateTotalScore接口针对某门课程查询该课程所有考核项配置和学生成绩记录按权重计算总评public BigDecimal calculateTotalScore(Long courseId, Long studentId) { ListAssessmentItem items assessmentItemMapper.selectList( new LambdaQueryWrapperAssessmentItem() .eq(AssessmentItem::getCourseId, courseId)); ListScoreRecord records scoreRecordMapper.selectList( new LambdaQueryWrapperScoreRecord() .eq(ScoreRecord::getCourseId, courseId) .eq(ScoreRecord::getStudentId, studentId)); MapLong, ScoreRecord recordMap records.stream() .collect(Collectors.toMap(ScoreRecord::getItemId, Function.identity())); BigDecimal total BigDecimal.ZERO; for (AssessmentItem item : items) { ScoreRecord record recordMap.get(item.getId()); if (record ! null record.getScore() ! null) { total total.add(record.getScore() .multiply(item.getWeight()) .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP)); } } return total.setScale(2, RoundingMode.HALF_UP); }整个服务层方法加上Transactional确保状态修改和成绩明细写入要么一起成功要么一起回滚防止出现“已发布但明细没写完”的中间状态。3.4 考核项配置与权重校验教师端配置考核项时表单里有item_name、item_type、weight、max_score这些字段。前端提交时要做一次权重总和校验但前端校验只是体验优化真正的校验必须放后端因为前端可以绕过。后端在保存考核项时会把这门课的所有项查出来加上新提交的这条一起校验权重之和是否需要严格等于100。权重之和是否强制等于100这里有个业务上的选择如果只是录平时成绩最终总评还要跟期末考试成绩按比例合成那么课程下的平时考核项权重之和可以是100%不必再留一个期末项如果系统把期末考试也作为一个考核项纳入管理那就把期末测试也配成一个权重占70%的考核项。我的系统两种方式都兼容但后端逻辑上统一要求一个课程下所有考核项权重之和必须等于100%如果不等于接口直接抛业务异常提示老师检查配置。4. 前端Vue实现的关键细节4.1 前端工程搭建与Axios封装前端工程我用的Vue3 Vite创建命令npm create vitelatest assessment-web -- --template vue目录结构上src下分为api、assets、components、router、store、views这几个常规目录。UI组件库用的Element Plus按需引入避免全量打包体积过大。Axios封装是每个前后端分离项目必须做的事。我在src/utils/request.js里统一创建axios实例设置baseURL为/api请求拦截器里从localStorage取出token并放到Authorization头响应拦截器里统一处理后端返回的code码401时自动跳转登录页并清理本地登录态。import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(error.response?.data?.msg || 网络异常) return Promise.reject(error) } ) export default request4.2 多角色路由守卫与菜单权限Vue Router里按角色分块设计路由登录页是公共路由admin、teacher、student三端各自的页面放在不同目录下。每个受保护路由的meta里带上roles数组比如{ path: /teacher/course, name: TeacherCourse, component: () import(/views/teacher/CourseList.vue), meta: { title: 我的课程, roles: [teacher] } }全局路由守卫beforeEach里先判断是否存在token没有就跳登录页有token再判断当前用户角色是否在路由meta.roles里不在就跳403页面。这个逻辑保证了学生访问教师端接口页面时直接在前端就被拦住减少无效请求。动态菜单这块我没有做太复杂直接在侧边栏组件里根据当前用户角色渲染不同菜单列表。因为系统角色只有三个菜单结构固定不需要从后端动态下发菜单配置那样反而增加维护成本。如果未来角色多了、权限细了再考虑用后端返回的权限码动态过滤菜单。4.3 学生端与教师端核心页面设计教师端最常用的页面是成绩录入页。这个页面我用一个el-table展示某个班级某个课程的所有学生行是学生列是考核项每个单元格是一个可编辑的el-input-number。页面布局上左侧是课程列表右侧是成绩录入表格考核项横向动态渲染。这里用到了Vue的动态列渲染根据后端返回的assessmentItemList来生成columns所以无论老师配置了几个考核项页面都不需要改代码。还有一个比较麻烦的需求是“学生查看过程性趋势”。学生端除了展示各考核项得分明细表我还用ECharts的雷达图展示学生在各考核项上的得分占max_score的比例让学生直观看到自己在哪方面强、哪方面弱。这个需求需要后端提供一个接口返回当前学生所有已发布成绩的明细前端组装成雷达图格式。学生端还有一个“成绩确认”按钮按照业务设计学生对已发布成绩没有疑问后点击确认状态从已发布变成已确认。这个确认动作要限制只能确认自己的成绩后端接口大概率会自己从token解析userId而不是信任前端传过来的studentId否则学生可以互相确认成绩。4.4 表单校验与Excel导入体验教师配置考核项时表单校验用Element Plus的rules就行前端先校验权重和是否等于100保证用户体验。但后端仍然要再校验一次这是底线思维。Excel导入时页面提供一个模板下载按钮教师下载模板、填好分数、再上传前端用el-upload限制文件类型为xlsx上传成功后后端返回成功数和失败明细。失败明细这条很重要否则老师不知道哪几行没进去、为什么没进去。我是把每行的校验错误拼成一个list返回前端以表格弹窗展示逐条说明“第3行学号2021010103不存在”老师能直接对照模板修改。5. 常见问题与排查经验实录5.1 跨域请求失败怎么排查前后端分离项目第一个碰到的问题就是跨域。本地开发时我建议不要用后端CORS全局放行的方式而是用Vite的proxy代理把所有/api请求代理到后端地址。// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这种方式的好处是浏览器看到的请求是同源的不会触发跨域拦截也不需要后端加CrossOrigin。如果项目已经上线生产环境由Nginx统一代理前端和后端同样可以规避跨域问题。所以前后端项目跨域我通常不建议在后端代码里写CORS配置而是交给网关或代理层解决更干净。5.2 MyBatis-Plus分页插件不生效的经典坑MyBatis-Plus做分页查询时如果你只写了Page和IPage但没注册PaginationInnerInterceptor你会发现分页结果里的total永远是0或者是把所有记录一次查出来再在内存里分的页。这个坑几乎每个人都踩过。我的分页配置写法Configuration MapperScan(com.example.assessment.mapper) public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }注意版本问题mybatis-plus-boot-starter 3.5.x版本下这个配置没问题但如果你用了3.4以下的老版本配置方式完全不同。排查分页问题的最好方式是把MyBatis日志打开看看打印的SQL里是否带有LIMIT关键字如果没带说明拦截器没生效。第二个坑是如果项目中自己写了多个MybatisPlusInterceptor的Bean最后一个Bean会覆盖前面的导致分页失效所以全项目只保留一个就好了。5.3 前端显示时间和数据库差8小时后端返回的时间在前端页面总比数据库时间少8小时或者反过来多8小时。这个问题的根因通常是JDBC连接串里没有指定时区MySQL默认使用系统时区而服务器和本地时间不一致。解决办法是在数据源连接串中明确serverTimezonespring.datasource.urljdbc:mysql://localhost:3306/assessment?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai同时确认Jackson序列化时区也统一spring.jackson.time-zoneAsia/Shanghai spring.jackson.date-formatyyyy-MM-dd HH:mm:ss如果这两处都设置了前端展示的时间基本不会出问题。这个问题特别隐蔽因为本地Windows开发和Linux服务器的默认时区可能一致也可能不一致很多人查半天最后发现是时区导致。5.4 成绩权重合计校验不通过的边界情况权重配置校验时前端算的权重和是99.99后端算的却是100.00或者反过来。这个就是浮点换算精度问题。解决办法很简单所有涉及权重的字段都用BigDecimal不要用double。前端校验逻辑里把w1w2...的和用Math.round(sum * 100) / 100处理一次再跟100比较。后端用BigDecimal的add方法累加后再用compareTo(BigDecimal.valueOf(100))比较注意不要用equals因为BigDecimal的equals会同时比较精度100.00和100用equals会返回false。5.5 Vue Router history模式刷新404问题前端打包部署到Nginx后如果路由用的history模式直接访问http://域名/teacher/course会404因为Nginx默认找不到这个路径对应的文件。解决办法是Nginx配置try_files让所有非静态资源的请求都回退到index.htmllocation / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }如果不想改Nginx也可以用hash模式URL里会多一个#不太美观但在受限环境下是可行的兜底方案。5.6 其他问题速查表问题描述可能原因解决方案登录成功后刷新页面就掉线token只存在内存变量中token存入localStorage刷新后重新读取学生端看到其他学生的成绩接口未校验登录者身份直接用前端传入studentId查询后端从token解析userId强制绑定查询条件Excel导入失败但无任何提示EasyExcel异常未捕获在导入方法上try-catch把错误行号与原因包装成自定义异常返回前端表格滑动卡顿渲染了上千行数据且每行都有复杂组件使用虚拟滚动或分批渲染接口请求很慢N1查询逐个查数据库使用JOIN查询或MyBatis-Plus批量查询避免循环查库部署到服务器后上传文件失败服务器没有可写目录权限检查文件存储路径权限尽量使用相对路径并配置独立上传目录Vue3组件中ref获取不到DOMonMounted中还没渲染完成使用nextTick包一层或把DOM操作放到请求回调中教师端成绩状态已发布但学生看不到状态字段没传或缓存穿透前端确认请求中status字段设置为2后端查询过滤status条件正确最后说一个我自己在项目中感触比较深的地方。这个系统上线之后我最意外的不是老师觉得录入方便了而是学生开始主动看自己的过程性数据了。以前期末成绩出来才知道自己考了多少分现在学生每个阶段都能看到自己在各项考核中的位置期中之后就有学生去找老师问“我的课堂表现分为什么这么低”这不只是系统的功劳更是形成性考核本身的教学价值被技术手段释放了。如果你正在做类似的教学管理系统我建议把“过程性反馈”当成第一优先级的功能来设计而不是把重点放在花哨的统计图表上。技术选型、架构设计都是手段最终要回答的问题是这个系统有没有让教和学都变得更好。这是我从这个项目里得到的最实在的一句话。